테마 전환

Docker Secrets 완벽 가이드: 컨테이너 비밀번호와 API 키를 안전하게 관리하는 모범 사례

Easton editorial illustration: registry transfer crane

휴대폰이 쉴 새 없이 울렸습니다. 데이터베이스 이상 경고 문자만 24통이었습니다. SSH로 서버에 접속해 보니 데이터베이스 연결 수가 폭증했고 누군가 무차별 스캔을 벌이고 있었습니다.

로그를 확인하자 공격자는 올바른 비밀번호를 그대로 사용했습니다. 석 달 전에 docker-compose.yml에 적어 둔 MySQL 비밀번호였습니다. 지난달 인턴 한 명이 연습하려고 프로젝트를 자신의 공개 저장소로 fork하면서 비밀번호가 유출된 것입니다.

코드 유출을 제외하더라도 누군가 docker inspect 명령을 실행할 수 있다면 모든 환경 변수를 평문으로 볼 수 있습니다. 이 글에서는 컨테이너 비밀번호 관리에서 피해야 할 위험한 방식과 민감 정보를 실제로 보호할 수 있는 도구를 살펴봅니다.

환경 변수가 안전하지 않은 이유

흔히 쓰지만 안전하지 않은 세 가지 방식

먼저 제가 직접 저질렀던 실수부터 이야기해 보겠습니다. 여러분도 같은 방식을 쓰고 있지 않은지 확인해 보세요.

첫 번째: Dockerfile에 직접 기록하기

FROM node:18
ENV DATABASE_PASSWORD=MyS3cr3tP@ssw0rd
ENV API_KEY=sk-1234567890abcdef

최악의 방식입니다. Dockerfile의 각 명령은 이미지 레이어를 하나씩 만들기 때문에 나중에 ENV DATABASE_PASSWORD=""로 덮어써도 비밀번호가 이전 레이어에 남습니다. 이미지를 입수한 사람은 누구나 docker history <이미지 이름>으로 찾아낼 수 있습니다.

믿기 어렵나요? 저도 동료가 직접 보여 주기 전까지는 믿지 않았습니다. 동료는 회사 내부 이미지를 하나 내려받고 몇 가지 명령만으로 AWS Access Key를 찾아냈습니다. 3년 전에 퇴사한 직원이 남긴 Key였는데 아무도 삭제하지 않은 상태였습니다.

두 번째: docker-compose.yml에 기록하기

version: '3.8'
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: SuperSecretPassword123
      MYSQL_DATABASE: myapp

저도 오랫동안 이 방식을 썼습니다. 이유는 단순했습니다. 편리했기 때문입니다. docker-compose up -d 한 번이면 바로 실행되어 개발과 디버깅 효율이 높았습니다.

문제는 이 파일을 Git에 커밋해야 한다는 점입니다. 저장소가 비공개여도 한 사람의 권한 관리에 구멍이 생기거나 제가 겪은 것처럼 누군가 공개 저장소로 fork하면 비밀번호가 모두 유출됩니다.

세 번째: .env 파일을 쓰면서 버전 관리에 포함하기

# .env 파일
DB_PASSWORD=password123
API_SECRET=abcdef123456

조금 더 영리해 보이는 방식입니다. 민감 정보를 별도 .env 파일로 분리하고 docker-compose에서는 ${DB_PASSWORD}로 참조합니다.

하지만 많은 사람은 과거의 저를 포함해 ‘팀원도 프로젝트를 실행할 수 있어야 한다’며 .env까지 Git에 커밋합니다. 결과는 docker-compose에 직접 적는 것과 다르지 않습니다.

올바른 방법은 .env.gitignore에 추가하고 .env.example 템플릿을 제공하는 것입니다. 하지만 솔직히 처음부터 여기까지 생각하는 사람이 얼마나 될까요?

환경 변수의 세 가지 주요 보안 위험

.env를 .gitignore에 추가하고 코드가 유출되지 않았더라도 환경 변수는 여전히 안전하지 않습니다. 왜 그럴까요?

위험 1: docker inspect로 모든 값을 볼 수 있음

다음 명령을 실행해 보세요.

docker inspect <컨테이너 ID> | grep -A 20 "Env"

비밀번호를 포함한 모든 환경 변수가 평문으로 표시됩니다.

이는 Docker 데몬에 접근할 수 있는 운영자, DevOps 담당자, 서버 권한이 있는 개발자라면 누구든 비밀번호를 볼 수 있다는 뜻입니다. 해킹이나 고급 기술은 필요하지 않습니다. 명령 하나면 됩니다.

예전에 근무했던 스타트업에서는 편의를 위해 모두가 운영 서버 SSH 권한을 갖고 있었습니다. 어느 날 새로 온 프론트엔드 개발자가 ‘컨테이너 안에 무엇이 설치됐을까’ 궁금해 docker inspect를 실행했고 데이터베이스 비밀번호가 화면에 그대로 나타났습니다. 같은 팀원이어서 다행이었지만 악의가 있는 사람이었다면 어땠을까요?

위험 2: 컨테이너 내부 프로세스가 읽을 수 있음

환경 변수는 Docker뿐 아니라 컨테이너 안의 모든 프로세스에서도 볼 수 있습니다. Linux에서는 프로세스의 환경 변수가 /proc/<PID>/environ 파일에 저장됩니다.

애플리케이션의 의존성 중 하나라도 취약해 코드가 주입되면 공격자가 환경 변수를 직접 읽어 비밀번호를 얻을 수 있다는 뜻입니다.

