개발자는 외부 API 키를 코드에서 지우고 .env 파일로 옮깁니다. 저장소에는 커밋하지 않았으니 안전하다고 생각합니다. 하지만 운영 서버의 환경 변수는 배포 로그와 진단 덤프에 찍히고, 같은 계정의 모든 애플리케이션이 하나의 DB 비밀번호를 공유하며, 3년 전에 만든 키가 한 번도 바뀌지 않았다면 위험은 그대로 남아 있습니다.
비밀정보(Secret)는 노출되면 다른 주체가 권한을 행사할 수 있는 값입니다. API key, 데이터베이스 비밀번호, OAuth client secret, 개인키, 암호화 키와 인증서 발급 자격 등이 해당합니다. 코드에서 분리하는 것은 시작일 뿐이고 생성, 전달, 사용, 교체, 폐기와 감사까지 전체 생명주기를 관리해야 합니다.
이번 글에서는 .env의 역할과 한계, 중앙 비밀 저장소, Workload Identity와 동적 자격증명, 런타임 전달 방법, 무중단 회전과 실제 유출 시 대응을 차례대로 살펴보겠습니다.
무엇이 비밀이고 무엇이 설정인지 구분한다
DB 호스트 이름과 기능 플래그는 일반 설정일 수 있지만 DB 비밀번호와 서명 개인키는 비밀입니다. 공개 클라이언트에서 사용하는 analytics ID처럼 보안 권한이 없는 식별자를 억지로 숨기는 데 시간을 쓰기보다 노출 시 실제로 인증·복호화·서명이 가능한 값을 보호해야 합니다.
비밀 목록을 만들 때 값 자체가 아니라 소유자, 사용 서비스, 접근 대상, 권한 범위, 생성일, 만료일과 회전 방법을 기록합니다. 어디서 사용하는지 모르는 키는 사고 때 폐기할 수 없고, 사용 여부를 몰라 영구적으로 남게 됩니다.
같은 값을 개발·스테이징·운영에서 공유하지 않습니다. 개발 환경이 침해돼 운영 데이터에 접근하지 못하도록 환경별 계정과 키를 분리하고, 서비스별로도 최소 권한을 부여합니다.
.env는 배포 편의 형식이지 비밀 저장소가 아니다
.env 파일은 로컬 개발에서 설정을 주입하기 편리하고 코드 하드코딩을 줄여줍니다. 하지만 파일 권한, 암호화, 접근 감사, 버전, 자동 회전과 만료를 자체적으로 제공하지 않습니다. 실수로 Git에 추가하거나 메신저와 티켓에 복사하기도 쉽습니다.
.gitignore는 앞으로의 커밋을 막을 뿐 이미 Git 기록에 들어간 비밀을 지우거나 폐기하지 않습니다. 저장소에서 문자열을 삭제해도 복제본, 빌드 캐시와 포크에 남을 수 있으므로 노출된 키는 즉시 회전해야 합니다.
운영에서는 배포 서버에 수동으로 .env를 복사하기보다 권한과 감사 기능이 있는 중앙 비밀 저장소에서 런타임에 제공하는 편이 안전합니다. 로컬 개발도 팀 공유 비밀번호 대신 개인별 단기 자격증명이나 개발 전용 저장소를 사용합니다.
중앙 Secret Manager가 해결하는 것
클라우드 Secret Manager나 Vault 계열 시스템은 비밀을 암호화해 저장하고 IAM 정책으로 읽기 권한을 제어하며, 버전과 접근 감사 로그를 제공합니다. 일부 시스템은 DB 계정을 동적으로 만들고 짧은 임대 시간이 지나면 자동 폐기할 수 있습니다.
중앙화의 가장 큰 장점은 모든 비밀이 한곳에 있다는 사실보다 누가 어떤 비밀을 언제 읽을 수 있는지 정책으로 관리하고 교체를 자동화할 수 있다는 것입니다. 사람에게 원문을 보여주지 않고 워크로드에만 전달할 수도 있습니다.
다만 Secret Manager를 도입했다고 애플리케이션 코드의 로그 출력이나 과도한 IAM 권한이 자동으로 해결되지는 않습니다. 저장소에 접근하는 초기 자격증명을 어디에 둘 것인지도 해결해야 합니다. 장기 access key를 또 다른 환경 변수에 두면 문제를 한 단계 옮긴 것에 불과합니다.
Workload Identity로 첫 번째 비밀번호를 없앤다
클라우드 VM, 컨테이너, Kubernetes ServiceAccount와 CI 작업은 플랫폼이 증명하는 워크로드 신원을 가질 수 있습니다. 애플리케이션은 고정 access key 없이 이 신원으로 짧은 토큰을 받아 Secret Manager나 DB에 접근합니다. 이를 Workload Identity, Managed Identity 또는 IAM Role 같은 이름으로 제공합니다.
신원과 권한을 분리하면 주문 서비스는 주문 DB 비밀만, 이미지 처리 작업은 특정 스토리지 경로만 읽게 할 수 있습니다. 같은 노드에서 실행된다고 모든 Pod가 같은 클라우드 권한을 가지지 않게 서비스 계정과 역할을 세분화합니다.
CI에서도 저장소에 클라우드 키를 등록하기보다 OIDC federation으로 빌드 작업의 저장소, 브랜치, 환경을 검증해 단기 권한을 발급할 수 있습니다. 작업이 끝나면 토큰도 곧 만료되므로 유출 창이 줄어듭니다.
비밀은 필요한 순간에 필요한 프로세스에만 전달한다
안전한 전달 흐름은 워크로드가 자신의 신원을 증명하고, 정책 엔진이 접근을 승인하며, 비밀 저장소가 짧은 수명과 최소 범위의 자격증명을 반환하는 구조입니다.

