โหมดมืด
บทที่ 05 — Security + Optimization
🟡 หมายเหตุระดับ: บทนี้กว้างมาก — หลายหัวข้อ (Cosign, SBOM, rootless, capabilities, seccomp) เป็น survey ระดับ "รู้จัก + ลองใช้" ไม่ใช่ deep dive. ก่อนใช้จริงใน production ให้กลับไปอ่าน official docs ของแต่ละเครื่องมือ และทดสอบบน staging ก่อนเสมอ — บทนี้คือ checklist ตั้งต้น ไม่ใช่ security audit guide
บทสุดท้ายของหมวด Docker — ทำให้ container ปลอดภัย และ เล็ก/เร็ว พร้อมขึ้น production จริง
อ่านจบจะ:
- เข้าใจว่า container ปลอดภัยไม่เท่าที่คิด — และจะปิดช่องโหว่ยังไง
- รัน container แบบ non-root, read-only, จำกัดสิทธิ์
- scan หาช่องโหว่ + เซ็น image
- ทำ image ให้เล็กที่สุด + build เร็วที่สุด
ใช้เวลา 3 ชั่วโมง
📚 ไม่ต้องอ่านรวดเดียวจบ — บทนี้มีหลายหัวข้อย่อย แบ่งอ่านทีละส่วนได้ ไม่ต้องรีบ
Part 1: ทำไม Container Security สำคัญ
จากบท 00 — container แยกตัวกันด้วย namespace แต่ ใช้ kernel ร่วมกัน
นี่คือจุดอ่อน: ถ้า attacker เจาะ container ได้ + เจอช่องโหว่ของ kernel → อาจ "หนีออกจาก container" (container escape — เอส-เคป, หลุดออกจาก container) ไปควบคุมเครื่อง host ทั้งเครื่อง → กระทบ container อื่นทุกตัว
📌 ไม่ใช่ทฤษฎี — เคยมี CVE ระดับโลกอย่าง runc "Leaky Vessels" (CVE-2024-21626)[^leaky] ในปี 2024 ที่เปิดทาง container escape — เป็นเหตุผลว่าทำไมต้อง patch kernel + runtime สม่ำเสมอ
mini glossary สำหรับย่อหน้านี้:
- CVE (Common Vulnerabilities and Exposures) = รหัสเลขช่องโหว่ความปลอดภัยที่ประกาศต่อสาธารณะ ใช้อ้างอิงระหว่างทีม security ทั่วโลก
- kernel (เคอร์-เนิล) = แกนกลางของ OS ที่คุยกับ hardware จริง ๆ และจัดการ process ทั้งหมด — ดูรายละเอียดในบท 00
- runc = "เครื่องยนต์" ระดับล่างที่ Docker/containerd เรียกใช้สร้าง container จริง ๆ
- file descriptor = ตัวเลขที่ kernel ใช้อ้างถึงไฟล์ที่โปรแกรมเปิดอยู่ (ถ้ารั่วก็เข้าถึงไฟล์นั้นได้ทาง process อื่น)
- "Leaky Vessels" = ชื่อเล่นที่นักวิจัยตั้งให้ช่องโหว่ชุดนี้ (แปลตรงตัว "ภาชนะที่รั่ว")
ดังนั้นหลักการคือ: ทำให้ container "ทำอะไรได้น้อยที่สุดเท่าที่จำเป็น" — ถ้าโดนเจาะ ความเสียหายจะจำกัด
นี่คือหลัก "least privilege" (ลีสท์-พริฟ-วิ-เลจ — สิทธิ์น้อยที่สุด) — หัวใจของ security ทุกเรื่อง ตัวอย่าง: แอปที่ทำแค่อ่าน/เขียน database ไม่ควรมีสิทธิ์สร้าง user ใหม่บน host หรือลงโปรแกรมได้
💡 เสริม (K8s 1.22+ GA): ใน Kubernetes มี
securityContext.seccompProfile.type: RuntimeDefaultที่บังคับใช้ seccomp profile default ของ container runtime — จำกัดว่า container เรียก syscall อะไรได้บ้าง ลด attack surface ระดับ kernel ลงอีกชั้น (จะเจอใน K8s บท Security). หมายเหตุ:seccompProfileเป็น stable field ตั้งแต่ K8s 1.22 (GA) — ใช้ได้กับ cluster รุ่น 2025–2026 ทุกตัว
Part 2: อย่ารัน Container เป็น root
2.1 ปัญหา
โดย default — process ใน container รันเป็น root (ผู้ใช้สูงสุด)
ถ้า attacker เจาะแอปได้ → เขาเป็น root ใน container → ถ้าหนีออกไป host ได้ → เป็น root บน host → จบเกม
2.2 พิสูจน์
ลองพิสูจน์ปัญหา root ด้วยตาตัวเอง — สั่ง whoami ใน container เปล่า ๆ จะเห็นว่ามันตอบ root ทันที ตอกย้ำว่าทุก container ที่ไม่ตั้งค่าอะไรเลยรันด้วยสิทธิ์สูงสุดโดย default:
bash
docker run --rm alpine whoami
# root ← เห็นไหม รันเป็น root!2.3 ทางแก้: สร้าง user แล้วใช้ USER
❌ Dockerfile ที่ไม่ปลอดภัย:
dockerfile
FROM node:22-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# ← ไม่มี USER → รันเป็น root✅ Dockerfile ที่ปลอดภัย (Alpine base):
dockerfile
FROM node:22-alpine
# สร้าง group + user ใหม่ (Alpine/BusyBox syntax)
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser # ← เปลี่ยนเป็น non-root
CMD ["node", "server.js"]⚠️ syntax สร้าง user ต่างตาม base image —
addgroup/adduser(พร้อม-D) เป็น Alpine/BusyBox. ถ้า base เป็น Debian/Ubuntu (เช่นnode:22,python:3.12) ต้องใช้groupadd/useraddแทน — ตัวอย่าง:dockerfile# Debian/Ubuntu base RUN groupadd -g 1000 appgroup && \ useradd -u 1000 -g appgroup -m appuserตรวจ
FROMของคุณก่อนเสมอ ทั้งสองตัวอย่างข้างบนใช้ Alpine จึงจับคู่ syntax ให้ถูกแล้ว
หรือใช้ user ที่ image เตรียมมาให้ — image ทางการหลายตัวมี non-root user อยู่แล้ว:
dockerfile
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node # ← image node มี user "node" อยู่แล้ว
CMD ["node", "server.js"]ตรวจสอบ:
bash
docker run --rm myapp whoami
# appuser ← ✅ ไม่ใช่ root แล้ว💡 ถ้า container ต้อง bind port ต่ำกว่า 1024 (เช่น port 80) — ปกติต้องเป็น root. แก้ด้วยการให้แอปฟัง port สูง (เช่น 8080) แล้วใช้
-p 80:8080map แทน
Part 3: จำกัดสิทธิ์ (Capabilities)
3.1 Capabilities คืออะไร
Linux แบ่งอำนาจของ "root" เป็นชิ้นเล็ก ๆ เรียกว่า capabilities — เช่น:
NET_BIND_SERVICE— bind port ต่ำกว่า 1024NET_ADMIN— จัดการ networkSYS_ADMIN— งานระดับ admin (อันตรายมาก)CHOWN— เปลี่ยนเจ้าของไฟล์
โดย default Docker ให้ container มี capability ชุดหนึ่ง — ซึ่งมากเกินจำเป็น สำหรับแอปทั่วไป
3.2 แนวทาง: ตัดทิ้งแล้วเพิ่มเฉพาะที่จำเป็น (ต้องทดสอบก่อนใช้จริง)
bash
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp⚠️ อย่า copy คำสั่งนี้ไปใช้ทุกแอป! image อย่าง
nginx/nodeมัก fail ตอน startup เพราะ entrypoint script ต้องการCHOWN/SETUID/SETGID. ดู note ข้างล่างก่อนใช้
--cap-drop=ALL— ตัด capability ทั้งหมด--cap-add=NET_BIND_SERVICE— เพิ่มกลับเฉพาะอันที่จำเป็น (ในที่นี้คือ bind port < 1024)
ใน Compose:
yaml
services:
api:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE→ ถ้า attacker เจาะได้ ก็ทำได้แค่ bind port — ทำงานระดับ admin ไม่ได้
⚠️ ระวัง — drop ALL แล้วเหลือ cap เดียวจะรันไม่ขึ้นกับ image หลายตัว
Docker default capability set มี capability ใช้บ่อย ที่ image ทางการมักต้องการตอน startup — สรุปเป็นตาราง:
Capability (คำอ่าน) หน้าที่ (ภาษาคน) ตัวอย่าง image ที่ต้องการ CHOWN(ซี-โอว-เอ็น)เปลี่ยน "เจ้าของไฟล์" nginx, postgres (เซ็ตเจ้าของ log/data) DAC_OVERRIDE(แด็ค-โอ-เวอร์-ไรด์)ข้ามการเช็คสิทธิ์ read/write ปกติ image ที่ entrypoint ต้องแตะไฟล์ของ user อื่น SETGID(เซ็ต-จีไอดี)เปลี่ยน group ของ process image ที่ใช้ gosu/su-execลด privilege ตอน bootSETUID(เซ็ต-ยูไอดี)เปลี่ยน user ของ process เหมือนด้านบน — เปลี่ยนจาก root → app user NET_BIND_SERVICE(เน็ต-ไบนด์-เซอร์-วิส)bind port ต่ำกว่า 1024 web server ที่ฟัง 80/443 ตรง ๆ NET_RAW(เน็ต-รอว์)สร้าง raw network packet ping (ICMP) — ปกติไม่ต้องใช้ในแอป production FOWNER(เอฟ-โอว-เนอร์)ข้ามเช็คเจ้าของไฟล์ตอน chmod บาง image ใช้ตอน setup permission image ทางการอย่าง nginx, node, postgres เปลี่ยน user ตอน boot → ต้องการ
SETGID/SETUID/CHOWNขั้นต่ำ.ถ้า drop ALL แล้วเหลือแค่
NET_BIND_SERVICE→ container อาจ exit เงียบ ๆ หรือ error แปลก ๆ.วิธีหา cap ที่ต้องใช้จริง:
- ลองรันแบบ drop ALL ก่อน → ดู log
- ค่อย ๆ
--cap-addเพิ่มทีละตัวจนรันได้ — หรือดูใน Dockerfile/entrypoint script ของ image ว่ามีคำสั่งอะไร (chown,su-exec,gosu→ ต้องการCHOWN/SETUID/SETGID)- ตัวอย่างจริง — nginx official มักต้อง
CHOWN,SETGID,SETUID,NET_BIND_SERVICE,DAC_OVERRIDE(ตรวจกับ image version ที่ใช้จริงก่อน — flagged 2026-06)หลักการคือ "เริ่มจากแน่น แล้วค่อยเปิดเท่าที่จำเป็น" ไม่ใช่ copy-paste คำสั่งข้างบนแล้วใช้ทุกแอป
Part 4: Read-only Filesystem
4.1 ไอเดีย
ถ้าแอปของเราไม่ต้องเขียนไฟล์ลง filesystem ของตัวเอง — ทำไมไม่ปิดการเขียนซะเลย?
ถ้า attacker เจาะได้แต่เขียนไฟล์ไม่ได้ → เขาลง malware ไม่ได้ ทำอันตรายได้น้อยลงมาก
bash
docker run --read-only --tmpfs /tmp myapp--read-only— filesystem ทั้งหมดเป็น read-only--tmpfs /tmp— ยกเว้น/tmp(เก็บใน RAM) เพราะหลายแอปต้องการเขียนไฟล์ชั่วคราว
ใน Compose:
yaml
services:
api:
read_only: true
tmpfs:
- /tmp
- /run→ ถ้าแอปต้องเขียนข้อมูลถาวรจริง ๆ ก็ mount volume เฉพาะโฟลเดอร์นั้น
Part 5: ปิดการยกระดับสิทธิ์
bash
docker run --security-opt no-new-privileges myappno-new-privileges ทำอะไร — แยกเป็น 3 ประเด็นสั้น ๆ:
- flag นี้บอก kernel ว่า: "process ใน container ห้ามได้สิทธิ์เพิ่มขึ้นจากที่มีตอนเริ่มต้น" — ปิดทาง privilege escalation (เอส-คา-เล-ชัน = การยกระดับสิทธิ์ เช่น เริ่มเป็น user ธรรมดาแล้วพยายามกลายเป็น root) ทั้งหมดในระดับ kernel
- setuid bit คืออะไร — เป็น "flag พิเศษ" บนไฟล์โปรแกรมใน Linux ที่บอกว่า "ตอนรันโปรแกรมนี้ ใช้สิทธิ์ของ เจ้าของไฟล์ ไม่ใช่สิทธิ์ของคนที่สั่งรัน" — ตัวอย่างเช่น
sudoใช้ setuid เพื่อเป็น root ชั่วคราว - ทำไม attacker ชอบใช้ — ถ้าเจาะ container เข้ามาแล้วเจอไฟล์ที่มี setuid + เจ้าของคือ root → รันไฟล์นั้น = ได้สิทธิ์ root ใน container ทันที.
no-new-privilegesบล็อก trick นี้
ใน Compose:
yaml
services:
api:
security_opt:
- no-new-privileges:truePart 6: จำกัดทรัพยากร (กัน DoS)
ถ้า container ตัวหนึ่งกิน RAM/CPU จนหมด → ทำให้ container อื่น (และเครื่อง host) ล่มไปด้วย — นี่คือการโจมตีแบบ DoS (Denial of Service) — หรือแค่บั๊กก็เกิดได้
จำกัดด้วย (ใช้ cgroups ที่เรียนบท 00):
bash
docker run --memory=512m --cpus=1 myappใน Compose:
yaml
services:
api:
deploy:
resources:
limits:
memory: 512M
cpus: "1"ถ้า container ใช้ RAM เกิน limit → Linux จะ kill มัน (เรียก OOMKill — Out Of Memory Kill) — เช็คได้:
bash
docker inspect myapp --format '{{.State.OOMKilled}}'
# true = โดน kill เพราะ memory เกิน→ ตั้ง resource limit เสมอใน production
Part 7: Rootless Docker
ที่ผ่านมาเราพูดถึง "อย่ารัน container เป็น root" — แต่ยังมีอีกชั้น: ตัว Docker daemon เองก็รันเป็น root!
ถ้า attacker เจาะ Docker daemon ได้ = ได้ root บน host
Rootless Docker = รัน Docker daemon ในนาม user ธรรมดา (ไม่ใช่ root)
⚠️ อ่านก่อนรันคำสั่งข้างล่าง — ต้องเตรียม prerequisite + ใช้ได้กับ Docker Engine บน Linux เท่านั้น (Docker Desktop ไม่ใช้สคริปต์นี้)
bash
# ติดตั้ง rootless mode (สำหรับ user ปัจจุบัน — ต้องติดตั้ง Docker Engine + เตรียม subuid/subgid แล้ว)
dockerd-rootless-setuptool.sh install⚠️ สคริปต์นี้มากับ Docker Engine บน Linux เท่านั้น
Rootless Docker ต้องการ:
- kernel ใหม่พอ (Linux kernel ที่รองรับ user namespace + cgroup v2 — ตรวจ official docs ของเวอร์ชัน Docker ที่ใช้)
- subuid / subgid mapping ใน
/etc/subuidและ/etc/subgidสำหรับ user ที่จะรัน (ส่วนใหญ่ distro ใหม่ ๆ set ให้แล้ว แต่ควรตรวจ)- fuse-overlayfs หรือ overlayfs ที่ kernel รองรับสำหรับ rootless
- slirp4netns หรือ pasta (default network — ขึ้นกับเวอร์ชัน Docker Engine) หรือ rootlesskit port driver
อ่าน official guide ก่อนติดตั้ง: https://docs.docker.com/engine/security/rootless/ (last reviewed: 2026-06 — ตรวจ release notes ทางการก่อนใช้)
ข้อดี: ถ้าเจาะ daemon ได้ ก็ยังไม่ใช่ root บน host — ความเสียหายจำกัด
ข้อเสีย:
- bind port ต่ำกว่า 1024 ทำไม่ได้ตรง ๆ (workaround: ใช้ rootlesskit port driver หรือ iptables redirect)
- Network performance ลดลงชัดเจน — แยกย่อยเป็นข้อ ๆ:
- ทำไมช้า — Rootless ใช้ network driver ใน user-space แทน Linux
bridgeปกติที่ทำงานในระดับ kernel → ทุก packet ผ่าน user-space เพิ่ม overhead. Docker Engine รุ่นเก่าใช้slirp4netns; ตั้งแต่ Docker Engine 24+ บน Linux ที่รองรับ จะเปลี่ยนเป็นpastanetwork driver ซึ่งเร็วกว่าslirp4netnsมาก แต่ยังช้ากว่า kernel-space bridge ปกติอยู่ - กระทบ workload แบบไหน — network-heavy เช่น throughput TCP (ปริมาณข้อมูลที่ส่งได้ต่อวินาที) สูง ๆ หรือ packet เล็กจำนวนมาก (เช่น RTP/VoIP, gaming)
- workaround 1: ใช้
--network=host— share network namespace กับ host (เร็วเท่าเดิม แต่เสีย network isolation) - workaround 2: เปลี่ยน rootlesskit port driver (กลไกเปิด port ของ rootless Docker) จาก
builtinเป็นslirp4netnsหรือbypass4netnsแล้วแต่ kernel — ดู docs ทางการ - (ตัวเลข exact ต้องการ source — flagged 2026-06)
- ทำไมช้า — Rootless ใช้ network driver ใน user-space แทน Linux
- storage driver / volume บางอย่างใช้ไม่ได้
💡 Podman (ทางเลือกของ Docker ที่พูดถึงในบท 00) — เป็น rootless ตั้งแต่ออกแบบ และไม่มี long-running daemon (สถาปัตยกรรม fork-exec ต่อ container). เทียบกับ rootless Docker ที่เป็น "patch on top of daemon" — ถ้าคุณต้องการ rootless แบบจริงจังเพื่อ production พิจารณา Podman เป็นทางเลือกอันดับต้น ๆ
Part 8: จัดการความลับ (Secrets) ให้ถูก
8.1 ❌ วิธีที่ผิด
dockerfile
ENV DB_PASSWORD=secret123→ password ติดอยู่ใน image ตลอดไป — ใครมี image ก็เห็น (docker history, docker inspect)
bash
docker build --build-arg PASSWORD=secret .→ ARG ก็โผล่ใน docker history — leak เหมือนกัน
dockerfile
COPY .env /app/.env→ ไฟล์ความลับติดอยู่ใน image layer
8.2 ✅ วิธีที่ถูก
1. ส่งผ่าน environment ตอนรัน (ไม่ใช่ตอน build)
bash
docker run -e DB_PASSWORD="$(cat /secure/db.txt)" myapp2. ใช้ BuildKit secret mount (ตอน build) — จากบท 01
dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm installbash
docker build --secret id=npm_token,src=./token.txt .→ token ใช้ตอน build แต่ไม่ติดใน image
⚠️ ต้องใส่
# syntax=docker/dockerfile:1บรรทัดแรกของ Dockerfile — ไม่งั้น--mount=type=secretparse ไม่ผ่าน (error แปลก ๆ). ดูบท Docker 01 §10 — BuildKit / Dockerfile frontend
3. ใช้ Docker Compose secrets
yaml
services:
db:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt→ secret ถูก mount ที่ /run/secrets/db_password ใน container
4. Production จริง — ใช้ external secret manager
- HashiCorp Vault
- AWS Secrets Manager / GCP Secret Manager
- Kubernetes Secret (ในหมวด K8s)
Part 9: Scan หาช่องโหว่
image ที่เราใช้ (โดยเฉพาะ base image) อาจมี CVE (ช่องโหว่ความปลอดภัยที่ถูกค้นพบและประกาศ) — เราต้อง scan หา
9.1 Trivy — เครื่องมือ scan ยอดนิยม
📦 ติดตั้ง Trivy:
- Mac:
brew install trivy- Linux: ดาวน์โหลด binary จาก https://github.com/aquasecurity/trivy/releases หรือใช้ package manager (apt/dnf — ดูคู่มือทางการ)
- Windows:
scoop install trivyหรือดาวน์โหลด binary- Docker (ลองครั้งเดียวไม่ต้องติดตั้ง):
docker run --rm aquasec/trivy:latest image <image-name>
bash
# scan image
trivy image myapp:v1
# scan เฉพาะระดับร้ายแรง
trivy image --severity HIGH,CRITICAL myapp:v1
# CI mode — ข้าม CVE ที่ยังไม่มี fix (ลด noise)
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:v1
# scan source code (หา dependency ที่มีช่องโหว่)
trivy fs .
# scan Dockerfile (หา config ที่ไม่ปลอดภัย)
trivy config DockerfileTrivy บอกว่า image มีช่องโหว่อะไร อยู่ใน package ไหน ต้อง update เป็นเวอร์ชันอะไร
💡
--ignore-unfixedเหมาะกับ CI — กรอง CVE ที่ upstream ยังไม่ปล่อย patch ออก (เรา fix ไม่ได้อยู่ดี). ตอน audit ทั่ว ๆ ไปอย่ากรองออก เพื่อให้เห็นภาพรวม
9.2 Docker Scout (มากับ Docker)
นอกจาก Trivy แล้ว Docker มีเครื่องมือสแกนช่องโหว่ในตัวชื่อ Docker Scout — ใช้ docker scout cves ดูช่องโหว่ใน image และ recommendations ดูคำแนะนำว่าควรอัปเดตอะไร โดยไม่ต้องติดตั้งเครื่องมือเพิ่ม:
bash
docker scout cves myapp:v1
docker scout recommendations myapp:v19.3 ใส่ใน CI/CD
ทำให้ทุก PR ถูก scan อัตโนมัติ — ถ้าเจอช่องโหว่ร้ายแรง ให้ build fail:
yaml
# GitHub Actions
- name: Scan image
uses: aquasecurity/trivy-action@0.28.0 # ตรวจเวอร์ชันล่าสุดที่ https://github.com/aquasecurity/trivy-action/releases แล้วอัปเดต — อย่าใช้ @master / @main
with:
image-ref: myapp:${{ github.sha }}
severity: 'CRITICAL,HIGH'
ignore-unfixed: true # ตัด CVE ที่ยังไม่มี fix
exit-code: 1 # ถ้าเจอ → fail⚠️ อย่าใช้
@masterหรือ@mainในuses:— เป็น supply-chain anti-pattern: ถ้า maintainer ถูก compromise หรือเปลี่ยนพฤติกรรม action บนสาขา default ทุก build ของคุณจะรันโค้ดใหม่โดยไม่รู้ตัว. pin เป็น version tag (เช่น@0.28.0) หรือ commit SHA เต็ม (เช่น@abc123...) เสมอ — commit SHA ปลอดภัยกว่า version tag เพราะ tag สามารถถูกย้ายได้ (tag ไม่ immutable) ส่วน commit SHA ไม่เปลี่ยนแน่นอน. ใช้ Dependabot/Renovate ช่วยอัปเดตให้ปลอดภัย
Part 10: เซ็น Image + SBOM
🚀 โซนขั้นสูง — ข้ามได้ Part 10 (เซ็น image + SBOM) เป็นเรื่อง "supply chain security" ที่ทีมใหญ่/องค์กรใช้ — มือใหม่ที่เพิ่งหัด อ่านพอรู้จักแล้วข้ามได้ ค่อยกลับมาตอนต้องวางระบบ deploy จริงจัง
10.1 ทำไมต้องเซ็น
ถ้า attacker แอบสลับ image ใน registry เป็นตัวที่มี malware — เราจะรู้ได้ยังไงว่า image ที่ดึงมา "ของแท้"?
→ เซ็น image ด้วยลายเซ็นดิจิทัล — แล้วตอนใช้ ตรวจลายเซ็น
10.2 Cosign
📦 ติดตั้ง Cosign:
- Mac:
brew install cosign- Linux: ดาวน์โหลด binary จาก https://github.com/sigstore/cosign/releases
- Windows:
scoop install cosign
bash
# สร้างคู่กุญแจ (จะถามตั้ง password ให้ private key)
cosign generate-key-pair
# เซ็น image — บน CI ตั้ง env COSIGN_PASSWORD ให้ตรงกับตอน generate
COSIGN_PASSWORD="$COSIGN_PASSWORD" \
cosign sign --key cosign.key ghcr.io/acme/myapp:v1
# หรือใช้ key จาก env (ไม่ต้องวางไฟล์)
cosign sign --key env://COSIGN_PRIVATE_KEY ghcr.io/acme/myapp:v1
# ตรวจลายเซ็น
cosign verify --key cosign.pub ghcr.io/acme/myapp:v1⚠️
cosign signจะถาม password ของ private key — ใน interactive shell มันเปิด prompt ให้พิมพ์ ถ้ารันใน CI ต้องตั้งCOSIGN_PASSWORDenv var ก่อน ไม่งั้น job ค้าง
→ ใน production ตั้งกฎว่า "รันได้เฉพาะ image ที่มีลายเซ็นถูกต้อง"
💡 Keyless signing (แนะนำสำหรับ supply chain สมัยใหม่)
Cosign 2.x รองรับ keyless signing — ไม่ต้องเก็บ private key เอง ใช้ identity ของ workload (เช่น GitHub Actions, Google Workload Identity) ในการเซ็น
mini glossary (ไม่ต้องเข้าใจรายละเอียด แค่รู้ว่าแต่ละชิ้นทำอะไร):
- Sigstore = "มาตรฐาน" รวมเครื่องมือชุดที่ทำให้เซ็น/ตรวจ artifact ได้ฟรีโดยไม่ต้องจัดการ key เอง
- OIDC (OpenID Connect) = protocol ยืนยันตัวตน — ระบุว่า "งานนี้รันโดย workflow ไหน" (เช่น GitHub Actions ของ repo X)
- Fulcio CA = "Certificate Authority (ออร์-กา-นิ-เซ-ชัน ที่ออก ใบรับรองดิจิทัล — เหมือนหน่วยงานรัฐที่ออกบัตรประชาชน)" ของ Sigstore — ออก cert ชั่วคราวให้ workload ใช้เซ็น
- Rekor transparency log = log สาธารณะที่บันทึกทุกลายเซ็น — ตรวจย้อนได้ว่า image นี้ถูกเซ็นเมื่อไร โดยใคร
รวม ๆ คือ "ระบบเซ็นแบบไม่ต้องมี key" — ใช้ identity ของ workflow แทน:
bash# เซ็นโดยไม่มี key (จะเปิด browser ให้ login OIDC) cosign sign ghcr.io/acme/myapp:v1 # ตรวจโดยระบุ identity ที่ยอมรับ cosign verify ghcr.io/acme/myapp:v1 \ --certificate-identity=https://github.com/acme/myapp/.github/workflows/release.yml@refs/heads/main \ --certificate-oidc-issuer=https://token.actions.githubusercontent.comใน CI/CD pipeline ปี 2026 keyless เป็นแนวที่กำลังได้รับความนิยม (ลดภาระ key management + เชื่อมกับ SLSA framework — มาตรฐาน supply chain ของ OpenSSF) — แต่ key-based ยังใช้ได้สำหรับ air-gapped (เครือข่ายปิด ไม่เชื่อมอินเทอร์เน็ต) หรือ environment ที่ไม่มี OIDC. (ตรวจ Sigstore / Cosign docs ทางการก่อนใช้ — last reviewed: 2026-06)
ใน Kubernetes ปี 2026 — บังคับ verify ที่ admission: ใช้ Sigstore Policy Controller, Connaisseur, หรือ Kyverno policy เพื่อปฏิเสธ pod ที่ใช้ image ที่ไม่มีลายเซ็นที่ถูกต้อง — ทำให้ "image ที่ไม่ผ่านการเซ็น run ไม่ได้" ใน cluster (จะเจอตัวอย่างใน K8s บท Security)
10.3 SBOM (Software Bill of Materials)
SBOM = "รายการส่วนผสม" ของ image — list ทุก library + เวอร์ชันที่อยู่ข้างใน
bash
# สร้าง SBOM ด้วย syft
syft myapp:v1 -o spdx-json > sbom.jsonประโยชน์: เมื่อมีข่าวช่องโหว่ใหม่ (เช่น "library X เวอร์ชัน Y มีช่องโหว่") — เราเช็ค SBOM ได้ทันทีว่า image ไหนของเราใช้ library นั้น
10.4 SBOM + Provenance ผ่าน BuildKit
ตั้งแต่ Docker Buildx ใหม่ ๆ คุณสร้าง SBOM และ provenance attestation ติดมากับ image ได้ในคำสั่ง build เลย — เก็บไว้ใน registry ข้าง ๆ image:
bash
docker buildx build \
--sbom=true \
--provenance=mode=max \
--tag ghcr.io/acme/myapp:v1 \
--push .--sbom=true= สร้าง SBOM (รายการ package ใน image) แนบ--provenance=mode=max= บันทึก build provenance (รันที่ไหน, commit อะไร, builder อะไร) — ส่วนหนึ่งของ SLSA Level 3- ทั้งสองอยู่ใน OCI image manifest — pull ลงมาตรวจได้ด้วย
cosign download sbom <image>หรือdocker buildx imagetools inspect
Part 11: ทำ Image ให้เล็ก
ทบทวน + เสริมจากบท 01:
11.1 Multi-stage build
(เรียนแล้วบท 01) — แยก stage build กับ stage runtime → image สุดท้ายมีแค่ผลลัพธ์
11.2 ใช้ base image เล็ก
การเลือก base image ที่เล็กตั้งแต่ต้นช่วยลดขนาดได้มากที่สุด — แต่ละภาษามี image รุ่นผอม (slim/alpine/distroless) หรือเริ่มจากศูนย์ (scratch) ให้เลือกตามความเหมาะสม:
- Go/Rust →
scratch - Java →
*-jre-alpineหรือ distroless - Node/Python → distroless หรือ
*-slim/*-alpine
💡 2026 — ทางเลือก distroless ที่กำลังได้รับความนิยม: Chainguard Images (สร้างจาก Wolfi — undistro Linux ที่ออกแบบมาเพื่อ container โดยเฉพาะ) มี base image ขนาดเล็ก, มี SBOM ติดมาให้, และมักมี CVE น้อยกว่า distroless ของ Google เพราะ refresh ถี่กว่า. ตัวอย่าง:
cgr.dev/chainguard/node,cgr.dev/chainguard/python. ลองเทียบดูในโปรเจกต์ใหม่
11.3 ลบ cache ในบรรทัด RUN เดียวกัน
เคล็ดลับสำคัญคือ "ติดตั้งแล้วลบ cache ใน RUN บรรทัดเดียวกัน" — เพราะแต่ละ RUN สร้าง layer แยก ถ้าลบ cache คนละบรรทัด ของเก่ายังค้างอยู่ใน layer ก่อนหน้า ทำให้ image ไม่เล็กลงจริง:
(เรียนแล้วบท 01)
dockerfile
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*11.4 ติดตั้งเฉพาะ production dependency
ตอน build เพื่อ deploy จริง เราไม่ต้องการ devDependencies (เครื่องมือ test/build) ติดไปด้วย — สั่งให้ติดตั้งเฉพาะ dependency ที่ production ใช้จริง (เช่น npm ci --omit=dev) จะตัดของไม่จำเป็นออกได้เยอะ:
dockerfile
RUN npm ci --omit=dev # ไม่เอา devDependencies11.5 ใช้ .dockerignore
(เรียนแล้วบท 01) — ไม่ส่งไฟล์ขยะเข้า build
11.6 ทำไม image เล็กถึงดี
- ดาวน์โหลดเร็ว — deploy/scale เร็วขึ้น
- attack surface น้อย — มีของน้อย = attacker ใช้ของได้น้อย
- ประหยัด storage ใน registry
Part 12: Build ให้เร็ว
12.1 จัดลำดับ layer ให้ดี
(เรียนแล้วบท 01) — "เปลี่ยนน้อย → เปลี่ยนบ่อย"
12.2 ใช้ BuildKit cache mount
BuildKit ให้เรา "เก็บ cache ของ package manager ไว้นอก layer" ผ่าน cache mount — รอบ build ถัดไปดึง cache เดิมกลับมาใช้ได้ ทำให้ npm ci/pip install ไม่ต้องโหลดใหม่ทั้งหมด build เร็วขึ้นมาก:
(เรียนแล้วบท 01)
dockerfile
# ถ้ารันเป็น root ระหว่าง build step:
RUN --mount=type=cache,target=/root/.npm npm ci
# ถ้า Dockerfile มี USER node (non-root) ก่อนบรรทัดนี้ — ต้องระบุ uid/gid และเปลี่ยน path:
# RUN --mount=type=cache,target=/home/node/.npm,uid=1000,gid=1000 npm ci --cache /home/node/.npm
# (ไม่งั้น cache ที่ /root/.npm จะ permission denied → npm โหลดใหม่ทุกครั้ง ทำให้ cache mount ไม่มีประโยชน์)12.3 Cache ผ่าน registry (สำหรับ CI)
ปัญหา: CI รันบนเครื่องใหม่ทุกครั้ง — ไม่มี cache เดิม
แก้: เก็บ cache layer ไว้ใน registry:
bash
docker buildx build \
--cache-from type=registry,ref=myapp:buildcache \
--cache-to type=registry,ref=myapp:buildcache,mode=max \
-t myapp:v1 .12.4 GitHub Actions cache
ถ้าใช้ GitHub Actions มีทางลัดยิ่งกว่า registry cache — ใช้ cache backend แบบ gha ที่ build-push-action จัดการให้อัตโนมัติ ไม่ต้องตั้ง registry เอง เหมาะกับ CI บน GitHub โดยตรง:
yaml
- uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=maxPart 13: Healthcheck + Logging (ทบทวน)
13.1 Healthcheck
HEALTHCHECK บอก Docker ว่าจะ "เช็คสุขภาพ" container ยังไง — รันคำสั่ง (เช่น curl ไปที่ /health) เป็นระยะ ถ้าล้มเหลวติดกันหลายครั้ง container จะถูกมาร์คว่า unhealthy แล้ว Docker/K8s เอาไปตัดสินใจ restart:
dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
# ใช้ wget แทน curl — node:22-alpine (และ alpine-based image ทั่วไป) มี wget แต่ไม่มี curl
# ถ้า image มี curl ให้ใช้: CMD curl -f http://localhost:3000/health || exit 1→ Docker (และ K8s) เอาไปตัดสินใจว่าจะ restart container ไหม
13.2 จำกัดขนาด log
yaml
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"→ ป้องกัน log โตจนเต็ม disk
Part 14: ภัยที่ต้องรู้ — Container Escape Surface
สิ่งที่ทำให้ container "หนีออกไป host" ได้ง่ายขึ้น — อย่าทำ เว้นแต่จำเป็นจริง ๆ:
1. --privileged — ให้สิทธิ์เต็มทุกอย่าง
bash
docker run --privileged ... # ⚠️ เกือบเท่ากับให้ root บน host💡 ใช้
--privilegedตอนไหน (กรณีที่จำเป็นจริงและน้อยมาก): รัน Docker-in-Docker ใน CI, รัน container ที่ต้องเข้าถึง device พิเศษ (USB/GPU บางรุ่นที่ไม่ผ่าน plugin), debug container ที่ต้อง mount kernel module. นอกจากนี้ — ห้ามใช้ ลองเปิด--cap-addทีละตัวแทน
2. Mount /var/run/docker.sock — ให้ container เข้าถึง Docker daemon
yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock # ⚠️ container คุม Docker ทั้งหมดได้→ ถ้าจำเป็นจริง ใช้ "Docker socket proxy" จำกัดสิทธิ์
3. --pid=host — แชร์ process namespace กับ host → เห็น process ทั้งเครื่อง
4. --network=host — ไม่มี network isolation
5. ให้ capability อันตราย — SYS_ADMIN, SYS_PTRACE
→ ทุกข้อข้างบน = ลด isolation ของ container ใช้เมื่อจำเป็นและเข้าใจความเสี่ยงเท่านั้น
Part 15: Production Hardening Checklist
ความปลอดภัย:
☐ รันเป็น non-root user (USER ใน Dockerfile)
☐ --cap-drop=ALL แล้ว --cap-add เฉพาะที่จำเป็น
☐ --read-only filesystem + tmpfs สำหรับที่ต้องเขียน
☐ --security-opt no-new-privileges
☐ จำกัด memory + cpu (กัน DoS / OOM)
☐ ไม่มี secret ใน image — ส่งตอน runtime / external manager
☐ scan image ด้วย Trivy ใน CI — fail ถ้าเจอช่องโหว่ร้ายแรง
☐ เซ็น image (Cosign) + ตรวจตอน deploy
☐ พิจารณา rootless Docker
☐ ไม่ mount docker.sock / ไม่ใช้ --privileged
☐ อัปเดต base image สม่ำเสมอ (รับ security patch)
ขนาด + ความเร็ว:
☐ ใช้ multi-stage build
☐ ใช้ base image เล็ก (distroless / alpine / scratch)
☐ รวม RUN + ลบ cache ในบรรทัดเดียว
☐ ติดตั้งแค่ production dependency
☐ มี .dockerignore
☐ จัดลำดับ layer ให้ cache ทำงานดี
☐ ใช้ BuildKit cache mount
การดำเนินการ:
☐ ใส่ HEALTHCHECK
☐ จำกัดขนาด log
☐ ใช้ tag เวอร์ชันชัดเจน (ไม่ใช่ latest)
☐ ตั้ง restart policyPart 16: Lab
Lab 1: Hardening
เริ่มจาก Dockerfile ธรรมดา → ปรับให้ปลอดภัย:
- เพิ่ม non-root user
- multi-stage + base image เล็ก
- รัน container ด้วย
--cap-drop=ALL --read-only --tmpfs /tmp --security-opt no-new-privileges --memory=256m - ทดสอบว่าแอปยังทำงานได้
⚠️ เลือก image ที่ออกแบบมาให้รัน rootless — ถ้าทำ Lab นี้กับ
nginxทางการตรง ๆ มันจะ exit ทันที เพราะ entrypoint script ต้องการCHOWN/SETUID/SETGID/NET_BIND_SERVICE/DAC_OVERRIDEแต่--cap-drop=ALLเพิ่งตัดออกหมด. ทางเลือก:
- ใช้ image ที่ออกแบบ rootless เช่น
nginxinc/nginx-unprivileged(ฟัง port 8080 default, ไม่ chown ตอน boot)- หรือใช้แอปของคุณเอง (Go/Node binary) ที่ฟัง port ≥ 1024 และไม่เปลี่ยน user
- หรือถ้าจะใช้ nginx ทางการ ต้อง
--cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID --cap-add=NET_BIND_SERVICE --cap-add=DAC_OVERRIDEกลับ — ตัด ALL ไม่ได้
Lab 2: Scan
แล็บนี้ให้เห็นผลของการเลือก base image กับความปลอดภัยชัด ๆ — สแกน image รุ่นเก่า (node:18) เทียบกับรุ่นใหม่ที่ผอมกว่า (node:22-alpine) ด้วย Trivy แล้วดูว่าจำนวนช่องโหว่ต่างกันมากแค่ไหน:
bash
trivy image node:18 # ⚠️ EOL ตั้งแต่ April 2025 — ไม่ได้รับ security patch อีกแล้ว
trivy image node:22-alpine # LTS ปัจจุบัน + base ผอม — เทียบจำนวนช่องโหว่⚠️ Node.js 18 หมด security support ตั้งแต่ April 2025 — ใช้สาธิตว่า image EOL อันตรายแค่ไหน แต่ อย่าใช้ใน production. ของจริงต้อง Node 20 LTS หรือ Node 22 LTS (รองรับถึง ~2027) เท่านั้น
Lab 3: เซ็น + ตรวจ
bash
# 0. ติดตั้ง cosign ก่อน (Mac: brew install cosign / Linux: ดาวน์โหลด binary)
# 1. รัน local registry — ไม่ต้องสมัคร Docker Hub
docker run -d -p 5000:5000 --name registry registry:2
# 2. สร้างคู่กุญแจ
cosign generate-key-pair
# (จะถามตั้ง password — จำไว้)
# 3. build + push
docker build -t localhost:5000/myapp:v1 .
docker push localhost:5000/myapp:v1
# 4. เซ็น + ตรวจ (ตั้ง COSIGN_PASSWORD ก่อนถ้าไม่อยากให้ prompt)
export COSIGN_PASSWORD='your-password-here'
cosign sign --key cosign.key localhost:5000/myapp:v1
cosign verify --key cosign.pub localhost:5000/myapp:v1
# cleanup
docker stop registry && docker rm registry💡 ทางเลือก — ถ้ามี Docker Hub account ใช้
docker.io/<yourname>/myapp:v1แทนlocalhost:5000/...ก็ได้ (ต้องdocker loginก่อน)
Part 17: Checkpoint
- ทำไม container security สำคัญ ทั้งที่ container แยกตัวกันแล้ว?
- "least privilege" คืออะไร?
- ทำไมไม่ควรรัน container เป็น root? แก้ยังไง?
--cap-drop=ALLทำอะไร?--read-onlyช่วยเรื่อง security ยังไง?- ทำไมไม่ควรใส่ secret ใน Dockerfile?
- Trivy ใช้ทำอะไร?
- ทำไมต้องเซ็น image? SBOM คืออะไร?
--privileged+ mountdocker.sockอันตรายยังไง?- ทำ image เล็ก 3 วิธี?
Part 18: สรุปบทนี้ + จบหมวด Docker
- Container security = least privilege — ทำให้ container ทำได้น้อยที่สุดเท่าที่จำเป็น
- non-root + cap-drop + read-only + no-new-privileges + resource limit = ชุด hardening พื้นฐาน
- Rootless Docker = ลดความเสี่ยงระดับ daemon
- Secret — ห้ามใส่ใน image — ส่ง runtime / external manager
- Scan (Trivy) + Sign (Cosign) + SBOM = security ของ supply chain
- Image เล็ก = multi-stage + base เล็ก + ลบ cache + production deps เท่านั้น
- Build เร็ว = layer order + BuildKit cache
- ห้าม
--privileged/ mountdocker.sockเว้นแต่จำเป็นและเข้าใจความเสี่ยง
🎉 จบหมวด Docker!
6 บท ครบเครื่อง ตั้งแต่ "container คืออะไร" จนถึง "production hardening"
ขั้นต่อไป — Kubernetes (kubernetes/) — เมื่อระบบโตเกินกว่าจะรันด้วย Docker Compose บนเครื่องเดียว เราต้องการ "ระบบจัดการ container ข้ามหลายเครื่อง" — นั่นคือ Kubernetes
ความรู้ Docker ทั้ง 6 บทนี้เป็นรากฐานสำคัญของ Kubernetes — image, container, network, volume, healthcheck — concept เดียวกันทั้งหมด
📖 Glossary — สรุปศัพท์สำคัญทั้งหมวด Docker
ตารางนี้รวมศัพท์ที่เจอตลอด 6 บท ของหมวด Docker — อ่านทบทวนเร็ว ๆ ก่อนสอบ/สัมภาษณ์
| ศัพท์ | คำอ่าน | ความหมายสั้น ๆ |
|---|---|---|
| Container | คอน-เท-เนอร์ | process ที่ถูกแยกด้วย namespace + cgroup — เหมือน "VM เบา ๆ" แต่ใช้ kernel ร่วมกับ host |
| Image | อิ-เมจ | template สำหรับสร้าง container — เป็น layer ซ้อนกัน read-only |
| Layer | เล-เยอร์ | "ชั้น" ในระบบไฟล์ของ image — แต่ละคำสั่งใน Dockerfile สร้าง 1 layer |
| Dockerfile | ด็อก-เกอร์-ไฟล์ | สคริปต์ที่บอกวิธี build image (FROM/RUN/COPY/CMD ...) |
| Registry | เร-จิส-ทรี | "คลัง" เก็บ image (เช่น Docker Hub, GitHub Container Registry, AWS ECR) |
| Volume | วอ-ลูม | พื้นที่เก็บข้อมูลที่ Docker จัดการให้ — อยู่นอก container layer |
| Bind Mount | ไบนด์-เมาท์ | ผูกโฟลเดอร์จาก host เข้า container ตรง ๆ |
| tmpfs | ทีเอ็มพี-เอฟเอส | mount ที่เก็บใน RAM (ไม่ลง disk) — หายเมื่อ container หยุด |
| Capability | คา-พา-บิ-ลิ-ตี้ | "ชิ้น" ของอำนาจ root ใน Linux เช่น CHOWN, NET_BIND_SERVICE |
| seccomp | เซ็ก-คอม | กลไก kernel ที่ filter syscall ที่ process เรียกได้ — ลด attack surface |
| namespace | เนม-สเปซ | กลไก kernel ที่ "แยกมุมมอง" ของ process (pid, network, mount, user, ...) |
| cgroup | ซี-กรุ๊ป | กลไก kernel ที่จำกัด CPU/RAM/IO ของ process group |
| OOMKill | โอ-โอ-เอ็ม-คิล | Out-Of-Memory Kill — Linux ฆ่า process ที่ใช้ RAM เกิน limit |
| CVE | ซี-วี-อี | รหัสช่องโหว่ความปลอดภัยที่ประกาศต่อสาธารณะ |
| SBOM | เอส-บอม | Software Bill of Materials — รายการ "ส่วนผสม" ทุก library ใน image |
| runc | รัน-ซี | container runtime ระดับล่างที่ Docker/containerd เรียกใช้ |
| rootless | รูท-เลส | รัน Docker daemon ในนาม user ปกติ ไม่ใช่ root |
| setuid bit | เซ็ต-ยูไอดี | flag บนไฟล์โปรแกรมที่ทำให้รันด้วยสิทธิ์ของเจ้าของไฟล์ |
| ephemeral | อี-เฟม-เม-รัล | "ใช้แล้วทิ้ง" — สร้างใหม่ได้ตลอดเวลา (ปรัชญาหลักของ container) |
| distroless | ดิส-โทร-เลส | base image ที่ไม่มี shell/package manager — เหลือแค่ runtime + แอป |
| BuildKit | บิลด์-คิท | build engine ใหม่ของ Docker — ทำ secret mount, cache mount, multi-platform |
| Compose | คอม-โพส | เครื่องมือรันหลาย container ด้วยไฟล์ YAML ไฟล์เดียว |
| least privilege | ลีสท์-พริฟ-วิ-เลจ | หลัก "ให้สิทธิ์น้อยที่สุดเท่าที่จำเป็น" — หัวใจของ security |
[^leaky]: ดูรายละเอียดทางเทคนิคของ CVE-2024-21626 ได้ที่ Snyk Research — Leaky Vessels (last reviewed: 2026-06 — ตรวจ vendor advisory ทางการก่อนยึดเป็น authoritative source)
← บทที่ 04 | สารบัญ | ➡️ ไป Kubernetes →
🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-12