지난해 한 Node.js 라이브러리에서 백도어가 발견됐는데, 환경 변수의 AWS 자격 증명을 몰래 읽어 외부로 전송하는 방식이었습니다. 피해 프로젝트는 수만 개가 넘었습니다.

위험 3: 로그를 통해 유출될 수 있음

더 알아채기 어려운 문제입니다.

애플리케이션은 시작할 때 설정 정보를 출력하거나 오류가 나면 환경 변수를 통째로 dump해 로그에 기록하기도 합니다. 로그 파일은 대개 권한 관리가 느슨하고 ELK 같은 중앙 로그 시스템으로 전송되기도 합니다.

한 팀이 모든 컨테이너 로그를 Elasticsearch 클러스터에 모으고 있었는데, 보안 감사에서 로그 속에 데이터베이스 비밀번호 수백 개가 그대로 남아 있는 것이 발견됐습니다. 개발자가 디버깅할 때 console.log(process.env)를 사용하고 지우는 것을 잊은 탓이었습니다.

비밀번호가 로그에 들어가면 백업, 아카이브 등 여러 곳에 복사본이 남기 때문에 완전히 제거하기도 어렵습니다.

실제 사례

이런 사고는 인터넷에서 적지 않게 발생하지만 많은 회사가 공개하지 않을 뿐입니다. 제가 아는 사례 몇 가지를 소개하겠습니다.

사례 1: GitHub 공개 저장소 유출

2023년 한 데이터 분석에 따르면 GitHub의 공개 저장소 100만 개 이상에 API 키, 데이터베이스 비밀번호 같은 민감 정보가 포함되어 있었습니다. 개발자가 어리석어서가 아니라 테스트 설정을 지우지 않았거나 실수로 .env를 커밋하는 등 순간적인 부주의에서 벌어지는 경우가 많습니다.

머신러닝 분야에서 일하는 친구의 팀은 학습 코드를 GitHub에 오픈 소스로 공개하면서 AWS 자격 증명도 함께 올렸습니다. 24시간 안에 누군가 그 자격 증명으로 GPU 인스턴스 10여 대를 띄워 채굴했고 청구액은 2만 달러를 훌쩍 넘었습니다.

사례 2: CI/CD pipeline 기록 유출

Jenkins, GitLab CI 같은 CI/CD 도구의 빌드 기록에는 환경 변수 로그가 남는 경우가 많습니다. 마스킹을 제대로 하지 않으면 그 로그 자체가 비밀번호 저장소가 됩니다.

제가 경험한 프로젝트는 GitLab CI로 자동 배포하고 있었습니다. 한 동료가 퇴사하기 전에 저장소 권한을 외주 팀에 열어 줬는데, 외주 직원 한 명이 과거 빌드 로그를 살펴보다 운영 데이터베이스 연결 정보를 찾아냈습니다. 다행히 최적화를 도우려다 발견해 먼저 알려 줬지만 악의가 있었다면 결과는 달랐을 것입니다.

사례 3: Docker Hub 이미지 스캔

Alibaba Cloud의 한 보안 보고서에서는 공개 Docker 이미지를 스캔한 결과 76%에 보안 취약점이 있었으며 그중 상당수에 하드코딩된 자격 증명이 포함됐다고 밝혔습니다.

일부 기업은 편의를 위해 내부 애플리케이션 이미지를 공개 Docker Hub에 올리면서 ‘우리처럼 작은 회사 이미지를 누가 일부러 뒤지겠나’라고 생각합니다. 하지만 이제는 이를 전문적으로 수행하는 자동 크롤러와 보안 스캔 도구가 많습니다. 이미지를 올린 지 몇 분 만에 전체 스캔이 끝날 수도 있습니다.

겁을 주려는 이야기가 아닙니다. 이런 문제는 실제로 발생하고 있으며 생각보다 훨씬 흔합니다. 중요한 점은 예방할 방법이 있다는 것입니다.

Docker Secrets란 무엇이며 어떻게 사용하는가

Docker Secrets의 작동 원리

이제 해결책을 살펴보겠습니다.

Docker Secrets는 Docker가 공식 제공하는 비밀 정보 관리 메커니즘입니다. 핵심 아이디어는 간단합니다. 키를 더 이상 평문 문자열로 두지 않고 암호화된 파일로 다룹니다.

작동 절차는 다음과 같습니다.

  1. secret을 만들면 Docker가 이를 암호화해 클러스터의 Raft 로그에 저장합니다.
  2. 컨테이너에 secret이 필요하면 Docker가 암호화된 TLS 연결을 통해 전달합니다.
  3. Secret은 컨테이너의 /run/secrets/ 디렉터리에 파일 형태로 마운트됩니다.
  4. 이 디렉터리는 디스크에 기록되지 않는 순수 메모리 파일 시스템인 tmpfs입니다.
  5. 컨테이너가 중지되면 secret은 메모리에서 자동으로 제거됩니다.

핵심은 secret이 환경 변수에 나타나지 않고 docker inspect 출력에도 보이지 않으며 docker commit으로 이미지에 포함되지 않는다는 점입니다.

조금 추상적으로 들리나요? 괜찮습니다. 저도 처음 Docker 문서를 읽었을 때는 이해하기 어려웠습니다. 직접 실행해 보면 금방 알 수 있습니다.

먼저 제약이 하나 있습니다. Docker Secrets는 기본적으로 Swarm 모드에서만 사용할 수 있습니다. 처음에는 이 점이 꽤 답답했습니다. 로컬의 단일 머신에서 개발하는데 왜 쓸 수 없을까요?

