Sealed Secrets 마스터 키 백업 안 해놓고 클러스터 옮겼다가 시크릿 47개 복구한 이야기

지난주 금요일 오후 4시. 평화롭던 사무실에 메신저 알림이 떴다. "스테이징 클러스터 새로 만든 거 거의 다 됐는데, ArgoCD가 SealedSecret 못 푼다고 에러 토하는 중인데 좀 봐주실 수 있어요?" 그 메시지를 본 순간 명치가 싸늘해졌다.
왜냐면 그 이전 클러스터의 sealed-secrets controller에서 발급된 master key를 백업해뒀는지 기억이 안 났거든. 결론부터 말하면 안 해놨다. 그래서 그날 퇴근은 새벽 1시였다.
무슨 일이 벌어진 건가
상황을 좀 정리해보자. 우리 팀은 EKS 1.28 → 1.31 업그레이드를 in-place로 안 하고 새 클러스터를 만들어서 옮기는 방식으로 진행하고 있었다. 스테이징부터. 매니페스트는 ArgoCD GitOps로 관리되니까 그냥 ArgoCD가 새 클러스터 바라보게 하고, App-of-Apps 한 번만 돌리면 깔끔하게 끝나야 한다 — 그게 원래 시나리오였다.
그런데 SealedSecret 47개가 전부 no key could decrypt secret 에러를 토하기 시작했다. controller 로그를 보면:
ERROR controllers.SealedSecret no key could decrypt secret <namespace>/<secret-name>
처음엔 "어 이거 controller만 다시 띄우면 되는 거 아닌가?" 했다. 그런데 다시 띄워도 똑같았다. 그제서야 깨달았다. 새 클러스터에 띄운 sealed-secrets controller는 새로 자동 생성된 master key를 들고 있고, 이전 클러스터에서 만든 SealedSecret들은 이전 클러스터의 public key로 암호화돼 있으니까 당연히 못 푸는 거다.
이게 sealed-secrets의 가장 기본적인 운영 함정인데, 알고 있으면서도 당했다. 솔직히 좀 자존심 상했다.
왜 백업이 없었나
거슬러 올라가 봤다. 이 클러스터는 2년 전 누군가가 처음 셋업했고, 그때 만들어진 master key는 controller가 자체 시크릿(sealed-secrets-keyXXXXX)으로 들고 있었다. 누구도 그걸 외부로 꺼내서 안전한 곳에 백업해놓지 않았다.
그동안 별 일 없이 잘 돌아갔던 이유는 단순했다 — 클러스터를 새로 만든 적이 없었으니까. controller가 죽어도 다시 뜨면 같은 namespace의 같은 secret을 읽어서 같은 key로 복원되니 문제가 없었던 거다.
그런데 클러스터 자체를 옮기면? 그 secret이 안 따라온다. 따로 export하지 않는 한.
Sealed Secrets 문서도 이걸 명시적으로 경고하고 있다. 최근 v0.36.5(2026년 4월 9일 릴리스)에서도 동일한 디자인이고, 여기서 master key는 30일마다 자동 갱신되긴 하지만 갱신은 백업이 아니다. 갱신 시 controller가 새 키를 추가로 만들고 기존 키는 그대로 두는 방식이라, controller pod 안에서는 디크립트가 잘 되지만 그 secret을 통째로 잃어버리면 끝이다.
멘탈 차리고 첫 시도: 이전 클러스터에서 키 꺼내기
다행히 이전 클러스터는 아직 살아있었다(곧 폐기 예정이었지만 아직 안 지웠다). 30분만 늦었어도 큰일이었다. 이 부분은 정말 운이 좋았다.
# 이전 클러스터 컨텍스트로 전환
kubectl config use-context staging-old
# kube-system에 있는 sealed-secrets controller key secret 다 꺼내기
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key=active \
-o yaml > master-keys-backup.yaml
여기서 한 가지 짚고 넘어갈 것. 위 라벨이 active로 돼있는 건 controller가 활성 키로 사용하는 것들이다. 30일 자동 갱신을 거쳐오면서 키가 여러 개 쌓여있을 수 있다. 우리 경우엔 4개가 나왔다. 2년 동안 24번 갱신이 됐어야 하는데 4개밖에 없는 건 좀 이상해서 controller 로그를 봤더니, 아 — 작년에 누가 controller deployment 다시 만들면서 keys-secret이 일부 날아갔던 흔적이 있었다. 이 얘기는 일단 옆으로 미뤄두고.
# 백업 파일에 namespace 정리 (선택사항이지만 안전)
sed -i '' 's/namespace: kube-system/namespace: kube-system/g' master-keys-backup.yaml
이 파일은 평문으로 master key가 들어있는 파일이다. 무조건 안전한 곳으로 옮기고 작업 끝나면 지워야 한다. 나는 작업 디렉토리를 ramdisk(macOS면 hdiutil로 임시 디스크)에 두고 작업했다. 디스크에 평문 키가 남는 게 너무 싫어서.
새 클러스터에 키 주입하기
kubectl config use-context staging-new
# 기존 controller가 만들어놓은 key secret 일단 백업
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key=active \
-o yaml > new-cluster-current-key.yaml
# 이전 키들 주입
kubectl apply -f master-keys-backup.yaml
# controller 재시작해서 키 다시 로드
kubectl rollout restart deployment sealed-secrets-controller -n kube-system
그러고 나서 controller 로그를 확인했더니:
INFO sealed-secrets-controller new key loaded: sealed-secrets-key-aXX123
INFO sealed-secrets-controller new key loaded: sealed-secrets-key-bYY456
INFO sealed-secrets-controller new key loaded: sealed-secrets-key-cZZ789
INFO sealed-secrets-controller new key loaded: sealed-secrets-key-dWW012
INFO sealed-secrets-controller controller version v0.36.5 ready
controller는 디렉토리에서 active 라벨이 붙은 키를 전부 로드한다. 이론상 여기서 끝나야 했다.
그런데 끝나지 않았다
ArgoCD를 sync 해봤다. 어떤 SealedSecret은 풀리고, 어떤 건 여전히 못 풀고 있었다. 패턴을 봤더니:
- 2024년 초반에 만들어진 SealedSecret → 풀림
- 2024년 후반 ~ 2025년 초 SealedSecret → 못 풀림
- 2025년 중반 이후 → 풀림
아까 잠깐 언급한 "작년에 누가 keys-secret 일부 날린 흔적"이 여기서 발목을 잡았다. 그 시기에 갱신된 키 한두 개가 영영 사라진 거다. 즉 그 키로 암호화된 SealedSecret은 원본 평문을 모르면 영원히 복구 불가다.
이게 사실 이론적으론 알고 있는 거였다. master key 잃으면 그 키로 암호화된 secret은 못 푼다. 알고는 있어도 새벽 0시에 직접 마주치니까 멘탈이 또 한 번 나갔다.
다행히 못 푸는 SealedSecret이 7개였고, 다 외부 SaaS API 키 같은 거여서 그쪽 콘솔에서 재발급 받아 다시 sealed로 만들면 되는 것들이었다. 8개 중 1개가 사내 레거시 시스템에서 발급된 거라 그쪽 담당자한테 새벽에 카톡을 보내야 했다. 미안했다.
# 못 푸는 시크릿들 리스트 추출
kubectl get sealedsecret --all-namespaces -o json | \
jq -r '.items[] | select(.status.conditions[]? | .type=="Synced" and .status=="False") | "\(.metadata.namespace)/\(.metadata.name)"'
이 리스트를 들고 하나하나 재발급/재봉인 작업을 했다. 4시간 걸렸다.
사후 조치로 만든 것들
이번 일을 겪고 두 가지를 만들었다.
첫째, master key 정기 백업 cronjob. AWS S3에 KMS 암호화로 올린다. 한 달에 한 번 controller가 키를 갱신하니까 그 주기에 맞춰 일주일에 한 번 백업한다. 굳이 매일 안 돌리는 건, S3 객체 버저닝으로 과거 시점 키도 다 살아있게 하려고.
apiVersion: batch/v1
kind: CronJob
metadata:
name: sealed-secrets-key-backup
namespace: kube-system
spec:
schedule: "0 3 * * 0" # 매주 일요일 새벽 3시
jobTemplate:
spec:
template:
spec:
serviceAccountName: sealed-secrets-backup
restartPolicy: OnFailure
containers:
- name: backup
image: amazon/aws-cli:2.x
command:
- /bin/sh
- -c
- |
set -e
kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > /tmp/keys-$(date +%Y%m%d).yaml
aws s3 cp /tmp/keys-$(date +%Y%m%d).yaml \
s3://our-secrets-backup/sealed-secrets/$(hostname)/ \
--sse aws:kms --sse-kms-key-id alias/secrets-backup
shred -u /tmp/keys-*.yaml
ServiceAccount에는 secrets get/list 권한과, IRSA로 S3 쓰기 권한만. 백업 버킷은 별도 AWS 계정에 두고 cross-account access로만 쓰고 있다. 같은 계정에 두면 그 계정 자체가 털릴 때 백업까지 같이 사라진다.
둘째, 신규 클러스터 셋업 runbook에 "마스터 키 사전 주입" 섹션 추가. 새 클러스터에 sealed-secrets controller 띄우기 전에 S3에서 백업 키 받아서 먼저 apply하고, 그 다음에 controller를 띄운다. 순서가 중요하다. controller가 먼저 뜨면 자체적으로 새 키를 생성해버리니까 좀 헷갈리는 상태가 된다(작동은 하는데 굳이).
그리고 한 가지 검토 중인 게, age 기반 SOPS로 옮기는 건 어떨까 하는 거다. 키 관리를 KMS 한 곳으로 단일화할 수 있고, sealed-secrets처럼 클러스터 안에서 키를 들고 있을 필요가 없다. ArgoCD에 ksops plugin 붙이면 GitOps도 그대로 된다. 다만 SOPS는 secret이 etcd에 평문으로 저장된다는 차이가 있어서, 우리 컴플라이언스 요건에 맞는지 다시 봐야 한다. 아직 결정 못 함.
교훈
새벽 1시에 책상에서 멍하니 천장을 보면서 정리한 것들이다.
키 관리는 처음 셋업할 때 가장 중요하지만, 가장 잊혀지기 쉽다. 일상 운영에서는 그냥 잘 돌아가니까 존재감이 없다가, 클러스터 마이그레이션 같은 비일상적인 이벤트가 터지는 그 순간 갑자기 모든 게 그 키 하나에 매달려 있다는 걸 알게 된다.
또 하나, "controller가 알아서 30일마다 갱신해주니까 안전하겠지"는 위험한 사고였다. 갱신은 키를 추가하는 거지 백업하는 게 아니다. 그리고 controller가 들고 있는 그 secret이 어떤 이유로든 사라지면, 그 시점까지 갱신된 키 전부가 같이 사라진다.
마지막으로, 이전 클러스터를 너무 빨리 폐기하지 말 것. 우리는 새 클러스터로 옮긴 후 30일은 무조건 둔다는 정책으로 바꿨다. 비용은 좀 더 들지만 보험이라 생각하기로.
혹시 이런 키 관리 하시는 분들 중에 더 좋은 방식 쓰시면 댓글 좀 달아주세요. 특히 SOPS로 옮긴 사례 있으면 정말 궁금합니다.