Rootful vs Rootless Podman
네트워크 격리 문제
같은 호스트, 다른 세계: 네트워크 네임스페이스 격리와 해결 방법
Podman을 사용하다 보면 묘한 상황에 직면합니다. 분명 같은 호스트에서 실행 중인 컨테이너들인데 서로 통신이 안 됩니다. curl http://localhost:8089는 실패하고, curl http://192.168.1.100:8089는 성공합니다. 왜 그럴까요?
답은 rootful과 rootless Podman이 서로 다른 네트워크 네임스페이스에서 동작하기 때문입니다. 이 글에서는 이 문제의 원인을 깊이 파헤치고, 실전에서 사용할 수 있는 해결 방법을 제시합니다.
문제 상황: 왜 localhost가 안 될까?
실제 케이스: 인증 서비스와 메타데이터 서비스
다음과 같은 아키텍처를 가정해봅시다:
- auth-service: Docker Registry 인증 서비스 (rootful systemd로 실행)
- metadata-service: 메타데이터 API 서비스 (rootless podman-compose로 실행)
- auth-service가 metadata-service의 API를 호출해야 함
# metadata-service의 docker-compose.yml
version: '3.8'
services:
api:
image: metadata-api:latest
ports:
- "8089:8089" # 이게 문제의 시작
mongodb:
image: mongo:7.0
# ports 섹션 없음 - 내부 전용
# auth-service 컨테이너에서 테스트
$ podman exec auth-service curl http://127.0.0.1:8089
# ❌ Connection refused
$ podman exec auth-service curl http://10.0.1.50:8089
# ✅ 성공!
127.0.0.1은 "내 네트워크 네임스페이스의 loopback"을 의미합니다. rootful과 rootless Podman은 서로 다른 네임스페이스에 있기 때문에, 같은 127.0.0.1이라도 완전히 다른 공간을 가리킵니다.
네트워크 네임스페이스: 격리의 원리
Linux 네트워크 네임스페이스란?
Linux 네트워크 네임스페이스는 독립된 네트워크 스택을 제공합니다. 각 네임스페이스는 다음을 독립적으로 가집니다:
- 네트워크 인터페이스 (eth0, lo 등)
- 라우팅 테이블
- 방화벽 규칙 (iptables/nftables)
- 소켓 (TCP/UDP)
네트워크 네임스페이스 격리
(root의 loopback)"] Auth["auth-service
(rootful systemd)"] Auth --> RootLo end subgraph UserNS["User 네트워크 네임스페이스"] UserLo["lo: 127.0.0.1
(user의 loopback)"] Meta["metadata-service
(rootless compose)"] Mongo["mongodb"] Meta --> UserLo Meta -.->|내부 DNS| Mongo end HostIP["물리 IP: 10.0.1.50
(공통 접점)"] RootNS -.->|접근 불가| UserNS UserNS -.->|접근 불가| RootNS Auth -->|통신 가능| HostIP Meta -->|통신 가능| HostIP end style RootLo fill:#fee2e2,stroke:#dc2626,stroke-width:2px style UserLo fill:#dcfce7,stroke:#22c55e,stroke-width:2px style HostIP fill:#dbeafe,stroke:#3b82f6,stroke-width:3px style Auth fill:#fed7aa,stroke:#f59e0b,stroke-width:2px style Meta fill:#d1fae5,stroke:#10b981,stroke-width:2px style Mongo fill:#e0e7ff,stroke:#6366f1,stroke-width:2px
Rootful vs Rootless: 실행 방식 비교
🔶 Rootful Podman
| 실행 권한 | root (sudo 필요) |
| 네임스페이스 | root 네트워크 네임스페이스 |
| 서비스 파일 | /etc/systemd/system/ |
| 시작 명령 | sudo systemctl start |
| 127.0.0.1 | root의 loopback |
🟢 Rootless Podman
| 실행 권한 | 일반 사용자 (sudo 불필요) |
| 네임스페이스 | 사용자별 네트워크 네임스페이스 |
| 서비스 파일 | ~/.config/systemd/user/ |
| 시작 명령 | systemctl --user start |
| 127.0.0.1 | 사용자의 loopback (다름!) |
User Namespace와 네트워크 격리
Rootless Podman은 User Namespace를 사용하여 컨테이너를 격리합니다. 이때 네트워크 네임스페이스도 함께 생성되므로:
# rootful auth-service의 관점
$ sudo ip netns list
# netns-xxxxx (auth-service의 네트워크 네임스페이스)
# rootless metadata-service의 관점
$ ip netns list
# netns-yyyyy (metadata-service의 네트워크 네임스페이스)
# 둘은 완전히 다른 공간!
해결 방법 1: 호스트 IP 사용 (권장)
가장 간단하고 안전한 방법은 호스트의 물리적 IP 주소를 사용하는 것입니다.
설정 방법
metadata-service를 모든 인터페이스에 바인딩
rootless 컨테이너의 포트를 0.0.0.0에 바인딩하여 외부에서 접근 가능하도록 설정
# docker-compose.yml
services:
api:
ports:
- "0.0.0.0:8089:8089" # 모든 인터페이스
auth-service가 호스트 IP로 접근
rootful 컨테이너에서 호스트의 실제 IP 주소를 사용
# auth-service 설정
METADATA_API_URL=http://10.0.1.50:8089
방화벽/보안 그룹으로 외부 접근 차단
포트는 열려 있지만 외부 인터넷에서는 접근 불가하도록 설정
# AWS Security Group
인바운드 규칙:
- 포트 8089: 차단 (외부 인터넷)
- 포트 8089: 허용 (VPC 내부만)
# 또는 iptables
sudo iptables -A INPUT -p tcp --dport 8089 -s 10.0.0.0/16 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8089 -j DROP
보안 계층화
네트워크 레벨
MongoDB는 포트를 외부에 노출하지 않음. podman-compose 네트워크 내부에서만 접근 가능.
방화벽 레벨
Security Group/iptables로 8089 포트를 내부 네트워크에서만 허용.
애플리케이션 레벨
/internal/* 엔드포인트는 API 키 또는 IP 기반 인증으로 추가 보호.
해결 방법 2: --network=host (신중하게)
--network=host를 사용하면 컨테이너가 호스트의 네트워크 네임스페이스를 직접 사용합니다.
--network=host는 네트워크 격리를 완전히 제거합니다. 컨테이너가 호스트의 모든 네트워크 서비스에 접근할 수 있으며, D-bus 같은 시스템 서비스에도 접근 가능합니다. 프로덕션 환경에서는 권장하지 않습니다.
# systemd 서비스 파일에서
ExecStart=/usr/bin/podman run \
--network host \
metadata-api:latest
# docker-compose.yml에서
services:
api:
network_mode: "host"
Rootless + --network=host 제약
Rootless 환경에서 --network=host를 사용할 때 주의할 점:
# rootless 컨테이너에서 --network=host 사용
$ podman run --network host myapp
# 이후 exec는 host 네임스페이스에 접근 불가!
$ podman exec myapp curl localhost:8089
# ❌ 실패 - host 네트워크 네임스페이스는 root 소유
Rootless 컨테이너에서 --network=host를 사용하면, 최초 podman run은 호스트 네임스페이스를 사용하지만, 이후 podman exec는 호스트 네임스페이스에 접근할 수 없습니다. 호스트 네트워크 네임스페이스가 root 사용자 소유이기 때문입니다.
해결 방법 3: 모두 rootful 또는 모두 rootless로 통일
옵션 A: 모두 rootful로 통일
장점
- 네트워크 격리 문제 해결
- localhost 통신 가능
- 기존 Docker 워크플로우와 유사
단점
- 보안 약화 (root 권한 필요)
- Rootless의 장점 상실
- 다중 사용자 환경 불리
# metadata-service도 rootful로 변경
sudo podman-compose up -d
# 또는 systemd 서비스 파일 생성
sudo podman generate systemd --new --files --name metadata-api
옵션 B: 모두 rootless로 통일 (권장)
장점
- 최고 수준의 보안
- 다중 사용자 환경 안전
- 네트워크 격리 문제 해결
- Podman의 철학에 부합
단점
- 기존 rootful 서비스 재구성 필요
- 1024 이하 특권 포트 제약
- 초기 설정 복잡
# auth-service를 rootless로 변경
# 1. 기존 rootful 서비스 중지
sudo systemctl stop container-auth-service
sudo systemctl disable container-auth-service
# 2. rootless Quadlet으로 재작성
# ~/.config/containers/systemd/auth-service.container
[Unit]
Description=Auth Service (Rootless)
After=network-online.target
[Container]
Image=auth-service:latest
PublishPort=8080:8080
Network=metadata-net.network
Environment=METADATA_API_URL=http://metadata-api:8089
[Service]
Restart=always
[Install]
WantedBy=default.target
# 3. 서비스 시작
systemctl --user daemon-reload
systemctl --user enable --now auth-service
해결 방법 4: Pasta 네트워크 모드 (Podman 5.0+)
Podman 5.0부터는 pasta가 기본 네트워크 드라이버입니다. pasta는 성능과 보안을 모두 개선합니다.
Pasta는 slirp4netns보다 빠르며, 호스트의 IP 주소와 라우팅을 자동으로 복사합니다. 동적 포트 포워딩을 지원하여 더 나은 네트워크 성능을 제공합니다.
# Pasta 네트워크 모드 확인
$ podman info -f '{{.Host.RootlessNetworkCmd}}'
pasta
# Pasta 옵션으로 호스트 접근 허용
$ podman run --network pasta:--map-gw myapp
# 이제 컨테이너에서 게이트웨이 주소로 호스트 접근 가능
$ podman exec myapp curl http://<gateway-ip>:8089
실전 아키텍처 예제
시나리오: 마이크로서비스 인증 시스템
권장 아키텍처
:8080
(systemd user)"] Meta["metadata-api
:8089
(podman-compose)"] Mongo["mongodb
:27017
(내부 전용)"] Auth -->|http://10.0.1.50:8089| Meta Meta -->|mongodb://mongodb:27017| Mongo end Client["외부 클라이언트"] -->|HTTPS| SG1 SG1 --> Auth Internal["내부 서비스"] -->|HTTP| SG2 SG2 --> Meta end style Auth fill:#d1fae5,stroke:#10b981,stroke-width:2px style Meta fill:#d1fae5,stroke:#10b981,stroke-width:2px style Mongo fill:#e0e7ff,stroke:#6366f1,stroke-width:2px style SG1 fill:#dcfce7,stroke:#22c55e,stroke-width:2px style SG2 fill:#fed7aa,stroke:#f59e0b,stroke-width:2px style SG3 fill:#fee2e2,stroke:#ef4444,stroke-width:2px
설정 파일
# ~/.config/containers/systemd/auth-service.container
[Unit]
Description=Authentication Service
After=network-online.target
[Container]
Image=localhost/auth-service:latest
PublishPort=8080:8080
Environment=METADATA_API_URL=http://10.0.1.50:8089
Environment=PORT=8080
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target
# ~/metadata-service/docker-compose.yml
version: '3.8'
services:
api:
image: metadata-api:latest
ports:
- "0.0.0.0:8089:8089" # 모든 인터페이스
environment:
- MONGO_URL=mongodb://mongodb:27017/metadata
depends_on:
- mongodb
networks:
- metadata-net
mongodb:
image: mongo:7.0
volumes:
- mongodb-data:/data/db
networks:
- metadata-net
# ports 섹션 없음 - 외부 노출 안 함
volumes:
mongodb-data:
networks:
metadata-net:
driver: bridge
트러블슈팅
문제 1: 호스트 IP를 모르겠어요
# 호스트 IP 확인
$ ip addr show | grep 'inet ' | grep -v '127.0.0.1'
inet 10.0.1.50/24 brd 10.0.1.255 scope global eth0
# 또는 간단하게
$ hostname -I
10.0.1.50
# AWS EC2에서
$ curl http://169.254.169.254/latest/meta-data/local-ipv4
10.0.1.50
문제 2: 포트가 이미 사용 중
# 포트 사용 프로세스 확인
$ sudo ss -tulpn | grep :8089
tcp LISTEN 0 128 0.0.0.0:8089 0.0.0.0:* users:(("podman",pid=12345))
# rootful vs rootless 컨테이너 구분
$ podman ps -a
# (rootful로 실행한 경우)
$ podman --remote ps -a
# (rootless로 실행한 경우)
문제 3: SELinux 컨텍스트 오류
# 볼륨 마운트 시 :Z 옵션 사용
$ podman run -v /host/path:/container/path:Z myapp
# SELinux 로그 확인
$ sudo ausearch -m avc -ts recent
# SELinux 임시 비활성화 (테스트용)
$ sudo setenforce 0
성능 고려사항
Rootless Podman은 pasta(또는 slirp4netns) 네트워크 드라이버를 사용하여 성능 페널티가 있습니다. 하지만 Socket Activation을 사용하면 이 페널티를 피할 수 있습니다.
Socket Activation으로 성능 개선
# ~/.config/systemd/user/metadata-api.socket
[Unit]
Description=Metadata API Socket
[Socket]
ListenStream=8089
[Install]
WantedBy=sockets.target
# ~/.config/systemd/user/metadata-api.service
[Unit]
Description=Metadata API Service
Requires=metadata-api.socket
After=metadata-api.socket
[Service]
ExecStart=/usr/bin/podman run --rm \
--name metadata-api \
--sdnotify=conmon \
localhost/metadata-api:latest
소켓 활성화를 사용하면 네트워크 트래픽이 pasta를 거치지 않고 직접 전달되어, 호스트 네트워크와 동일한 성능을 제공합니다. 필요할 때만 컨테이너를 시작하여 리소스도 절약할 수 있습니다.
권장 사항 정리
| 상황 | 권장 방법 | 이유 |
|---|---|---|
| 프로덕션 서버 | 모두 rootless + 호스트 IP | 최고 보안, 최소 설정 변경 |
| 개발/테스트 | --network=host | 빠른 설정, 보안 덜 중요 |
| 레거시 시스템 | 호스트 IP + 방화벽 | 기존 인프라 변경 최소화 |
| 신규 프로젝트 | 모두 rootless Quadlet | 미래 지향적, 모범 사례 |
| 마이크로서비스 | rootless + 내부 DNS | 서비스 디스커버리 용이 |
결론
Rootful과 rootless Podman 간의 네트워크 격리는 보안을 위한 설계상 특성입니다. 이를 "문제"로 볼 것이 아니라, 올바른 아키텍처를 설계하는 기회로 삼아야 합니다.
핵심 원칙
같은 localhost라도 네트워크 네임스페이스가 다르면 완전히 다른 공간입니다. 호스트 IP만이 공통 접점입니다.
보안 우선
가능하면 모두 rootless로 통일하세요. 보안은 나중에 추가하기 어렵습니다.
미래 지향
Podman 5.0+에서는 pasta가 기본이며, Quadlet과 함께 사용하면 최고의 경험을 제공합니다.
- ✓ 모든 서비스를 rootless로 통일할 수 있는지 검토
- ✓ 호스트 IP를 사용할 때 방화벽/보안 그룹 설정 확인
- ✓ MongoDB 같은 내부 서비스는 포트 노출 안 함
- ✓ SELinux 환경에서는 :Z 옵션 필수
- ✓ 성능이 중요하면 Socket Activation 고려
참고 자료
- Podman Rootless 튜토리얼: Official Guide
- 네트워크 네임스페이스: Basic Networking
- 성능 가이드: Performance Guide
- Socket Activation: Socket Activation Tutorial
- Pasta 네트워크: Pasta Documentation
'docker' 카테고리의 다른 글
| Rootless 컨테이너 운영 필수 설정:Linger 활성화 (1) | 2026.01.19 |
|---|---|
| Docker Compose 대안 (0) | 2026.01.13 |
| Podman vs Docker 2026 (0) | 2026.01.13 |
| Docker Registry v3 권한 제어: docker_auth + Casbin + JWT/JWK로 인증/인가 구현 (0) | 2026.01.12 |
| Podman과 Caddy로 구축하는 프라이빗 컨테이너 레지스트리: 완벽 가이드 (0) | 2025.12.09 |