나중에 docker-compose도 기능은 약하지만 file secrets를 지원한다는 것을 알았습니다. 운영 환경 클러스터라면 Swarm mode나 Kubernetes를 사용하면 됩니다.

단계별 실습: MySQL + WordPress 사례

완전한 예제로 살펴보겠습니다. MySQL root 비밀번호와 사용자 비밀번호를 보호해야 하는 WordPress 블로그를 배포한다고 가정합니다.

1단계: secret 파일 만들기

먼저 비밀번호를 로컬 파일에 기록합니다. 이 파일은 Git에 커밋하면 안 됩니다.

echo "MyRootPassword123" > db_root_password.txt
echo "MyUserPassword456" > db_password.txt

2단계: Swarm 초기화하기(아직 초기화하지 않은 경우)

docker swarm init

명령은 이것 하나뿐입니다. 머신에서 Swarm 모드가 활성화되지 않았다면 이 명령을 실행하면 됩니다. 단일 머신에서도 Swarm을 사용할 수 있으며 여러 머신으로 클러스터를 구성할 필요는 없습니다.

3단계: Docker secrets 만들기

docker secret create mysql_root_password db_root_password.txt
docker secret create mysql_password db_password.txt

생성이 끝나면 로컬 비밀번호 파일을 즉시 삭제합니다.

rm db_root_password.txt db_password.txt

매우 중요한 단계입니다. Docker가 파일을 읽어 암호화해 저장했다면 로컬 평문 파일은 폐기해야 합니다.

secrets가 정상적으로 만들어졌는지 확인합니다.

docker secret ls

다음과 비슷한 출력이 나타납니다.

ID                          NAME                  CREATED         UPDATED
abc123...                   mysql_root_password   5 seconds ago   5 seconds ago
def456...                   mysql_password        3 seconds ago   3 seconds ago

secret의 내용은 볼 수 없고 이름과 메타데이터만 확인할 수 있습니다. 이것이 핵심입니다. secret은 일단 생성되면 관리자조차 평문 내용을 가져올 수 없습니다.

4단계: docker-compose.yml 만들기

version: '3.8'

secrets:
  mysql_root_password:
    external: true
  mysql_password:
    external: true

services:
  db:
    image: mysql:8.0
    secrets:
      - mysql_root_password
      - mysql_password
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
      MYSQL_USER: wordpress
      MYSQL_DATABASE: wordpress
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    image: wordpress:latest
    depends_on:
      - db
    ports:
      - "8080:80"
    secrets:
      - mysql_password
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password
      WORDPRESS_DB_NAME: wordpress

volumes:
  db_data:

몇 가지 핵심 사항에 주목하세요.

  1. 최상위 secrets 설정: 사용할 secrets를 선언합니다. external: true는 secret이 compose가 생성할 대상이 아니라 이미 존재한다는 뜻입니다.
  2. services.db.secrets: 해당 서비스가 사용할 secrets를 지정합니다.
  3. MYSQL_ROOT_PASSWORD_FILE: MySQL 공식 이미지는 _FILE 접미사가 붙은 환경 변수를 지원하며 환경 변수에서 직접 읽는 대신 파일에서 비밀번호를 읽습니다.

_FILE 접미사는 중요하지만 모든 이미지가 지원하는 것은 아닙니다. 이미지가 지원하지 않으면 시작 스크립트에서 직접 파일을 읽어야 합니다.

# entrypoint.sh 예시
export DB_PASSWORD=$(cat /run/secrets/db_password)
# 그런 다음 애플리케이션 시작

5단계: 배포하기

docker stack deploy -c docker-compose.yml myapp

여기서는 docker-compose up이 아니라 docker stack deploy를 사용합니다. Swarm mode에서는 stack 명령을 써야 합니다.

6단계: 검증하기

MySQL 컨테이너에 들어가 secret이 제대로 마운트됐는지 확인합니다.

docker exec -it <컨테이너 ID> sh
ls -la /run/secrets/

다음과 같은 출력이 나타납니다.

total 8
-r--r--r-- 1 root root 18 Dec 18 10:00 mysql_password
-r--r--r-- 1 root root 18 Dec 18 10:00 mysql_root_password

직접 읽어 봅니다.

cat /run/secrets/mysql_root_password

비밀번호가 표시됩니다. 하지만 핵심은 이 비밀번호가 메모리에만 존재하고 디스크에 기록되지 않으며 환경 변수에도 나타나지 않는다는 점입니다.

docker inspect에 비밀번호가 보이지 않는지도 확인합니다.

docker inspect <컨테이너 ID> | grep -i password

출력에는 MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password 같은 경로만 있고 비밀번호 자체는 보이지 않습니다.

이것이 Docker Secrets의 강점입니다. 컨테이너 안의 애플리케이션은 여전히 비밀번호를 읽을 수 있지만 외부에서는 평문을 얻을 수 없습니다.

Docker Secrets의 제약과 대안

솔직히 Docker Secrets가 완벽한 도구는 아닙니다. 주된 제약은 다음과 같습니다.

제약 1: Swarm 모드가 필수임

가장 골치 아픈 부분입니다. 로컬 개발 중이거나 단일 머신에서 컨테이너를 실행할 때도 secrets를 쓰려면 먼저 docker swarm init을 해야 합니다. 단일 머신에서도 Swarm을 활성화할 수 있지만 간단한 일에 지나치게 큰 도구를 쓰는 느낌이 듭니다.

