Theme:

docker run -e DB_PASSWORD=mysecret123이라고 실행하면 비밀번호가 어디에 저장될까요? docker inspect 한 번이면 누구나 볼 수 있습니다.

환경변수에 비밀번호를 넣으면 안 되는 이유

환경변수는 편리하지만, 민감한 정보를 담기에는 여러 노출 경로가 있습니다.

1. docker inspect로 노출

BASH
docker run -d --name myapp -e DB_PASSWORD=supersecret myapp:latest

# 누구나 비밀번호를 볼 수 있음
docker inspect myapp --format '{{json .Config.Env}}' | jq
# [
#   "DB_PASSWORD=supersecret",
#   "PATH=/usr/local/sbin:..."
# ]

2. /proc 파일시스템으로 노출

BASH
# 호스트에서 (root 권한)
cat /proc/$(docker inspect --format '{{.State.Pid}}' myapp)/environ | tr '\0' '\n'
# DB_PASSWORD=supersecret

3. 자식 프로세스에 상속

BASH
# 컨테이너 내부에서 실행되는 모든 프로세스가 환경변수를 상속
docker exec myapp env | grep DB_PASSWORD
# DB_PASSWORD=supersecret

4. 로그에 출력

PYTHON
# 실수로 로그에 환경변수가 포함될 수 있음
import os
print(f"Starting with config: {os.environ}")
# Starting with config: {'DB_PASSWORD': 'supersecret', ...}

5. 이미지 히스토리에 남음

DOCKERFILE
# Dockerfile에서 ENV로 설정하면 이미지에 영구 저장
ENV DB_PASSWORD=supersecret
# docker history로 누구나 확인 가능

Docker Secrets (Compose)

Docker Compose에서 시크릿을 파일 기반으로 안전하게 전달하는 방법입니다.

YAML
# compose.yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

  api:
    image: myapi:latest
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
BASH
# 시크릿 파일 생성 (git에 포함하지 않도록 .gitignore에 추가)
mkdir -p secrets
echo -n "mysupersecretpassword" > secrets/db_password.txt
echo -n "ak_1234567890abcdef" > secrets/api_key.txt

컨테이너 내부에서의 동작

BASH
# 시크릿은 /run/secrets/에 파일로 마운트됨
docker compose exec api ls -la /run/secrets/
# -r--r--r--    1 root root   20 Mar 19 10:00 db_password
# -r--r--r--    1 root root   22 Mar 19 10:00 api_key

# 파일 내용 읽기
docker compose exec api cat /run/secrets/db_password
# mysupersecretpassword

시크릿은 tmpfs(메모리)에 마운트되어 디스크에 쓰이지 않습니다.

_FILE 접미사 패턴

많은 공식 이미지가 _FILE 접미사 환경변수를 지원합니다.

YAML
services:
  db:
    image: postgres:16-alpine
    environment:
      # 비밀번호를 직접 넣는 대신 파일 경로 지정
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
    secrets:
      - mysql_root_password

애플리케이션에서 시크릿 읽기

_FILE 패턴을 지원하지 않는 이미지에서는 직접 파일을 읽어야 합니다.

JAVASCRIPT
// Node.js
const fs = require('fs');

function getSecret(name) {
    try {
        return fs.readFileSync(`/run/secrets/${name}`, 'utf8').trim();
    } catch (e) {
        // 시크릿 파일이 없으면 환경변수 fallback (개발 환경)
        return process.env[name.toUpperCase()];
    }
}

const dbPassword = getSecret('db_password');
PYTHON
# Python
from pathlib import Path
import os

def get_secret(name):
    secret_path = Path(f'/run/secrets/{name}')
    if secret_path.exists():
        return secret_path.read_text().strip()
    return os.environ.get(name.upper())

db_password = get_secret('db_password')
JAVA
// Java
import java.nio.file.Files;
import java.nio.file.Path;

public String getSecret(String name) {
    Path secretPath = Path.of("/run/secrets/" + name);
    if (Files.exists(secretPath)) {
        return Files.readString(secretPath).trim();
    }
    return System.getenv(name.toUpperCase());
}

BuildKit Secret Mount — 빌드 시 시크릿 사용

빌드 과정에서 프라이빗 레지스트리 인증, 패키지 다운로드 등에 시크릿이 필요한 경우입니다.

잘못된 방법

