Konteynerda root — nega xavfli va qanday yopiladi
Stack'ingizdagi API konteyneriga kiring va id yozing:
$ docker compose exec api id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)Dockerfile'da USER bo'lmasa, jarayon root'da ishlaydi. Rasmiy python va node image'lari standart holatda aynan shunday, nginx da esa master jarayon root. Odatda bunga "baribir konteyner ichida" deb qaraladi. Keling, bu "ichida" aslida qancha narsani anglatishini ko'raylik.
Konteyner — virtual mashina emas#
Konteyner — host yadrosida ishlaydigan oddiy jarayon. Uni boshqalardan namespace'lar (o'z PID, tarmoq va fayl tizimi ko'rinishi), cgroup'lar, capability'lar, seccomp va AppArmor/SELinux ajratib turadi. Alohida yadro yo'q. Docker standart holatda user namespace'ni yoqmaydi, shuning uchun konteyner ichidagi UID 0 host yadrosi uchun ham UID 0. Linux serverda buni o'zingiz ko'ring:
$ ps -o user= -p "$(docker inspect -f '{{.State.Pid}}' "$(docker compose ps -q api)")"
rootBu root'ni qisman cheklaydigan narsa — capability'lar. Docker jarayonga root'ning hamma huquqlarini emas, 14 tasini qoldiradi:
$ docker compose exec api grep -E 'CapEff|CapBnd' /proc/1/status
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fba80425fb niqobi ichida: CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, SETGID, SETUID, SETPCAP, NET_BIND_SERVICE, NET_RAW, SYS_CHROOT, MKNOD, AUDIT_WRITE, SETFCAP (capsh --decode=00000000a80425fb bilan tekshirsa bo'ladi). SYS_ADMIN yoki SYS_MODULE kabi eng xavflilari yo'q — lekin qolgani ham oz emas.
Bu tenglikni tubdan buzadigan yo'l ham bor: userns-remap yoki rootless Docker'da konteyner ichidagi 0 host'da imtiyozsiz UID'ga aylanadi. Lekin ular standart holatda yoqilmagan va o'z cheklovlari bor. Quyidagi qadamlar esa istalgan Docker'da, bitta compose faylida ishlaydi.
Ilova buzilsa, hujumchi nima oladi#
Tasavvur qiling: bog'liqliklardan biridagi zaiflik orqali hujumchi ilova nomidan buyruq bajara oladi. Keyingi hamma narsa shu jarayonning huquqlariga bog'liq. Root'da u:
- konteynerdagi istalgan faylni almashtiradi — ilova kodini,
/usr/local/bindagi binarlarni; bu konteyner qayta yaratilguncha saqlanadi; - tashqi tarmoq ochiq bo'lsa,
apk addbilan kerakli vositalarni o'rnatadi; - ulangan volume va host papkalaridagi hamma narsani root sifatida o'qiydi va yozadi —
/etcyoki loyiha papkasi ulangan bo'lsa, bular host fayllari; DAC_OVERRIDEbilan fayl huquqlarini chetlab o'tadi,NET_RAWbilan tarmoqqa qo'lda yasalgan paketlar yuboradi (masalan, qo'shni konteynerlarga ARP spoofing);- izolyatsiyadagi har qanday yoriqdan root sifatida foydalanadi. Haqiqiy misol — CVE-2019-5736: runc'dagi bu zaiflik konteyner ichida root bo'lgan jarayonga host'dagi
runcbinarini qayta yozib, host'da root bo'lish imkonini bergan.
UID 10001 da esa xuddi shu zaiflik faqat shu foydalanuvchi yeta oladigan joylarga yetadi. Quyidagi qadamlarning maqsadi — aynan shu doirani imkon qadar toraytirish.
Qadamma-qadam qulflash#
Boshlang'ich fayl:
services:
api:
build: .
command: uvicorn app:app --host 0.0.0.0 --port 8000
ports:
- "8000:8000"
db:
image: postgres:latest
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:1. Root'dan chiqish#
Eng to'g'ri joy — Dockerfile. Foydalanuvchini yarating va oxirida unga o'ting:
FROM python:3.12-alpine
RUN addgroup -S -g 10001 app && adduser -S -D -H -u 10001 -G app app
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER 10001:10001COPY fayllarni root nomidan yozadi: ilova o'z kodini o'qiydi, lekin o'zgartira olmaydi — bu ham himoya. Compose'da esa xuddi shuni majburiy qiling, image keyinchalik o'zgarsa ham:
user: "10001:10001"UID'ni raqam bilan yozing: /etc/passwd da yozuv bo'lmasa ham ishlaydi, Kubernetes'ning runAsNonRoot tekshiruvi esa faqat raqamli UID'ni tasdiqlay oladi. Ilova biror joyga yozishi kerak bo'lsa (yuklangan fayllar, kesh), o'sha papkani USER dan oldin yarating va egasini bering: RUN mkdir /data && chown 10001:10001 /data. Yangi nomlangan volume birinchi ulanishda image'dagi papkaning egasini oladi.
2. Fayl tizimini muzlatish#
read_only: true
tmpfs:
- /tmpEndi kod ham, binarlar ham o'zgarmaydi — root bo'lsa ham:
$ docker compose exec api sh -c 'echo x > /app/app.py'
sh: can't create /app/app.py: Read-only file systemIlova vaqtinchalik fayl yozadigan joylarga tmpfs bering. U xotirada yashaydi va konteynerning xotira chegarasiga kiradi.
tmpfs image'dagi papkaning huquqlarini oladi. /tmp da bu 1777 — hamma yozadi. Lekin rasmiy nginx image'ida /var/cache/nginx va /run — 755, egasi root. Root'siz nginx u yerga yozolmaydi va mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied) bilan yiqiladi. Egasini mount parametrlarida ko'rsating:
proxy:
image: nginx:1.27-alpine
user: "101:101"
read_only: true
tmpfs:
- /var/cache/nginx:uid=101,gid=101
- /run:uid=101,gid=101
cap_drop: [ALL]
security_opt:
- no-new-privileges:true101 — image ichidagi nginx foydalanuvchisi. Ishga tushishda the "user" directive makes sense only if the master process runs with super-user privileges, ignored ogohlantirishi chiqadi — bu kutilgan holat: nginx.conf dagi user nginx; endi kerak emas, chunki jarayon boshidanoq shu foydalanuvchi.
3. Capability'larni tashlash#
Root'dan chiqqan jarayonning ishchi huquqlari (CapEff) allaqachon nol. Lekin chegaraviy to'plam (CapBnd) hali 14 ta — setuid binar yoki fayl capability'si orqali ular qaytib kelishi mumkin. cap_drop: [ALL] chegarani ham nolga tushiradi:
cap_drop: [ALL]Ba'zi image'lar entrypoint'ni root'da boshlab, keyin o'z foydalanuvchisiga o'tadi. Rasmiy postgres shunday: data papkasini chown/chmod qiladi va postgres foydalanuvchisiga o'tadi. Unga faqat shu ishlar uchun kerakli huquqlarni qaytaring:
db:
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID]SETUID/SETGID bo'lmasa, entrypoint failed switching to 'postgres': operation not permitted bilan to'xtaydi. Ro'yxatni taxmin qilmang: ALL ni tashlang, ishga tushiring va logdagi Operation not permitted ga qarab bittadan qaytaring. Yana ham toza variant — user: "70:70" (image ichidagi postgres foydalanuvchisi): volume'dagi fayllar egasi shu bo'lsa, entrypoint root'siz ishlaydi va cap_add umuman kerak bo'lmaydi.
4. Huquq oshirishni yopish#
su yoki passwd kabi setuid bitli dasturlar kim ishga tushirishidan qat'i nazar, fayl egasi nomidan ishlaydi — egasi root bo'lsa, root. no-new-privileges yadroga aytadi: bu jarayon va uning bolalari execve orqali hech qachon yangi huquq olmaydi (laboratoriya).
security_opt:
- no-new-privileges:true5. Jarayonlar sonini cheklash#
pids_limit: 100Fork bomba yoki nazoratsiz worker'lar konteyner ichida qoladi va host'ning jarayonlar jadvalini to'ldirmaydi. Chegaraga yetganda yangi jarayon shunchaki ochilmaydi:
sh: can't fork: Resource temporarily unavailable6. Ichki portlarni chiqarmaslik#
ports konteynerni host'ning barcha interfeyslarida ochadi va Docker buning uchun o'z iptables qoidalarini yozadi — ular ufw qoidalaridan oldin ishlaydi. Faervolda 5432 yopiq bo'lsa ham, "5432:5432" bazani internetga ochadi. Bir tarmoqdagi konteynerlar bir-birining portlariga ports siz ham yetadi, shuning uchun db ga u umuman kerak emas. Host'dan kirish zarur bo'lsa, faqat loopback'ga bog'lang: "127.0.0.1:5432:5432" (laboratoriya).
7. Image'ni qotirish#
latest — ko'chib yuruvchi yorliq: bugungi image ertangisi emas. Postgres uchun bu ayniqsa xavfli — yangi major versiya eski versiyada yaratilgan data papkasini ochmaydi. Aniq tegdan boshlang (postgres:16-alpine, laboratoriya). Teg ham qayta nashr qilinadi; to'liq takrorlanuvchanlik kerak bo'lsa, digest'ga bog'lang:
$ docker buildx imagetools inspect postgres:16-alpine
Name: docker.io/library/postgres:16-alpine
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080eaDigest qiymatini teg yoniga yozing: postgres:16-alpine@sha256:.... Bu index digest'i, shuning uchun amd64 va arm64 serverlarda ham ishlaydi. Narxi ham bor: xavfsizlik yamoqlari endi o'zi kelmaydi — digest'ni Renovate, Dependabot yoki rejali yangilash bilan muntazam ko'taring.
Yakuniy fayl#
Hammasini bitta deploy'da kiritish shart emas. Eng arzon va eng katta foyda — user: va keraksiz ports ni olib tashlash. read_only ilova qayerga yozishini aniqlashni talab qiladi, shuning uchun uni alohida deploy bilan, loglarni kuzatib kiriting. Hammasi joyida bo'lganda fayl shunday ko'rinadi:
services:
api:
build: .
command: uvicorn app:app --host 0.0.0.0 --port 8000
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
pids_limit: 100
ports:
- "127.0.0.1:8000:8000" # faqat host'dagi proksi uchun
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID]
security_opt:
- no-new-privileges:true
pids_limit: 200 # max_connections (standart 100) + fon jarayonlari
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Tekshirish#
Konfiguratsiyaga emas, ishlab turgan konteynerga ishoning:
$ docker compose exec api id
uid=10001(app) gid=10001(app) groups=10001(app)
$ docker compose exec api grep -E 'CapEff|CapBnd|NoNewPrivs' /proc/1/status
CapEff: 0000000000000000
CapBnd: 0000000000000000
NoNewPrivs: 1
$ docker inspect -f 'ro={{.HostConfig.ReadonlyRootfs}} capdrop={{.HostConfig.CapDrop}} pids={{.HostConfig.PidsLimit}}' $(docker compose ps -q api)
ro=true capdrop=[ALL] pids=100Tashqaridan esa portlarni boshqa mashinadan nmap bilan tekshiring: server ichidan qaralgan faervol qoidasi dalil emas.
Amaliyot#
Laboratoriyalar shu qadamlarni bittadan, oxirgisi esa hammasini bitta servisda beradi. Mashg'ulot — server darajasidagi qo'shnisi: faervol, SSH va ochiq portlar.