2026-03-2614 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(bài này)
- 3.Container Hardening 101 — Từ 'Chạy Được' Đến 'An Toàn'
- 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
Floating Tags & Auto-Updaters — Backdoor Thầm Lặng Trong Docker Stack
Năm 2013, Docker Hub ra mắt. Mọi image đều có tag :latest mặc định. Ý tưởng rất đơn giản: bạn chạy docker pull nginx mà không cần nhớ số phiên bản — Docker tự lấy bản mới nhất. Tiện lợi. Nhanh gọn. Ai cũng dùng.
Mười năm sau, :latest trở thành một trong những attack vector phổ biến nhất trong container ecosystem. Không phải vì Docker thiết kế sai. Mà vì cách chúng ta hiểu nó sai ngay từ đầu.
Rồi Watchtower ra đời — một container nhỏ gọn, mục tiêu duy nhất là giữ cho các container khác luôn "mới nhất". Poll registry mỗi vài giờ, thấy image mới thì pull về, restart. Vận hành tự động, không cần chạm tay. Hoàn hảo cho homelab, cho side project, cho AI server cá nhân.
Và cũng hoàn hảo cho attacker — vì nó biến mỗi floating tag thành một cánh cửa mở sẵn lúc 4 giờ sáng.
1. Vì Sao :latest Không Phải Là Một Phiên Bản
Hiểu lầm cốt lõi nằm ở đây: :latest trông giống một version, nhưng nó không phải. Nó là một mutable pointer — một con trỏ có thể thay đổi bất cứ lúc nào.
Khi bạn viết:
image: ghcr.io/berriai/litellm:main-latestBạn đang nói với Docker: "Lấy cho tôi bất cứ thứ gì đang được gắn tag main-latest tại thời điểm pull." Hôm nay nó trỏ tới bản v1.82.6. Ngày mai maintainer push bản mới — nó trỏ tới v1.83.0. Tuần sau ai đó compromise CI pipeline — nó trỏ tới bản chứa malware. Cùng một tag, nội dung hoàn toàn khác nhau.
Đây là điều quan trọng cần phân biệt:
- Tag (
:latest,:main,:stable,:main-latest): Mutable. Người có quyền push lên registry có thể thay đổi nó trỏ tới image nào, bất cứ lúc nào, không cần thông báo. - Digest (
sha256:a1b2c3d4...): Immutable. Là hash SHA256 của image manifest. Không thể thay đổi mà không thay đổi nội dung.
Khi bạn dùng tag, bạn đang implicit trust chuỗi sau: registry (GitHub Container Registry, Docker Hub) → maintainer account → CI/CD pipeline của maintainer → mọi dependency trong build process. Bất kỳ mắt xích nào bị compromise, image bạn pull về có thể chứa bất cứ thứ gì.
Và đây không chỉ là lý thuyết. Quay lại bài 1 trong series — vụ LiteLLM bị compromise bắt đầu từ Trivy (security scanner), lan qua CI credentials, và kết quả là image bị nhiễm được push lên registry. Nếu bạn dùng floating tag, bạn sẽ nhận image đó mà không biết.
Một số tag trông có vẻ "an toàn" nhưng vẫn mutable:
| Tag | Có vẻ ổn? | Thực tế |
|---|---|---|
:latest | Không | Mutable, thay đổi liên tục |
:main | Không | Mutable, cập nhật mỗi commit vào main |
:main-latest | Không | Mutable, tệ hơn vì tên dài gây cảm giác specific |
:stable | Không | Mutable, maintainer quyết định "stable" nghĩa là gì |
:v1.82.6 | Tốt hơn | Thường immutable theo convention, nhưng kỹ thuật vẫn có thể bị ghi đè |
@sha256:abc... | An toàn nhất | Immutable, nội dung cố định |
2. Mô Hình Tin Cậy Của Docker Image
Để hiểu tại sao floating tag nguy hiểm, cần biết Docker image hoạt động thế nào bên trong.
OCI Image Spec
Một Docker image theo chuẩn OCI (Open Container Initiative) gồm ba phần:
- Manifest: File JSON mô tả image — liệt kê các layers, config, platform target. Đây là thứ mà digest hash tham chiếu tới.
- Layers: Các tầng filesystem xếp chồng. Mỗi layer là một tarball chứa file changes. Layer cuối cùng thường chứa application code.
- Config: Metadata — ENV, CMD, ENTRYPOINT, labels.
Khi bạn docker pull image:tag, Docker gọi tới registry API, hỏi "tag này trỏ tới manifest nào?". Registry trả về manifest digest. Docker so sánh với local — nếu khác thì pull layers mới. Tag chỉ là lookup key, không phải nội dung.
Watchtower Hoạt Động Thế Nào
Watchtower chạy theo lịch (mặc định mỗi 5 phút, nhiều người cấu hình cron schedule). Quy trình:
- Duyệt qua tất cả container đang chạy trên host
- Với mỗi container, gọi registry API để lấy manifest digest hiện tại của tag
- So sánh digest từ registry với digest local
- Nếu khác: pull image mới → stop container cũ → start container mới với image mới
- Nếu
WATCHTOWER_CLEANUP=true: xóa image cũ (mất forensic evidence)
Bước 4 là nơi vấn đề xảy ra. Container mới khởi động với tất cả environment variables bạn đã cấu hình: API keys, database URLs, master secrets. Nếu image mới chứa malware, malware đó có quyền đọc hết.
Docker Socket — Chìa Khóa Vạn Năng
Để Watchtower làm được việc trên, nó cần mount Docker socket:
volumes:
- /var/run/docker.sock:/var/run/docker.sockDocker socket là root-equivalent access trên host. Bất kỳ container nào có Docker socket đều có thể:
- Tạo container mới với bất kỳ volume mount nào (kể cả
/của host) - Đọc environment variables của mọi container khác
- Stop/start/remove container
- Pull và chạy image tùy ý
Đây là Watchtower Paradox: nó cần Docker socket để hoạt động, nhưng chính quyền đó biến nó thành crown jewel target. Nếu Watchtower bị compromise (qua image bị nhiễm của chính nó, hoặc qua container khác cùng host), attacker có toàn quyền trên host.
Chuỗi Tin Cậy Ngầm
Khi bạn kết hợp floating tag + Watchtower + Docker socket, bạn đã xây dựng chuỗi sau mà không nhận ra:
Maintainer CI/CD → Registry → Watchtower poll → Auto-pull → Auto-restart → Full env access
Mỗi mắt xích là một điểm tin cậy. Bạn tin maintainer không bị hack. Bạn tin registry không bị tamper. Bạn tin CI pipeline không bị inject. Và bạn đã tự động hóa toàn bộ — không có bước nào yêu cầu bạn review trước khi deploy.
3. Cấu Hình: Trước và Sau
Hãy nhìn hai cấu hình cụ thể.
Trước — Nguy hiểm
services:
litellm:
image: ghcr.io/berriai/litellm:main-latest
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DATABASE_URL=${DATABASE_URL}
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *Vấn đề:
litellmdùng floating tag → nội dung thay đổi không kiểm soátwatchtowercũng dùng floating tag (implicit:latest)- Docker socket mount với full write access
WATCHTOWER_CLEANUP=truexóa image cũ → mất bằng chứng forensic- Chạy lúc 4 giờ sáng → không ai giám sát
Sau — Hardened
services:
litellm:
image: ghcr.io/berriai/litellm:main-v1.82.6
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DATABASE_URL=${DATABASE_URL}
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
watchtower:
image: containrrr/watchtower:1.7.1
environment:
- WATCHTOWER_MONITOR_ONLY=true
- WATCHTOWER_CLEANUP=false
- WATCHTOWER_NOTIFICATIONS=slack
- WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=${SLACK_WEBHOOK}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:roThay đổi quan trọng:
- Pin version tag:
main-v1.82.6thay vìmain-latest - Monitor-only mode: Watchtower chỉ kiểm tra và thông báo, không tự pull
- Cleanup off: Giữ lại image cũ để so sánh nếu cần điều tra
- Notification: Gửi alert khi phát hiện image mới
- Read-only socket:
:rogiảm attack surface (Watchtower monitor-only không cần write)
Mức Cao Nhất: Pin Bằng SHA256 Digest
Nếu bạn cần bảo mật tối đa:
services:
litellm:
image: ghcr.io/berriai/litellm@sha256:9f3e8a7b2c1d5e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f2a4b6c8d0e2fDigest pinning đảm bảo chính xác image nào sẽ chạy. Không ai có thể thay đổi nội dung mà không thay đổi hash. Trade-off là bạn phải cập nhật digest thủ công mỗi khi muốn upgrade.
Để lấy digest hiện tại:
# Từ container đang chạy
docker inspect --format='{{index .RepoDigests 0}}' litellm
# Từ registry (không cần pull)
docker manifest inspect ghcr.io/berriai/litellm:main-v1.82.6 \
| jq -r '.digest'
# Hoặc khi pull
docker pull ghcr.io/berriai/litellm:main-v1.82.6 2>&1 \
| grep "Digest:"4. Quy Trình: Audit và Migration
Nếu bạn đang chạy Docker stack với floating tags, đây là quy trình chuyển đổi từng bước.
Bước 1: Tìm tất cả floating tags
# Tìm trong docker-compose files
grep -rn -E ':(latest|main|master|stable|dev)(\s|$|")' docker-compose*.yml
# Tìm image không có tag (implicit :latest)
grep -rn 'image:' docker-compose*.yml | grep -v -E ':[a-z0-9]+-v?[0-9]|@sha256:'Bước 2: Ghi lại digest hiện tại (forensic baseline)
Trước khi thay đổi bất cứ thứ gì, ghi lại chính xác image nào đang chạy:
# Liệt kê tất cả container và digest hiện tại
docker ps --format '{{.Names}} {{.Image}}' | while read name image; do
digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$name" 2>/dev/null)
echo "$name | $image | $digest"
done > image-baseline-$(date +%Y%m%d).txtFile này là bằng chứng. Nếu sau này phát hiện bị compromise, bạn biết chính xác image nào đang chạy tại thời điểm audit.
Bước 3: Pin từng image
Với mỗi service trong docker-compose:
- Tìm phiên bản hiện tại: check release page / changelog của project
- Thay floating tag bằng version tag cụ thể
- (Optional) Thay bằng digest nếu cần security cao
Bước 4: Cấu hình lại Watchtower
watchtower:
image: containrrr/watchtower:1.7.1
environment:
- WATCHTOWER_MONITOR_ONLY=true
- WATCHTOWER_CLEANUP=false
- WATCHTOWER_NOTIFICATIONS=slack
- WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=${SLACK_WEBHOOK}
- WATCHTOWER_POLL_INTERVAL=86400 # 1 lần/ngày là đủ
volumes:
- /var/run/docker.sock:/var/run/docker.sock:roMonitor-only nghĩa là Watchtower vẫn kiểm tra registry, nhưng chỉ gửi notification khi có image mới — không tự pull hay restart.
Bước 5: Thiết lập quy trình cập nhật thủ công
Khi nhận notification có image mới:
- Đọc changelog: Xem có gì thay đổi, có breaking changes không
- Kiểm tra security advisories: Project có bị report CVE nào không
- Test trên môi trường staging (nếu có): Pull image mới, chạy smoke test
- Cập nhật docker-compose: Thay version tag cũ bằng mới
- Pull và restart:
docker compose pull && docker compose up -d - Verify: Kiểm tra health check, logs, behavior
Có chậm hơn auto-update không? Có. Nhưng bạn biết chính xác cái gì đang chạy trên server của mình.
Bước 6: Tự động hóa phần an toàn
Bạn vẫn có thể tự động hóa phần notification và kiểm tra. Chỉ phần deploy cuối cùng cần human approval:
#!/bin/bash
# update-check.sh — chạy hàng ngày qua cron
COMPOSE_FILE="/opt/docker/docker-compose.yml"
# Kiểm tra image mới
updates=$(docker compose -f "$COMPOSE_FILE" pull --dry-run 2>&1 | grep "Pulling")
if [ -n "$updates" ]; then
echo "Available updates:"
echo "$updates"
# Gửi notification — bạn quyết định có update không
fi5. Ứng Dụng Cho AI Infrastructure
Tất cả những gì đã thảo luận ở trên áp dụng cho mọi Docker stack. Nhưng AI infrastructure có đặc thù riêng khiến vấn đề nghiêm trọng hơn.
Tốc Độ Release Nhanh Bất Thường
Các công cụ AI đang trong giai đoạn phát triển cực nhanh. LiteLLM push bản mới gần như hàng ngày. Ollama release liên tục khi có model mới. Open WebUI cập nhật UI hàng tuần. Vaultwarden, SearXNG, các MCP server — tất cả đều chạy đua tính năng.
Nhịp release nhanh tạo áp lực tâm lý: "Nếu không dùng bản mới nhất, mình sẽ bỏ lỡ model mới, thiếu tính năng quan trọng." Áp lực đó dẫn đến :latest. Và :latest dẫn đến bạn chạy code chưa review trên production mỗi sáng.
Stack Nhiều Container
Một AI server điển hình không phải 1-2 container. Nó thường có 10-20 services:
- LLM Gateway (LiteLLM)
- Model Runner (Ollama)
- Web UI (Open WebUI, LibreChat)
- Vector Database (Qdrant, ChromaDB)
- Search (SearXNG)
- Monitoring (Prometheus, Grafana)
- Proxy (Traefik, Caddy)
- Auth (Authentik, Keycloak)
- Password Manager (Vaultwarden)
- MCP Servers
- Watchtower
- ...
Mỗi service là một entry point tiềm năng. Nếu 15 service dùng floating tag, bạn có 15 cánh cửa mở. Attacker chỉ cần compromise một maintainer account, một CI pipeline, push image bị nhiễm lên registry — và Watchtower sẽ tận tình kéo về cho bạn.
API Keys Là Mục Tiêu Cao Giá
Container AI không chứa data thông thường. Chúng chứa:
- API keys tới Claude, GPT-4, Gemini — mỗi key có thể tiêu hàng nghìn USD
- Database credentials với dữ liệu conversation history
- Master keys cho toàn bộ proxy layer
- SSH keys nếu bạn mount volume từ host
Một floating tag bị poison → Watchtower pull về → malware đọc ENV → exfiltrate API keys. Toàn bộ quá trình diễn ra trong vài giây, im lặng, lúc 4 giờ sáng.
Cách Tiếp Cận Đúng
Nguyên tắc không phức tạp:
- Pin version cho mọi image: Không ngoại lệ, kể cả Watchtower, kể cả Traefik
- Subscribe release channels: Watch GitHub repo, enable release notifications
- Update có chủ đích: Đọc changelog → test → deploy, không phải tự động hóa mù quáng
- Giảm thiểu secret exposure: Dùng Docker secrets thay vì environment variables khi có thể. Với Compose, dùng file secret thay vì inline value
- Audit định kỳ: Mỗi tuần chạy script kiểm tra floating tags, image không rõ nguồn gốc
Nếu bạn vận hành AI stack cho team hoặc production: cân nhắc private registry mirror. Pull image từ upstream về internal registry, scan, rồi mới cho phép deploy. Thêm một bước, nhưng kiểm soát hoàn toàn nội dung.
Khi Tiện Lợi Trở Thành Lỗ Hổng
:latest được thiết kế cho tiện lợi trong development. Watchtower được thiết kế để giảm operational burden. Cả hai đều không sai trong context ban đầu. Nhưng khi bạn đặt chúng cạnh nhau trên một server chứa API keys trị giá hàng nghìn USD, bạn đã tạo ra một automated deployment pipeline — mà bạn không kiểm soát đầu vào.
Bảo mật container không phải chỉ là scan CVE. Nó bắt đầu từ câu hỏi đơn giản: "Ai quyết định code nào chạy trên server của tôi? Tôi, hay một CI pipeline của người lạ?"
Pin version. Monitor thay vì auto-update. Review trước khi deploy. Ba hành động nhỏ, nhưng là sự khác biệt giữa kiểm soát và hy vọng.
Đây là bài 2/7 trong series Bảo Mật Trong Kỷ Nguyên AI Agent.
Bài tiếp trong series: Container Hardening 101 — từ "chạy được" đến "an toàn".