- 들어가며 — .env를 .gitignore에 넣었으니 안전한가
- 환경변수가 실제로 새어 나가는 경로
- 시크릿이 이미 커밋됐다면 — 폐기가 1순위, 히스토리는 2순위
- 저장 방식 비교 — 그리고 쿠버네티스 Secret의 base64는 암호화가 아닙니다
- 정적 키를 없애는 구조 — 짧은 수명 자격증명과 OIDC 페더레이션
- 로테이션을 가능하게 만드는 설계 — 두 개의 유효 키
- 탐지 — 사전 커밋 훅과 저장소 스캐닝이 못 하는 일
- 마치며 — 숨겼는가가 아니라 몇 분에 바꿀 수 있는가
들어가며 — .env를 .gitignore에 넣었으니 안전한가
시크릿 관리에 대한 표준적인 조언은 짧습니다. 코드에 하드코딩하지 말고 환경변수로 빼라, .env 파일은 .gitignore에 넣어라. 여기까지가 대부분의 문서가 다루는 범위입니다.
문제는 이 조언이 딱 한 가지 위협만 막는다는 것입니다. 소스 코드 저장소에 평문으로 들어가는 것. 그런데 실제 사고 보고서를 읽어 보면 유출 경로는 훨씬 다양합니다. 프로세스 메모리, 크래시 리포트, CI 로그, 컨테이너 이미지 레이어, APM 대시보드, 그리고 잘못 노출된 디버그 엔드포인트입니다.
더 중요한 문제는 그다음입니다. 시크릿이 새었다는 것을 알았을 때, 몇 분 안에 그 키를 무효화하고 새 키로 교체할 수 있습니까? 대부분의 팀에서 이 질문의 답은 "모른다"입니다. 어디에 몇 벌이 복사되어 있는지 모르고, 교체하면 무엇이 죽는지도 모르기 때문입니다.
이 글은 두 가지를 다룹니다. 환경변수가 실제로 어디까지 새는지, 그리고 새어도 5분 안에 대응할 수 있는 구조를 어떻게 만드는지입니다.
환경변수가 실제로 새어 나가는 경로
환경변수는 프로세스 속성입니다. 파일이 아니라 커널이 프로세스마다 들고 있는 문자열 배열이고, 리눅스에서는 그것을 파일처럼 읽을 수 있습니다.
ps -eo pid,user,comm | grep -E 'node|python'
2841 app node
3102 app python3
# 같은 UID 또는 root라면 프로세스의 환경변수를 통째로 읽는다
tr '\0' '\n' < /proc/2841/environ | grep -iE 'key|token|secret|password'
DATABASE_URL=postgres://app:pr0d-Db-Pass@db.internal:5432/app
STRIPE_SECRET_KEY=sk_live_51NxbQ2Lk9vHc0pMz7RtYaWq
JWT_SIGNING_KEY=8f2a1c9d4e7b6a305f18c2d9e0b7a4f1
여기서 중요한 사실은 이 파일이 프로세스가 살아 있는 동안 계속 읽힌다는 것입니다. .env 파일을 로드한 뒤 삭제해도 소용이 없습니다. 값은 이미 커널에 있습니다.
컨테이너 안이라고 다르지 않습니다. 같은 파드의 사이드카, 노드에 접근 가능한 사람, 그리고 디버그 컨테이너가 모두 같은 것을 봅니다.
kubectl debug -it deploy/payments --image=busybox --target=app -- sh
# 디버그 컨테이너에서
tr '\0' '\n' < /proc/1/environ
두 번째 경로는 자식 프로세스입니다. 환경변수는 상속됩니다. 애플리케이션이 이미지 변환기나 백업 스크립트 같은 외부 도구를 실행하면, 그 도구가 자기 로그에 환경 전체를 덤프하는 순간 시크릿이 로그 수집기로 흘러 들어갑니다.
세 번째는 크래시 리포트와 디버그 페이지입니다. 다음은 실제로 자주 보이는 안티패턴입니다.
// 절대 하면 안 되는 코드
process.on('uncaughtException', (err) => {
logger.error({ err, env: process.env }, 'fatal error, dumping context')
process.exit(1)
})
이 한 줄로 모든 시크릿이 로그 인덱스에 영구 저장됩니다. 로그 수집 시스템은 대개 애플리케이션보다 접근 권한이 넓습니다. 프레임워크의 디버그 페이지도 같은 위험을 갖습니다. Django의 디버그 모드는 예외 페이지에 설정 값을 렌더링하고, Werkzeug 디버거는 대화형 콘솔까지 열어 줍니다. 스테이징에서 켜 둔 채로 인터넷에 노출된 사례가 반복해서 나옵니다.
네 번째는 CI 로그입니다. 시크릿 마스킹 기능이 있어도 변형되면 뚫립니다.
# 마스킹은 정확히 일치하는 문자열만 가린다
set -x
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/deploy
echo "$API_TOKEN" | base64 # base64로 인코딩되면 마스킹을 통과한다
+ curl -H 'Authorization: Bearer ***' https://api.example.com/deploy
+ base64
c2tfbGl2ZV81MU54YlEyTGs5dkhjMHBNejdSdFlhV3E=
마지막은 컨테이너 이미지입니다. 빌드 인자로 넘긴 값은 이미지 히스토리에 그대로 남습니다.
docker build --build-arg NPM_TOKEN=npm_9f3aQ2v8Lx -t myapp:1.4.2 .
docker history --no-trunc myapp:1.4.2 | grep -o 'NPM_TOKEN=[A-Za-z0-9_]*'
NPM_TOKEN=npm_9f3aQ2v8Lx
RUN 단계에서 파일을 지워도 이전 레이어에 남습니다. 레지스트리에 푸시했다면 그 이미지를 받은 모든 사람이 읽을 수 있습니다. 빌드 시점 시크릿은 반드시 마운트 방식으로 넘겨야 합니다.
# syntax=docker/dockerfile:1
FROM node:22-slim
RUN \
npm ci --omit=dev
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp:1.4.2 .
시크릿이 이미 커밋됐다면 — 폐기가 1순위, 히스토리는 2순위
깃 히스토리에서 키를 발견했을 때 가장 흔한 반응은 히스토리 재작성입니다. 이것은 순서가 틀렸습니다. 올바른 순서는 폐기, 영향 조사, 히스토리 정리입니다.
이유는 단순합니다. 공개 저장소에 푸시된 자격증명은 수 초에서 수 분 안에 자동화된 스캐너에 수집됩니다. 히스토리를 아무리 깨끗하게 지워도 이미 복사된 값은 되돌릴 수 없습니다. 그리고 히스토리 재작성만으로는 다음이 남습니다.
포크된 저장소, 이미 클론한 개발자들의 로컬 사본, 플랫폼이 보관하는 풀 리퀘스트 참조(커밋 해시를 알면 여전히 접근 가능한 경우가 많습니다), 검색 엔진과 코드 검색 서비스의 캐시, CI 캐시와 아티팩트입니다. 즉 히스토리 재작성은 유출을 되돌리는 조치가 아니라 재발을 줄이는 위생 작업입니다.
1순위는 무효화입니다.
# 새 키를 먼저 만들고 배포한 뒤, 구 키를 비활성화하고 삭제한다
aws iam create-access-key --user-name ci-deployer
aws iam update-access-key --access-key-id AKIAIOSFODNN7EXAMPLE \
--status Inactive --user-name ci-deployer
aws iam delete-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --user-name ci-deployer
2순위는 영향 조사입니다. 그 키가 언제부터 어디서 쓰였는지 확인합니다.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAIOSFODNN7EXAMPLE \
--start-time 2026-07-01T00:00:00Z \
--query 'Events[].[EventTime,EventName,Username,CloudTrailEvent]' --output text | head -20
낯선 IP, 평소 쓰지 않던 리전, 예상 밖의 API 호출이 보이면 침해 대응으로 전환해야 합니다.
3순위가 히스토리 정리입니다. 먼저 어느 커밋에 들어갔는지 찾습니다.
git log --all --oneline -S 'AKIAIOSFODNN7EXAMPLE'
7c19ae4 chore: add deploy script
2f80b13 fix: correct region in deploy script
정리는 git filter-repo가 표준입니다. filter-branch는 느리고 실수하기 쉬워 더 이상 권장되지 않습니다.
cat > replacements.txt <<'EOF'
AKIAIOSFODNN7EXAMPLE==>REDACTED
pr0d-Db-Pass==>REDACTED
EOF
git filter-repo --replace-text replacements.txt --force
git push --force-with-lease --all
git push --force-with-lease --tags
이후 모든 협업자에게 새로 클론하도록 공지해야 합니다. 기존 클론에서 그대로 푸시하면 지운 커밋이 되살아납니다.
저장 방식 비교 — 그리고 쿠버네티스 Secret의 base64는 암호화가 아닙니다
시크릿 관리 도구를 고르는 기준은 "암호화되는가"가 아닙니다. 노출 경로가 몇 개인가, 폐기와 교체가 몇 분 걸리는가, 누가 언제 읽었는지 알 수 있는가입니다.
| 저장 방식 | 실제 노출 경로 | 로테이션 비용 | 접근 감사 |
|---|---|---|---|
| 소스에 하드코딩 | 저장소, 포크, 클론, 코드 검색 캐시 | 재배포 필요, 사실상 불가 | 없음 |
| .env 파일 + 환경변수 | 프로세스 환경, 자식 프로세스, 크래시 리포트, 로그 | 수동 배포, 누락 발생 | 없음 |
| 이미지에 굽기(ENV, ARG) | 위 전부 + 이미지 레이어, 레지스트리 접근자 전원 | 이미지 재빌드와 전체 롤아웃 | 없음 |
| CI 변수 | 빌드 로그, 포크 PR 워크플로, 아티팩트 | 콘솔에서 교체, 소비처 추적 난망 | 제한적 |
| 쿠버네티스 Secret 기본 | etcd 평문, get secrets 권한자 전원, 파드 환경 | 재시작 필요 | API 감사 로그 |
| 쿠버네티스 Secret + KMS 봉투 | get secrets 권한자, 파드 환경 | 재시작 필요 | API 감사와 KMS |
| 시크릿 매니저 정적 값 | 애플리케이션 메모리, 캐시 TTL 동안 | 콘솔 또는 API 호출 한 번 | 읽기 단위 기록 |
| 시크릿 매니저 동적 자격증명 | 짧은 수명 자격증명만, 리스 만료 후 자동 무효 | 자동, 인간 개입 없음 | 리스 단위 기록 |
| 워크로드 아이덴티티(OIDC) | 저장된 정적 키 없음, 토큰만 수명 동안 | 로테이션 대상 자체가 없음 | 토큰 발급 기록 |
아래로 갈수록 좋은 것이 아니라, 아래로 갈수록 사고 시 대응 시간이 짧아집니다. 이 표에서 실무자가 가장 자주 오해하는 줄이 쿠버네티스 Secret이므로 그것부터 짚습니다.
Secret 매니페스트를 보면 값이 base64로 인코딩되어 있어 암호화된 것처럼 보입니다. base64는 인코딩이지 암호화가 아닙니다. 키가 없고, 되돌리는 데 명령 하나면 됩니다.
kubectl get secret app-db -o jsonpath='{.data.password}' | base64 -d; echo
pr0d-Db-Pass
기본 설정에서 Secret은 etcd에 평문으로 저장됩니다. etcd 백업 파일, 스냅숏, 디스크 이미지를 얻은 사람은 모든 Secret을 읽습니다. 확인해 봅니다.
ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets/default/app-db | hexdump -C | head -4
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000010 73 2f 64 65 66 61 75 6c 74 2f 61 70 70 2d 64 62 |s/default/app-db|
00000020 0a 6b 38 73 00 0a 0c 0a 02 76 31 12 06 53 65 63 |.k8s.....v1..Sec|
00000030 72 65 74 12 8e 01 0a 6f 0a 06 61 70 70 2d 64 62 |ret....o..app-db|
평문이 그대로 보입니다. 저장 시 암호화를 켜면 값 앞에 프로바이더 접두사가 붙고 본문은 읽을 수 없게 됩니다.
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms:
apiVersion: v2
name: cloud-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
- identity: {}
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000020 0a 6b 38 73 3a 65 6e 63 3a 6b 6d 73 3a 76 32 3a |.k8s:enc:kms:v2:|
identity는 반드시 목록의 마지막에 두어야 합니다. 앞에 오면 평문 저장으로 되돌아갑니다. 설정을 바꿔도 기존 Secret은 다시 쓰기 전까지 평문으로 남으므로 전체를 한 번 갱신해야 합니다.
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
두 번째 오해는 RBAC입니다. 네임스페이스에 get secrets 권한이 있는 사람은 그 네임스페이스의 모든 Secret을 읽습니다. 개발자에게 편의로 준 편집 권한이 사실상 프로덕션 자격증명 열람 권한인 경우가 매우 흔합니다.
kubectl auth can-i get secrets --namespace payments --as dev@example.com
yes
실무에서 자주 쓰이는 절충안이 외부 시크릿 오퍼레이터입니다. 실제 값은 시크릿 매니저에 두고, 클러스터에는 참조만 커밋합니다.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-db
namespace: payments
spec:
refreshInterval: 15m
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: app-db
data:
- secretKey: password
remoteRef:
key: prod/payments/db
property: password
이 매니페스트는 깃에 커밋해도 됩니다. 값이 없기 때문입니다. 다만 오퍼레이터가 값을 클러스터 Secret으로 실체화하므로 앞의 RBAC와 저장 시 암호화 문제는 그대로 남습니다. 시크릿 매니저를 쓴다고 클러스터 쪽 위생이 면제되지는 않습니다.
정적 키를 없애는 구조 — 짧은 수명 자격증명과 OIDC 페더레이션
지금까지의 논의는 모두 "정적 키를 어디에 둘 것인가"였습니다. 더 나은 답은 정적 키를 만들지 않는 것입니다.
CI에서 클라우드에 접근할 때 예전 방식은 액세스 키를 만들어 CI 변수에 넣는 것이었습니다. 이 키는 만료되지 않고, 누가 복사했는지 알 수 없으며, 로테이션은 사람이 기억해야 합니다. 지금은 러너가 발급받은 OIDC 토큰을 클라우드 자격증명으로 교환합니다.
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: ap-northeast-2
- run: aws sts get-caller-identity
{
"UserId": "AROAY3EXAMPLEID:GitHubActions",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/gha-deploy/GitHubActions"
}
저장된 키가 없습니다. 자격증명은 잡 실행 시간 동안만 유효합니다.
여기서 가장 자주 나오는 설정 실수가 신뢰 정책의 조건절입니다. 다음은 위험한 설정입니다.
{
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:acme/*"
}
}
}
이 조건은 조직의 어떤 저장소, 어떤 브랜치, 어떤 풀 리퀘스트에서도 이 역할을 가져갈 수 있게 합니다. 포크에서 올라온 PR 워크플로가 프로덕션 배포 역할을 얻는 경로가 생깁니다. 브랜치나 환경까지 못박아야 합니다.
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/payments:environment:production"
}
}
}
쿠버네티스 파드도 같은 구조를 씁니다. 프로젝티드 서비스 어카운트 토큰은 대상과 만료 시간을 지정할 수 있고, 클라우드 STS가 그것을 검증합니다.
spec:
serviceAccountName: payments
volumes:
- name: aws-token
projected:
sources:
- serviceAccountToken:
audience: sts.amazonaws.com
expirationSeconds: 3600
path: token
데이터베이스 자격증명도 동적으로 만들 수 있습니다. Vault의 데이터베이스 시크릿 엔진은 요청 시점에 사용자를 생성하고 리스가 끝나면 삭제합니다.
vault read database/creds/payments-readonly
Key Value
--- -----
lease_id database/creds/payments-readonly/9tKq2xL0
lease_duration 1h
lease_renewable true
password A1a-3mQx9Zr7Kt2Vb0Ns
username v-token-payments-readonly-8Xk1qP
이 자격증명은 한 시간 뒤 스스로 사라집니다. 유출되어도 창이 좁고, 로그에 사용자 이름이 남아 어느 요청에서 발급된 것인지 추적됩니다. 정적 키를 완벽하게 숨기려 애쓰는 것보다 수명을 한 시간으로 줄이는 편이 거의 항상 더 효과적입니다.
로테이션을 가능하게 만드는 설계 — 두 개의 유효 키
로테이션이 실패하는 이유는 대개 저장소 문제가 아닙니다. 코드가 "유효한 키는 정확히 하나"라고 가정하기 때문입니다. 이 가정 아래에서 키 교체는 반드시 순간적인 불일치 구간을 만들고, 그래서 아무도 로테이션을 하지 않게 됩니다.
해결책은 검증과 서명을 분리하는 것입니다. 서명은 항상 현재 키 하나로 하고, 검증은 유효 키 집합 전체에 대해 수행합니다.
웹훅 서명 검증을 예로 들면 취약한 구현은 이렇습니다.
import hmac, hashlib, os
SECRET = os.environ["WEBHOOK_SECRET"]
def verify(body: bytes, signature: str) -> bool:
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
키를 바꾸는 순간 이전 키로 서명된 요청이 전부 거부됩니다. 전송 측과 수신 측을 원자적으로 동시에 바꿀 방법이 없으므로 사실상 교체가 불가능합니다.
집합으로 바꾸면 교체가 가능해집니다.
import hmac, hashlib, os
# 콤마로 구분된 유효 키 목록. 첫 번째가 현재 서명 키.
KEYS = [k.strip() for k in os.environ["WEBHOOK_SECRETS"].split(",") if k.strip()]
def sign(body: bytes) -> str:
return hmac.new(KEYS[0].encode(), body, hashlib.sha256).hexdigest()
def verify(body: bytes, signature: str) -> bool:
for key in KEYS:
expected = hmac.new(key.encode(), body, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, signature):
return True
return False
이제 교체 절차가 무중단으로 성립합니다.
1단계 신 키를 유효 목록 끝에 추가하고 배포한다 → 구 키로 서명, 둘 다 검증
2단계 신 키를 목록 맨 앞으로 옮기고 배포한다 → 신 키로 서명, 둘 다 검증
3단계 구 키로 서명된 요청이 사라졌는지 확인한다 → 지표로 확인
4단계 구 키를 목록에서 제거하고 배포한다 → 신 키만 유효
5단계 구 키를 발급처에서 폐기한다
3단계를 눈으로 확인하려면 어떤 키가 매칭됐는지 지표로 남겨야 합니다.
def verify(body: bytes, signature: str) -> bool:
for index, key in enumerate(KEYS):
expected = hmac.new(key.encode(), body, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, signature):
metrics.increment("webhook.signature.verified", tags=[f"key_index:{index}"])
return True
metrics.increment("webhook.signature.failed")
return False
key_index:1의 카운터가 0이 되면 구 키를 지워도 안전하다는 뜻입니다. 로테이션을 추측이 아니라 관측으로 진행할 수 있게 됩니다.
JWT라면 같은 원리가 kid 헤더와 JWKS로 구현됩니다. 발급자는 JWKS에 신 키를 먼저 공개하고, 검증자가 그것을 가져간 뒤에 서명 키를 바꿉니다. 데이터베이스 계정은 사용자 두 개를 번갈아 쓰는 방식이 표준입니다. 짝수 회차에는 app_a, 홀수 회차에는 app_b를 갱신하고, 애플리케이션은 시크릿 매니저에서 현재 활성 사용자를 읽습니다. 어느 순간에도 유효한 자격증명이 하나 이상 존재하므로 중단이 없습니다.
핵심은 이것입니다. 로테이션은 운영 절차가 아니라 설계 속성입니다. 코드가 키 하나만 가정하면 아무리 좋은 시크릿 매니저를 써도 실제 교체는 일어나지 않습니다.
탐지 — 사전 커밋 훅과 저장소 스캐닝이 못 하는 일
마지막 층이 탐지입니다. 도구는 성숙해 있습니다.
# 작업 트리와 히스토리 전체 스캔
gitleaks detect --source . --redact --report-format json --report-path leaks.json
# 커밋 직전 스테이지된 변경만 검사
gitleaks protect --staged --redact
Finding: STRIPE_SECRET_KEY=sk_live_REDACTED
Secret: REDACTED
RuleID: stripe-access-token
File: deploy/staging.env
Line: 14
Commit: 7c19ae4a2f3b18c0d95e7f4a6b2c1d80e3f9a5b6
사전 커밋 훅으로 걸어 두면 개발자 로컬에서 잡힙니다.
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
검증 기능이 있는 스캐너는 노이즈를 크게 줄여 줍니다. 발견한 값으로 실제 API를 호출해 살아 있는 키만 보고하는 방식입니다.
trufflehog git file://. --only-verified --json | jq -r '.DetectorName + " " + .Verified'
여기까지가 도구가 해 주는 일이고, 다음이 한계입니다.
사전 커밋 훅은 로컬 설정이라 우회됩니다. git commit --no-verify 한 번이면 끝이고, 새로 합류한 사람은 훅을 설치하지 않은 상태로 며칠을 보냅니다. 실제 강제력은 서버 쪽 푸시 보호에서 나옵니다. 훅은 편의 장치, 서버 검사는 통제 장치로 구분해야 합니다.
탐지 규칙은 형태가 뚜렷한 값에만 잘 듣습니다. AKIA로 시작하는 액세스 키나 sk_live_ 접두사가 붙은 토큰은 정확히 잡히지만, 데이터베이스 비밀번호나 사설 서비스의 API 키처럼 형태가 없는 값은 엔트로피 추정에 의존합니다. 엔트로피 규칙은 오탐이 많아 임계값을 올리게 되고, 그러면 진짜 비밀번호를 놓칩니다.
그리고 스캐너는 코드만 봅니다. 위키, 이슈 첨부파일, 채팅 기록, 스프레드시트, 로컬 노트에 복사된 사본은 대상이 아닙니다. 실제 사고에서 유출 지점이 저장소가 아닌 경우가 상당수입니다.
그래서 탐지에 투자하기 전에 확인할 것이 하나 있습니다. 이 시크릿이 노출됐다고 가정할 때 폐기와 교체에 몇 분이 걸리는가. 이 숫자가 크면 탐지를 아무리 촘촘히 해도 사고 시 손실은 줄지 않습니다. 반대로 이 숫자가 작으면 탐지가 늦어도 피해가 제한됩니다. 우선순위는 언제나 대응 시간 단축이 먼저입니다.
마치며 — 숨겼는가가 아니라 몇 분에 바꿀 수 있는가
정리하면 세 문장입니다.
환경변수는 저장 위치가 아니라 전달 방식입니다. 프로세스 환경은 같은 호스트에서 읽히고, 로그와 크래시 리포트와 이미지 레이어로 계속 새어 나갑니다. .gitignore는 그중 한 경로만 막습니다.
시크릿이 노출됐을 때의 순서는 폐기, 조사, 히스토리 정리입니다. 반대로 하면 이미 복제된 값을 그대로 둔 채 정리 작업에 시간을 쓰게 됩니다.
그리고 가장 확실한 개선은 정적 키를 줄이는 것입니다. CI는 OIDC 페더레이션으로, 데이터베이스는 동적 자격증명으로, 남는 정적 키는 유효 키 집합 설계로 언제든 교체 가능하게 만듭니다.
팀의 시크릿 관리 성숙도를 측정하는 질문은 하나면 충분합니다. 지금 이 자격증명이 인터넷에 공개됐다고 가정할 때, 폐기하고 새 값으로 교체해 서비스를 정상화하는 데 몇 분이 걸립니까. 그 답이 시간 단위라면 도구를 더 사는 것보다 로테이션 경로를 먼저 만들어야 합니다.
현재 단락 (1/253)
시크릿 관리에 대한 표준적인 조언은 짧습니다. 코드에 하드코딩하지 말고 환경변수로 빼라, `.env` 파일은 `.gitignore`에 넣어라. 여기까지가 대부분의 문서가 다루는 범...