본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 2장: 컨테이너 이미지 보안 기초
2026년 3월 9일·인프라·

2장: 컨테이너 이미지 보안 기초

최소 베이스 이미지, 멀티스테이지 빌드, 루트 없는 컨테이너 등 Dockerfile 보안 모범 사례와 불변 이미지 전략을 실습합니다.

14분510자8개 섹션
securitykubernetesdevopsinfrastructure
공유
container-security2 / 10
12345678910
이전1장: 컨테이너 보안의 필요성과 위협 모델다음3장: 이미지 스캐닝과 취약점 관리

학습 목표

  • 최소 베이스 이미지의 종류와 선택 기준을 이해합니다.
  • **Multi-stage Build(멀티스테이지 빌드)**로 이미지 크기와 공격 표면을 줄이는 방법을 실습합니다.
  • 루트 없는 컨테이너를 구성하는 방법을 익힙니다.
  • 이미지 레이어 구조를 이해하고 불변 이미지 전략을 적용합니다.

이미지 레이어 이해

컨테이너 이미지는 읽기 전용 레이어의 스택으로 구성됩니다. Dockerfile의 각 명령어(FROM, RUN, COPY 등)가 하나의 레이어를 생성하며, 이 레이어들은 **Union Filesystem(유니온 파일시스템)**을 통해 하나의 파일시스템처럼 보입니다.

보안 관점에서 중요한 점은, 한번 레이어에 포함된 데이터는 삭제해도 이전 레이어에 남아있다는 것입니다.

잘못된 예시: 시크릿이 레이어에 남음
dockerfile
FROM ubuntu:22.04
COPY secret.key /tmp/secret.key
RUN ./setup.sh --key-file /tmp/secret.key
RUN rm /tmp/secret.key  # 이전 레이어에 여전히 존재

위 예시에서 rm 명령으로 파일을 삭제해도, 두 번째 레이어에 secret.key가 그대로 남아있습니다. docker history나 레이어 추출 도구로 누구든 확인할 수 있습니다.

Warning

Dockerfile에 시크릿을 절대 포함하지 마세요. 빌드 시 시크릿이 필요하다면 Docker BuildKit의 --mount=type=secret 기능을 사용하세요. 이 방법은 시크릿을 레이어에 남기지 않습니다.


최소 베이스 이미지 선택

베이스 이미지의 크기가 클수록 포함된 패키지가 많고, 그만큼 알려진 취약점이 포함될 가능성이 높아집니다. 최소 베이스 이미지를 선택하는 것이 이미지 보안의 첫걸음입니다.

주요 베이스 이미지 비교

이미지크기셸 포함패키지 매니저적합한 용도
ubuntu:22.04~77MBOapt개발/디버깅
alpine:3.19~7MBOapk경량 범용
gcr.io/distroless/static~2MBXXGo 바이너리
gcr.io/distroless/cc~20MBXXC/C++ 앱
cgr.dev/chainguard/static~2MBXX정적 바이너리

Distroless 이미지

Google이 관리하는 Distroless 이미지는 애플리케이션 실행에 필요한 최소한의 런타임만 포함합니다. 셸, 패키지 매니저, 기타 유틸리티가 모두 제거되어 있어 공격자가 컨테이너에 침투하더라도 할 수 있는 행동이 극히 제한됩니다.

Distroless 이미지 사용 예시
dockerfile
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

Chainguard Images

Chainguard는 보안에 특화된 컨테이너 이미지를 제공합니다. 모든 이미지에 SBOM이 포함되어 있고, 서명이 되어 있으며, 알려진 CVE가 0인 상태를 목표로 합니다.

Chainguard 이미지의 CVE 확인
bash
# Chainguard의 Node.js 이미지 스캔
trivy image cgr.dev/chainguard/node:latest
 
# 일반 Node.js 이미지 스캔 (비교용)
trivy image node:20

일반 node:20 이미지에서 수백 개의 취약점이 발견되는 반면, Chainguard 이미지에서는 취약점이 거의 없는 것을 확인할 수 있습니다.

Tip

프로덕션 이미지는 Distroless나 Chainguard를 기본으로 사용하고, 디버깅이 필요할 때만 debug 태그가 붙은 변형 이미지를 임시로 사용하세요. Distroless는 gcr.io/distroless/static-debian12:debug 태그로 셸이 포함된 버전을 제공합니다.


