Skip to content

บทที่ 05 — Security + Optimization

← บทที่ 04 | สารบัญ

🟡 หมายเหตุระดับ: บทนี้กว้างมาก — หลายหัวข้อ (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 imageaddgroup/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:8080 map แทน


Part 3: จำกัดสิทธิ์ (Capabilities)

3.1 Capabilities คืออะไร

Linux แบ่งอำนาจของ "root" เป็นชิ้นเล็ก ๆ เรียกว่า capabilities — เช่น:

  • NET_BIND_SERVICE — bind port ต่ำกว่า 1024
  • NET_ADMIN — จัดการ network
  • SYS_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 ของ processimage ที่ใช้ gosu/su-exec ลด privilege ตอน boot
SETUID (เซ็ต-ยูไอดี)เปลี่ยน user ของ processเหมือนด้านบน — เปลี่ยนจาก root → app user
NET_BIND_SERVICE (เน็ต-ไบนด์-เซอร์-วิส)bind port ต่ำกว่า 1024web server ที่ฟัง 80/443 ตรง ๆ
NET_RAW (เน็ต-รอว์)สร้าง raw network packetping (ICMP) — ปกติไม่ต้องใช้ในแอป production
FOWNER (เอฟ-โอว-เนอร์)ข้ามเช็คเจ้าของไฟล์ตอน chmodบาง image ใช้ตอน setup permission

image ทางการอย่าง nginx, node, postgres เปลี่ยน user ตอน boot → ต้องการ SETGID/SETUID/CHOWN ขั้นต่ำ.

ถ้า drop ALL แล้วเหลือแค่ NET_BIND_SERVICEcontainer อาจ exit เงียบ ๆ หรือ error แปลก ๆ.

วิธีหา cap ที่ต้องใช้จริง:

  1. ลองรันแบบ drop ALL ก่อน → ดู log
  2. ค่อย ๆ --cap-add เพิ่มทีละตัวจนรันได้ — หรือดูใน Dockerfile/entrypoint script ของ image ว่ามีคำสั่งอะไร (chown, su-exec, gosu → ต้องการ CHOWN/SETUID/SETGID)
  3. ตัวอย่างจริง — 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 myapp

no-new-privileges ทำอะไร — แยกเป็น 3 ประเด็นสั้น ๆ:

  1. flag นี้บอก kernel ว่า: "process ใน container ห้ามได้สิทธิ์เพิ่มขึ้นจากที่มีตอนเริ่มต้น" — ปิดทาง privilege escalation (เอส-คา-เล-ชัน = การยกระดับสิทธิ์ เช่น เริ่มเป็น user ธรรมดาแล้วพยายามกลายเป็น root) ทั้งหมดในระดับ kernel
  2. setuid bit คืออะไร — เป็น "flag พิเศษ" บนไฟล์โปรแกรมใน Linux ที่บอกว่า "ตอนรันโปรแกรมนี้ ใช้สิทธิ์ของ เจ้าของไฟล์ ไม่ใช่สิทธิ์ของคนที่สั่งรัน" — ตัวอย่างเช่น sudo ใช้ setuid เพื่อเป็น root ชั่วคราว
  3. ทำไม attacker ชอบใช้ — ถ้าเจาะ container เข้ามาแล้วเจอไฟล์ที่มี setuid + เจ้าของคือ root → รันไฟล์นั้น = ได้สิทธิ์ root ใน container ทันที. no-new-privileges บล็อก trick นี้

ใน Compose:

yaml
services:
  api:
    security_opt:
      - no-new-privileges:true

Part 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 ที่รองรับ จะเปลี่ยนเป็น pasta network 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)
  • 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)" myapp

2. ใช้ 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 install
bash
docker build --secret id=npm_token,src=./token.txt .

token ใช้ตอน build แต่ไม่ติดใน image

⚠️ ต้องใส่ # syntax=docker/dockerfile:1 บรรทัดแรกของ Dockerfile — ไม่งั้น --mount=type=secret parse ไม่ผ่าน (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 Dockerfile

Trivy บอกว่า 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:v1

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

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_PASSWORD env 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        # ไม่เอา devDependencies

11.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=max

Part 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 policy

Part 16: Lab

Lab 1: Hardening

เริ่มจาก Dockerfile ธรรมดา → ปรับให้ปลอดภัย:

  1. เพิ่ม non-root user
  2. multi-stage + base image เล็ก
  3. รัน container ด้วย --cap-drop=ALL --read-only --tmpfs /tmp --security-opt no-new-privileges --memory=256m
  4. ทดสอบว่าแอปยังทำงานได้

⚠️ เลือก 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

  1. ทำไม container security สำคัญ ทั้งที่ container แยกตัวกันแล้ว?
  2. "least privilege" คืออะไร?
  3. ทำไมไม่ควรรัน container เป็น root? แก้ยังไง?
  4. --cap-drop=ALL ทำอะไร?
  5. --read-only ช่วยเรื่อง security ยังไง?
  6. ทำไมไม่ควรใส่ secret ใน Dockerfile?
  7. Trivy ใช้ทำอะไร?
  8. ทำไมต้องเซ็น image? SBOM คืออะไร?
  9. --privileged + mount docker.sock อันตรายยังไง?
  10. ทำ 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 / mount docker.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