리눅스 컨테이너: 컨테이너는 작은 가상 머신이 아니라 호스트 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 버전에 따라 달라질 수 있으므로 각 단계에서 실제 상태를 확인하십시오.

리눅스 컨테이너와 가상 머신 차이
| 항목 | 컨테이너 | 가상 머신 |
|---|---|---|
| Kernel | 호스트 kernel 공유 | Guest OS별 kernel |
| 격리 | namespaces·LSM·seccomp·capabilities | hypervisor와 가상 하드웨어 |
| 이미지 | OCI layer와 metadata | 전체 디스크 이미지 |
| 시작 | 격리된 프로세스 생성 | Guest OS 부팅 |
| 자원 | cgroups와 runtime 제한 | vCPU·RAM·가상 장치 할당 |
| 위험 경계 | kernel 취약점 영향을 공유 | Guest와 host 사이 별도 kernel 경계 |
리눅스 컨테이너 구성 요소
| 구성 요소 | 역할 | 대표 확인 방법 |
|---|---|---|
| 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
리눅스 컨테이너 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가 로그인 없이 계속 실행되는 운영·보안 영향을 승인한 뒤 적용하십시오.
리눅스 컨테이너 진단 순서
- podman ps –all과 service 상태로 exit code·restart loop를 확인합니다.
- podman logs와 events로 애플리케이션·engine 이벤트 시각을 맞춥니다.
- image ID·digest·command·environment·mount·security option을 inspect합니다.
- port binding, rootless network, DNS와 host firewall 경계를 확인합니다.
- SELinux AVC, volume label, UID mapping과 파일 권한을 확인합니다.
- cgroup memory·PID·CPU 제한과 OOM·pressure 지표를 확인합니다.
- 같은 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을 검증했습니다.
리눅스 컨테이너 공식 문서와 관련 글
- Podman rootless 공식 문서
- podman run 보안·리소스 옵션
- Podman Quadlet 공식 문서
- Linux kernel cgroup v2 공식 문서
- Linux namespaces 매뉴얼
- 리눅스 커널 구조와 진단 가이드
- NetBox Docker 보안 구축·백업 가이드
정리
리눅스 컨테이너: 안전한 운영은 이미지를 실행하는 명령 하나가 아니라 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을 검증해야 재현 가능한 서비스가 됩니다.