멀티스테이지 빌드

**Multi-stage Build(멀티스테이지 빌드)**는 빌드 도구와 런타임 환경을 분리하여 최종 이미지에 불필요한 도구가 포함되지 않도록 하는 기법입니다.

Go 애플리케이션 예시

Dockerfile.multistage
dockerfile
# === 빌드 스테이지 ===
FROM golang:1.22-alpine AS builder
 
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
 
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
 
# === 런타임 스테이지 ===
FROM gcr.io/distroless/static-debian12:nonroot
 
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

빌드 스테이지에서는 Go 컴파일러와 소스 코드가 포함되지만, 최종 이미지에는 컴파일된 바이너리만 들어갑니다. 이 차이는 이미지 크기뿐만 아니라 보안 측면에서도 큰 의미가 있습니다.

  • 빌드 스테이지의 이미지: ~300MB (컴파일러, 소스코드, 빌드 도구 포함)
  • 최종 이미지: ~5MB (바이너리 + 최소 런타임)

Node.js 애플리케이션 예시

Dockerfile.node-multistage
dockerfile
# === 의존성 설치 스테이지 ===
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile --prod
 
# === 빌드 스테이지 ===
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
 
# === 런타임 스테이지 ===
FROM cgr.dev/chainguard/node:latest
 
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
 
USER nonroot
EXPOSE 3000
CMD ["dist/server.js"]

의존성 설치, 빌드, 런타임을 세 단계로 분리함으로써 최종 이미지에 devDependencies와 소스 코드가 포함되지 않습니다.


루트 없는 컨테이너

컨테이너 내부에서 root로 프로세스를 실행하면, 컨테이너 탈출 시 호스트에서도 root 권한을 얻을 수 있습니다. **Rootless Container(루트 없는 컨테이너)**는 이 위험을 줄이는 가장 기본적인 방어책입니다.

Dockerfile에서 사용자 설정

루트 없는 컨테이너 설정
dockerfile
FROM node:20-alpine
 
# 전용 사용자 생성
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -D appuser
 
WORKDIR /app
COPY --chown=appuser:appgroup . .
 
# 루트가 아닌 사용자로 전환
USER appuser
 
EXPOSE 3000
CMD ["node", "server.js"]

쿠버네티스 SecurityContext

Dockerfile에서 USER를 설정하더라도, 쿠버네티스에서 **SecurityContext(보안 컨텍스트)**를 통해 추가적인 제약을 걸어야 합니다.

pod-security-context.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    runAsGroup: 1001
    fsGroup: 1001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

위 설정에서 주목할 항목들을 살펴보겠습니다.

  • runAsNonRoot: true: root 사용자로 실행되는 컨테이너를 차단합니다.
  • allowPrivilegeEscalation: false: 프로세스가 부모보다 높은 권한을 획득하는 것을 방지합니다.
  • readOnlyRootFilesystem: true: 파일시스템을 읽기 전용으로 만들어 악성코드 작성을 차단합니다.
  • capabilities.drop: ALL: 모든 리눅스 **Capability(커널 권한)**를 제거합니다.
Info

readOnlyRootFilesystem을 활성화하면 임시 파일을 쓸 수 없으므로, 필요한 경로에 emptyDir 볼륨을 마운트해야 합니다. 위 예시에서는 /tmp에 임시 볼륨을 연결했습니다.


Dockerfile 보안 가이드라인

실전에서 적용할 수 있는 Dockerfile 보안 체크리스트입니다.

이미지 태그 고정

태그 고정
dockerfile
# 나쁜 예: latest 태그는 언제든 바뀔 수 있음
FROM node:latest
 
# 좋은 예: 버전과 다이제스트를 명시
FROM node:20.12.0-alpine@sha256:abc123...

latest 태그는 언제든 다른 이미지를 가리킬 수 있으므로, 정확한 버전과 다이제스트를 함께 명시해야 빌드 재현성과 보안을 동시에 확보할 수 있습니다.

불필요한 패키지 제거

최소 패키지 설치
dockerfile
# 나쁜 예: 추천 패키지까지 모두 설치
RUN apt-get update && apt-get install -y curl wget git vim
 
