ข้ามไปยังเนื้อหา

Security และ HEALTHCHECK

โดย default process ภายใน Docker container จะรันในฐานะ root (UID 0) หาก attacker ใช้ประโยชน์จากช่องโหว่ใน application พวกเขาจะได้รับ root privilege ภายใน container ซึ่งขึ้นอยู่กับ host configuration อาจแปลงเป็น host-level access ได้ การเปลี่ยนแปลง Dockerfile สองอย่างง่ายๆ สามารถลดความเสี่ยงที่พบบ่อยที่สุดได้

instruction USER กำหนด user ที่ instruction RUN, CMD และ ENTRYPOINT ที่ตามมาทั้งหมดจะ execute

# syntax=docker/dockerfile:1
FROM node:22-alpine
# สร้าง user และ group เฉพาะ
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --chown=app:app package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=app:app src/ ./src/
# เปลี่ยนไปใช้ non-root user ก่อน CMD
USER app
EXPOSE 3000
CMD ["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 ต้องวางหลัง instruction RUN ทั้งหมดที่ต้องการ root (การ install package เป็นต้น)

ทุก 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

วิธีที่ถูกต้อง:

  1. 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 .

  2. Runtime environment variables — ส่ง secret ตอน docker run ไม่ใช่ตอน build:

    Terminal window
    docker run -e DATABASE_URL="postgres://user:pass@host/db" myapp
  3. Secrets managers — inject secret ผ่าน Vault, AWS Secrets Manager หรือ Kubernetes Secrets ตอน pod startup

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 1

Parameters:

  • --interval — ความถี่ในการรัน check (default: 30s)
  • --timeout — รอนานแค่ไหนก่อนถือว่า check ล้มเหลว (default: 30s)
  • --start-period — ระยะเวลาผ่อนผันหลัง container start ก่อนที่การ fail จะนับ (default: 0s)
  • --retries — จำนวนครั้งที่ fail ต่อเนื่องก่อนทำเครื่องหมาย unhealthy (default: 3)

ตรวจสอบ health status:

Terminal window
docker ps --format "table {{.Names}}\t{{.Status}}"
# NAME STATUS
# myapp Up 2 minutes (healthy)

Docker Scout วิเคราะห์ layer ของ image เทียบกับ vulnerability database และรายงาน CVE

Terminal window
# Scan local image
docker scout cves myapp:prod
# เปรียบเทียบสอง tag
docker scout compare myapp:prod myapp:naive

รายงานทั่วไปจะมีลักษณะดังนี้:

0C 2H 12M 3L | myapp:prod
0C 8H 45M 12L | myapp:naive

C = Critical, H = High, M = Medium, L = Low การเปลี่ยนไปใช้ slim หรือ distroless base มักจะขจัด CVE ระดับ High และ Critical ส่วนใหญ่ได้

รวม scan เข้าไปใน CI เพื่อตรวจจับ regression ก่อนถึง production:

Terminal window
docker scout cves --exit-code --only-severity critical,high myapp:prod

--exit-code ทำให้ command return non-zero exit code เมื่อพบ CVE ที่ตรงเงื่อนไข ซึ่ง block pipeline ได้

# 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")
ตัวเลือกBenefitCost
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 ไว้ใน RUN instruction แล้วคิดว่าการ 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 ที่ค้างแบบเงียบๆ ได้มาก

ทำไมการรัน container ในฐานะ root จึงถือเป็นความเสี่ยงด้านความปลอดภัย?
เกิดอะไรขึ้นกับ secret ที่เขียนใน `RUN` instruction แล้วถูกลบใน layer ต่อมา?
option `--start-period` ใน HEALTHCHECK ควบคุมอะไร?
flag ใดของ `docker scout` ที่ทำให้ command return non-zero exit code เมื่อพบ vulnerability — เหมาะสำหรับ block CI pipeline?