대상 OS: Rocky Linux 9 (Podman 기반, Docker도 개념은 동일)

컨테이너 보안은 “도구를 많이 쓰는 것”보다 최소 단위(기본기)를 먼저 갖추는 게 효과가 큽니다. 현장에서 가장 자주 터지는 지점은 아래 3가지입니다.

  • 태그(tag)를 믿고 배포 → 같은 태그가 다른 이미지로 바뀌어도 눈치채기 어려움
  • 스캔은 하는데 기준(정책)이 없음 → HIGH/CRITICAL이 나와도 그냥 넘어감
  • 이미지는 괜찮아도 실행 권한이 과다 → 침해 시 호스트 영향 범위가 커짐

0) 왜 ‘태그 고정’이 먼저인가

태그는 “사람이 보기 편한 이름”일 뿐, 불변(immutable) 보장이 없습니다. 운영에서 재현성(같은 것을 배포한다)과 검증(스캔한 그것을 배포한다)을 만들려면 digest(sha256)로 고정하는 게 최소 단위입니다.


1) 태그 대신 digest로 고정해서 pull/run

Rocky 9에서는 Podman + Skopeo 조합이 가볍고 유용합니다.

# 도구 설치
sudo dnf -y install podman skopeo

# 예: 태그의 digest 확인
skopeo inspect docker://docker.io/library/nginx:1.25 | head -n 60

출력에서 Digest (예: sha256:...)를 확보한 뒤, 그 digest를 기준으로 pull/run 합니다.

# digest로 고정 pull
podman pull docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST

# 실행도 digest로 고정(동일 이미지 재현)
podman run -d --name web \
  -p 127.0.0.1:8080:80 \
  docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST


2) 이미지 스캔: ‘도구’보다 ‘정책’이 먼저

스캔 도구는 무엇을 쓰든 상관없지만, 통과/차단 기준이 없으면 운영이 무너집니다. 최소 정책 예시는:

  • 배포 전 스캔 필수
  • CRITICAL은 차단 (예외는 승인/기간/완화책 기록)
  • HIGH는 서비스 등급에 따라 차단 또는 기한 내 개선

예시로 Trivy를 듭니다(설치 방식은 조직 표준을 우선하세요).

# (예시) Trivy 설치/사용은 환경마다 다름
# 여기서는 '명령 형태'만 예시로 제시

# 스캔(고정된 digest를 대상으로 수행)
trivy image --severity CRITICAL,HIGH \
  docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST


3) 권한 최소화: 실행 옵션 6가지(효과 대비 가성비)

이미지가 안전해 보이더라도, 실행 권한이 과하면 “취약점 1개”가 “호스트 장악”으로 커집니다.

3-1) rootless(가능하면) + 전용 사용자

Podman의 rootless는 호스트 root를 덜 건드리는 데 도움이 됩니다(완전 격리는 아니지만 피해 범위를 줄이는 방향).

# rootless 사용은 사용자 환경/권한에 따라 사전 설정이 필요할 수 있음
# 우선은 운영 표준에 맞춰 rootless 전환을 검토

podman info | head


3-2) read-only + tmpfs로 쓰기 경로를 제한

podman run -d --name app \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=256m \
  --tmpfs /run:rw,noexec,nosuid,size=64m \
  docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST


3-3) Capabilities 최소화(cap-drop)

기본 capability가 과할 때가 많습니다. “필요한 것만 add”로 접근합니다.

podman run -d --name app \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST


3-4) 네트워크 바인딩 최소화(특히 로컬 바인딩)

# 외부에 바로 노출하지 않고 로컬에만 바인딩(앞단 프록시/로드밸런서 사용 전제)
podman run -d --name web \
  -p 127.0.0.1:8080:80 \
  docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST


3-5) SELinux를 끄지 말고, 볼륨 라벨을 맞춘다

Rocky 9는 SELinux Enforcing이 기본인 환경이 많습니다. 볼륨 마운트가 막힌다고 SELinux를 끄는 건 “보안 장치를 제거”하는 방향이라 피해야 합니다.

# :Z(private) 또는 :z(shared) 라벨 옵션을 상황에 맞게 사용
podman run -d --name web \
  -v /srv/web:/usr/share/nginx/html:Z \
  docker.io/library/nginx@sha256:REPLACE_WITH_DIGEST


3-6) privileged 금지(정말 필요한 경우만 승인/기간/대안 포함)

--privileged는 “편의”로 쓰기 시작하면 곧 사고로 이어집니다. 대안(필요 device만, 필요한 cap만, 필요한 mount만)을 먼저 찾으세요.


4) 이미지 자체도 단순하게: 공격면 줄이기(최소 원칙)

  • 불필요 패키지/툴 제거(쉘, 컴파일러 등)
  • 런타임만 포함하는 베이스(가능하면 distroless/슬림 계열) 검토
  • 빌드 단계/런 단계 분리(multi-stage build)


5) 운영 체크리스트(진짜 최소)

  • 배포는 digest로 고정했는가
  • 스캔 결과 CRITICAL이 ‘0’인가(예외는 문서화/기한/완화책 포함)
  • read-only/cap-drop/로컬 바인딩 등 최소 권한 옵션이 적용됐는가
  • 볼륨 마운트는 필요한 경로만, SELinux 라벨로 처리했는가

사례(현장에서 자주 겪는 상황)

  • “latest” 태그로 배포 → 어느 날 같은 태그인데 동작이 바뀌어 장애/보안 검증이 꼬임(재현 불가).
  • 스캔은 하지만 차단 기준이 없음 → CRITICAL이 떠도 “나중에”로 미루다 그대로 운영 반영.
  • SELinux 때문에 안 된다고 SELinux를 끔 → 컨테이너 경계가 약해져서 침해 시 호스트로의 영향이 커짐.
  • 문제 해결용으로 privileged를 켬 → 그 상태가 표준이 되어 장기적으로 고위험 운영이 됨.

트러블슈팅(증상→원인→해결)

  • 증상: 태그로 pull할 때마다 이미지 내용이 달라져 동작이 바뀜
    원인: 태그는 불변 보장이 없고 레지스트리에서 갱신될 수 있음
    해결: digest로 고정해 pull/run, 스캔도 동일 digest 대상으로 수행
  • 증상: 볼륨 마운트 후 컨테이너가 Permission denied로 실패
    원인: SELinux 컨텍스트 미일치(Enforcing에서 흔함)
    해결: SELinux OFF 대신 :Z/:z 라벨 옵션 사용, 필요 시 audit 로그로 원인 확인
  • 증상: read-only 적용 후 애플리케이션이 실행 중 오류
    원인: 애플리케이션이 특정 경로에 쓰기를 필요로 함(/tmp, /var/cache 등)
    해결: 필요한 경로만 tmpfs/볼륨으로 열어주고 나머지는 read-only 유지(쓰기 경로를 문서화)

요약: 컨테이너 이미지 보안의 최소 단위는 (1) digest 고정, (2) 스캔 정책, (3) 실행 권한 최소화입니다. 이 3가지를 갖추면 ‘도구’가 바뀌어도 보안 수준이 쉽게 무너지지 않습니다.

- 이 글은 ai가 random적으로 만들어 올리는 글입니다. -