Container chạy quyền root là cho hacker thêm một bậc thang
Dockerfile AI viết thường để app chạy bằng root, không giới hạn RAM, CPU, và có khi còn mount cả docker.sock. Sáu dòng cấu hình trong Dockerfile và docker-compose giúp một lỗ hổng trong app không biến thành mất cả máy chủ.
Xem nhanh trong 1 phút
Gõ docker exec <container> id vào container đang chạy app của bạn. Nếu kết quả là uid=0(root), app đang chạy bằng root. Đây là mặc định của Docker: Dockerfile không có dòng USER thì mọi thứ trong container chạy bằng root, và phần lớn Dockerfile do AI viết không có dòng đó.
Nhiều người nghĩ “root trong container thì có sao, nó bị nhốt mà”. Container là lớp cô lập, không phải máy ảo. Root trong container mặc định cũng là UID 0 trên máy chủ, chỉ bị giới hạn bởi các lớp kernel bao quanh. Khi một trong các lớp đó có lỗi thì root trong container là người đứng gần cửa nhất.
Chuyện đã xảy ra với runc
Đầu năm 2024, runc, thành phần chạy container bên dưới Docker và containerd, có lỗ hổng CVE-2024-21626 (CVSS 8,6). Một file descriptor nội bộ bị rò, cho phép tiến trình trong container chạm tới hệ thống file của máy chủ, ghi đè file chạy của máy chủ, tức là thoát khỏi container. Bản 1.0.0-rc93 tới 1.1.11 bị ảnh hưởng, bản vá là 1.1.12.
Chạy bằng user thường không vá được lỗi kiểu này, nhưng làm hậu quả nhỏ đi nhiều: tiến trình thoát ra mang UID của một user không có quyền gì trên máy chủ thay vì UID 0. Và quan trọng hơn, đa số sự cố không bắt đầu từ lỗi kernel mà từ lỗi trong chính app: một gói npm độc, một lỗi upload file, một route chạy lệnh shell. Lúc đó, mọi giới hạn bạn đặt cho container quyết định hacker làm được gì tiếp.
Sáu dòng nên có
1. Chạy bằng user thường
Image Node chính thức đã có sẵn user node, chỉ cần dùng:
FROM node:22-slimWORKDIR /appCOPY --chown=node:node package*.json ./RUN npm ci --omit=devCOPY --chown=node:node . .USER nodeCMD ["node", "server.js"]
User thường không mở được cổng dưới 1024, nên cho app nghe ở 3000 hay 8000 rồi map cổng bằng ports.
2. Bỏ hết capability, chỉ thêm cái cần
Root trong container vẫn giữ một bộ quyền kernel (capability) mặc định. Phần lớn app web không cần cái nào. OWASP khuyên bỏ hết rồi thêm lại đúng cái cần.
3. Chặn leo quyền
no-new-privileges ngăn tiến trình lấy thêm quyền qua file setuid kiểu sudo hay su, kể cả khi trong image có sẵn.
4. Hệ thống file chỉ đọc
Hacker vào được app thường việc đầu tiên là tải thêm công cụ về, sửa file của app. read_only: true chặn việc đó. Chỗ nào app cần ghi (thư mục tạm, cache) thì cấp riêng bằng tmpfs hoặc volume.
5. Giới hạn RAM, CPU, số tiến trình
Không giới hạn thì một container bị chiếm để đào coin, hay đơn giản là một vòng lặp lỗi, ăn hết máy chủ và kéo sập mọi container khác. pids_limit chặn kiểu tấn công đẻ tiến trình vô hạn.
6. Không mount docker.sock
Dòng - /var/run/docker.sock:/var/run/docker.sock xuất hiện trong nhiều file compose mẫu cho các công cụ quản lý, dashboard, reverse proxy tự động. Ai điều khiển được socket này là điều khiển được Docker trên máy chủ, tức là chạy được container mới với quyền root của máy chủ. OWASP ghi rõ mount kiểu :ro cũng không làm API của Docker thành chỉ đọc.
Gộp lại trong docker-compose
services: app: build: . user: "node" # hoặc "1000:1000" read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL mem_limit: 512m cpus: "1.0" pids_limit: 200 ports: - "127.0.0.1:3000:3000" # chỉ mở cho reverse proxy trên cùng máy restart: on-failure:5 environment: - NODE_ENV=production
Vài ghi chú:
ports có 127.0.0.1: phía trước để cổng chỉ nghe trên máy chủ. Viết "3000:3000" là mở ra internet, và Docker tự thêm luật tường lửa đi vòng qua ufw.
Database (Postgres, Redis) cũng làm tương tự, và thường không cần ports gì cả nếu chỉ app trong cùng network compose gọi tới.
Next.js chạy next start cần ghi vào .next/cache. Nếu bật read_only, thêm tmpfs cho thư mục đó hoặc dùng bản build output: "standalone".
Bật read_only và cap_drop rồi app lỗi thì xem log, thêm đúng thứ nó cần, đừng gỡ cả dòng.
Kiểm tra app của bạn trong vài phút
Xem user đang chạy:
docker ps --format '{{.Names}}' | xargs -I{} sh -c 'echo "{}: $(docker exec {} id -u 2>/dev/null)"'
Tìm docker.sock trong mọi file compose: grep -rn "docker.sock" .
Xem các cổng đang mở ra ngoài: docker ps --format '{{.Names}} {{.Ports}}'. Dòng nào có 0.0.0.0: mà không phải cổng 80, 443 của reverse proxy thì xem lại.
Kiểm phiên bản runc: runc --version hoặc docker info | grep -i runc. Thấp hơn 1.1.12 là phải cập nhật Docker.
Khi nhờ AI viết Dockerfile, thêm thẳng vào yêu cầu: “chạy bằng user không phải root, kèm docker-compose có read_only, cap_drop ALL, no-new-privileges, giới hạn RAM và CPU”.
Container trên VPS của bạn đang chạy bằng user nào? Vào cộng đồng Vibe Code & Bảo Mật chia sẻ file compose của bạn (nhớ xóa key trước) để mọi người cùng góp ý nhé.
Dockermáy chủhạ tầng/anh/web/container-chay-quyen-root-480.webpDocker · máy chủ · hạ tầng
Container chay quyen root la cho hacker them mot bac thang Dockerfile AI viet thuong de app chay bang root, khong gioi han RAM, CPU, va co khi con mount ca docker.sock. Sau dong cau hinh trong Dockerfile va docker-compose giup mot lo hong trong app khong bien thanh mat ca may chu. Docker may chu ha tang Go `docker exec <container> id` vao container dang chay app cua ban. Neu ket qua la `uid=0(root)`, app dang chay bang root. Day la mac dinh cua Docker: Dockerfile khong co dong `USER` thi moi thu trong container chay bang root, va phan lon Dockerfile do AI viet khong co dong do.
Nhieu nguoi nghi "root trong container thi co sao, no bi nhot ma". Container la lop co lap, khong phai may ao. Root trong container mac dinh cung la UID 0 tren may chu, chi bi gioi han boi cac lop kernel bao quanh. Khi mot trong cac lop do co loi thi root trong container la nguoi dung gan cua nhat.