Swarm을 활성화하면 익숙한 docker-compose up 대신 docker stack deploy를 써야 하므로 명령 사용 습관도 바꿔야 합니다.

제약 2: 만든 secret은 수정할 수 없음

Docker Secrets는 생성 후 내용이 읽기 전용입니다. 비밀번호를 바꾸려면 삭제한 뒤 다시 만들어야 합니다.

docker secret rm mysql_password
echo "NewPassword789" | docker secret create mysql_password -

그리고 이 secret을 사용하는 모든 서비스도 업데이트해야 합니다.

docker service update --secret-rm mysql_password --secret-add mysql_password myapp_db

상당히 번거롭습니다. 환경 변수라면 값을 바꾸고 컨테이너를 재시작하면 끝납니다.

제약 3: 독립 실행 컨테이너에는 사용할 수 없음

docker run으로 컨테이너를 직접 시작할 때는 Docker Secrets를 사용할 수 없습니다. docker service createdocker stack으로 배포한 서비스여야 합니다.

빠른 테스트에는 불편합니다. 데이터베이스 컨테이너를 잠깐 띄워 확인하고 싶을 뿐인데 Swarm 절차 전체를 준비해야 할 때가 있습니다.

단일 머신 개발의 대안: docker-compose file secrets

다행히 docker-compose에는 우회 방법인 file secrets가 있습니다. Swarm 모드가 필요하지 않고 로컬 파일을 secret으로 직접 마운트합니다.

설정은 대략 다음과 같습니다.

version: '3.8'

secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  db:
    image: mysql:8.0
    secrets:
      - db_password
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_password

이어서 로컬 비밀번호 파일을 만듭니다.

mkdir secrets
echo "MyPassword123" > secrets/db_password.txt

주의: 반드시 secrets/ 디렉터리를 .gitignore에 추가하세요.

이 방식은 적어도 비밀번호가 docker inspect에 나타나거나 이미지에 포함되지 않으므로 환경 변수보다 안전합니다. 하지만 다음 이유로 진짜 Docker Secrets만큼 안전하지는 않습니다.

  • 비밀번호 파일이 로컬 디스크에 여전히 평문으로 존재합니다.
  • 암호화 전송이 없습니다.
  • 중앙 집중식 관리가 없습니다.

그래도 로컬 개발 환경에는 충분합니다. 실제 운영 환경에서는 Swarm이나 Kubernetes를 사용하는 편이 좋습니다.

고급 활용 팁

기본 사용법에 익숙해졌다면 몇 가지 실용적인 팁도 알아 두세요.

팁 1: secret 마운트 경로 사용자 지정

기본적으로 secret은 /run/secrets/<secret_name>에 마운트되지만 경로와 파일 이름을 바꿀 수 있습니다.

services:
  myapp:
    image: myapp:latest
    secrets:
      - source: db_password
        target: /app/config/database.pwd
        mode: 0400

여기서 mode: 0400은 파일 권한을 소유자만 읽을 수 있도록 설정해 보안을 높입니다.

팁 2: 여러 환경 관리

개발, 테스트, 운영 환경에서는 서로 다른 비밀번호를 쓰되 secret 이름은 동일하게 유지합니다.

# 운영 환경
docker secret create db_password prod_password.txt

# 테스트 환경(다른 Swarm 클러스터)
docker secret create db_password test_password.txt

애플리케이션 코드는 실제 값을 신경 쓰지 않고 /run/secrets/db_password만 읽습니다. 따라서 코드는 완전히 동일하게 유지하고 서로 다른 환경의 클러스터에 배포할 수 있습니다.

팁 3: secret 교체

비밀번호를 정기적으로 바꾸는 것은 좋은 습관입니다. Docker의 secret 교체 절차가 조금 복잡해 보이지만 어렵지는 않습니다.

# 1. 새 비밀번호 생성
echo "NewPassword" | docker secret create db_password_v2 -

# 2. 서비스를 업데이트해 새 secret을 추가하고 기존 secret 제거
docker service update \
  --secret-rm db_password \
  --secret-add source=db_password_v2,target=/run/secrets/db_password \
  myapp_db

# 3. 서비스가 정상인지 확인한 뒤 기존 secret 삭제
docker secret rm db_password

여기에는 한 가지 요령이 있습니다. 새 secret의 이름은 db_password_v2지만 target 매개변수로 마운트 경로는 계속 /run/secrets/db_password를 사용합니다. 따라서 애플리케이션 코드를 바꿀 필요가 없습니다.

팁 4: stdin에서 secret 생성

디스크에 흔적을 남기고 싶지 않다면 표준 입력에서 바로 생성할 수 있습니다.

echo "MySecretPassword" | docker secret create db_password -

마지막 -는 stdin에서 읽는다는 뜻입니다. 임시 파일조차 만들 필요가 없습니다.

더 안전하게 하려면 shell 기록에 남지 않도록 직접 입력합니다.

docker secret create db_password -
# 비밀번호를 붙여 넣고 Ctrl+D를 눌러 종료

다른 비밀 정보 관리 도구 비교

네 가지 도구 비교표

Docker Secrets만이 유일한 선택지는 아닙니다. 프로젝트 규모와 인프라에 따라 다른 도구가 필요할 수 있습니다. 비교표를 정리하면 다음과 같습니다.

