이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 환경 변수와 secret manager는 어떤 역할이 다르고 각각 어디에서 새기 쉬운가?
- secret rotation은 왜 단순히 값을 바꾸는 작업이 아닌가?
- Java/Spring 운영에서 secret 접근 로그, 버전, 회전 실패를 어떻게 추적해야 하는가?
개요
Secret은 API key, database password, JWT signing key, OAuth client secret, encryption key처럼 유출되면 인증이나 암호 경계가 깨지는 값이다. secret 관리는 값을 숨겨 두는 것이 아니라 수명주기를 관리하는 일이다.
환경 변수는 애플리케이션에 secret을 전달하는 수단으로 흔히 쓰인다. 하지만 접근 제어, 조회 감사, 자동 회전, 버전 관리, 폐기 확인을 자동으로 제공하지 않는다. process dump, debug page, container inspect, crash report, CI log에서 노출될 수도 있다.
Vault나 cloud secret manager는 secret 저장과 접근, 감사, rotation을 중앙화하기 위한 도구다. 도구를 도입해도 secret이 Docker image, git history, application log에 남으면 방어가 깨진다.
이 문서의 핵심은 “secret을 어디에 넣었는가”가 아니라 “누가 언제 읽고 어떻게 바꾸고 언제 폐기되는가”다.
공격 시나리오
- 공격자는 public Git 저장소와 git history에서 API key와
.env파일을 찾는다. - container image layer에 bake된 secret을 registry 접근만으로 얻는다.
- debug endpoint, actuator env, crash dump, support bundle에서 환경 변수를 읽는다.
- CI/CD log에 출력된 secret을 이용해 cloud API나 DB에 접근한다.
- 오래된 secret이 회전되지 않아 퇴사자, 과거 배포, 유출된 백업에서 계속 사용된다.
- rotation 중 old/new secret 동시 수용 기간이 너무 길어 유출 secret이 계속 통한다.
취약한 요청/응답 예시
FROM eclipse-temurin:21
ENV DB_PASSWORD=prod-db-password
ENV JWT_SECRET=plain-text-signing-secret
COPY app.jar /app.jarlevel=INFO event=startup env=prod DB_PASSWORD=prod-db-password JWT_SECRET=plain-text-signing-secret이 설계는 image pull 권한, log read 권한, crash dump 접근 권한을 secret read 권한으로 만든다.
주제별 핵심 판단
- secret은 source code, git history, image layer, log에 들어가면 안 된다.
- 환경 변수는 전달 경로일 뿐 secret lifecycle 관리가 아니다.
- secret manager는 접근 제어, 감사 로그, 버전, rotation, 폐기를 제공해야 한다.
- rotation은 create, deploy, verify, cutover, revoke 단계로 나눠야 한다.
- signing key는
kid와 key set으로 old/new key 동시 검증 기간을 설계한다. - DB password rotation은 connection pool 재연결과 rollback을 고려한다.
- secret 조회 권한은 애플리케이션과 운영자 역할별 최소 권한으로 나눈다.
개선된 요청/응답 예시
spring:
config:
import: optional:configtree:/run/secrets//run/secrets/
db-password
jwt-signing-key-current
jwt-signing-key-previous@ConfigurationProperties(prefix = "security.jwt")
public record JwtKeyProperties(
String currentKid,
String currentKeyRef,
String previousKid,
String previousKeyRef
) {}애플리케이션은 secret 값을 코드와 image에 넣지 않고 런타임에 주입받는다. key 값보다 key reference와 kid를 기준으로 운영한다.
방어 설계
- secret inventory를 만든다. 이름, 용도, owner, 저장 위치, rotation 주기, 마지막 회전 시각을 기록한다.
- local 개발, CI, staging, production secret을 분리한다.
- secret은 build 단계가 아니라 deploy/runtime 단계에서 주입한다.
- secret manager 접근 권한은 workload identity와 최소 권한으로 제한한다.
- secret read 이벤트와 rotation 이벤트를 감사 로그로 남긴다.
- rotation은 old/new 동시 지원 기간을 명시하고 자동 검증을 둔다.
- rotation 실패 시 rollback과 revoke 기준을 정한다.
- 유출 대응은 secret 폐기, 영향 범위 분석, 재발 방지까지 runbook으로 둔다.
Spring/HTTP 예시
@Component
public class SigningKeyResolver {
private final SecretClient secretClient;
public Key resolve(String keyRef) {
SecretValue value = secretClient.read(keyRef);
audit.recordSecretRead(keyRef, "jwt-signing");
return keyParser.parse(value.plainText());
}
}level=INFO event=secret_rotation_started result=allowed secret=db-password version=v42 owner=platform traceId=af7d9010
level=INFO event=secret_rotation_completed result=allowed secret=db-password oldVersion=v41 newVersion=v42 verification=db_connection_ok traceId=af7d9010코드 예시는 경계를 보여 주기 위한 것이다. 실제 구현에서는 secret 원문을 method 밖으로 오래 들고 다니지 않고, audit에는 secret reference와 version만 남긴다.
운영 로그와 감사 지점
level=WARN event=secret_exposure_detected result=blocked location=ci-log secretType=api-key detector=secret-scan traceId=af7d9010level=WARN event=secret_rotation_failed result=blocked secret=db-password version=v42 reason=connection_pool_using_old_password rollback=started traceId=af7d9010- secret 원문은 절대 남기지 않는다.
- secret read 로그에는 actor/workload, secret reference, version, result, traceId를 남긴다.
- rotation 로그에는 old version, new version, verification result, rollback 여부를 남긴다.
- secret scan 결과는 commit, image, artifact, CI log별로 분류한다.
- 유출 의심 secret은 사용 여부와 무관하게 폐기 대상으로 본다.
실전 판단 기준
.env파일이 gitignore되어 있어도 이미 commit된 적이 있으면 유출로 본다.- 환경 변수는 편리하지만 process inspection과 dump 노출 위험이 있다.
- secret manager를 써도 애플리케이션이 시작 시 모든 secret을 로그로 찍으면 실패다.
- rotation이 문서에만 있고 자동 검증이 없으면 장애가 두려워 실행되지 않는다.
- JWT signing key rotation에는 old token 검증 기간과
kid전략이 필요하다. - DB password rotation에는 connection pool refresh와 long-running transaction 영향이 있다.
테스트 포인트
- secret이 git history, container image layer, CI log에 없는지 스캔한다.
- actuator/env 또는 debug endpoint가 운영에서 secret을 노출하지 않는지 확인한다.
- secret read 감사 로그가 workload identity 단위로 남는지 확인한다.
- DB password rotation 후 기존 connection pool이 새 secret으로 재연결되는지 확인한다.
- JWT signing key rotation에서 old token 검증과 new token 서명이 의도대로 동작하는지 확인한다.
- 유출 secret 폐기 runbook을 staging에서 리허설한다.
위험 신호!
- production secret이
.env나 application.yml에 들어 있다. - secret이 Dockerfile
ENV또는 image layer에 bake되어 있다. - secret 원문이 startup log에 출력된다.
- rotation 주기와 owner가 없는 secret이 많다.
- old secret이 언제 폐기되는지 모른다.
- secret read 권한이 모든 개발자와 모든 서비스에 열려 있다.
확인 질문
확인 질문
- 환경 변수는 왜 secret manager가 아닌가?
- 값을 전달할 수는 있지만 접근 감사, 버전, 회전, 폐기, 세밀한 권한 제어를 자체로 제공하지 않기 때문이다.
- rotation은 어떤 단계로 봐야 하는가?
- 새 secret 생성, 배포, 검증, cutover, old secret 폐기, 실패 rollback으로 나눠 봐야 한다.
- secret 로그에는 무엇을 남겨야 하는가?
- secret 원문이 아니라 reference, version, actor/workload, action, result, reason, traceId를 남긴다.