DOCKERFILE
# 절대 하지 마세요! ARG/ENV로 전달하면 이미지 레이어에 남음
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
RUN npm ci
RUN rm .npmrc  # 삭제해도 이전 레이어에 남아있음!

올바른 방법: --mount=type=secret

DOCKERFILE
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app

COPY package*.json ./

# 시크릿을 임시로 마운트 (이미지에 포함되지 않음)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

COPY . .
RUN npm run build
BASH
# 빌드 시 시크릿 전달
docker buildx build \
    --secret id=npmrc,src=$HOME/.npmrc \
    -t myapp:latest .

SSH 키 마운트

프라이빗 Git 리포지토리에서 패키지를 가져와야 할 때입니다.

DOCKERFILE
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app

# SSH 에이전트를 통한 인증
RUN --mount=type=ssh \
    git clone git@github.com:myorg/private-repo.git
BASH
# SSH 에이전트 포워딩으로 빌드
docker buildx build --ssh default -t myapp:latest .

Docker Swarm Secrets

Docker Swarm에서는 시크릿이 암호화되어 전송되고 저장됩니다.

BASH
# 시크릿 생성
echo "mysecretpassword" | docker secret create db_password -

# 또는 파일에서
docker secret create db_password ./secrets/db_password.txt

# 시크릿 목록
docker secret ls

# 서비스에 시크릿 연결
docker service create \
    --name myapi \
    --secret db_password \
    myapi:latest

Swarm 시크릿의 특징:

  • Raft 합의 알고리즘으로 매니저 노드에 암호화 저장
  • TLS로 암호화된 채널을 통해 워커 노드에 전송
  • 해당 시크릿이 필요한 컨테이너에만 마운트
  • 메모리(tmpfs)에만 존재

외부 시크릿 관리 도구 연동

HashiCorp Vault

YAML
# compose.yaml — Vault 사이드카 패턴
services:
  vault-agent:
    image: hashicorp/vault
    command: agent -config=/vault/config/agent.hcl
    volumes:
      - ./vault-config:/vault/config:ro
      - shared-secrets:/vault/secrets

  api:
    image: myapi:latest
    volumes:
      - shared-secrets:/run/secrets:ro
    depends_on:
      - vault-agent

volumes:
  shared-secrets:
HCL
# vault-config/agent.hcl
vault {
  address = "https://vault.example.com:8200"
}

auto_auth {
  method "kubernetes" {
    mount_path = "auth/kubernetes"
    config = {
      role = "myapp"
    }
  }
}

template {
  source      = "/vault/config/db-password.tpl"
  destination = "/vault/secrets/db_password"
}

AWS Secrets Manager + init 컨테이너

YAML
services:
  init-secrets:
    image: amazon/aws-cli
    command: >
      sh -c "aws secretsmanager get-secret-value
      --secret-id myapp/db-password
      --query SecretString --output text > /secrets/db_password"
    volumes:
      - secrets:/secrets
    profiles:
      - init

  api:
    image: myapi:latest
    volumes:
      - secrets:/run/secrets:ro

volumes:
  secrets:

.gitignore 설정

GITIGNORE
# .gitignore
secrets/
*.secret
*.key
*.pem
.env.local
.env.production

민감 정보 관리 원칙 정리

방법안전성복잡도사용 사례
환경변수 (-e)낮음낮음개발 환경만
.env 파일낮음낮음개발 환경만
Docker Secrets (Compose)보통보통소규모 프로덕션
Docker Secrets (Swarm)높음보통Swarm 환경
BuildKit --mount=type=secret높음보통빌드 시 시크릿
Vault/AWS SM매우 높음높음대규모 프로덕션

정리

  • 환경변수에 민감한 정보를 넣으면 docker inspect, /proc, 로그 등으로 노출될 수 있습니다. 프로덕션에서는 사용하지 마세요.
  • Docker Secrets는 시크릿을 tmpfs 파일로 마운트하여 디스크에 쓰지 않고, inspect에도 노출되지 않습니다.
  • 빌드 시 시크릿이 필요하면 **BuildKit의 --mount=type=secret**을 사용합니다. ARG/ENV로 전달하면 이미지 레이어에 남습니다.
  • 대규모 프로덕션에서는 HashiCorp Vault, AWS Secrets Manager 같은 전용 도구를 사용합니다.
  • 시크릿 파일은 반드시 .gitignore에 추가하여 버전 관리 시스템에 포함되지 않도록 합니다.
댓글 로딩 중...