환경 변수 주입은 구현이 쉽고 대부분의 프레임워크가 지원하지만 자식 프로세스, 진단 화면과 crash dump에 노출될 수 있으며 실행 중 갱신이 어렵습니다. 컨테이너 이미지의 ENV 명령으로 값을 굽는 것은 이미지 레이어와 레지스트리에 영구적으로 남기 때문에 피해야 합니다.
메모리 파일 시스템의 mounted file은 파일 권한을 적용하고 변경을 감지해 다시 읽을 수 있습니다. 하지만 애플리케이션이 시작할 때 한 번만 읽으면 회전된 값을 사용하지 못합니다. Sidecar나 agent가 비밀을 가져와 파일·로컬 소켓으로 제공하는 방식은 애플리케이션의 저장소 연동 코드를 줄이지만 agent와 공유 볼륨의 권한을 관리해야 합니다.
애플리케이션이 SDK로 직접 조회하면 최신 버전과 오류 처리를 세밀하게 제어할 수 있습니다. 요청마다 원격 조회하지 말고 짧은 메모리 캐시와 갱신 정책을 두되, 비밀값을 디스크 캐시와 일반 로그에 남기지 않습니다.
장기 비밀번호보다 단기 동적 자격증명이 낫다
1년 동안 유효한 DB 비밀번호가 유출되면 발견하고 폐기할 때까지 계속 악용될 수 있습니다. 동적 비밀은 애플리케이션이 요청할 때 제한된 권한의 계정을 만들고 수분 또는 수시간의 lease를 부여합니다. 만료되면 자동으로 접근할 수 없습니다.
짧은 수명은 유출 피해를 줄이지만 갱신 실패가 가용성 문제로 이어질 수 있습니다. 만료 직전이 아니라 충분한 여유를 두고 갱신하고, 일시적 저장소 장애에서는 백오프하며, 새 자격증명을 얻지 못했을 때 무한히 오래된 값을 사용하지 않도록 hard expiration을 둡니다.
DB 연결 풀은 자격증명이 바뀌어도 기존 연결이 살아 있을 수 있습니다. 새 연결부터 새 자격증명을 사용하고 기존 연결을 점진적으로 배출하거나, 동적 계정의 lease와 풀의 최대 수명을 조정해야 합니다.
최소 권한은 비밀 하나당 피해 범위를 줄인다
모든 서비스가 root DB 계정 하나를 공유하면 어느 서비스에서 유출됐는지 알기 어렵고 한 번의 유출이 전체 데이터에 영향을 줍니다. 서비스별 계정, 읽기·쓰기 역할과 테이블 범위를 나눕니다. 마이그레이션 권한은 평소 애플리케이션 런타임 계정에 주지 않습니다.
외부 API key도 가능한 경우 프로젝트, 환경, 허용 IP, 호출 API와 quota를 제한합니다. 개발자가 개인 PC에서 운영 키를 직접 사용할 필요가 없도록 진단과 운영 절차를 만듭니다.
Secret Manager 접근 정책은 경로 와일드카드를 최소화합니다. 한 애플리케이션이 전체 조직의 모든 비밀을 나열할 수 있다면 중앙 저장소 침해의 폭발 반경이 커집니다. 비밀 목록 조회 권한과 특정 값 읽기 권한도 구분할 수 있습니다.
암호화와 비밀 관리는 같은 문제가 아니다
비밀을 암호화해서 Git에 넣는 방식은 평문 커밋보다 낫지만 복호화 키가 어디에 있는지, 누가 사용할 수 있는지, 회전과 감사가 가능한지 해결해야 합니다. 암호문이 있다고 자동으로 안전한 배포가 되는 것은 아닙니다.
KMS는 데이터 암호화 키를 보호하는 데 사용되고, Secret Manager는 애플리케이션이 사용할 비밀의 값과 버전·정책을 관리합니다. Envelope encryption에서는 데이터마다 생성한 키로 내용을 암호화하고 그 키를 KMS 키로 다시 암호화합니다. KMS의 master key를 애플리케이션 설정에 원문으로 넣지 않습니다.
해시는 비밀번호 검증처럼 원래 값을 복원할 필요가 없을 때 사용합니다. 외부 API에 전달해야 하는 key는 해시만 저장할 수 없으므로 암호화된 비밀 저장이 필요합니다. 목적에 따라 해시, 암호화와 토큰화를 구분합니다.
로그와 오류 메시지가 가장 흔한 유출 경로다
환경 변수 전체를 디버그 로그로 출력하거나 HTTP 요청 헤더와 본문을 그대로 남기면 비밀 저장소를 잘 구성해도 값이 로그 플랫폼으로 복사됩니다. URL query string에 API key를 넣으면 프록시, 브라우저 기록과 분석 도구에도 남을 수 있습니다.
로깅 계층에서 authorization, cookie, token, password, secret 같은 필드를 기본 마스킹하고 허용 목록 방식으로 기록합니다. 그러나 마스킹 정규식만 믿지 말고 애초에 비밀 객체가 문자열 직렬화되지 않도록 타입과 API를 설계합니다.
예외 메시지와 third-party SDK의 verbose 모드, CI 명령의 echo와 shell tracing도 확인합니다. CI 시스템의 masking 기능은 이미 등록된 정확한 문자열에는 도움이 되지만 인코딩·분할된 값이나 새로 발급된 토큰까지 완전히 막지는 못합니다.
무중단 회전은 새 키를 추가한 뒤 이전 키를 제거한다
키 회전은 기존 값을 즉시 폐기하고 새 값을 배포하는 한 단계 작업이 아닙니다. 여러 애플리케이션 인스턴스와 모바일·외부 소비자가 동시에 바뀌지 않기 때문입니다. 안전한 순서는 확장과 축소로 나뉩니다.