도구적합한 상황핵심 장점주요 단점학습 난이도
Docker SecretsDocker Swarm 클러스터기본 지원, 추가 비용 없음, 간단한 설정Swarm에서만 사용 가능, 비교적 기본적인 기능, 플랫폼 간 사용 불가⭐ 낮음
Kubernetes SecretsK8s 운영 환경K8s 생태계 기본 지원, Pod 수명 주기와 연동etcd가 기본적으로 암호화되지 않음, 기능이 기본적이며 안전하게 쓰려면 추가 설정 필요⭐⭐ 보통
HashiCorp Vault엔터프라이즈, 멀티 클라우드 환경, 엄격한 규정 준수가장 강력한 기능, 동적 키, 세밀한 권한, 감사 로그, 다양한 백엔드 지원복잡한 아키텍처, 별도 배포와 유지 관리 필요, 가파른 학습 곡선⭐⭐⭐ 높음
AWS Secrets Manager모든 인프라가 AWS에 있고 RDS, Lambda 등을 사용AWS 서비스와 긴밀하게 통합, RDS 비밀번호 자동 교체, 관리형 서비스라 운영 불필요AWS 종속, 멀티 클라우드 사용이 어려움, 사용 비용 발생⭐⭐ 보통

간단히 정리하면 다음과 같습니다.

  • 소규모 프로젝트, Docker Swarm → 무료이고 간단한 Docker Secrets면 충분합니다.
  • Kubernetes 운영 환경 → 먼저 K8s Secrets를 쓰고 External Secrets Operator로 외부 Vault와 연동합니다.
  • 대기업, 멀티 클라우드, 높은 보안 요구 → 초기 투자 후 장기적으로 이점이 있는 Vault를 선택합니다.
  • AWS 중심 환경 → RDS, Lambda와 잘 연동되는 Secrets Manager가 가장 편합니다.

절대적으로 최고의 도구는 없으며 상황에 가장 알맞은 도구만 있을 뿐입니다. 저의 선택 과정은 이랬습니다. 처음에는 소규모 프로젝트에서 docker-compose file secrets를 썼고 팀이 커지면서 Docker Swarm + Docker Secrets로 옮겼습니다. 지금은 회사가 K8s로 이전하면서 Vault 도입 여부를 검토하고 있습니다.

이전할 때마다 비용이 들었지만 보안은 실제로 단계적으로 강화됐습니다.

선택 기준

구체적으로 무엇을 선택해야 할까요? 몇 가지 판단 기준을 제시하겠습니다.

다음 상황이라면…

개인 개발자/소규모 프로젝트:

  • 로컬 개발: docker-compose file secrets면 충분합니다.
  • VPS에 서비스 몇 개 배포: Docker Swarm + Docker Secrets를 사용합니다.
  • 예산이 제한적이고 복잡한 운영을 원하지 않음: 너무 무거운 Vault는 피합니다.

스타트업(10~50명):

  • Docker 사용: Docker Secrets를 선택합니다.
  • K8s 사용: K8s Secrets + External Secrets Operator를 사용합니다. 점진적으로 도입하고 나중에 Vault로 원활하게 이전할 수 있습니다.
  • 모든 인프라가 AWS에 있음: 관리가 편한 Secrets Manager를 바로 사용합니다.

대기업/엔터프라이즈:

  • 멀티 클라우드 환경: Vault가 사실상 유일한 선택입니다.
  • 감사와 규정 준수 필요: Vault를 사용합니다.
  • 전담 보안 팀이 있음: Vault를 사용합니다.
  • 예산이 충분하고 학습 비용을 감당할 수 있음: Vault를 사용합니다.

결정 트리를 단순화하면 다음과 같습니다.

K8s를 사용하나요?
 └─ 예 → K8s Secrets 사용, 고급 기능이 필요하면 Vault
 └─ 아니요 → Docker Swarm을 사용하나요?
      └─ 예 → Docker Secrets
      └─ 아니요 → AWS를 사용하나요?
           └─ 예 → AWS Secrets Manager
           └─ 아니요 → docker-compose file secrets(개발) / Vault(운영)

실제 사례

제가 알고 있는 실제 선택 과정 두 가지를 소개하겠습니다.

사례 1: 스타트업의 발전 과정

친구 회사는 SaaS를 만듭니다. 처음에는 5명이었고 모든 서비스를 단일 VPS에 Docker로 배포했습니다. 초기에는 docker-compose + file secrets를 사용해 비밀번호 파일을 서버에 저장하고 수동으로 관리했습니다.

6개월 뒤 엔젤 투자를 받고 팀이 15명으로 늘면서 서비스를 마이크로서비스 아키텍처로 분리했습니다. 이때 Docker Swarm을 도입하고 Docker Secrets로 이전했습니다. 각 서버에 비밀번호 파일을 둘 필요 없이 Swarm 클러스터에서 secrets를 중앙 관리할 수 있어 편리했기 때문입니다.

1년 뒤 시리즈 A 투자를 받은 후 DevOps 담당자를 채용하고 K8s로 이전해 K8s Secrets를 사용하기 시작했습니다. 동시에 AWS RDS, S3 등의 서비스를 도입하면서 AWS 관련 자격 증명은 Secrets Manager로 관리하고 K8s의 secret은 External Secrets Operator로 Secrets Manager에서 동기화했습니다.

현재는 멀티 클라우드 전략에 따라 GCP를 백업 용도로 검토하고 있어, AWS에 종속되는 Secrets Manager 대신 Vault로 통합 이전할지를 평가하고 있습니다.

보셨나요? 보안 아키텍처는 비즈니스 성장에 맞춰 발전하므로 처음부터 가장 복잡한 방식을 도입할 필요는 없습니다.

