docker

Docker Registry v3 권한 제어: docker_auth + Casbin + JWT/JWK로 인증/인가 구현

JohnnyDeveloper 2026. 1. 12. 15:34

Docker Registry v3 완벽 권한 제어

docker_auth + Casbin + JWT/JWK로 Enterprise급 인증/인가 구현

아키텍처 설계

sequenceDiagram participant Client as 클라이언트 participant Proxy as Reverse Proxy participant Registry as Registry participant Auth as docker_auth participant Script as htpasswd_auth.sh participant Casbin as Casbin Policy Client->>Proxy: podman pull image:tag Proxy->>Registry: GET /v2/image/manifests/tag Registry->>Client: 401 Unauthorized
WWW-Authenticate: Bearer realm=/auth Client->>Proxy: GET /auth?service=...&scope=... Proxy->>Auth: 토큰 요청 (Basic Auth) Auth->>Script: "username password" Script->>Script: htpasswd 검증 Script-->>Auth: exit 0 (성공) Auth->>Casbin: 인가 확인 (username, image, action) Casbin-->>Auth: allow Auth->>Auth: JWT 토큰 생성 및 서명 Auth-->>Client: {token: "eyJ..."} Client->>Proxy: GET /v2/image/manifests/tag
Authorization: Bearer eyJ... Proxy->>Registry: 요청 + 토큰 Registry->>Registry: JWK로 토큰 검증 Registry-->>Client: 200 OK + Manifest

디렉터리 구조

/srv/registry/
├── data/                    # Registry 이미지 저장소
├── auth/
│   └── htpasswd            # 기존 사용자 인증 파일
├── config/
│   └── config.yml          # Registry 설정
└── docker_auth/
    ├── Dockerfile          # apache2-utils 포함 커스텀 이미지
    ├── config/
    │   └── auth_config.yml # docker_auth 메인 설정
    ├── scripts/
    │   └── htpasswd_auth.sh # 외부 인증 스크립트
    ├── casbin/
    │   ├── model.conf      # Casbin RBAC 모델
    │   └── policy.csv      # 권한 정책
    └── ssl/
        ├── server.pem      # 공개키 (토큰 검증용)
        ├── server-key.pem  # 비밀키 (토큰 서명용)
        └── jwks.json       # JWK Set (Registry v3)

인증 스크립트 구현

htpasswd_auth.sh

#!/usr/bin/env sh set -eu HTPASSWD_FILE="${HTPASSWD_FILE:-/auth/htpasswd}" HTPASSWD_BIN="${HTPASSWD_BIN:-/usr/bin/htpasswd}" # 1. stdin에서 "user pass" 읽기 (docker_auth가 공백으로 구분) LINE="$(cat)" # 2. CR 제거 (Windows/Proxy 환경 대응) LINE=$(printf '%s' "$LINE" | tr -d '\r') # 3. 사용자명/비밀번호 분리 USER="$(printf '%s' "$LINE" | awk '{print $1}')" PASS="${LINE#"$USER"}" PASS="$(printf '%s' "$PASS" | sed -e 's/^[[:space:]]\+//')" # 4. 검증 [ -n "$USER" ] || exit 1 [ -n "$PASS" ] || exit 1 [ -r "$HTPASSWD_FILE" ] || exit 1 # 5. htpasswd 검증 (-i: stdin으로 비밀번호 전달) if printf '%s' "$PASS" | "$HTPASSWD_BIN" -v -i "$HTPASSWD_FILE" "$USER" >/dev/null 2>&1; then exit 0 else exit 1 fi
💡 핵심 포인트
  • awk/sed 파싱: docker_auth의 ext_auth는 "username password" 형식으로 stdin 전달
  • CR 제거: Windows 클라이언트나 프록시를 거칠 때 \r\n 개행 문제 방지
  • -i 옵션: 비밀번호를 명령줄이 아닌 stdin으로 전달 (ps 명령어에 노출 방지)

Casbin 권한 제어

model.conf

