Security และ HEALTHCHECK
ทำไม container security จึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไม container security จึงสำคัญ”โดย default process ภายใน Docker container จะรันในฐานะ root (UID 0) หาก attacker ใช้ประโยชน์จากช่องโหว่ใน application พวกเขาจะได้รับ root privilege ภายใน container ซึ่งขึ้นอยู่กับ host configuration อาจแปลงเป็น host-level access ได้ การเปลี่ยนแปลง Dockerfile สองอย่างง่ายๆ สามารถลดความเสี่ยงที่พบบ่อยที่สุดได้
การรันในฐานะ non-root user
หัวข้อที่มีชื่อว่า “การรันในฐานะ non-root user”instruction USER กำหนด user ที่ instruction RUN, CMD และ ENTRYPOINT ที่ตามมาทั้งหมดจะ execute
# syntax=docker/dockerfile:1FROM node:22-alpine
# สร้าง user และ group เฉพาะRUN addgroup -S app && adduser -S app -G app
WORKDIR /appCOPY --chown=app:app package.json package-lock.json ./RUN npm ci --omit=devCOPY --chown=app:app src/ ./src/
# เปลี่ยนไปใช้ non-root user ก่อน CMDUSER app
EXPOSE 3000CMD ["node", "src/server.js"]รายละเอียดสำคัญ:
addgroup -Sและadduser -Sสร้าง system group และ user (ไม่มี login, ไม่มี home dir) — เหมาะสำหรับ service account--chown=app:appในCOPYกำหนด ownership ใน layer เดียวกัน ป้องกันการใช้RUN chownแยกต่างหากUSER appต้องวางหลัง instructionRUNทั้งหมดที่ต้องการ root (การ install package เป็นต้น)
อย่าฝัง secret ใน image
หัวข้อที่มีชื่อว่า “อย่าฝัง secret ใน image”ทุก layer ใน Docker image ถูกเก็บไว้บน disk และสามารถตรวจสอบได้ด้วย docker history หรือดึงออกจาก registry Secret ที่เขียนใน RUN instruction แม้จะถูกลบใน layer ต่อมา ก็ยังมองเห็นได้ใน build history
# ผิด — token จะอยู่ใน image history ตลอดไปRUN curl -H "Authorization: Bearer \${SECRET_TOKEN}" https://api.example.com/dataวิธีที่ถูกต้อง:
-
Build-time secrets ผ่าน BuildKit — ไม่ถูกเก็บใน image:
RUN --mount=type=secret,id=api_token \curl -H "Authorization: Bearer $(cat /run/secrets/api_token)" https://api.example.com/dataส่งด้วย:
docker build --secret id=api_token,env=SECRET_TOKEN . -
Runtime environment variables — ส่ง secret ตอน
docker runไม่ใช่ตอน build:Terminal window docker run -e DATABASE_URL="postgres://user:pass@host/db" myapp -
Secrets managers — inject secret ผ่าน Vault, AWS Secrets Manager หรือ Kubernetes Secrets ตอน pod startup
HEALTHCHECK
หัวข้อที่มีชื่อว่า “HEALTHCHECK”instruction HEALTHCHECK บอก Docker วิธีทดสอบว่า container ทำงานปกติหรือไม่ Docker จะรัน command เป็นระยะๆ หากล้มเหลวต่อเนื่อง status ของ container จะเปลี่ยนเป็น unhealthy และ orchestrator (Swarm, Kubernetes liveness probes เทียบเท่า) จะ restart อัตโนมัติ
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:3000/health || exit 1Parameters:
--interval— ความถี่ในการรัน check (default: 30s)--timeout— รอนานแค่ไหนก่อนถือว่า check ล้มเหลว (default: 30s)--start-period— ระยะเวลาผ่อนผันหลัง container start ก่อนที่การ fail จะนับ (default: 0s)--retries— จำนวนครั้งที่ fail ต่อเนื่องก่อนทำเครื่องหมาย unhealthy (default: 3)
ตรวจสอบ health status:
docker ps --format "table {{.Names}}\t{{.Status}}"# NAME STATUS# myapp Up 2 minutes (healthy)การ Scan ด้วย docker scout
หัวข้อที่มีชื่อว่า “การ Scan ด้วย docker scout”Docker Scout วิเคราะห์ layer ของ image เทียบกับ vulnerability database และรายงาน CVE
# Scan local imagedocker scout cves myapp:prod
# เปรียบเทียบสอง tagdocker scout compare myapp:prod myapp:naiveรายงานทั่วไปจะมีลักษณะดังนี้:
0C 2H 12M 3L | myapp:prod 0C 8H 45M 12L | myapp:naiveC = Critical, H = High, M = Medium, L = Low การเปลี่ยนไปใช้ slim หรือ distroless base มักจะขจัด CVE ระดับ High และ Critical ส่วนใหญ่ได้
รวม scan เข้าไปใน CI เพื่อตรวจจับ regression ก่อนถึง production:
docker scout cves --exit-code --only-severity critical,high myapp:prod--exit-code ทำให้ command return non-zero exit code เมื่อพบ CVE ที่ตรงเงื่อนไข ซึ่ง block pipeline ได้
ลงมือทำ: non-root และ HEALTHCHECK Dockerfile
หัวข้อที่มีชื่อว่า “ลงมือทำ: non-root และ HEALTHCHECK Dockerfile”# syntax=docker/dockerfile:1
FROM node:22-alpine
# Create a non-root user
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# Inline a minimal HTTP server
RUN echo 'const http=require("http");' > server.js && echo 'http.createServer((req,res)=>{' >> server.js && echo ' if(req.url==="/health"){res.writeHead(200);res.end("ok\n");}' >> server.js && echo ' else{res.writeHead(200);res.end("Hello (running as non-root)!\n");}' >> server.js && echo '}).listen(3000,()=>console.log("Listening on 3000"));' >> server.js
# Set ownership and switch user
RUN chown -R app:app /app
USER app
EXPOSE 3000
# Health check — polls /health every 10s
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node","server.js"]
# --- Build and run ---
# docker build -t myapp:secure .
# docker run --rm -p 3000:3000 myapp:secure
# docker ps (check STATUS for "healthy")ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| Runtime base เป็น alpine | มี shell ให้ debug เข้าไปตรวจสอบ container ที่กำลังรันได้ | ยังมี shell และ package manager ให้ผู้โจมตีใช้ประโยชน์ได้หากเจาะเข้ามา |
| Runtime base เป็น distroless | ไม่มี shell ไม่มี package manager attack surface เล็กกว่ามาก | debug interactive แทบทำไม่ได้เลย ต้องพึ่ง log และ HEALTHCHECK แทน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ฝัง secret เช่น API key หรือ npm token ไว้ใน
RUNinstruction แล้วคิดว่าการRUN rmในภายหลังลบออกได้ ทั้งที่ secret ยังอยู่ใน layer history ที่docker historyมองเห็น - ไม่ใส่
HEALTHCHECKทำให้ orchestrator แยกไม่ออกระหว่าง container ที่ hang กับ container ที่ทำงานปกติ และไม่ restart ให้อัตโนมัติ - รัน process หลักในฐานะ root โดยไม่ตั้งใจ ทำให้ blast radius ของช่องโหว่ขยายจาก application ไปถึงระดับ container privilege
💡 ตัวอย่างจากของจริง
Kubernetes ใช้กลไกแบบเดียวกับ
HEALTHCHECKผ่าน liveness และ readiness probe เพื่อตัดสินใจ restart pod ที่ hang โดยอัตโนมัติ ทีมที่ deploy image แบบ distroless คู่กับ health check ที่ตั้งค่าถูกต้องจึงลด downtime จาก container ที่ค้างแบบเงียบๆ ได้มาก