홈으로 돌아가기

일회성 Docker Swarm 시크릿을 위한 FIFO

이 글은 명명된 FIFO 채널을 통해 Docker Swarm 시크릿을 보호하는 방법을 설명합니다. 시크릿은 애플리케이션으로 한 번 전송되며, docker exec를 통한 접근을 배제합니다. 공유 tmpfs 볼륨을 가진 두 컨테이너 기반 구현.

일회성 Docker Swarm 시크릿: docker exec에 대한 FIFO
Advertisement 728x90

Docker Swarm에서 FIFO 파이프를 활용한 일회성 시크릿 관리

Docker Swarm은 시크릿을 /run/secrets/에 읽기 전용 tmpfs 볼륨으로 마운트합니다. docker exec 접근 권한이 있는 누구나 언제든지 이를 읽을 수 있습니다. 해결책: 컨테이너 시작 시 FIFO(명명된 파이프)를 사용해 일회성 시크릿 전달.

표준 접근 방식의 문제점

시크릿은 매니저 노드의 Raft 로그에 저장되어 mTLS를 통해 워커로 전송되고, 컨테이너에 마운트됩니다. docker-compose.yml에서 다음과 같이 정의됩니다:

version: "3.8"
services:
  app:
    image: myapp:latest
    secrets:
      - db_password
      - api_key
secrets:
  db_password:
    external: true
  api_key:
    external: true

앱에서 읽기:

Google AdInline article slot
with open("/run/secrets/db_password") as f:
    db_password = f.read().strip()

docker exec을 통한 접근:

$ docker exec -it <container_id> cat /run/secrets/db_password
SuperSecretPassword123

호스트에서 접근:

sudo cat /var/lib/docker/containers/<container_id>/mounts/secrets/ioz4pc6q8fjsfhem4pwsr35u6
SuperSecretPassword123

기존 우회 방법의 실패 이유

  • rm /truncate/chmod: 읽기 전용 tmpfs
  • umount: CAP_SYS_ADMIN 필요
  • 환경 변수: /proc/1/environ에 노출, 자식 프로세스에 상속
  • 외부 매니저 (Vault): 추가 인프라 필요

FIFO 파이프 작동 원리

명명된 파이프의 주요 특징:

Google AdInline article slot
  • 읽는 프로세스가 없으면 쓰기가 블록됨
  • 쓰는 프로세스가 없으면 읽기가 블록됨
  • 첫 번째 읽기 시 데이터가 소모
  • 이후 읽기는 대기 상태

데모:

$ mkfifo /tmp/my_pipe
echo "secret_value" > /tmp/my_pipe  # 블록됨
cat /tmp/my_pipe                    # secret_value 출력, 데이터 소모
cat /tmp/my_pipe                    # 대기

솔루션 아키텍처

두 컨테이너가 FIFO가 포함된 tmpfs 볼륨을 공유:

  • secret-injector: FIFO 생성, 시크릿 쓰기 후 종료
  • app: FIFO에서 한 번 읽고, 메모리에만 시크릿 유지

앱 컨테이너에는 /run/secrets/가 없습니다.

Google AdInline article slot

구현

docker-compose.yml

version: "3.8"
services:
  secret-injector:
    image: alpine:3.19
    secrets:
      - db_password
      - api_key
    volumes:
      - secrets_pipe:/shared
    entrypoint: ["sh", "-c", "
      mkfifo /shared/db_password /shared/api_key &&
      cat /run/secrets/db_password > /shared/db_password &
      cat /run/secrets/api_key > /shared/api_key &
      wait
    "]
    deploy:
      restart_policy:
        condition: none
      resources:
        limits:
          memory: 16M
  app:
    image: myapp:latest
    volumes:
      - secrets_pipe:/shared:ro
    entrypoint: ["sh", "-c", "
      DB_PASSWORD=$(cat /shared/db_password) &&
      API_KEY=$(cat /shared/api_key) &&
      exec myapp
    "]
    depends_on:
      - secret-injector
volumes:
  secrets_pipe:
    driver: local
    driver_opts:
      type: tmpfs
      device: tmpfs
      o: size=1m,noexec,nosuid
secrets:
  db_password:
    external: true
  api_key:
    external: true

실행 순서

  • secret-injector:

- mkfifo /shared/db_password /shared/api_key

- cat /run/secrets/... > /shared/... (블록됨)

- wait로 컨테이너 유지

  • app:

- DB_PASSWORD=$(cat /shared/db_password) → 쓰기 프로세스 해제

- FIFO에서 데이터 소모

  • secret-injector 종료 (restart_policy: none)
  • app 실행 메모리에 시크릿 보관

공격자가 보는 내용

app에서:

$ docker exec -it <app_container> sh
$ ls /run/secrets/
ls: /run/secrets/: No such file or directory
$ cat /shared/db_password  # 대기
^C

secret-injector에서:

$ docker exec -it <injector_container> sh
Error: container is not running

강화: 파일 디스크립터 사용

환경 변수를 피하고 파일 디스크립터(FD)로 전달:

Entrypoint:

#!/bin/sh
exec 3< /shared/db_password
exec 4< /shared/api_key
exec myapp --db-password-fd=3 --api-key-fd=4

Python:

import os

def read_secret_from_fd(fd_num: int) -> str:
    with os.fdopen(fd_num, 'r') as f:
        return f.read().strip()

db_password = read_secret_from_fd(3)
api_key = read_secret_from_fd(4)

Go:

package main

import (
	"fmt"
	"io"
	"os"
)

func readSecretFromFD(fd int) (string, error) {
	f := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd))
	if f == nil {
		return "", fmt.Errorf("invalid file descriptor: %d", fd)
	}
	defer f.Close()
	data, err := io.ReadAll(f)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

시크릿은 프로세스 메모리에만 존재합니다.

주요 요점

  • FIFO로 한 번만 읽고 데이터 소모
  • secret-injector 종료로 docker exec 불가
  • 앱에 /run/secrets/ 없음
  • 환경 변수 대신 파일 디스크립터 선호
  • 외부 서비스 불필요

— Editorial Team

Advertisement 728x90

다음 읽기