docker

Rootful vs Rootless Podman네트워크 격리 문제

JohnnyDeveloper 2026. 1. 13. 16:49

Rootful vs Rootless Podman
네트워크 격리 문제

같은 호스트, 다른 세계: 네트워크 네임스페이스 격리와 해결 방법

Network Isolation Host IP 통신 User Namespace

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)

네트워크 네임스페이스 격리

graph TB subgraph Host["Linux Host (10.0.1.50)"] subgraph RootNS["Root 네트워크 네임스페이스"] RootLo["lo: 127.0.0.1
(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 주소를 사용하는 것입니다.

설정 방법

1

metadata-service를 모든 인터페이스에 바인딩

rootless 컨테이너의 포트를 0.0.0.0에 바인딩하여 외부에서 접근 가능하도록 설정

# docker-compose.yml
services:
  api:
    ports:
      - "0.0.0.0:8089:8089"  # 모든 인터페이스
2

auth-service가 호스트 IP로 접근

rootful 컨테이너에서 호스트의 실제 IP 주소를 사용

# auth-service 설정
METADATA_API_URL=http://10.0.1.50:8089
3

방화벽/보안 그룹으로 외부 접근 차단

포트는 열려 있지만 외부 인터넷에서는 접근 불가하도록 설정

# 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 소유
💡
Podman 공식 문서

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 vs Slirp4netns

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

실전 아키텍처 예제

시나리오: 마이크로서비스 인증 시스템

권장 아키텍처

graph TB subgraph AWS["AWS EC2 Instance (10.0.1.50)"] subgraph SG["Security Group"] SG1["8080: 외부 허용"] SG2["8089: VPC 내부만"] SG3["27017: 차단"] end subgraph Rootless["Rootless Podman (사용자: appuser)"] direction TB Auth["auth-service
: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 네트워크 성능

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
🚀
Socket Activation의 장점

소켓 활성화를 사용하면 네트워크 트래픽이 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 고려

참고 자료