## Chuyen da xay ra voi runc
Dau nam 2024, runc, thanh phan chay container ben duoi Docker va containerd, co lo hong CVE-2024-21626 (CVSS 8,6). Mot file descriptor noi bo bi ro, cho phep tien trinh trong container cham toi he thong file cua may chu, ghi de file chay cua may chu, tuc la thoat khoi container. Ban 1.0.0-rc93 toi 1.1.11 bi anh huong, ban va la 1.1.12.
Chay bang user thuong khong va duoc loi kieu nay, nhung lam hau qua nho di nhieu: tien trinh thoat ra mang UID cua mot user khong co quyen gi tren may chu thay vi UID 0. Va quan trong hon, da so su co khong bat dau tu loi kernel ma tu loi trong chinh app: mot goi npm doc, mot loi upload file, mot route chay lenh shell. Luc do, moi gioi han ban dat cho container quyet dinh hacker lam duoc gi tiep.
## Sau dong nen co
### 1. Chay bang user thuong
Image Node chinh thuc da co san user `node`, chi can dung:
```dockerfile
FROM node:22-slim
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
```
Image Python hay image khac khong co san thi tao:
```dockerfile
FROM python:3.12-slim
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
COPY --chown=app:app . .
RUN pip install --no-cache-dir -r requirements.txt
USER app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
User thuong khong mo duoc cong duoi 1024, nen cho app nghe o 3000 hay 8000 roi map cong bang `ports`.
### 2. Bo het capability, chi them cai can
Root trong container van giu mot bo quyen kernel (capability) mac dinh. Phan lon app web khong can cai nao. OWASP khuyen bo het roi them lai dung cai can.
### 3. Chan leo quyen
`no-new-privileges` ngan tien trinh lay them quyen qua file setuid kieu `sudo` hay `su`, ke ca khi trong image co san.
### 4. He thong file chi doc
Hacker vao duoc app thuong viec dau tien la tai them cong cu ve, sua file cua app. `read_only: true` chan viec do. Cho nao app can ghi (thu muc tam, cache) thi cap rieng bang `tmpfs` hoac volume.
### 5. Gioi han RAM, CPU, so tien trinh
Khong gioi han thi mot container bi chiem de dao coin, hay don gian la mot vong lap loi, an het may chu va keo sap moi container khac. `pids_limit` chan kieu tan cong de tien trinh vo han.
### 6. Khong mount docker.sock
Dong `- /var/run/docker.sock:/var/run/docker.sock` xuat hien trong nhieu file compose mau cho cac cong cu quan ly, dashboard, reverse proxy tu dong. Ai dieu khien duoc socket nay la dieu khien duoc Docker tren may chu, tuc la chay duoc container moi voi quyen root cua may chu. OWASP ghi ro mount kieu `:ro` cung khong lam API cua Docker thanh chi doc.
## Gop lai trong docker-compose
```yaml
services:
app:
build: .
user: "node" # hoac "1000:1000"
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
mem_limit: 512m
cpus: "1.0"
pids_limit: 200
ports:
- "127.0.0.1:3000:3000" # chi mo cho reverse proxy tren cung may
restart: on-failure:5
environment:
- NODE_ENV=production
```
Vai ghi chu:
- `ports` co `127.0.0.1:` phia truoc de cong chi nghe tren may chu. Viet `"3000:3000"` la mo ra internet, va Docker tu them luat tuong lua di vong qua `ufw`.
- Database (Postgres, Redis) cung lam tuong tu, va thuong khong can `ports` gi ca neu chi app trong cung network compose goi toi.
- Next.js chay `next start` can ghi vao `.next/cache`. Neu bat `read_only`, them `tmpfs` cho thu muc do hoac dung ban build `output: "standalone"`.
- Bat `read_only` va `cap_drop` roi app loi thi xem log, them dung thu no can, dung go ca dong.
## Kiem tra app cua ban trong vai phut
1. Xem user dang chay:
```bash
docker ps --format '{{.Names}}' | xargs -I{} sh -c 'echo "{}: $(docker exec {} id -u 2>/dev/null)"'
```
So `0` la root.
2. Xem cau hinh bao mat cua mot container:
```bash
docker inspect <ten-container> --format 'User={{.Config.User}} ReadOnly={{.HostConfig.ReadonlyRootfs}} CapDrop={{.HostConfig.CapDrop}} SecOpt={{.HostConfig.SecurityOpt}} Mem={{.HostConfig.Memory}} Pids={{.HostConfig.PidsLimit}}'
```
`Mem=0` nghia la khong gioi han RAM.
3. Tim docker.sock trong moi file compose: `grep -rn "docker.sock" .`
4. Xem cac cong dang mo ra ngoai: `docker ps --format '{{.Names}} {{.Ports}}'`. Dong nao co `0.0.0.0:` ma khong phai cong 80, 443 cua reverse proxy thi xem lai.
5. Kiem phien ban runc: `runc --version` hoac `docker info | grep -i runc`. Thap hon 1.1.12 la phai cap nhat Docker.
Khi nho AI viet Dockerfile, them thang vao yeu cau: "chay bang user khong phai root, kem docker-compose co read_only, cap_drop ALL, no-new-privileges, gioi han RAM va CPU".
---
Container tren VPS cua ban dang chay bang user nao? Vao [cong dong Vibe Code & Bao Mat](https://www.facebook.com/groups/1867387324580254) chia se file compose cua ban (nho xoa key truoc) de moi nguoi cung gop y nhe.
## Nguon
- [Docker Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html), OWASP
- [Services top-level elements](https://docs.docker.com/reference/compose-file/services/), Docker Docs
- [runc: container breakout through process.cwd trickery and leaked fds (CVE-2024-21626)](https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv), GitHub Security Advisory