چکلیست امنیت کانتینر و ایمیجهای Docker: از Dockerfile تا سرور و Registry
Docker بهطور پیشفرض «بهاندازهی کافی امن» است تا یک اپلیکیشن را بالا بیاورید، اما نه بهاندازهای که بشود با خیال راحت آن را در Production رها کرد. کانتینری که با کاربر root اجرا میشود، ایمیجی که کلید API داخل یکی از لایههایش جا مانده، یا پورتی که فکر میکنید فایروال بسته است ولی در واقع باز است، همه از اشتباهات رایجی هستند که در بررسی زیرساخت تیمها زیاد میبینیم.
این مقاله یک چکلیست عملی است، در چهار لایه: ایمیج، اجرای کانتینر (Runtime)، سرور میزبان و Registry و زنجیرهی تأمین. هر مورد را میتوانید همین امروز روی پروژهی خودتان بررسی کنید.
لایهی ۱: ایمیج
۱. از ایمیج پایهی کوچک و رسمی استفاده کنید
هر پکیجی که داخل ایمیج است، یک آسیبپذیری بالقوه است. بهجای ایمیجهای کامل مثل ubuntu یا node:22، از نسخههای slim، alpine یا ایمیجهای Distroless استفاده کنید و ایمیج پایه را فقط از منابع رسمی یا تأییدشده بگیرید. ساخت Multi-stage کمک میکند ابزارهای Build (کامپایلر، Composer، npm) به ایمیج نهایی نرسند:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
نمونههای کامل برای لاراول، جنگو و Node.js را در مقالهی داکرایز کردن لاراول، جنگو و Node.js آوردهایم.
۲. ایمیج را با Digest پین کنید
Tag قابل تغییر است؛ nginx:1.27-alpine امروز و ماه بعد ممکن است دو ایمیج متفاوت باشند. در Production ایمیج پایه و ایمیج استقرار را با Digest مشخص کنید:
docker buildx imagetools inspect nginx:1.27-alpine
# خروجی شامل Digest است، مثلاً sha256:...
FROM nginx:1.27-alpine@sha256:<digest>
برای اینکه پین کردن باعث عقب ماندن از وصلههای امنیتی نشود، بهروزرسانی Digestها را با ابزاری مثل Renovate یا یک بازبینی منظم خودکار کنید.
۳. ایمیجها را با Trivy اسکن کنید
Trivy یک اسکنر متنباز است که آسیبپذیریهای پکیجهای سیستمعامل و وابستگیهای زبان (npm، pip، Composer و…) را پیدا میکند:
trivy image --severity HIGH,CRITICAL myapp:1.0
# در CI: اگر آسیبپذیری بحرانی بود، Build شکست بخورد
trivy image --severity CRITICAL --exit-code 1 --ignore-unfixed myapp:1.0
اسکن را در Pipeline قرار دهید، نه فقط یک بار دستی. نمونهی Pipeline را در راهاندازی CI/CD با GitLab و Docker ببینید. توجه کنید Trivy برای بهروزرسانی پایگاه آسیبپذیریها به اینترنت نیاز دارد و ممکن است از ایران به آن دسترسی نداشته باشد؛ در این حالت اسکن را روی Runner خارج از ایران یا در Harbor انجام دهید.
۴. هیچ Secretی در لایهها نگذارید
دستور RUN rm .env فایل را از لایههای قبلی پاک نمیکند؛ هر کس به ایمیج دسترسی داشته باشد، با docker history یا استخراج لایهها آن را پیدا میکند. قواعد:
- فایل
.dockerignoreبسازید و.env،.git، کلیدها و فایلهای بکاپ را در آن بگذارید. - Secret را با
ARGیاENVبه Build ندهید. برای نیازهای زمان Build (مثل توکن مخزن خصوصی) از BuildKit Secret استفاده کنید:
# Dockerfile
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
# Build
docker build --secret id=npm_token,src=./npm_token.txt -t myapp:1.0 .
- Secretهای زمان اجرا را با متغیر محیطی از فایل خارج از مخزن، Docker Secrets یا یک ابزار مدیریت اسرار به کانتینر بدهید.
- Trivy میتواند Secretهای جامانده را هم پیدا کند:
trivy image --scanners secret myapp:1.0
لایهی ۲: اجرای کانتینر (Runtime)
۵. کانتینر را با کاربر غیر root اجرا کنید
بهطور پیشفرض پروسهی داخل کانتینر root است. اگر مهاجم از کانتینر فرار کند، با دسترسی بالا روی میزبان قرار میگیرد. در Dockerfile یک کاربر بسازید و با USER به آن سوئیچ کنید، یا در Compose تعیین کنید:
services:
app:
image: registry.example.com/myapp@sha256:<digest>
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
pids_limit: 200
بررسی سریع کانتینرهای در حال اجرا:
docker ps -q | xargs docker inspect --format '{{.Name}} user={{.Config.User}}'
خروجی خالی برای user یعنی کانتینر با root اجرا میشود.
۶. فایلسیستم فقطخواندنی
با read_only: true، مهاجم نمیتواند وبشل یا باینری جدید داخل کانتینر بنویسد. پوشههایی که واقعاً نیاز به نوشتن دارند (مثل /tmp یا کش) را با tmpfs یا Volume مشخص کنید.
۷. Capabilityها را حذف کنید
با cap_drop: [ALL] شروع کنید و فقط چیزی را که لازم است با cap_add برگردانید؛ مثلاً NET_BIND_SERVICE برای گوش دادن روی پورت زیر ۱۰۲۴. گزینهی no-new-privileges هم جلوی بالا رفتن سطح دسترسی از طریق باینریهای setuid را میگیرد.
۸. هرگز --privileged
کانتینر privileged عملاً همهی محدودیتها را کنار میگذارد و به دستگاههای میزبان دسترسی دارد. اگر ابزاری این گزینه را میخواهد، دقیقاً بررسی کنید به چه Capability یا Device نیاز دارد و فقط همان را بدهید. پیدا کردن کانتینرهای privileged:
docker ps -q | xargs docker inspect --format '{{.Name}} privileged={{.HostConfig.Privileged}}'
۹. سوکت Docker را Mount نکنید
Mount کردن /var/run/docker.sock داخل کانتینر یعنی دادن دسترسی root کامل به میزبان به آن کانتینر. اگر ابزاری مثل Traefik یا یک ابزار مانیتورینگ به آن نیاز دارد، حداقل بهصورت :ro و ترجیحاً از طریق یک Socket Proxy که فقط APIهای لازم را باز میکند استفاده کنید؛ و توجه داشته باشید که :ro روی سوکت جلوی ارسال دستور به API را نمیگیرد.
۱۰. محدودیت منابع
بدون محدودیت حافظه و CPU، یک کانتینر دچار نشت حافظه یا حملهی DoS میتواند کل سرور را از کار بیندازد. memory، cpus و pids_limit را برای هر سرویس تعیین کنید. اشتباهات دیگر Compose در Production را در اشتباهات رایج Docker Compose در Production مرور کردهایم.
لایهی ۳: سرور میزبان
۱۱. میزبان و Docker Engine را بهروز نگه دارید
کانتینرها کرنل میزبان را به اشتراک میگذارند؛ آسیبپذیری کرنل یعنی آسیبپذیری همهی کانتینرها. وصلههای امنیتی سیستمعامل و Docker Engine را منظم نصب کنید و سرور را فقط برای اجرای کانتینرها نگه دارید. دسترسی SSH را هم طبق راهنمای امنسازی SSH محکم کنید.
۱۲. گروه docker را جدی بگیرید
عضویت در گروه docker معادل دسترسی root است. فهرست اعضا را بررسی کنید:
getent group docker
اگر محیط اجازه میدهد، Rootless Mode را بررسی کنید که در آن خود Docker Daemon هم بدون root اجرا میشود:
sudo apt install -y uidmap docker-ce-rootless-extras
dockerd-rootless-setuptool.sh install
حالت Rootless محدودیتهایی دارد (مثلاً در شبکه و پورتهای پایین)، پس قبل از Production مستندات رسمی را بخوانید.
۱۳. هشدار مهم: UFW و Docker
این یکی از غافلگیرکنندهترین مشکلات است: پورتهایی که با -p یا ports: منتشر میکنید، از قوانین UFW عبور میکنند، چون Docker قوانین iptables خودش را قبل از قوانین UFW اضافه میکند. یعنی ممکن است ufw deny 5432 زده باشید و دیتابیس همچنان از اینترنت در دسترس باشد. راهحلها:
- سرویسهای داخلی را اصلاً منتشر نکنید و از شبکهی داخلی Docker استفاده کنید.
- اگر لازم است، فقط روی localhost منتشر کنید:
"127.0.0.1:5432:5432" - قوانین محدودکننده را در زنجیرهی
DOCKER-USERاضافه کنید که Docker به آن احترام میگذارد.
همیشه از بیرون سرور تست کنید، نه از خود سرور: nmap -p- your-server-ip
۱۴. لاگ و ممیزی
برای Docker لاگ چرخشی تنظیم کنید تا لاگ کانتینرها دیسک را پر نکند (در /etc/docker/daemon.json، "log-opts": {"max-size": "10m", "max-file": "3"}). ابزار متنباز Docker Bench for Security هم یک بررسی خودکار بر اساس توصیههای CIS انجام میدهد و نقطهی شروع خوبی است.
لایهی ۴: Registry و زنجیرهی تأمین
- Registry خصوصی با احراز هویت و HTTPS: هرگز Registry را بدون رمز روی اینترنت باز نکنید. Harbor برای این کار گزینهی کاملی است؛ آموزش آن را در راهاندازی Harbor ببینید.
- توکن با کمترین دسترسی: CI فقط باید روی پروژهی خودش Push کند و سرورها فقط Pull. برای هر کدام حساب Robot جدا بسازید.
- ایمیجهای عمومی از مسیر مطمئن: میرورهای عمومی ناشناس را در Production استفاده نکنید. در ایران، یک Proxy Cache اختصاصی هم مشکل دسترسی را حل میکند و هم کنترل را دست شما نگه میدارد (دسترسی به Docker Hub از ایران).
- اسکن مداوم در Registry: ایمیجی که امروز تمیز است، ماه بعد ممکن است آسیبپذیری شناختهشده داشته باشد. اسکن زمانبندیشده در Registry را فعال کنید.
- امضای ایمیج: با ابزارهایی مثل Cosign ایمیجها را امضا کنید تا فقط ایمیجهایی که CI خودتان ساخته مستقر شوند.
- Tag تغییرناپذیر: در Registry قانون Immutable Tag برای نسخههای Release تعریف کنید تا کسی نتواند
v1.4.0را بازنویسی کند.
خلاصهی چکلیست
| لایه | موارد |
|---|---|
| ایمیج | پایهی کوچک و رسمی، Multi-stage، پین با Digest، اسکن Trivy، بدون Secret در لایهها |
| Runtime | کاربر غیر root، read_only، cap_drop ALL، no-new-privileges، بدون privileged و docker.sock، محدودیت منابع |
| میزبان | وصلهها، کنترل گروه docker، Rootless در صورت امکان، مشکل UFW، لاگ چرخشی، Docker Bench |
| Registry | HTTPS و احراز هویت، Robot Account، Proxy Cache اختصاصی، اسکن مداوم، امضا، Immutable Tag |
جمعبندی
امنیت کانتینر یک تنظیم واحد نیست؛ مجموعهای از تصمیمهای کوچک در هر لایه است که با هم، دامنهی آسیب یک نفوذ را محدود میکنند. پیشنهاد ما این است که با سه مورد پربازده شروع کنید: اجرای غیر root، اسکن Trivy در CI و بررسی پورتهای منتشرشده از بیرون سرور.
نمیخواهید این کار را خودتان انجام دهید؟ تیم مروانت بررسی امنیتی ایمیجها و زیرساخت کانتینری، اسکن خودکار در CI/CD و امنسازی سرورها را انجام میدهد. صفحههای DevSecOps و امنیت زیرساخت و برونسپاری پروژههای Docker را ببینید.
