리눅스 컨테이너: 컨테이너는 작은 가상 머신이 아니라 호스트 Linux kernel을 공유하면서 process·mount·network·user namespace로 보이는 범위를 나누고 cgroups로 CPU·memory·PID 같은 자원을 제한한 프로세스입니다. 이 차이를 이해해야 권한, 이미지, volume, 네트워크와 장애 범위를 올바르게 설계할 수 있습니다.

이 실습은 Rocky Linux 9 계열의 cgroup v2와 rootless Podman을 기준으로 합니다. SELinux와 firewalld를 유지하고, 원격 설치 스크립트를 파이프로 실행하지 않으며, 이미지 tag만 믿지 않고 digest와 provenance를 기록합니다. 명령 결과는 설치된 Podman·kernel 버전에 따라 달라질 수 있으므로 각 단계에서 실제 상태를 확인하십시오.

리눅스 컨테이너 구조: rootless 사용자, OCI 이미지 계층, namespaces, 공유 커널과 cgroups v2
OCI 이미지 계층에서 생성된 여러 격리 환경이 하나의 Linux kernel을 공유하고 cgroups로 자원을 제한하는 구조

리눅스 컨테이너와 가상 머신 차이

항목 컨테이너 가상 머신
Kernel 호스트 kernel 공유 Guest OS별 kernel
격리 namespaces·LSM·seccomp·capabilities hypervisor와 가상 하드웨어
이미지 OCI layer와 metadata 전체 디스크 이미지
시작 격리된 프로세스 생성 Guest OS 부팅
자원 cgroups와 runtime 제한 vCPU·RAM·가상 장치 할당
위험 경계 kernel 취약점 영향을 공유 Guest와 host 사이 별도 kernel 경계
rootless는 container root를 host의 비특권 UID 범위에 매핑해 피해를 줄이지만 완전한 보안 경계는 아닙니다. host kernel, container engine, OCI runtime과 이미지의 취약점을 계속 패치하고 불필요한 capability·device·socket·host namespace를 주지 마십시오.

리눅스 컨테이너 구성 요소

구성 요소 역할 대표 확인 방법
OCI image root filesystem layer와 실행 metadata podman image inspect·history
Container engine pull·build·network·storage·lifecycle podman info
OCI runtime namespace·cgroup을 만들고 process 실행 crun –version 또는 runc –version
conmon container process 감시·stdio·exit 처리 podman info –debug
namespaces process가 보는 ID·mount·network 격리 lsns·/proc/PID/ns
cgroups v2 CPU·memory·I/O·PID 회계와 제한 podman stats·/sys/fs/cgroup
SELinux/seccomp 허용된 파일·system call 범위 제한 getenforce·podman inspect

리눅스 컨테이너 실습 환경 확인

Podman과 kernel 기능 확인

uname -r
cat /etc/os-release
podman --version
podman info --debug

podman info --format '{{.Host.CgroupsVersion}}'
stat -fc %T /sys/fs/cgroup
getenforce
systemctl is-active firewalld

cgroup2fs와 Podman의 cgroup v2 보고가 일치하는지 확인합니다. Quadlet은 cgroup v2를 요구합니다. SELinux가 Enforcing인 상태에서 volume label을 올바르게 설정해야 하며, 동작하지 않는다는 이유로 보안 기능 전체를 비활성화해서는 안 됩니다.

rootless UID·GID 범위 확인

id
grep -E "^${USER}:" /etc/subuid /etc/subgid
command -v newuidmap newgidmap
command -v pasta

podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map

rootless Podman은 /etc/subuid와 /etc/subgid의 추가 ID 범위를 사용해 container UID를 host의 비특권 UID로 매핑합니다. 범위가 없으면 관리자가 usermod –add-subuids와 –add-subgids로 충돌하지 않는 범위를 배정해야 합니다. 공유 NFS home은 user namespace를 이해하지 못하므로 rootless graphroot를 로컬 filesystem에 두는 설계를 검토하십시오.

리눅스 컨테이너 namespaces 실습

