2026-03-2615 phút đọcVI
- 1.Khi AI Tool Bị Đầu Độc — Giải Phẫu Một Supply Chain Attack
- 2.Floating Tags & Auto-Updaters — Backdoor Thầm Lặng Trong Docker Stack
- 3.Container Hardening 101 — Từ 'Chạy Được' Đến 'An Toàn'(bài này)
- 4.Dependency Poisoning — Khi pip install Trở Thành Vũ Khí
- 5.Security Tools Là Attack Surface — Nghịch Lý Của Defense-in-Depth
- 6.Quản Lý Credentials Trong AI Infrastructure — Vượt Ra Ngoài .env
- 7.Bảo Mật Trong Kỷ Nguyên AI Agent — Chúng Ta Chưa Sẵn Sàng
Container Hardening 101 — Từ "Chạy Được" Đến "An Toàn"
Hai container chạy trên cùng một server.
Container thứ nhất là hệ thống ERP nội bộ. Filesystem read-only, tất cả Linux capabilities bị loại bỏ, process không thể tự nâng quyền. Attacker tìm được một vulnerability trong application code, exploit thành công, có shell bên trong container. Nhưng không ghi được file — filesystem bất biến. Không escalate được quyền — no-new-privileges chặn. Không thấy container khác — network bị phân tách. Không ăn hết RAM — resource limits giới hạn. Attacker vào được, nhưng không làm được gì đáng kể. Đội vận hành phát hiện qua log, patch, tiếp tục làm việc.
Container thứ hai là AI proxy service. Chạy root, full capabilities, bind 0.0.0.0 trên mọi port, không resource limits, nằm chung network với database. Cùng attacker, cùng vulnerability tương tự. Nhưng lần này: ghi malware vào filesystem, escalate lên root, đọc environment variables chứa API keys tới Claude và GPT-4, kết nối thẳng tới database, exfiltrate conversation history. Toàn bộ diễn ra trong vài phút.
Sự khác biệt không nằm ở application code. Không nằm ở firewall. Nằm ở cách container được cấu hình.
Bài trước trong series, chúng ta nói về floating tags và auto-updaters — cách attacker đưa image nhiễm vào hệ thống. Bài này nói về chuyện gì xảy ra sau khi attacker đã ở bên trong: làm sao để giới hạn thiệt hại xuống mức gần bằng không.
1. Sáu Tầng Phòng Thủ — Defense in Depth
Container hardening không phải một setting duy nhất. Nó là nhiều tầng chồng lên nhau, mỗi tầng chặn một loại hành vi khác nhau. Nếu attacker vượt qua tầng 1, tầng 2 chặn tiếp. Vượt tầng 2, tầng 3 chờ sẵn. Nguyên lý này gọi là defense in depth — phòng thủ theo chiều sâu.
Tầng 1: read_only: true — Filesystem Bất Biến
Khi bạn bật read_only: true trong docker-compose, toàn bộ filesystem của container trở thành read-only. Không process nào bên trong có thể tạo file mới, sửa file cũ, hay xóa bất cứ thứ gì.
Tại sao điều này quan trọng? Phần lớn malware sau khi exploit được vulnerability sẽ cần ghi file vào container: tải payload xuống /tmp, tạo script trong /var, cài reverse shell. read_only: true chặn toàn bộ hành vi này ngay từ gốc.
Nhưng application cũng cần ghi file — log, cache, temp data. Giải pháp là tmpfs — filesystem tạm trên RAM, chỉ mount vào những thư mục cụ thể mà application thực sự cần:
read_only: true
tmpfs:
- /tmp:size=100M
- /app/cache:size=200Mtmpfs tồn tại trên RAM, biến mất khi container restart, và bị giới hạn dung lượng. Malware có thể ghi vào /tmp, nhưng dung lượng chỉ 100MB, không persist qua restart, và toàn bộ filesystem còn lại vẫn bất biến.
Tầng 2: cap_drop: [ALL] — Loại Bỏ Linux Capabilities
Linux kernel không chỉ có hai mức quyền "root" và "user". Giữa hai mức đó là hệ thống capabilities — chia nhỏ quyền root thành khoảng 40 capability riêng biệt:
CAP_NET_ADMIN: Thay đổi cấu hình mạng, tạo interface, sửa routing tableCAP_SYS_ADMIN: Quyền gần như root — mount filesystem, thay đổi namespace, load kernel moduleCAP_DAC_OVERRIDE: Bỏ qua permission check khi đọc/ghi fileCAP_NET_RAW: Tạo raw socket — dùng cho packet sniffing, ARP spoofingCAP_SYS_PTRACE: Debug process khác — attacker dùng để đọc memory của process khác
Docker mặc định cấp cho container một danh sách dài capabilities. Hầu hết application không cần bất kỳ capability nào trong đó. Nhưng nếu bạn không chủ động loại bỏ, chúng vẫn có sẵn — và attacker biết cách khai thác.
cap_drop: [ALL] loại bỏ tất cả capabilities. Sau đó bạn chỉ thêm lại những gì thực sự cần:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Cho phép bind port dưới 1024Nguyên tắc: bắt đầu từ zero, thêm từng cái một. Nếu service chạy bình thường với cap_drop: [ALL] mà không thêm gì, hoàn hảo — để nguyên. Nếu lỗi, đọc error log để biết cần thêm capability nào. Không bao giờ thêm CAP_SYS_ADMIN — nếu application yêu cầu capability đó, cần xem xét lại kiến trúc.
Tầng 3: no-new-privileges: true — Chặn Privilege Escalation
Một kỹ thuật phổ biến của attacker sau khi vào được container là tìm cách nâng quyền. Ví dụ: tìm binary có setuid bit, exploit kernel vulnerability để chuyển từ user thường lên root bên trong container.
no-new-privileges ngăn chặn điều này ở cấp kernel. Khi flag này bật, không process nào trong container có thể nhận thêm privilege mới — kể cả thông qua setuid, setgid, hay bất kỳ cơ chế nào khác.
security_opt:
- no-new-privileges:trueMột dòng cấu hình, nhưng đóng lại toàn bộ lớp attack surface liên quan đến privilege escalation. Hầu như không có application nào cần tắt flag này.
Tầng 4: 127.0.0.1 Port Binding — Chỉ Localhost
Mặc định, khi bạn viết ports: ["8080:8080"] trong docker-compose, Docker bind port đó lên 0.0.0.0 — nghĩa là mọi network interface. Server có IP public? Port đó mở ra internet. Server trên VPN? Port đó accessible từ toàn bộ VPN.
Điều này đặc biệt nguy hiểm vì Docker tự thêm rule vào iptables, bypass UFW và firewall rules mà bạn đã cấu hình cẩn thận. Nhiều người setup UFW chặn mọi port, nhưng Docker vẫn expose container port ra ngoài.
Cách khắc phục đơn giản:
ports:
- "127.0.0.1:8080:8080"Bây giờ port chỉ accessible từ localhost. Muốn expose ra ngoài? Dùng reverse proxy (Nginx, Traefik, Caddy) — reverse proxy kiểm soát TLS, authentication, rate limiting, và quan trọng nhất: nằm trong tầm kiểm soát của bạn.
Tầng 5: Network Segmentation — Phân Tách Mạng
Docker network mặc định là bridge network — tất cả container trên cùng network có thể liên lạc với nhau tự do. Nếu attacker compromise một container, họ có thể scan và tấn công mọi container cùng network.
Network segmentation chia container thành các nhóm cô lập:
networks:
app-core:
name: app-core
app-data:
name: app-data
internal: true
app-monitor:
name: app-monitor
internal: trueKeyword quan trọng ở đây là internal: true. Network nội bộ không thể kết nối ra internet. Database nằm trên app-data (internal) — kể cả attacker chiếm được database container, họ cũng không thể exfiltrate data ra ngoài vì network đó không có đường ra internet.
Mỗi service chỉ join vào network mà nó thực sự cần:
- API service: join
app-core(nhận request) +app-data(truy cập database) - Database: chỉ join
app-data(internal, không ra internet) - Monitoring: chỉ join
app-monitor(internal, thu thập metrics)
Attacker compromise API service? Thấy database trên app-data, nhưng không thấy monitoring trên app-monitor. Compromise database? Không ra được internet. Mỗi container chỉ nhìn thấy đúng phần mà nó cần.
Tầng 6: Resource Limits — Giới Hạn Tài Nguyên
Không có resource limits, một container bị compromise có thể:
- Ăn hết RAM của host → crash tất cả container khác (DoS)
- Chiếm 100% CPU → server không phản hồi
- Ghi log liên tục → đầy disk → host mất khả năng hoạt động
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"Resource limits không chỉ ngăn abuse — chúng cũng giúp bạn phát hiện bất thường. Container bình thường dùng 500MB RAM, đột nhiên chạm 2GB limit? Có gì đó sai. Alert, kiểm tra, phản ứng.
Logging limits cũng quan trọng không kém. Không giới hạn, Docker log có thể phình tới hàng chục GB. max-size: "10m" + max-file: "5" = tối đa 50MB log per container, xoay vòng tự động.
2. Khái Niệm Nền Tảng
Linux Capabilities — Hệ Thống Phân Quyền Kernel
Trước Linux 2.2, chỉ có hai trạng thái: privileged (root, UID 0) và unprivileged (mọi user khác). Root có toàn quyền, user thường không có gì. Điều này vi phạm nguyên tắc least privilege — nhiều chương trình chỉ cần một mảnh nhỏ quyền root nhưng phải chạy với toàn bộ.
Capabilities chia nhỏ quyền root thành các đơn vị độc lập. Một web server cần bind port 80? Chỉ cần CAP_NET_BIND_SERVICE, không cần toàn bộ root. Một monitoring tool cần đọc network statistics? Chỉ cần CAP_NET_ADMIN.
Docker cấp cho container một subset capabilities mặc định — đủ để hầu hết application chạy, nhưng cũng đủ để attacker khai thác. Danh sách mặc định bao gồm CAP_NET_RAW (cho phép tạo raw socket — công cụ yêu thích cho network sniffing), CAP_CHOWN, CAP_SETUID, CAP_SETGID (cho phép thay đổi ownership và user context).
Seccomp Profiles — Lọc System Calls
Nếu capabilities kiểm soát quyền thì seccomp kiểm soát hành vi. Seccomp (Secure Computing Mode) giới hạn system calls mà process có thể thực hiện. Docker có seccomp profile mặc định chặn khoảng 44 syscalls nguy hiểm — nhưng bạn có thể tạo profile chặt hơn tùy theo application.
Một AI inference service thông thường chỉ cần: đọc file model, mở network socket, allocate memory. Nó không cần mount, reboot, kexec_load, hay ptrace. Seccomp profile chặt giúp chặn những hành vi bất thường ngay ở cấp kernel.
Docker Network — Không Chỉ Là Connectivity
Docker hỗ trợ ba loại network chính cho single-host:
- Bridge (mặc định): Container giao tiếp qua virtual bridge. Có thể truy cập internet.
- Host: Container dùng trực tiếp network stack của host. Nhanh hơn nhưng không cô lập.
- None: Không có network — container hoàn toàn cô lập.
Và trong bridge network, flag internal: true tạo ra network không có route ra internet. Container trên internal network vẫn giao tiếp được với nhau, nhưng không gửi được packet ra ngoài. Đây là phòng tuyến cuối cho data exfiltration — kể cả khi attacker có shell, có root, có mọi thứ bên trong container, dữ liệu vẫn không ra được ngoài.
Resource Limits — Deploy vs Runtime
Docker Compose có hai cách đặt resource limits:
# Deploy limits (Docker Swarm / Compose v3)
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
# Runtime limits (Compose v2 legacy)
mem_limit: 2GTrong Compose hiện đại, dùng deploy.resources. limits là giới hạn tối đa — container vượt quá sẽ bị OOM killed. reservations là mức tối thiểu được đảm bảo — Docker scheduler sẽ cố gắng giữ ít nhất từng đó cho container.
3. Cấu Hình Hoàn Chỉnh
Template Cơ Bản — Áp Dụng Toàn Bộ 6 Tầng
services:
my-service:
image: my-image:1.2.3 # Pin version, không dùng :latest
read_only: true
tmpfs:
- /tmp:size=100M
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Chỉ thêm capability thực sự cần
ports:
- "127.0.0.1:8080:8080" # Chỉ localhost
networks:
- app-core
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"Mỗi dòng trong template này tương ứng với một tầng phòng thủ. Không có dòng nào là "nice to have" — mỗi dòng chặn một loại attack cụ thể.
YAML Anchors — DRY Cho Security Config
Khi bạn có 5-10 services, copy-paste security config cho mỗi service là con đường dẫn đến sai sót. Quên một service là mở một lỗ hổng. YAML anchors giải quyết vấn đề này:
x-security: &security
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
x-logging: &logging
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
api-gateway:
image: api-gateway:2.1.0
<<: *security
<<: *logging
cap_add:
- NET_BIND_SERVICE
ports:
- "127.0.0.1:443:443"
networks:
- app-core
deploy:
resources:
limits:
memory: 1G
worker:
image: worker:1.5.0
<<: *security
<<: *logging
tmpfs:
- /tmp:size=50M
networks:
- app-core
- app-data
deploy:
resources:
limits:
memory: 4G
database:
image: postgres:16.2-alpine
<<: *security
<<: *logging
cap_add:
- CHOWN
- SETUID
- SETGID
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-data
deploy:
resources:
limits:
memory: 2GAnchor &security định nghĩa một lần, dùng *security cho mọi service. Thêm capability? Override cap_add cho từng service cụ thể. Cần writable directory? Thêm tmpfs hoặc named volume. Security baseline nhất quán, customization theo nhu cầu.
Network Segmentation Hoàn Chỉnh
networks:
app-core:
name: app-core
# Không có 'internal' → có thể kết nối internet
# Dành cho services cần nhận request từ bên ngoài
app-data:
name: app-data
internal: true
# Không kết nối internet → data không ra ngoài được
# Database, cache nằm ở đây
app-monitor:
name: app-monitor
internal: true
# Monitoring stack cô lập
# Prometheus, Grafana, alertingQuy tắc phân bổ:
- Service nhận request từ client →
app-core - Service truy cập data store →
app-data - Service cần cả hai → join cả
app-corelẫnapp-data - Monitoring →
app-monitor, tách biệt hoàn toàn
4. Quy Trình Hardening — 12 Bước Tuần Tự
Đừng áp dụng tất cả cùng lúc. Mỗi bước thay đổi một thứ, test, confirm service vẫn hoạt động, rồi mới tiếp.
- Pin tất cả image tags — Thay
:latestbằng version cụ thể. Chạydocker compose pullđể verify image tồn tại - Thêm
read_only: true— Test từng service. Nếu lỗi "read-only file system", thêmtmpfscho thư mục cần ghi - Thêm
cap_drop: [ALL]— Service nào cần capability cụ thể, thêmcap_addriêng. Postgres cầnCHOWN,SETUID,SETGID. Web server cầnNET_BIND_SERVICEnếu bind port dưới 1024 - Thêm
no-new-privileges: true— Hiếm khi gây lỗi. Nếu lỗi, kiểm tra xem application có dùng setuid binary không - Bind port về
127.0.0.1— Kiểm tra mọiportsentry. Cấu hình reverse proxy cho traffic từ bên ngoài - Tạo network segmentation — Định nghĩa networks theo chức năng: core, data, monitor
- Đánh dấu data network là
internal: true— Database, cache, message queue không cần internet - Thêm resource limits — Bắt đầu từ mức rộng rãi (2x mức sử dụng bình thường), tinh chỉnh sau
- Thêm logging limits —
max-size: "10m",max-file: "5"cho mọi service - Docker socket: mount read-only — Nếu bắt buộc phải mount (
/var/run/docker.sock), thêm:ro. Nếu có thể tránh, đừng mount - Test từng service — Chạy full workflow: healthcheck, request/response, edge cases. Không chỉ kiểm tra "container start được"
- Monitor 24 giờ — Theo dõi log, resource usage, network traffic. Nếu không có bất thường sau 24h, hardening thành công
Bước 11 là quan trọng nhất và hay bị bỏ qua. read_only: true có thể làm service lỗi âm thầm — application ghi cache file thất bại nhưng không crash, chỉ chậm dần. Test kỹ, kiểm tra log cẩn thận.
5. Container AI — Nhu Cầu Đặc Biệt, Nguyên Tắc Không Đổi
Container chạy AI workload có profile khác biệt so với web service thông thường. GPU passthrough, model cache hàng chục GB, memory usage cao. Nhưng nguyên tắc hardening vẫn hoàn toàn áp dụng được — chỉ cần điều chỉnh thông số.
GPU Passthrough
NVIDIA GPU passthrough trong Docker Compose dùng deploy.resources.reservations.devices:
services:
inference:
image: ollama/ollama:0.5.4
<<: *security
deploy:
resources:
limits:
memory: 16G
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
tmpfs:
- /tmp:size=500M
networks:
- app-coreGPU config nằm trong deploy.resources — hoàn toàn tương thích với read_only, cap_drop, no-new-privileges. Không có lý do gì để nới lỏng security vì GPU.
Model Cache — Named Volumes, Không Phải Bind Mounts
AI service cần cache model files — từ 4GB cho model 7B đến 40GB+ cho model 70B. Hai cách tiếp cận:
# Không tốt — bind mount, attacker có thể ghi file lên host
volumes:
- ./models:/root/.ollama
# Tốt — named volume, Docker quản lý, cô lập khỏi host filesystem
volumes:
- ollama-models:/root/.ollama
volumes:
ollama-models:
name: ollama-modelsNamed volumes kết hợp với read_only: true tạo ra pattern mạnh: filesystem container bất biến, nhưng model cache vẫn persistent qua restart. Attacker không ghi được file ở đâu ngoài volume mount point — và volume đó chỉ chứa model files, không chứa application code hay config.
Memory — Set Limit, Luôn Luôn
AI inference cần nhiều RAM — model 7B cần 4-8GB, model 13B cần 8-16GB, model 70B cần 40-80GB. Nhưng "cần nhiều RAM" không có nghĩa "không giới hạn RAM".
deploy:
resources:
limits:
memory: 16G # Giới hạn cứng
reservations:
memory: 8G # Đảm bảo tối thiểuReservation đảm bảo container luôn có đủ RAM cho inference bình thường. Limit ngăn memory leak hoặc attacker cố tình ăn hết RAM host. Khoảng cách giữa reservation và limit cho phép burst khi cần mà không gây rủi ro cho toàn bộ server.
Multi-Model Stack
Stack AI điển hình có nhiều service: LLM inference (Ollama), API proxy (LiteLLM), embedding service, vector database. Mỗi service là một attack surface. Nhưng pattern hardening không thay đổi:
services:
llm:
image: ollama/ollama:0.5.4
<<: *security
networks:
- app-core
# ... GPU config, model volume
proxy:
image: ghcr.io/berriai/litellm:main-v1.82.6
<<: *security
ports:
- "127.0.0.1:4000:4000"
networks:
- app-core
- app-data
embedding:
image: ghcr.io/huggingface/text-embeddings-inference:1.6
<<: *security
networks:
- app-core
vectordb:
image: qdrant/qdrant:v1.13.2
<<: *security
volumes:
- qdrant-data:/qdrant/storage
networks:
- app-data # internal: true → không ra internetMỗi service pin version, read_only, cap_drop: ALL, no-new-privileges. Database trên internal network. Proxy là service duy nhất expose port — và chỉ expose ra localhost. Stack có 4 services nhưng attacker chỉ thấy một cổng vào — và cổng đó đã được reverse proxy bảo vệ.
Hardening Là Quá Trình, Không Phải Sự Kiện
Không ai hardened hết mọi thứ trong một buổi chiều. Bắt đầu từ bước 1 — pin image tag. Tuần sau thêm read_only. Tuần sau nữa thêm cap_drop. Mỗi tuần container của bạn an toàn hơn một chút. Sau 12 bước, attacker vào được container nhưng thấy: filesystem không ghi được, quyền không nâng được, network không thoát được, resource bị giới hạn. Blast radius thu gọn từ "mất toàn bộ server" xuống "mất một container rỗng".
Security không bao giờ tuyệt đối. Nhưng khoảng cách giữa container chạy root không giới hạn và container hardened đúng cách — khoảng cách đó đủ lớn để attacker chọn mục tiêu khác.
Đây là bài 3/7 trong series Bảo Mật Trong Kỷ Nguyên AI Agent.
Bài tiếp trong series: Dependency Poisoning — Khi pip install Trở Thành Vũ Khí.