- 새 자격증명을 생성하고 기존 자격증명과 동시에 유효하게 둡니다.
- 워크로드가 새 버전을 읽도록 갱신하거나 재배포합니다.
- 새 키로 연결과 실제 업무 요청이 성공하는지 확인합니다.
- 감사 로그에서 이전 키 사용량이 0인지 확인합니다.
- 이전 키를 비활성화하고 짧게 관찰한 뒤 폐기합니다.
- 설정·문서·복구 저장소에서 이전 버전을 정리합니다.
대상 시스템이 키 하나만 허용한다면 계정 두 개를 번갈아 사용하는 방식이나 프록시 계층을 검토합니다. 회전 가능성을 공급자 선택과 API 설계 단계에서 확인해야 합니다.
서명 키와 인증서는 검증 측 전파 시간이 중요하다
JWT 서명 키를 바꿀 때 새 private key로 즉시 서명하면 검증 서비스가 새 public key를 받기 전에 토큰을 거절할 수 있습니다. 먼저 새 public key를 JWKS에 게시하고 충분히 전파한 뒤 새 kid로 서명을 시작합니다. 이전 토큰의 최대 수명 동안 구 public key를 유지한 뒤 제거합니다.
TLS 인증서도 새 인증서를 배포하고 모든 endpoint와 load balancer에서 제공되는지 확인한 뒤 이전 인증서를 폐기합니다. 인증서 파일만 교체해도 프로세스가 다시 읽지 않으면 반영되지 않을 수 있습니다. 만료일 알림보다 자동 갱신과 실제 handshake 점검이 중요합니다.
암호화 키 회전은 새 데이터부터 새 키로 암호화하고 과거 데이터는 필요에 따라 점진적으로 다시 암호화할 수 있습니다. 과거 데이터를 읽기 위해 이전 키 버전이 필요한 기간을 고려해야 하며, 키 삭제는 데이터 삭제와 같을 수 있으므로 복구 정책을 신중히 정합니다.
자동 회전은 실패도 자동화한다
회전 스케줄만 자동화하고 검증과 롤백이 없으면 정기적으로 자동 장애를 만들 수 있습니다. 새 키 생성, 대상 시스템 등록, 소비자 갱신, synthetic check, 이전 키 비활성화와 결과 기록을 하나의 상태 머신으로 관리합니다.
단계별 timeout과 재시도, 사람이 개입할 지점을 정합니다. 비활성화 전 마지막 사용 시각과 사용 주체를 확인하고 예상치 못한 구버전 소비자가 있으면 중단합니다. 회전 성공률, 평균 소요 시간과 기한을 넘긴 비밀 수를 운영 지표로 봅니다.
회전은 실제로 실행해 보지 않으면 사고 시 작동하지 않습니다. 낮은 위험의 비밀부터 짧은 주기로 훈련하고, 외부 공급자의 수동 절차와 담당자 연락처도 런북에 포함합니다.
저장소와 CI에서 유출을 조기에 막는다
로컬 pre-commit과 서버 측 CI에 secret scanning을 적용해 알려진 key 패턴과 높은 엔트로피 문자열을 탐지합니다. 오탐을 무시 목록에 넣을 때는 이유와 만료를 기록하고, 테스트용 가짜 키도 실제 형식과 구분되는 값을 사용합니다.
커밋 전 검사만으로 과거 기록, issue, wiki, 패키지와 컨테이너 이미지를 놓칠 수 있습니다. 저장소 전체 이력과 이미지 layer, artifact, 공개된 문서도 주기적으로 검사합니다. 공급자가 제공하는 유출 탐지와 자동 폐기 알림도 연결합니다.
탐지 결과에 실제 비밀 원문을 다시 저장하지 않도록 보안 도구의 로그와 알림 권한을 제한합니다. 발견한 경로와 fingerprint, 소유자만 기록해도 대응할 수 있습니다.
유출이 의심되면 삭제보다 폐기가 먼저다
Git에서 파일을 지우거나 채팅 메시지를 삭제해도 이미 누가 복사했는지 알 수 없습니다. 유출 가능성이 있다면 값은 침해된 것으로 간주하고 먼저 비활성화하거나 회전합니다.
- 비밀의 권한과 연결된 시스템, 소유자를 식별합니다.
- 서비스 영향에 맞춰 새 값을 발급하고 소비자를 전환합니다.
- 노출된 값을 즉시 폐기하고 비정상 사용을 차단합니다.
- 감사 로그에서 최초 노출 시점 이후의 접근과 행위를 조사합니다.
- 저장소 기록, 로그와 artifact의 복사본을 정리합니다.
- 과도한 권한, 긴 수명과 탐지 실패의 원인을 개선합니다.
비밀이 데이터 읽기 권한을 가졌다면 단순 회전으로 끝나지 않습니다. 실제 데이터 접근과 변경 여부, 추가 자격증명 생성, 외부 반출을 조사하고 필요한 통지 절차를 따릅니다.
Secret Manager 장애도 설계해야 한다
애플리케이션이 매 요청마다 중앙 저장소에 의존하면 저장소 지연이 전체 서비스 지연이 됩니다. 시작 시 또는 갱신 시 비밀을 메모리에 캐시하고 요청 경로에서는 로컬 값을 사용합니다. 갱신은 백그라운드에서 충분히 일찍 수행합니다.
새 인스턴스가 비밀 저장소 장애 중 시작할 수 있는지, 기존 인스턴스가 캐시된 자격증명으로 얼마나 유지되는지 정책을 정합니다. 캐시를 무기한 사용하면 폐기한 키가 계속 살아 있고, 즉시 중단하면 일시 장애에 취약합니다. 비밀별 최대 stale 시간과 hard expiration을 구분합니다.
Break-glass 접근은 평소 차단하되 심각한 장애 때 제한된 담당자가 사용할 수 있게 별도 보호합니다. 사용 시 강한 인증, 시간 제한, 사후 알림과 감사가 필요하며 정기적으로 복구 훈련을 합니다.
운영에서 봐야 할 지표와 감사
- 비밀별 마지막 회전일, 만료 예정일과 소유자
- 서비스별 읽기 성공·실패와 갱신 지연
- 이전 버전 자격증명의 마지막 사용 시각
- 거부된 접근과 비정상 지역·주체의 읽기
- 장기 키와 과도한 권한을 가진 비밀 수
- 저장소·이미지·로그에서 발견된 노출 후보
- 자동 회전 성공률, 소요 시간과 롤백 횟수
- Break-glass 사용과 관리자 정책 변경
감사 로그에는 비밀의 이름과 버전, 요청 주체, 결과와 시각을 기록하되 값 자체는 절대 남기지 않습니다. 누가 읽을 수 있는지뿐 아니라 누가 정책과 비밀 내용을 변경할 수 있는지도 분리합니다.
도입 체크리스트
- 조직의 비밀 목록, 소유자와 사용처를 파악하고 있는가?
- 개발·스테이징·운영과 서비스별 자격증명이 분리되어 있는가?
- 코드, Git 기록, 이미지와 CI 로그에 비밀이 없는가?
- 워크로드가 고정 키 대신 플랫폼 신원으로 저장소에 접근하는가?
- 비밀 접근 권한이 경로와 작업별 최소 범위인가?
- 런타임 전달 방식이 갱신과 메모리·로그 노출을 고려하는가?
- 구·신 키를 함께 허용하는 무중단 회전 절차가 있는가?
- 인증서·서명 키·DB 연결 풀의 고유한 회전 동작을 테스트했는가?
- 유출 탐지 후 폐기·조사·복구 런북이 준비되어 있는가?
- Secret Manager 장애와 break-glass 접근을 훈련했는가?
좋은 비밀 관리는 값을 숨기는 것보다 수명을 줄이는 일이다
비밀을 완벽하게 아무도 볼 수 없게 만드는 것은 현실적으로 어렵습니다. 실행 중인 애플리케이션은 어느 순간 해당 권한을 사용해야 합니다. 그래서 중요한 것은 노출 가능성을 줄이고, 노출되더라도 권한과 수명이 작으며, 빠르게 탐지하고 폐기할 수 있게 만드는 것입니다.
핵심 원칙을 요약하면 이렇습니다. 비밀은 코드와 이미지에서 분리하고, 워크로드 신원으로 필요한 값만 받아 사용하며, 가능한 한 단기로 발급하고, 새 값을 먼저 배포한 뒤 이전 값을 폐기하라. 이 생명주기가 자동화될 때 키 회전은 두려운 이벤트가 아니라 일상적인 보안 운영이 됩니다.