namespace는 process가 보는 global resource의 view를 분리합니다. PID namespace 안에서는 process 번호가 다르게 보이고, mount namespace는 mount table, network namespace는 interface·route·port, user namespace는 UID·GID와 capability 범위를 분리합니다.

현재 namespace 목록

lsns
readlink /proc/self/ns/user
readlink /proc/self/ns/pid
readlink /proc/self/ns/mnt
readlink /proc/self/ns/net

비특권 user·PID namespace 만들기

unshare --user --map-root-user --pid --fork sh -c '
  id
  echo "namespace PID: $$"
  readlink /proc/self/ns/user
  readlink /proc/self/ns/pid
'

출력의 uid=0은 새 user namespace 내부의 root일 뿐 host root가 아닙니다. host UID mapping과 허용된 capability가 제한되므로 host의 모든 파일을 읽거나 장치를 제어할 수 없습니다. 단, kernel을 공유하므로 kernel 취약점과 잘못된 device·socket mount는 여전히 위험합니다.

리눅스 컨테이너 OCI 이미지와 digest

이미지는 변경 불가능한 layer와 config, entrypoint, 환경 변수 같은 metadata로 구성됩니다. tag는 registry에서 다른 digest를 가리킬 수 있으므로 운영 배포에서는 검증한 manifest digest를 기록하고 서명·SBOM·취약점 정책을 함께 적용합니다.

정규화된 이미지 이름으로 pull

IMAGE='docker.io/library/busybox:1.36.1'
podman pull "$IMAGE"

podman image inspect "$IMAGE"   --format 'ID={{.Id}} Digest={{.Digest}} Created={{.Created}}'
podman history --no-trunc "$IMAGE"
podman images --digests

short name은 registries.conf 설정에 따라 다른 registry로 해석될 수 있으므로 registry와 namespace를 포함한 전체 이름을 사용합니다. tag를 개발 편의로 썼다면 inspect가 반환한 digest를 승인 기록에 남기고 Quadlet·배포 manifest에서는 image@sha256 형식으로 고정하십시오.

OCI manifest 확인

skopeo inspect   docker://docker.io/library/busybox:1.36.1   | jq '{Name,Digest,Created,Architecture,Os}'

skopeo inspect --raw   docker://docker.io/library/busybox:1.36.1   | jq .

리눅스 컨테이너 rootless Podman 실행

BusyBox httpd를 loopback에만 공개하고 root filesystem을 read-only로 두며 모든 기본 capability, privilege 상승, 과도한 PID와 memory·CPU 사용을 제한합니다. 애플리케이션에 필요한 쓰기 경로만 tmpfs 또는 명시적 volume으로 제공합니다.

IMAGE='docker.io/library/busybox:1.36.1'

podman run --detach --rm   --name web-demo   --read-only   --cap-drop=all   --security-opt=no-new-privileges   --pids-limit=128   --memory=256m   --cpus=0.50   --publish 127.0.0.1:8080:8080   --tmpfs /tmp:rw,noexec,nosuid,nodev,size=32m   "$IMAGE" httpd -f -p 8080

실행 상태와 제한 확인

podman ps
podman port web-demo
curl --fail --silent --show-error http://127.0.0.1:8080/

podman stats --no-stream web-demo
podman top web-demo user hpid pid args
podman inspect web-demo > web-demo.inspect.json
jq '.[0].HostConfig | {ReadonlyRootfs,Memory,NanoCpus,PidsLimit}'   web-demo.inspect.json
–publish에서 host IP를 생략하면 모든 interface에 공개될 수 있습니다. 로컬 reverse proxy 뒤에 둘 서비스는 127.0.0.1에 바인딩하고, 외부 공개가 필요하면 firewalld zone·source·TLS·인증 정책을 별도로 검토하십시오.

리눅스 컨테이너 process와 namespace 추적

HOST_PID=$(podman inspect --format '{{.State.Pid}}' web-demo)
printf 'host PID=%s
' "$HOST_PID"

