~/blog / konteynerda-root-xavfiback

Konteynerda root — nega xavfli va qanday yopiladi

7 daqiqa o'qish

Stack'ingizdagi API konteyneriga kiring va id yozing:

console
$ 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:

console
$ ps -o user= -p "$(docker inspect -f '{{.State.Pid}}' "$(docker compose ps -q api)")"
root

Bu root'ni qisman cheklaydigan narsa — capability'lar. Docker jarayonga root'ning hamma huquqlarini emas, 14 tasini qoldiradi:

console
$ docker compose exec api grep -E 'CapEff|CapBnd' /proc/1/status
CapEff:	00000000a80425fb
CapBnd:	00000000a80425fb

a80425fb 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/bin dagi binarlarni; bu konteyner qayta yaratilguncha saqlanadi;
  • tashqi tarmoq ochiq bo'lsa, apk add bilan kerakli vositalarni o'rnatadi;
  • ulangan volume va host papkalaridagi hamma narsani root sifatida o'qiydi va yozadi — /etc yoki loyiha papkasi ulangan bo'lsa, bular host fayllari;
  • DAC_OVERRIDE bilan fayl huquqlarini chetlab o'tadi, NET_RAW bilan 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 runc binarini 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:

yaml
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:

dockerfile
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:10001

COPY 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:

yaml
    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#

yaml
    read_only: true
    tmpfs:
      - /tmp

Endi kod ham, binarlar ham o'zgarmaydi — root bo'lsa ham:

console
$ docker compose exec api sh -c 'echo x > /app/app.py'
sh: can't create /app/app.py: Read-only file system

Ilova 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:

yaml
  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:true

101 — 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:

yaml
    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:

yaml
  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).

yaml
    security_opt:
      - no-new-privileges:true

5. Jarayonlar sonini cheklash#

yaml
    pids_limit: 100

Fork bomba yoki nazoratsiz worker'lar konteyner ichida qoladi va host'ning jarayonlar jadvalini to'ldirmaydi. Chegaraga yetganda yangi jarayon shunchaki ochilmaydi:

text
sh: can't fork: Resource temporarily unavailable

6. 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:

console
$ docker buildx imagetools inspect postgres:16-alpine
Name:      docker.io/library/postgres:16-alpine
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea

Digest 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:

yaml
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:

console
$ 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=100

Tashqaridan 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.

labosonKonteyner ichida root — host'da ham deyarli rootIlova root'da ishlayapti. Bitta zaiflik — va hujumchi root.mashqni ochish labo'rtaFayl tizimini muzlatishHujumchi konteynerga skript yozib qo'ydi. Yoziladigan joy bo'lmasa — yozolmaydi.mashqni ochish labo'rtaKerak bo'lmagan yadro huquqlarini tashlashDocker standart holatda 14 ta capability beradi. Ilovaga birortasi kerak emas.mashqni ochish labqiyinBitta servis, to'liq qulfHardening ro'yxatining hammasi bitta servisda.mashqni ochish buyruq mashqi8-bosqich · 16 vazifaTarmoq va xavfsizlikOchiq portlar, kalitlar, sertifikatlar va sirlar. Serverni himoyalashning kundalik ishi.bosqichni boshlash