Docker 시크릿과 민감 정보 관리 — 환경변수에 비밀번호를 넣으면 안 되는 이유
docker run -e DB_PASSWORD=mysecret123이라고 실행하면 비밀번호가 어디에 저장될까요?docker inspect한 번이면 누구나 볼 수 있습니다.
환경변수에 비밀번호를 넣으면 안 되는 이유
환경변수는 편리하지만, 민감한 정보를 담기에는 여러 노출 경로가 있습니다.
1. docker inspect로 노출
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 파일시스템으로 노출
# 호스트에서 (root 권한)
cat /proc/$(docker inspect --format '{{.State.Pid}}' myapp)/environ | tr '\0' '\n'
# DB_PASSWORD=supersecret
3. 자식 프로세스에 상속
# 컨테이너 내부에서 실행되는 모든 프로세스가 환경변수를 상속
docker exec myapp env | grep DB_PASSWORD
# DB_PASSWORD=supersecret
4. 로그에 출력
# 실수로 로그에 환경변수가 포함될 수 있음
import os
print(f"Starting with config: {os.environ}")
# Starting with config: {'DB_PASSWORD': 'supersecret', ...}
5. 이미지 히스토리에 남음
# Dockerfile에서 ENV로 설정하면 이미지에 영구 저장
ENV DB_PASSWORD=supersecret
# docker history로 누구나 확인 가능
Docker Secrets (Compose)
Docker Compose에서 시크릿을 파일 기반으로 안전하게 전달하는 방법입니다.
# 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
# 시크릿 파일 생성 (git에 포함하지 않도록 .gitignore에 추가)
mkdir -p secrets
echo -n "mysupersecretpassword" > secrets/db_password.txt
echo -n "ak_1234567890abcdef" > secrets/api_key.txt
컨테이너 내부에서의 동작
# 시크릿은 /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 접미사 환경변수를 지원합니다.
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 패턴을 지원하지 않는 이미지에서는 직접 파일을 읽어야 합니다.
// 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
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
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 — 빌드 시 시크릿 사용
빌드 과정에서 프라이빗 레지스트리 인증, 패키지 다운로드 등에 시크릿이 필요한 경우입니다.
잘못된 방법
# 절대 하지 마세요! ARG/ENV로 전달하면 이미지 레이어에 남음
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
RUN npm ci
RUN rm .npmrc # 삭제해도 이전 레이어에 남아있음!
올바른 방법: --mount=type=secret
# 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
# 빌드 시 시크릿 전달
docker buildx build \
--secret id=npmrc,src=$HOME/.npmrc \
-t myapp:latest .
SSH 키 마운트
프라이빗 Git 리포지토리에서 패키지를 가져와야 할 때입니다.
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
# SSH 에이전트를 통한 인증
RUN --mount=type=ssh \
git clone git@github.com:myorg/private-repo.git
# SSH 에이전트 포워딩으로 빌드
docker buildx build --ssh default -t myapp:latest .
Docker Swarm Secrets
Docker Swarm에서는 시크릿이 암호화되어 전송되고 저장됩니다.
# 시크릿 생성
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
# 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:
# 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 컨테이너
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
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에 추가하여 버전 관리 시스템에 포함되지 않도록 합니다.
댓글 로딩 중...