ps -o user,pid,ppid,cmd -p "$HOST_PID"
sudo lsns -p "$HOST_PID"
sudo readlink "/proc/${HOST_PID}/ns/user"
sudo readlink "/proc/${HOST_PID}/ns/net"
sudo cat "/proc/${HOST_PID}/cgroup

container 내부 PID 1도 host에서는 일반 PID로 보입니다. nsenter는 격리 경계 안을 진단할 수 있는 강한 권한이므로 운영 접근을 제한하고, 먼저 podman exec·logs·inspect 같은 engine 인터페이스를 사용하십시오.

podman exec web-demo sh -c '
  echo "container PID=$$"
  id
  cat /proc/self/status | grep -E "^(Name|Pid|NSpid|CapEff|NoNewPrivs):"
'

podman logs web-demo
podman events --since 10m --filter container=web-demo

리눅스 컨테이너 cgroups v2 실습

cgroups v2는 격리가 아니라 자원 회계와 제한을 담당합니다. memory 제한은 OOM 위험을, pids 제한은 fork bomb 확산을, CPU quota는 noisy neighbor 영향을 줄입니다. rootless 환경에서는 systemd user delegation과 host 정책에 따라 일부 controller 제한이 허용되지 않을 수 있습니다.

podman stats --no-stream web-demo
podman inspect web-demo --format '{{.State.CgroupPath}}'

systemd-cgls --user-unit "user@${UID}.service"
systemctl --user status

podman update   --memory=192m   --pids-limit=96   web-demo
podman stats --no-stream web-demo

limit을 지나치게 낮추면 정상 부하에서도 OOM kill이나 요청 실패가 발생합니다. 애플리케이션 메모리 모델, JVM·worker 수, health check와 재시작 정책을 함께 조정하고 host 전체 여유 자원과 pressure stall information도 모니터링하십시오.

리눅스 컨테이너 volume과 SELinux

전용 host 디렉터리와 private label

install -d -m 0750 "$HOME/container-data"
printf 'hello from a labeled volume
' > "$HOME/container-data/index.html"

podman run --rm   --read-only   --cap-drop=all   --security-opt=no-new-privileges   --volume "$HOME/container-data:/data:ro,Z"   docker.io/library/busybox:1.36.1   cat /data/index.html

ls -Zd "$HOME/container-data

:Z는 해당 경로를 한 container 전용 private SELinux label로 relabel합니다. 여러 container가 공유해야 하는 경로는 :z를 검토하지만, system directory 전체나 민감한 home을 광범위하게 relabel하면 host 서비스가 망가질 수 있습니다. 전용 디렉터리만 mount하고 backup·ownership·UID mapping을 먼저 설계하십시오.

리눅스 컨테이너 보안 안티패턴

안티패턴 위험 권장 대안
전체 특권 모드 device·capability·LSM 경계 대부분 해제 필요한 capability·device만 개별 허용
host network/PID host namespace와 관찰 범위 공유 전용 network namespace와 명시적 port
Podman/Docker socket mount host에서 임의 container·mount 실행 가능 제한된 API proxy 또는 별도 자동화 계정
latest tag 재배포 결과가 변함 검증한 digest와 서명 정책
보안 기능 전체 비활성화 SELinux·방화벽 방어 계층 상실 label·port·policy를 필요한 범위만 수정
curl 결과를 shell로 연결 검토·무결성 확인 없이 원격 코드 실행 공식 package와 서명·checksum 검증

권한과 mount 감사

podman inspect web-demo   --format '{{json .HostConfig.SecurityOpt}} {{json .HostConfig.CapDrop}}'

podman inspect web-demo   --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'

podman diff web-demo
podman top web-demo capeff label

리눅스 컨테이너 Quadlet 운영

일회성 podman run을 shell script로 감싸기보다 Quadlet .container 파일을 rootless user unit 검색 경로에 두면 systemd가 service lifecycle과 로그를 관리할 수 있습니다. 운영에서는 아래 Image 값을 검증한 sha256 digest로 바꾸십시오.

일회성 실습 container 종료