사례 2: 전통 기업의 일괄 도입

다른 친구는 은행에서 기술 업무를 합니다. 그곳에서는 지난해 컨테이너 전환을 진행했습니다. 금융 업계라 규정 준수 요구가 엄격했기 때문에 처음부터 Vault를 도입했습니다.

학습 곡선은 가팔랐지만 다음과 같은 이점이 있었습니다.

  • 모든 키 접근 기록이 남는 완전한 감사 로그
  • 동적 키 지원(사용 후 자동으로 만료되는 임시 데이터베이스 자격 증명)
  • 기존 LDAP와 연동한 권한 제어

구축과 교육에 2인월을 투입했지만 비밀 정보 관리 문제를 한 번에 해결했고 이후 유지 관리 비용도 매우 낮았습니다.

상황에 따라 선택이 달라집니다. 소규모 회사가 Vault를 운영하면 비용 대비 효과가 떨어질 수 있고, 대기업에는 Docker Secrets만으로 부족할 수 있습니다.

운영 환경 모범 사례 체크리스트

지금 바로 적용할 보안 조치

Docker Secrets를 사용하는지와 관계없이 다음 조치는 지금 당장 해야 합니다.

✅ Git 기록의 민감 정보 확인

# 비밀번호일 가능성이 있는 키워드 검색
git log -p -S 'password' -S 'secret' -S 'api_key'

# .env 파일이 커밋된 적 있는지 확인
git log --all --full-history -- .env

발견했다면 git filter-repoBFG Repo-Cleaner로 기록에서 완전히 제거합니다. 그리고 유출된 모든 비밀번호를 즉시 교체합니다.

✅ .gitignore 설정

# .gitignore
.env
*.env
secrets/
db_password.txt
*_password.txt
*_secret.txt
*.pem
*.key

요행을 바라지 말고 반드시 추가하세요.

✅ GitHub Secret Scanning 활성화

GitHub를 사용한다면 저장소 설정에서 Security → Secret scanning alerts를 활성화하세요. 실수로 키를 push하면 GitHub가 자동으로 경고합니다.

무료 버전은 공개 저장소만 지원하고 비공개 저장소에는 Pro/Team/Enterprise 계정이 필요합니다. 특히 공개 저장소에 더 필요하므로 바로 켜세요.

✅ 유출됐을 가능성이 있는 모든 자격 증명 교체

다음 항목을 점검하세요.

  • 데이터베이스 비밀번호
  • API 키(AWS/OpenAI/Stripe 등)
  • JWT secret
  • OAuth client secrets
  • SSL 비공개 키

바꿀 수 있는 것은 모두 바꾸세요. 비밀번호가 이미 유출됐더라도 지금 조치하면 늦지 않습니다.

Docker Secrets 적용 단계

Docker Secrets를 사용하기로 결정했다면 다음 순서로 진행합니다.

1단계: 민감 정보 목록 작성

민감한 정보를 모두 나열합니다.

✓ MySQL root 비밀번호
✓ 애플리케이션 데이터베이스 비밀번호
✓ Redis 비밀번호
✓ JWT 서명 키
✓ 서드파티 API 키(결제/문자 메시지 등)
✓ SSL 인증서 비공개 키
✓ OAuth client secret

2단계: 도구 선택

앞의 비교를 바탕으로 Docker Secrets, K8s Secrets, Vault 중 하나를 선택합니다. 너무 오래 고민하지 말고 먼저 하나를 사용하세요. 나중에 이전할 수 있습니다.

3단계: secrets 생성

# Docker Swarm을 사용하는 경우
docker swarm init
docker secret create db_password <(echo "비밀번호")
docker secret create api_key <(echo "키")