[request_definition] r = account, type, name, service, ip, action, labels [policy_definition] p = account, type, name, service, ip, action, labels [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = r.account == "admin" || ( g(r.account, p.account) && r.type == p.type && keyMatch2(r.name, p.name) && r.service == p.service && ipMatch(r.ip, p.ip) && (p.action == "*" || r.action == p.action) )

policy.csv

# format: p, account, type, name, service, ip, action, labels # 1. admin: 모든 권한 p, admin, registry, catalog, registry.example.com, 0.0.0.0/0, *, {} p, admin, repository, *, registry.example.com, 0.0.0.0/0, pull, {} p, admin, repository, *, registry.example.com, 0.0.0.0/0, push, {} # 2. 익명 사용자: 읽기 전용 p, "", repository, *, registry.example.com, 0.0.0.0/0, pull, {} # 3. 개발자별 권한 p, developer, repository, app-backend, registry.example.com, 0.0.0.0/0, pull, {} p, developer, repository, app-backend, registry.example.com, 0.0.0.0/0, push, {}

ACL (동적 네임스페이스)

# auth_config.yml의 acl 섹션 acl: # 1. 사용자는 자기 네임스페이스(username/*)에 모든 권한 - match: { account: "/.+/", name: "${account}/*" } actions: ["*"] # 2. 인증된 사용자는 catalog 조회 가능 - match: { account: "/.+/", type: "registry", name: "catalog" } actions: ["*"] # 3. 인증된 사용자는 모든 이미지 pull 가능 - match: { account: "/.+/" } actions: ["pull"]
✅ ACL의 장점

${account} 동적 치환으로 사용자별 네임스페이스 자동 할당

예: alice 사용자는 alice/* 이미지에 자동으로 모든 권한 획득

→ 사전 정책 추가 불필요!

JWK 변환 (Registry v3 핵심)

⚠️ Registry v3 필수 요구사항

Registry v3는 JWT 토큰 검증을 위해 JWK(JSON Web Key) 형식을 권장합니다.

docker_auth의 RSA 공개키를 JWK Set으로 변환하여 Registry에 제공해야 합니다.

Step 1: 토큰에서 kid 추출

# 1. 테스트 토큰 발급 TOKEN=$(curl -s -u admin:password \ 'https://registry.example.com/auth?service=registry.example.com&scope=registry:catalog:*' \ | jq -r '.token // .access_token') # 2. JWT 헤더에서 kid(Key ID) 추출 KID=$(echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null | jq -r .kid) echo "Extracted KID: $KID" # kid가 비어있으면 docker_auth 설정 문제 # → auth_config.yml에서 disable_legacy_key_id: true 확인

Step 2: RSA 공개키 추출

# docker_auth의 비밀키에서 공개키 추출 sudo openssl rsa -in /srv/registry/docker_auth/ssl/server-key.pem -pubout \ -out /srv/registry/docker_auth/ssl/jwt-pub.pem # 공개키 확인 cat /srv/registry/docker_auth/ssl/jwt-pub.pem # 출력 예시: # -----BEGIN PUBLIC KEY----- # MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... # -----END PUBLIC KEY-----

Step 3: PEM → JWK 변환

# 1. pem-jwk 도구 설치 sudo npm install -g pem-jwk # 2. PEM을 JWK로 변환 pem-jwk /srv/registry/docker_auth/ssl/jwt-pub.pem \ > /srv/registry/docker_auth/ssl/jwk.json # 3. kid 추가 + JWKS 형식으로 래핑 jq --arg kid "$KID" \ '(.kid=$kid) | .alg="RS256" | .use="sig" | {keys:[.]}' \ /srv/registry/docker_auth/ssl/jwk.json \ > /srv/registry/docker_auth/ssl/jwks.json # 4. 최종 JWKS 확인 cat /srv/registry/docker_auth/ssl/jwks.json | jq .
💡 JWKS 형식
{ "keys": [ { "kty": "RSA", "n": "xGOr1...", // 공개키 모듈러스 (Base64) "e": "AQAB", // 공개 지수 "kid": "ABC123", // 토큰 헤더의 kid와 매칭 "alg": "RS256", // 서명 알고리즘 "use": "sig" // 용도 (서명 검증) } ] }

Step 4: Registry config.yml 설정

# /srv/registry/config/config.yml version: 0.1 log: level: info http: addr: :5000 storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true auth: token: realm: https://registry.example.com/auth service: registry.example.com issuer: "Registry Auth" rootcertbundle: /docker_auth_ssl/server.pem jwks: /docker_auth_ssl/jwks.json # ← Registry v3 필수!

Step 5: Registry 컨테이너 재시작

# 1. 기존 컨테이너 중지 sudo systemctl stop container-registry.service sudo podman rm -f registry # 2. docker_auth SSL 마운트 포함하여 재생성 sudo podman run -d --name registry \ -p 127.0.0.1:5000:5000 \ -v /srv/registry/data:/var/lib/registry \ -v /srv/registry/config:/etc/docker/registry \ -v /srv/registry/docker_auth/ssl:/docker_auth_ssl:ro \ docker.io/library/registry:3 \ serve /etc/docker/registry/config.yml # 3. systemd 유닛 재생성 sudo podman generate systemd --name registry --files sudo mv container-registry.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now container-registry.service # 4. 로그 확인 (JWKS 로드 성공 메시지) sudo podman logs registry --tail 20

Step 6: JWK 검증

# 1. 새 토큰 발급 TOKEN=$(curl -s -u admin:password \ 'https://registry.example.com/auth?service=registry.example.com&scope=registry:catalog:*' \ | jq -r '.token') # 2. 토큰 페이로드 확인 echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq . # 기대 결과: { "iss": "Registry Auth", "sub": "admin", "aud": "registry.example.com", "exp": 1704067890, "access": [ { "type": "registry", "name": "catalog", "actions": ["*"] } ] } # 3. Registry API 호출 테스트 curl -H "Authorization: Bearer $TOKEN" \ https://registry.example.com/v2/_catalog # 성공 시: {"repositories":["app-backend","app-frontend"]}

배포 및 검증

전체 검증 시나리오

1 익명 사용자 읽기
podman logout registry.example.com podman pull registry.example.com/app-backend:latest # → 성공 (익명 pull 허용)
2 익명 사용자 쓰기 차단
podman push registry.example.com/test:1.0 # → 403 Forbidden (익명은 읽기만 가능)
3 Admin 전체 권한
podman login registry.example.com -u admin podman tag alpine:latest registry.example.com/production/app:1.0 podman push registry.example.com/production/app:1.0 # → 성공
4 일반 사용자 제한
podman login registry.example.com -u developer podman push registry.example.com/production/app:1.0 # → 403 Forbidden (production 이미지 쓰기 권한 없음)
5 사용자 네임스페이스 (ACL)
podman login registry.example.com -u alice podman tag alpine:latest registry.example.com/alice/myapp:1.0 podman push registry.example.com/alice/myapp:1.0 # → 성공 (ACL의 ${account} 동적 치환) podman login registry.example.com -u bob podman push registry.example.com/alice/myapp:2.0 # → 403 Forbidden (bob은 alice 네임스페이스 쓰기 불가)

운영 표준 절차

정책 관리 워크플로우

상황 조치 정책 파일
개인 프로젝트 username/project:tag 사용 정책 추가 불필요 (ACL 자동)
조직 공용 이미지 project:tag 사용 policy.csv에 수동 추가
팀 네임스페이스 team/project:tag 사용 policy.csv에 팀 권한 추가
임시 권한 부여 특정 사용자+이미지 policy.csv에 한시적 정책

새 이미지 등록 절차

# Case 1: 개인 네임스페이스 (정책 추가 불필요) podman login registry.example.com -u dev1 podman tag myimage:1.0 registry.example.com/dev1/myproject:1.0 podman push registry.example.com/dev1/myproject:1.0 # → ACL이 자동 처리 # Case 2: 조직 공용 이미지 (정책 추가 필요) # 1. policy.csv 편집 sudo vi /srv/registry/docker_auth/casbin/policy.csv # 2. 정책 추가 p, dev1, repository, backend-api, registry.example.com, 0.0.0.0/0, push, {} p, dev1, repository, backend-api, registry.example.com, 0.0.0.0/0, pull, {} # 3. docker_auth 재시작 sudo systemctl restart container-docker-auth.service # 4. 푸시 podman push registry.example.com/backend-api:1.0

정책 적용 주의사항

⚠️ 토큰 캐싱 지연

정책 변경 후 즉시 반영되지 않을 수 있습니다:

  • Hot-Reload 미지원: docker_auth 재시작 필수
  • 토큰 만료 시간: 기본 15분 (expiration: 900)
  • 클라이언트 캐싱: podman/docker가 토큰 재사용

즉시 반영 방법:

podman logout registry.example.com podman login registry.example.com

롤백 및 트러블슈팅

롤백 절차

# 1. docker_auth 중지 sudo systemctl stop container-docker-auth.service sudo systemctl disable container-docker-auth.service # 2. Registry 중지 sudo systemctl stop container-registry.service sudo podman rm -f registry # 3. 기존 설정 복원 sudo cp /srv/registry/config/config.yml.backup \ /srv/registry/config/config.yml sudo cp /etc/caddy/Caddyfile.backup \ /etc/caddy/Caddyfile # 4. Registry 기존 방식으로 재시작 sudo podman run -d --name registry \ -p 127.0.0.1:5000:5000 \ -v /srv/registry/data:/var/lib/registry \ -v /srv/registry/config:/etc/docker/registry \ docker.io/library/registry:3 # 5. Caddy 재시작 sudo systemctl restart caddy # 6. 검증 curl -u admin:password https://registry.example.com/v2/_catalog

트러블슈팅: kid 없음

증상

토큰 헤더에 kid가 없거나 null

# 원인: docker_auth 설정 누락 sudo vi /srv/registry/docker_auth/config/auth_config.yml # 확인: token: disable_legacy_key_id: true # ← 이 줄 필수! # 재시작 sudo systemctl restart container-docker-auth.service

트러블슈팅: JWKS 로드 실패

# Registry 로그 확인 sudo podman logs registry | grep -i jwks # 원인 1: 파일 권한 sudo chmod 644 /srv/registry/docker_auth/ssl/jwks.json # 원인 2: JWK 형식 오류 cat /srv/registry/docker_auth/ssl/jwks.json | jq . # keys 배열이 있어야 함: { "keys": [...] # ← 필수! } # 재생성 jq '{keys:[.]}' /srv/registry/docker_auth/ssl/jwk.json \ > /srv/registry/docker_auth/ssl/jwks.json

트러블슈팅: 정책 미반영

# 1. docker_auth 재시작 (필수) sudo systemctl restart container-docker-auth.service # 2. 기존 토큰 무효화 podman logout registry.example.com podman login registry.example.com # 3. 토큰 만료 시간 단축 (선택) sudo vi /srv/registry/docker_auth/config/auth_config.yml token: expiration: 300 # 5분 (기본 900초) sudo systemctl restart container-docker-auth.service
✅ 구현 완료 체크리스트
  • ✅ docker_auth 컨테이너 정상 실행
  • ✅ htpasswd 인증 스크립트 동작
  • ✅ Casbin 정책 로드 성공
  • ✅ JWK Set 생성 및 kid 매칭
  • ✅ Registry가 JWT 토큰 검증 성공
  • ✅ 익명 읽기, 인증 쓰기 분리
  • ✅ 사용자 네임스페이스 ACL 동작
  • ✅ 정책 변경 워크플로우 확립
  • ✅ 롤백 절차 준비

작성일: 2025년 1월

Docker Registry v3 docker_auth JWT/JWK Casbin RBAC Token Authentication Enterprise Security