podman stop web-demo
podman ps --all --filter name=web-demo

# 앞에서 --rm으로 실행했으므로 정상 종료 뒤 container가 남지 않아야 합니다.
podman container exists web-demo; printf 'exit=%s
' "$?"

~/.config/containers/systemd/web-demo.container

[Unit]
Description=Rootless read-only web demo

[Container]
Image=docker.io/library/busybox:1.36.1
ContainerName=web-demo
Exec=httpd -f -p 8080
PublishPort=127.0.0.1:8080:8080
ReadOnly=true
NoNewPrivileges=true
DropCapability=all
PidsLimit=128
Memory=256M

[Service]
Restart=on-failure
TimeoutStartSec=120

[Install]
WantedBy=default.target

Quadlet 생성 결과와 service 확인

mkdir -p "$HOME/.config/containers/systemd"
chmod 0700 "$HOME/.config/containers/systemd"
chmod 0644 "$HOME/.config/containers/systemd/web-demo.container"

systemctl --user daemon-reload
systemctl --user start web-demo.service
systemctl --user status web-demo.service
journalctl --user -u web-demo.service --since '-10 min'

podman info --format '{{.Host.CgroupsVersion}}'
curl --fail --silent --show-error http://127.0.0.1:8080/

Quadlet generated service를 직접 enable하는 대신 .container 파일의 [Install] WantedBy가 generator에서 반영되게 합니다. 로그아웃 뒤에도 rootless service를 유지하려면 관리자가 loginctl enable-linger를 사용할 수 있지만, 해당 사용자의 process가 로그인 없이 계속 실행되는 운영·보안 영향을 승인한 뒤 적용하십시오.

리눅스 컨테이너 진단 순서

  1. podman ps –all과 service 상태로 exit code·restart loop를 확인합니다.
  2. podman logs와 events로 애플리케이션·engine 이벤트 시각을 맞춥니다.
  3. image ID·digest·command·environment·mount·security option을 inspect합니다.
  4. port binding, rootless network, DNS와 host firewall 경계를 확인합니다.
  5. SELinux AVC, volume label, UID mapping과 파일 권한을 확인합니다.
  6. cgroup memory·PID·CPU 제한과 OOM·pressure 지표를 확인합니다.
  7. 같은 digest와 최소 입력으로 격리된 host에서 재현합니다.
podman ps --all --size
podman inspect web-demo
podman logs --timestamps web-demo
podman events --since 30m
podman stats --no-stream web-demo

journalctl --user -u web-demo.service --since '-30 min'
sudo ausearch -m AVC,USER_AVC -ts recent
ss -lntp | grep ':8080'

리눅스 컨테이너 운영 체크리스트

  • container와 VM의 shared-kernel 차이, namespace와 cgroup의 책임을 구분했습니다.
  • rootless subuid·subgid, cgroup v2, runtime, local storage와 network 도구를 확인했습니다.
  • 전체 registry 이름과 검증한 image digest, 서명·SBOM·취약점 결과를 기록했습니다.
  • read-only rootfs, no-new-privileges, capability drop, PID·memory·CPU 제한을 적용했습니다.
  • port는 필요한 host IP에만 bind하고 privileged·host namespace·engine socket을 피했습니다.
  • 전용 volume과 SELinux label을 사용하고 보안 기능을 비활성화하지 않았습니다.
  • Quadlet service, 로그, health, backup·restore와 업데이트 rollback을 검증했습니다.

리눅스 컨테이너 공식 문서와 관련 글

정리

리눅스 컨테이너: 안전한 운영은 이미지를 실행하는 명령 하나가 아니라 shared kernel의 위험을 이해하고 namespace, cgroups v2, user mapping, SELinux와 OCI supply chain을 함께 관리하는 일입니다. rootless를 기본으로 하고 digest 고정, 최소 capability·mount·port, read-only filesystem과 resource limit을 적용하십시오. 마지막으로 Quadlet과 systemd 로그, health·backup·rollback을 검증해야 재현 가능한 서비스가 됩니다.