Tutorial

รวมมิตรวิธีการออกแบบ Docker Image ให้ปลอดภัย

By Arnon Puitrakul - 19 ธันวาคม 2025

รวมมิตรวิธีการออกแบบ Docker Image ให้ปลอดภัย

ปัจจุบัน นักพัฒนาอย่างเรา ๆ ขยับมาใช้ Container ตั้งแต่การพัฒนาจนไปถึงการ Deploy Application ต่าง ๆ ขึ้นเต็มตัวจนเป็นเรื่องปกติกันไปแล้ว แต่อย่างไรก็ตาม ความสะดวกสบายที่ว่านี้มันมาพร้อมกับความชิบหายในเรื่องของความปลอดภัยที่ซับซ้อนขึ้น ประกอบกับภัยคุกคามใหม่ ๆ ที่มีความพยายามในการโจมตี Container เพิ่มมากขึ้น วันนี้เราจะมาคุยกันว่า มันมีวิธีการอะไรบ้างที่เราจะสร้างสามารถทำให้ Image ของเรา แข็งแกร่งทนทาน ปลอดภัยมากขึ้นกัน

จริง ๆ ที่หยิบเทคนิคมาเขียนนี้ เกิดจากการที่เรานั่งคุยกับเพื่อนบนโต๊ะอาหารว่า เออ Container Technology เป็นอะไรที่ดีมาก ๆ มีประโยชน์หลายอย่าง แต่มันก็แอบกลัวเรื่องความปลอดภัยมาก ๆ เพราะ กำแพงระหว่าง Service และ Host มันบางลงมาก ๆ แตกต่างจากวิธีการสมัยก่อน อย่าง VM ที่การจะหลุดออกจาก Hypervisor เข้า Kernel ของ Host OS มันค่อนข้างยากกว่า Container เลยเขียนออกมาเป็นวิธีการพวกนี้แหละ ซึ่งวิธีการทั้งหมดที่จะเล่า มันเกิดจากไอเดียที่พยายามลด Attack Surface เท่านั้นเลย

การเลือก Base Image

การจะสร้าง Image ขึ้นมาสักตัว ขั้นตอนแรก ก็น่าจะหนีไม่พ้น การเลือก Base Image ขึ้นมา โดยทั่ว ๆ ไป เราอาจจะใช้งานเป็น Linux Distro สักตัวนึง เช่น Ubuntu และ Debian หรือ ถ้าอยากได้ความ Slim และ Lean มากขึ้นก็ไปดูกลุ่ม Alpine แต่การใช้งาน Linux Distro ที่ใส่ของมาเต็มพิกัดขนาดนี้ มันย่อมเป็น Attack Surface ชั้นเยี่ยมที่รอเพียงวันที่ เครื่องมือที่ใส่มาเหล่านี้มีช่องโหว่ เราเรียกการโจมตีพวกนี้ว่า Living of the Land (LotL) ที่จะโจมตีผ่านเครื่องมือทั่ว ๆ ไปอย่าง curl, netcat หรือ gcc ก็เล่นกันมาแล้ว

นอกจากนี้ Image ขนาดใหญ่ มีเครื่องมือใส่เข้ามาเยอะ ยังก่อให้เกิดปัญหา CVE Fatigue หรือ การที่ Security Scanner เตือนช่องโหว่ออกมาเยอะมาก ๆ โดยเฉพาะในเครื่องมือที่เราไม่ได้ใช้ ทำให้เราจะต้องเสียเวลาคัดกรองช่องโหว่ที่แท้จริงเกี่ยวกับเราออกมา และทำการแก้ไข ดังนั้นทางเลือกที่ดีกว่าคือการใช้ Undistro Base Image อย่าง Wolfi ที่เขาจะเน้นการ Update Package ภายในให้สดใหม่ มุ่งเน้นการที่มีการแจ้งเตือนช่องโหว่เป็น 0 (Zero-CVE)

อีกวิธีคือ การใช้ Distroless ไปเลย ที่ตัดเครื่องมือออกรัว ๆ หมดทุกอย่าง เรียกว่า เหลือแค่ Application เราที่จะรันได้แล้วละ นั่นทำให้ลด Attack Surface ไปได้มหาศาล ซึ่งส่วนตัวเราจะค่อนข้างแนะนำการใช้ Distroless และติดตั้งเฉพาะส่วนที่จำเป็นต่อการทำงานของ Application เราเท่านั้นพอแล้ว

Multistage Build แยกส่วนการ Build และ Run ออกจากกัน

ปกติใน DockerFile ของเรา จะมีตั้งแต่ การ Build, Configure และ Run Application ซึ่งบางคนก็คือ รันทั้งหมดในก้อนเดียวไปเลย แซ่บ ๆ นั่นแปลว่า ใน Runtime Image เราจะมีเครื่องมือสำหรับการ Build และ Configure อย่าง Compiler, SDK และ Library ทั้งหลายอยู่ใน Image ของเรา ซึ่งแน่นอนว่า เมื่อมันมีเครื่องมือพวกนี้อยู่ มันย่อมเป็น Attack Surface ชั้นดีสำหรับ Hacker ที่จะหาทางเข้าเลยทีเดียว

การใช้ Multi-Stage Builds เป็นวิธีการเข้ามาแก้ปัญหานี้ได้เป็นอย่างดี คือ เราจะแยก Build Environment ออกจาก Runtime Environment ส่วนของการ Build ที่เราจำเป็นต้องใช้ Compiler, SDK และ Library ต่าง ๆ เราจะเอามันมาใช้แค่ภายใน Build Environment เท่านั้น เมื่อ Project ถูก Build เสร็จทั้งหมดแล้ว ก็ค่อยนำมาใช้ใน Runtime Environment ที่เหลือเพียงไฟล์ Artifacts ที่จำเป็นจริง ๆ ต่อการทำงานเท่านั้น

# Stage 1: Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myapp .

# Stage 2: Runtime 
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /
COPY --from=builder /app/myapp /myapp
USER 65532:65532
ENTRYPOINT ["/myapp"]

อีกข้อดีของการทำ Multi-Stage Builds คือ Runtime Image ที่เราจะต้องคาอยู่ในเครื่องเราตลอดการทำงาน มันจะมีขนาดเล็กมาก เพราะเราไม่ต้องแบกส่วนที่เราไม่ใช้แล้วตลอดเวลานั่นเอง

Non-Root User ป้องกันการทะลุถึง Host

อีกความผิดพลาดที่เจอกันเยอะมาก ๆ คือ การให้ Container รันด้วย UID 0 หรือ Root ซึ่งแน่นอนว่า ตาม The Principle of Least Privilege (PoLP) เราจะไม่ให้สิทธิ์การเข้าถึงที่เกินกว่าความจำเป็น ซึ่ง Root นี่ไปไกลมาก ๆ และอีกอย่างมันมีช่องโหว่ที่เกิดเพราะเรื่องพวกนี้อย่างพวก Container Breakout อย่าง Leaky Vessels (CVE-2024-21626) คนที่โจมตีสามารถหลุดออกจาก Container และได้ Root บนเครื่องทันที นั่นหมายถึงการเข้าควบคุมเครื่องทั้งเครื่อง ดังนั้น ในความเป็นจริง เราควรกำหนดให้ Application ของเรารันด้วยสิทธิ์ที่จำกัดอยู่เสมอ เพื่อจำกัดขอบเขตความเสียหายหาก Container นั้นไม่ใช่คนดีขึ้นมา

# Create new group and user with UID/GID greater than 10,000 which will not collide with host's uid
RUN addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S appuser -G appgroup
# Change to new user after preparing the image
USER 10001:10001

การกำหนด User ตั้งแต่ใน DockerFile เป็นการสร้างความปลอดภัยตั้งแต่ต้นทาง ลดการพึ่งพาของ Admin ในการ Deploy เข้าสู่ Production

Secret Management

อีกอย่างที่สร้างความบรรลัยกันมานัดต่อนัดแล้วคือ การจัดการ Secret เช่นพวก API Key และ Password ทั้งหลาย ห้ามใส่และใช้งานมันผ่านคำสั่ง ENV และ COPY เป็นไฟล์เข้าไปใน Image อย่างเด็ดขาด เพราะ Docker จะเก็บประวัติการทำงานทั้งหมดไว้เป็น Layer แม้เราจะสั่งลบไฟล์ทิ้งใน Layer ถัดไป แต่ข้อมูลยังสามารถกู้คืนได้ด้วยคำสั่ง docker history คำแนะนำคือ เราควรใช้ Build Secret Mount บน Docker ได้ ที่จะ Mount Secret File เข้ามาชั่วคราวระหว่างการ Build เท่านั้น โดยข้อมูลพวกนี้จะไม่ถูกเขียนไว้ในอะไรทั้งสิ้นเลย

FROM alpine
# Mount Secret
RUN --mount=type=secret,id=db_password \
    DB_PASS=$(cat /run/secrets/db_password) && ./setup_app.sh

วิธีการนี้สามารถช่วยป้องกันการรั่วไหวของ Secret ที่จะไปสู่ Registry หรือคนที่นำ Image ของเราไปใช้อย่างยั่งยืน

Supply Chain Auditing

และเรื่องสุดท้าย เป็นเรื่องที่หลายคนมองข้าม แต่มันเกิดขึ้นได้จริง คือ Image ที่เรา Pull มาใช้นั้นอาจจะเกิดการถูกแก้ไขระหว่างทาง อาจจะมีการสอด Malware หรือ Script บางอย่างเข้าไป ทำให้หลาย ๆ องค์กรเริ่มมาใช้ Sigstore และ Cosign เพื่อ Sign Image ทำให้มั่นใจได้ว่า Image ที่ Pull มาไม่ได้ถูกแก้ไขระหว่างทาง และ การทำ SBOM (Software Bill of Materials) หรือบัญชีรายชื่อของส่วนประกอบทั้งหมดใน Image ก็เป็นเรื่องจำเป็นเช่นกัน เพื่อให้เราสามารถตอบสนองกับช่องโหว่ใหม่ ๆ ได้อย่างรวดเร็ว เมื่อมีการค้นพบช่องโหว่ใหม่ภายใน Library หรือ ส่วนประกอบต่าง ๆ ที่เราใช้งาน

สรุป

จริง ๆ แล้วยังมีอีกหลายเทคนิคที่ทำให้ Docker Image ของเราปลอดภัยมาขึ้นกว่าเดิม วิธีการที่เราเอามาเล่าในบทความนี้ มันจะอยู่ในหมวดของการจัดการ Image แต่จริง ๆ แล้ว มันยังมีกลไกอื่น ๆ ที่เราสามารถใส่เข้ามาใน Project ของเราได้ด้วย อย่างการจัดการ CI/CD Pipeline ให้มีการตรวจสอบ Image ของเราอยู่ตลอดเวลา เป็นการจัดการความปลอดภัยแบบ Proactive ที่ช่วยทำให้เรา Detect ปัญหาและช่องโหว่ได้เร็วขึ้นก่อนจะเจอและโดนจริง ๆ นั่นเอง