# 좋은 예: 필요한 패키지만 최소 설치 후 캐시 정리
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl ca-certificates && \
    rm -rf /var/lib/apt/lists/*

BuildKit 시크릿 마운트

BuildKit 시크릿 사용
dockerfile
# syntax=docker/dockerfile:1
 
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
 
# 시크릿을 레이어에 남기지 않고 사용
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN && \
    npm install --production && \
    npm config delete //registry.npmjs.org/:_authToken

빌드 시 다음과 같이 실행합니다.

빌드 명령
bash
DOCKER_BUILDKIT=1 docker build \
  --secret id=npm_token,src=./.npm-token \
  -t myapp:latest .

불변 이미지 전략

Immutable Image(불변 이미지) 전략은 한 번 빌드된 이미지를 수정하지 않고, 변경이 필요하면 항상 새로운 이미지를 빌드하는 접근법입니다.

이 전략의 핵심 원칙은 다음과 같습니다.

  1. 태그 재사용 금지: 동일한 태그에 다른 이미지를 푸시하지 않습니다. 커밋 해시나 시맨틱 버전을 태그로 사용합니다.
  2. 실행 중 수정 금지: readOnlyRootFilesystem으로 컨테이너 내 파일 변경을 차단합니다.
  3. 설정은 외부에서 주입: 환경 변수, ConfigMap, Secret으로 설정을 주입합니다.
불변 이미지 태그 전략
bash
# Git 커밋 해시를 태그로 사용
docker build -t myapp:$(git rev-parse --short HEAD) .
 
# 시맨틱 버전 + 빌드 번호
docker build -t myapp:1.2.3-build.456 .

정리

이번 장에서는 컨테이너 이미지 보안의 기초를 다루었습니다. 핵심 내용을 정리하면 다음과 같습니다.

  • 이미지 레이어는 한번 기록되면 삭제해도 이전 레이어에 남으므로, 시크릿을 포함하지 않아야 합니다.
  • Distroless나 Chainguard 같은 최소 베이스 이미지로 공격 표면을 줄입니다.
  • 멀티스테이지 빌드로 빌드 도구와 런타임을 분리합니다.
  • USER 지시어와 SecurityContext로 루트 실행을 방지합니다.
  • 불변 이미지 전략으로 빌드 재현성과 보안을 동시에 확보합니다.

다음 장에서는 빌드된 이미지에 포함된 취약점을 자동으로 탐지하는 이미지 스캐닝을 다룹니다. Trivy, Grype, Snyk을 비교하고, CI/CD 파이프라인에 스캐닝 게이트를 통합하는 방법을 실습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#security#kubernetes#devops#infrastructure

관련 글

인프라

3장: 이미지 스캐닝과 취약점 관리

Trivy, Grype, Snyk 컨테이너 스캐너를 비교하고, CI/CD 파이프라인에 취약점 스캐닝 게이트를 통합하여 안전한 이미지만 배포하는 방법을 다룹니다.

2026년 3월 11일·15분
인프라

1장: 컨테이너 보안의 필요성과 위협 모델

컨테이너 환경에서 마주하는 보안 위협과 공격 벡터를 분석하고, 방어 심층 전략과 OWASP 쿠버네티스 보안 체크리스트를 기반으로 한 보안 로드맵을 소개합니다.

2026년 3월 7일·16분
인프라

4장: SBOM 생성과 관리

SBOM(소프트웨어 자재 명세서)의 개념과 필요성, SPDX와 CycloneDX 형식을 비교하고, Syft와 Trivy로 SBOM을 생성하여 공급망 가시성을 확보하는 방법을 실습합니다.

2026년 3월 13일·14분
이전 글1장: 컨테이너 보안의 필요성과 위협 모델
다음 글3장: 이미지 스캐닝과 취약점 관리

댓글

목차

약 14분 남음
  • 학습 목표
  • 이미지 레이어 이해
  • 최소 베이스 이미지 선택
    • 주요 베이스 이미지 비교
    • Distroless 이미지
    • Chainguard Images
  • 멀티스테이지 빌드
    • Go 애플리케이션 예시
    • Node.js 애플리케이션 예시
  • 루트 없는 컨테이너
    • Dockerfile에서 사용자 설정
    • 쿠버네티스 SecurityContext
  • Dockerfile 보안 가이드라인
    • 이미지 태그 고정
    • 불필요한 패키지 제거
    • BuildKit 시크릿 마운트
  • 불변 이미지 전략
  • 정리