# docker-compose file secrets를 사용하는 경우
mkdir secrets
echo "비밀번호" > secrets/db_password.txt
echo "키" > secrets/api_key.txt
chmod 600 secrets/*

4단계: 설정 파일 수정

docker-compose.yml의 environment 필드를 secrets로 바꿉니다.

# 변경 전
services:
  app:
    environment:
      DB_PASSWORD: "hardcoded_password"  # ❌

# 변경 후
services:
  app:
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password  # ✅

5단계: 애플리케이션 코드 수정(필요한 경우)

프레임워크가 _FILE 접미사를 지원하지 않는다면 파일을 읽도록 코드를 바꿔야 합니다.

// Node.js 예시
const fs = require('fs');
const dbPassword = process.env.DB_PASSWORD_FILE
  ? fs.readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim()
  : process.env.DB_PASSWORD;

6단계: 테스트와 검증

먼저 테스트 환경에서 실행해 다음 사항을 확인합니다.

  • 서비스가 정상적으로 시작됩니다.
  • 비밀번호를 올바르게 읽을 수 있습니다.
  • docker inspect에 평문이 보이지 않습니다.
  • 기능이 정상적으로 작동합니다.

7단계: 운영 배포

문제가 없다면 운영 환경에 배포합니다. 안전을 위해 기존 환경 변수 설정을 fallback으로 잠시 유지하고 새 방식이 안정적인 것을 확인한 뒤 제거하세요.

지속적인 보안 유지 관리

Docker Secrets를 도입했다고 모든 일이 끝나는 것은 아닙니다. 계속 관리해야 합니다.

키 정기 교체(90일마다 권장)

분기마다 비밀번호를 바꾸도록 일정 알림을 설정하세요. 특히 직원이 퇴사한 뒤에는 관련 비밀번호를 반드시 교체해야 합니다.

키 접근 로그 모니터링

Vault를 쓴다면 audit 로그를 정기적으로 확인해 비정상적인 접근 패턴이 있는지 살펴봅니다.

Docker Secrets와 K8s Secrets에는 기본 감사 로그가 없다는 단점이 있습니다. 감사가 필요하면 Falco 같은 다른 도구와 함께 사용해야 합니다.

최소 권한 원칙

모든 서비스가 모든 secrets에 접근하게 하지 마세요. 서비스마다 필요한 항목만 제공합니다.

services:
  frontend:
    secrets:
      - api_key  # 프론트엔드에는 API key만 필요

  backend:
    secrets:
      - db_password  # 백엔드에는 데이터베이스 비밀번호 필요
      - api_key

키 스캔 도구 사용

CI/CD에 다음 도구를 통합합니다.

pre-commit hook에서 한 번 실행하면 많은 실수성 커밋을 막을 수 있습니다.

피해야 할 다섯 가지 실수

마지막으로 흔히 빠지는 함정을 정리하겠습니다.

1. ARG로 민감 정보를 전달하지 마세요

# ❌ 잘못된 예
ARG DB_PASSWORD=secret
ENV DATABASE_URL=postgres://user:${DB_PASSWORD}@db/myapp

ARG는 이미지 빌드 기록에 남으며 docker history에서 볼 수 있습니다.

2. secrets를 stdout에 출력하지 마세요

# ❌ 잘못된 예
password = open('/run/secrets/db_password').read()
print(f"Using password: {password}")  # docker logs에 기록됨!

로그 시스템은 stdout 출력을 모두 수집합니다. 비밀번호가 로그에 들어가면 완전히 삭제하기가 매우 어렵습니다.

3. 여러 환경에서 같은 secret을 사용하지 마세요

개발, 테스트, 운영 환경의 비밀번호를 분리하세요. 편의를 위해 같은 값을 쓰면 테스트 환경이 유출됐을 때 운영 환경까지 피해를 입습니다.

4. 컨테이너 안에서 secret을 다른 경로로 복사하지 마세요

# ❌ 잘못된 예
cp /run/secrets/db_password /app/config/password.txt

/run/secrets는 메모리 파일 시스템인 tmpfs이므로 컨테이너가 중지되면 내용이 사라집니다. 다른 경로로 복사하면 디스크에 기록되어 오히려 안전하지 않을 수 있습니다.

그대로 /run/secrets에서 읽으면 됩니다.

5. secret의 만료와 교체를 모니터링하는 것을 잊지 마세요

Vault가 지원하는 만료 시간이 있는 동적 키를 사용한다면 자동 갱신이나 교체 메커니즘을 설정해야 합니다. 한밤중에 비밀번호가 만료되어 모든 서비스가 중단된 뒤에야 알아차리는 일이 없어야 합니다.

결론

긴 이야기를 한 문장으로 요약하면 비밀번호를 다른 사람이 볼 수 있는 곳에 두지 마세요.

환경 변수는 편리해 보이지만 docker inspect 한 번이면 모두 노출됩니다. Dockerfile의 ENV 명령은 이미지 레이어에 영구히 남으므로 더 위험합니다. 많은 보안 사고가 이렇게 편의를 좇은 작은 방심에서 시작됩니다.

Docker Secrets는 완벽하지 않습니다. Swarm 모드가 필요하고 수정할 수 없으며 단일 머신 개발에 불편합니다. 하지만 비밀번호 암호화 저장, 암호화 전송, 환경 변수와 이미지에 노출되지 않는다는 핵심 문제는 해결합니다. Docker Swarm 클러스터에는 가장 간단하고 직접적인 선택입니다.

Kubernetes를 쓴다면 K8s Secrets, 엔터프라이즈급 요구가 있다면 Vault, 모든 인프라가 AWS에 있다면 Secrets Manager를 고려하세요. 최고의 도구가 아니라 상황에 가장 적합한 도구를 선택하면 됩니다.

중요한 것은 행동입니다. 오늘 바로 다음 항목을 확인해 보세요.

  • Git 기록에 비밀번호가 있나요?
  • .env 파일을 .gitignore에 추가했나요?
  • docker inspect로 비밀번호가 보이나요?

답이 각각 ‘예’, ‘아니요’, ‘예’라면 시간을 내어 고쳐야 합니다. 처음부터 Vault를 완벽하게 도입할 필요는 없습니다. 환경 변수 대신 file secrets로 바꾸는 것만으로도 지금보다 안전해집니다.

보안은 일회성 설정이 아니라 지속적인 과정입니다. 비밀번호를 정기적으로 교체하고 비정상 접근을 모니터링하며 스캔 도구로 코드를 검사하세요. 이런 습관은 도구 자체보다 더 중요합니다.

마지막으로 이 글을 팀에 공유하세요. 비밀 정보 관리는 한 사람만의 일이 아니라 팀 전체의 공동 책임입니다. 표준을 통일하고 정기적으로 review해야 위험을 실제로 줄일 수 있습니다.

새벽 3시 경고 알림에 다시는 깨지 않기를 바랍니다.


참고 자료:

Docker Secrets 보안 관리 전체 절차

컨테이너 비밀번호와 API 키를 안전하게 관리하고 Docker, K8s, Vault 등의 도구를 비교하며 운영 환경용 전체 체크리스트를 제공합니다.

⏱️ Estimated time: 1 hr

  1. 1

    Step 1: 보안 문제와 잘못된 방식을 이해하기

    보안 문제:
    • 공격자가 올바른 비밀번호, 즉 docker-compose.yml에 적어 둔 MySQL 비밀번호를 그대로 사용함
    • 지난달 인턴 한 명이 연습하려고 프로젝트를 자신의 공개 저장소로 fork함
    • 비밀번호 유출로 데이터베이스가 무차별 대입 공격을 받음

    잘못된 방식:
    • 비밀번호를 설정 파일에 직접 기록함
    • 환경 변수를 쓰기만 하면 안전하다고 생각함
    • 심지어 '저장소가 비공개라서 괜찮다'고 말하기도 함
    • 모두 보안 위험 요소임

    흔한 실수:
    • Dockerfile에 비밀번호를 기록함
    • docker-compose.yml에 비밀번호를 기록함
    • 환경 변수에 비밀번호를 넣고 Git에 커밋함
    • .env 파일을 쓰면서 .gitignore에 추가하는 것을 잊음
  2. 2

    Step 2: Docker Secrets로 키 관리하기

    Docker Secrets 방식:
    • docker secret create로 키 생성: docker secret create mysql_password -
    • docker-compose.yml에서 secrets 설정: secrets: - mysql_password
    • 컨테이너 안에서 /run/secrets/ 경로를 통해 키에 접근: cat /run/secrets/mysql_password
    • 키는 Docker Swarm에 암호화되어 저장됨

    키 생성:
    • echo "your-password" | docker secret create mysql_password -
    • docker-compose.yml에서 참조: secrets: - mysql_password
    • 컨테이너에서 읽기: cat /run/secrets/mysql_password
  3. 3

    Step 3: 다른 방식 비교와 모범 사례

    다른 방식 비교:
    • Docker Secrets(Docker Swarm에 적합, 키 암호화 저장)
    • Kubernetes Secrets(K8s 환경에 적합, Base64 인코딩)
    • HashiCorp Vault(엔터프라이즈급 키 관리, 동적 키 지원)
    • AWS Secrets Manager(AWS 환경용, AWS 서비스와 통합)
    • 환경 변수 파일(.env, 운영 환경에는 권장하지 않음)

    모범 사례:
    • 운영 환경에서는 반드시 Secrets로 민감 정보를 관리함
    • 키를 정기적으로 교체함
    • 키 스캔 도구(trufflehog)로 Git 저장소를 검사함
    • 접근 제어를 설정해 키 유출을 방지함
    • 코드에 비밀번호를 하드코딩하지 않음

FAQ

비밀번호를 Dockerfile이나 docker-compose.yml에 넣으면 안 되는 이유는 무엇인가요?
보안 문제: 공격자는 docker-compose.yml에 적어 둔 MySQL 비밀번호를 그대로 사용했습니다. 지난달 인턴 한 명이 연습하려고 프로젝트를 자신의 공개 저장소로 fork했고, 비밀번호가 유출되어 데이터베이스가 무차별 대입 공격을 받았습니다.

잘못된 방식:
• 비밀번호를 설정 파일에 직접 기록함
• 환경 변수를 쓰기만 하면 안전하다고 생각함
• 심지어 '저장소가 비공개라서 괜찮다'고 말하기도 함
• 모두 보안 위험 요소임

흔한 실수:
• Dockerfile에 비밀번호를 기록함
• docker-compose.yml에 비밀번호를 기록함
• 환경 변수에 비밀번호를 넣고 Git에 커밋함
• .env 파일을 쓰면서 .gitignore에 추가하는 것을 잊음
Docker Secrets로 키를 관리하려면 어떻게 해야 하나요?
Docker Secrets 방식:
• docker secret create로 키 생성(docker secret create mysql_password -)
• docker-compose.yml에서 secrets 설정(secrets: - mysql_password)
• 컨테이너 안에서 /run/secrets/ 경로를 통해 키에 접근(cat /run/secrets/mysql_password)
• 키는 Docker Swarm에 암호화되어 저장됨

키 생성:
• echo "your-password" | docker secret create mysql_password -
• docker-compose.yml에서 참조: secrets: - mysql_password
• 컨테이너에서 읽기: cat /run/secrets/mysql_password
Docker Secrets는 다른 비밀 정보 관리 방식과 무엇이 다른가요?
다른 방식 비교:
• Docker Secrets(Docker Swarm에 적합, 키 암호화 저장)
• Kubernetes Secrets(K8s 환경에 적합, Base64 인코딩)
• HashiCorp Vault(엔터프라이즈급 키 관리, 동적 키 지원)
• AWS Secrets Manager(AWS 환경용, AWS 서비스와 통합)
• 환경 변수 파일(.env, 운영 환경에는 권장하지 않음)

선택 기준:
• Docker Swarm 환경에서는 Docker Secrets
• K8s 환경에서는 Kubernetes Secrets
• 엔터프라이즈급 요구에는 Vault
• AWS 환경에서는 Secrets Manager
운영 환경에서 비밀 정보를 관리하는 모범 사례는 무엇인가요?
모범 사례:
• 운영 환경에서는 반드시 Secrets로 민감 정보를 관리함
• 키를 정기적으로 교체함
• 키 스캔 도구(trufflehog)로 Git 저장소를 검사함
• 접근 제어를 설정해 키 유출을 방지함
• 코드에 비밀번호를 하드코딩하지 않음

정기 점검:
• trufflehog로 Git 저장소를 스캔함
• 키 유출 여부를 확인함
• CI/CD 절차에 자동 스캔을 설정함
• 유출을 발견하면 즉시 키를 교체함

5분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog