2026년 3월 9일
Argo CD Vault Plugin에서 Vault Secrets Operator로 전환하기: GitOps 시크릿 주입 방식의 진화
[DevOps] Argo CD Vault Plugin에서 Vault Secrets Operator로 전환하기: GitOps 시크릿 주입 방식의 진화
Argo CD 3.1 업그레이드 이후 발생한 Vault Plugin(AVP)의 호환성 한계와 보안 취약점을 극복하기 위해 Vault Secrets Operator(VSO)로 마이그레이션한 실무 경험과 시행착오 해결 과정을 공유합니다.
GitOps를 도입하면서 마주하는 가장 까다로운 숙제 중 하나는 바로 안전한 시크릿(비밀 정보)의 동기화입니다.
저희 조직은 시크릿 데이터의 중앙 집중식 보관과 라이프사이클 관리를 위해 하시코프 볼트(HashiCorp Vault)를 도입하여 운용하고 있습니다. 볼트 내부의 데이터 관리는 매우 안정적으로 이루어지고 있지만, 저장된 시크릿을 Git에 노출하지 않으면서 쿠버네티스 클러스터 내부로 어떻게 효율적으로 전달할 것인가는 아키텍처 설계의 핵심적인 고민거리였습니다. 즉, 시크릿 저장소 자체의 보안만큼이나 이를 클러스터로 가져오는 주입(Injection) 및 동기화 방식이 전체 GitOps 파이프라인의 보안 경계와 운영 경험을 결정짓게 되기 때문입니다.
초기에는 Argo CD Vault Plugin(이하 AVP)을 주력으로 사용했습니다. 별도의 오퍼레이터를 관리할 필요 없이 어노테이션과 플레이스홀더만으로 시크릿 주입이 가능해 설정이 매우 직관적이었고, 덕분에 저희의 초기 파이프라인 구축 단계에서 훌륭한 효율성을 제공해 주었습니다.
하지만 최근 Argo CD를 3.1 버전으로 업그레이드한 이후, AVP가 매니페스트를 정상적으로 렌더링하지 못하거나 간헐적으로 동기화에 실패하는 등 불안정한 동작이 관찰되었습니다.
문제의 근본 원인을 파악하기 위해 트러블슈팅을 진행하던 중, 깃허브 레포지토리의 활동량이 급격히 저조해진 점에 주목했습니다. 커뮤니티의 최신 이슈들에 대한 피드백이 전무하고 공식 릴리스가 사실상 멈춰있는 상태를 보며, 더 이상 AVP를 신뢰할 수 있는 운영 도구로 유지하기 어렵다는 기술적 판단을 내렸습니다.
이는 단순히 일시적인 버그가 아니라, 급변하는 쿠버네티스와 Argo CD의 버전업 주기를 플러그인이 더 이상 따라가지 못해 발생하는 구조적 한계였습니다. 결과적으로 최신 생태계와의 완벽한 호환성을 장기적으로 담보하기 어려울 뿐만 아니라, 향후 치명적인 보안 취약점이 발견되더라도 즉각적인 패치 대응을 기대할 수 없다는 것을 의미했습니다. 보안이 최우선이어야 할 시크릿 관리 도구로서 이는 운영 환경에서 감당할 수 없는 리스크였습니다.
저희 팀은 이러한 불안정한 도구에 임시방편(Hotfix)을 적용하며 리스크를 안고 가기보다, 하시코프에서 공식 지원하며 쿠버네티스 네이티브한 특성을 가진 Vault Secrets Operator (이하 VSO) [1]로 전환하여 시크릿 관리의 지속 가능성을 확보하기로 결정했습니다. Argo CD에서 매니페스트 생성 단계에 시크릿 값을 주입하는 방식에서 벗어나, 오퍼레이터를 통한 동적 동기화 방식으로 아키텍처를 전면 개편한 과정과 그 이점을 본문에서 상세히 공유하고자 합니다.
1. 기존 주입 방식: Argo CD Vault Plugin (AVP)의 한계
AVP는 Argo CD의 커스텀 플러그인 기능을 이용하여, 매니페스트가 클러스터에 배포되기 직전(Generate 단계)에 볼트에서 시크릿을 읽어와 매니페스트에 직접 주입(Injection)하는 방식입니다.
운영 환경이 커지고 시스템이 고도화될수록 다음과 같은 구조적인 단점들이 발목을 잡았습니다.
- 단점 1. 불확실한 호환성 및 유지보수 (가장 큰 전환 사유) 앞서 언급한 Argo CD 3.1 버전 업그레이드 이후의 이슈처럼, 최신 생태계를 신속히 반영하지 못하는 도구는 운영 리스크가 큽니다. 현재 공식 깃허브 레포지토리는 업데이트가 오랫동안 멈춰있으며, Argo CD 공식 문서[2]조차 매니페스트 생성 기반의 시크릿 관리를 더 이상 추천하지 않습니다.
- 단점 2. 보안 취약점 (Argo CD 캐시에 민감 정보 보관 가능성) AVP는 렌더링 과정에서 시크릿 실제 값을 텍스트로 치환하며, 성능을 위해 렌더링된 매니페스트를 Argo CD의 Redis 캐시에 보관할 수 있습니다. 이는 곧 Argo CD 인프라 영역에 민감 정보가 평문(Plaintext)으로 남을 수 있는 보안상의 허점을 노출합니다.
- 단점 3. 권한 분리의 어려움 Argo CD의 repo-server가 볼트에서 직접 시크릿을 가져와야 하므로, Argo CD가 모든 서비스의 시크릿에 접근할 수 있는 광범위한 권한을 소유해야 합니다. 이는 최소 권한 원칙(Least Privilege)을 준수하기 어렵게 만듭니다.
- 단점 4. 시크릿 변경 시 Sync 관리의 피로감 볼트 내부에서 시크릿 내용이 변경되더라도 Git의 선언 상태가 변하지 않으면 Argo CD가 매니페스트 재생성을 트리거하지 않습니다. 따라서 운영자가 매번 수동으로 하드 리프레시(Hard Refresh)를 실행해야만 반영되는 불편함이 있었습니다.
2. 새로운 동기화 방식: Vault Secrets Operator (VSO)
VSO는 쿠버네티스 네이티브한 CRD 방식으로 볼트의 시크릿을 클러스터로 안전하게 동기화합니다. VSO 도입을 통해 다음과 같은 이점을 얻을 수 있었습니다.
- 보안 강화: Argo CD는 데이터가 없는 CR 껍데기만 배포하고, 실제 시크릿은 각 타겟 클러스터 내부에서만 VSO를 통해 호출되므로 보안 경계가 훨씬 명확해집니다.
- 자동 갱신 및 재시작 트리거: 볼트 값이 바뀌면 VSO가 이를 감지하여 Kubernetes Secret 리소스를 자동 갱신합니다. 또한 rolloutRestartTargets 옵션을 활용하면 시크릿 갱신 시 연결된 Deployment 등을 자동으로 롤링 재시작하여 변경 사항을 즉각 반영할 수 있습니다.
- 네이티브 마운트: 표준 쿠버네티스 Secret 리소스를 그대로 활용하므로, 별도의 코드 수정 없이 기존 envFrom 설정을 유지할 수 있습니다.
3. 마이그레이션 가이드 및 실무 트러블슈팅
저희 팀이 진행했던 마이그레이션의 핵심 단계를 공유합니다. 특히 권한 설정 과정에서 겪었던 시행착오와 해결책을 중점적으로 다루었습니다.
Step 1. 타겟 클러스터 볼트 인증 및 정책 설정 (Vault CLI)
VSO가 클러스터에서 볼트에 접근하려면 볼트 서버 측에서 쿠버네티스 인증(Auth Method)과 접근 권한을 정의하는 정책(Policy)을 구성해야 합니다.
1-1. Vault 정책(Policy) 생성 (공통)
VSO가 실제 KV 저장소 경로에 접근할 수 있도록 권한을 정의합니다.
# vso-policy.hcl 파일 작성
cat <<EOF > vso-policy.hcl path "secret/data/prod-kv/*" { capabilities = ["read", "list"] } EOF
# Vault 서버에 정책 등록
vault policy write vso-policy vso-policy.hcl
1-2. 인증 및 Role 설정 (환경에 따라 선택)
[In-cluster 기준: Vault가 있는 동일 클러스터]
vault auth enable -path=kubernetes-incluster kubernetes
vault write auth/kubernetes-incluster/config \
kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \
disable_iss_validation=true
# Role 등록 (1-1에서 만든 vso-policy 연결)
vault write auth/kubernetes-incluster/role/vso-role \
bound_service_account_names=default \
bound_service_account_namespaces=vso-system,default \
policies=vso-policy \
ttl=24h
[Remote-cluster 연동 시: 볼트가 없는 원격 클러스터]
1. 타겟 클러스터(vso-system ns)에서 리뷰어용 SA 및 토큰 Secret 생성
# 1. 리뷰어용 ServiceAccount 생성
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-reviewer
namespace: vso-system
---
# 2. 볼트의 토큰 검증을 위한 auth-delegator 권한 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-vault-auth-delegator
subjects:
- kind: ServiceAccount
name: vault-reviewer
namespace: vso-system
roleRef:
kind: ClusterRole
name: system:auth-delegator
apiGroup: rbac.authorization.k8s.io
---
# 3. K8s 1.24+ 대응을 위한 장기 토큰 Secret 수동 생성
apiVersion: v1
kind: Secret
metadata:
name: vault-reviewer-token
namespace: vso-system
annotations:
kubernetes.io/service-account.name: vault-reviewer
type: kubernetes.io/service-account-token
2. 원격 클러스터 정보를 볼트 서버에 등록 및 Role 생성
export REVIEWER_TOKEN=$(kubectl get secret vault-reviewer-token -n vso-system -o jsonpath='{.data.token}' | base64 -d)
export K8S_HOST=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.server}')
kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt
vault auth enable -path=kubernetes-remote kubernetes
# 원격 클러스터 인증 설정 등록
vault write auth/kubernetes-remote/config \
kubernetes_host="$K8S_HOST" \
kubernetes_ca_cert=@ca.crt \
token_reviewer_jwt="$REVIEWER_TOKEN" \
disable_local_ca_jwt=true
# kubernetes-remote 경로에 Role 등록 (1-1에서 만든 vso-policy 연결)
vault write auth/kubernetes-remote/role/vso-role \
bound_service_account_names=default \
bound_service_account_namespaces=vso-system,default \
policies=vso-policy \
ttl=24h
💡 실무 팁: 클러스터 및 네임스페이스별 Role 분리를 통한 보안 강화
저희 팀은 보안 경계 격리를 위해 클러스터별 전용 Role을 생성하여 사용하고 있으며, 이를 강력히 권장합니다. 특정 클러스터의 보안 침해 사고가 발생하더라도 피해가 타 클러스터로 확산되는 것을 차단(Blast Radius 제한)하고, 환경별(DEV/PRD)로 독립적인 접근 정책을 유연하게 관리할 수 있기 때문입니다.
한 단계 더 나아가, 아주 민감한 데이터를 다루는 환경이라면 네임스페이스별로 Role을 더 쪼개어 관리하는 것도 방법입니다. 이렇게 하면 동일 클러스터 내에서도 서비스 간의 시크릿 접근 권한을 완전히 격리하여 보안성을 극대화할 수 있습니다.
Step 2. Vault Secrets Operator 설치
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
--namespace vso-system \
--create-namespace \
--set defaultVaultConnection.address="$VAULT_ADDR"
Step 3. VSO 인증 객체(VaultAuth) 구성
예제는 중앙 네임스페이스 vso-system에 공용 VaultAuth를 생성하고 각 애플리케이션에서 이를 참조합니다.
💡 핵심 포인트: 볼트(Vault) 서버 측 역할(Role)에 앱 네임스페이스 허용 추가
신규 앱 네임스페이스가 추가될 때마다 bound_service_account_namespaces 목록에 해당 네임스페이스를 추가해야 인증 거부 에러를 방지할 수 있습니다.
보안을 위해서 네임스페이스별 vaultauth를 별도로 사용하는것을 추천 합니다
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: common-vault-auth
namespace: vso-system
spec:
method: kubernetes
mount: kubernetes-remote
kubernetes:
role: vso-role
serviceAccount: default
Step 4. 매니페스트 리팩토링 및 시크릿 배포
케이스 1. 일반적인 Secret 리소스 변환
가장 기본적이고 많이 쓰이는 형태입니다. 애플리케이션 파드의 Deployment 매니페스트는 전혀 수정할 필요 없이 기존 envFrom 설정을 그대로 유지하고, 기존의 Secret 파일만 삭제한 뒤 VaultStaticSecret으로 교체하면 됩니다.
[VSO 방식의 VaultStaticSecret YAML]
이 매니페스트를 배포하면, VSO는 볼트에서 데이터를 읽어와 destination.name에 지정된 이름으로 일반 쿠버네티스 Secret을 생성합니다.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: test-app-secret-vso # VSO가 관리하는 CRD 이름
namespace: my-app-ns
spec:
vaultAuthRef: vso-system/common-vault-auth
mount: secret
path: prod-kv/my-app
type: kv-v2
destination:
name: test-app-secret # ★ 중요: 실제 생성될 K8s Secret의 이름
create: true
rolloutRestartTargets:
- kind: Deployment
name: my-app-deployment
위 설정을 배포하면 클러스터 내에 test-app-secret이라는 일반 Secret이 생성됩니다. 따라서 기존 애플리케이션의 Deployment는 아무런 수정 없이 해당 시크릿을 참조하여 작동합니다.
# 애플리케이션 Deployment (기존 코드 유지)
envFrom:
- secretRef:
name: test-app-secret # VSO가 생성해준 Secret 이름을 그대로 참조
케이스 2. Helm Chart 환경에서의 리팩토링 (n8n)
n8n과 같이 Helm 차트를 통해 배포되는 서비스는 values.yaml에 직접 플레이스홀더를 적어주던 기존 방식을 버리고, VSO가 동기화한 시크릿 리소스를 참조하도록 변경하여 Git 매니페스트와 실제 시크릿 값 사이의 의존성을 분리합니다.
1. 기존 values.yaml의 AVP 설정 제거
기존에는 values.yaml에 볼트 경로(path:secret/data/…)를 직접 명시했지만, 이제는 해당 필드를 비워두거나 삭제하여 Git에서 민감 정보에 대한 흔적을 완전히 지웁니다.
# values.yaml (AS-IS)
# dbPassword: "<path:prod-kv/data/n8n#postgrepass>" (AVP 방식 삭제)
2. VaultStaticSecret 생성 및 특정 Key 추출
볼트의 경로(prod-kv/data/n8n) 안에 여러 데이터가 섞여 있을 경우, transformation 옵션을 사용해 필요한 데이터만 정제하여 쿠버네티스 시크릿으로 생성할 수 있습니다.
helm template 기능을 이용하여 배포하면 편리 합니다
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: n8n-vso-secret
namespace: n8n-namespace
spec:
type: kv-v2
mount: secret
path: prod-kv/n8n
destination:
name: n8n-db-secret # ★ 생성될 K8s Secret 이름
create: true
transformation:
includes:
- "^postgrepass$" # 볼트 데이터 중 DB 암호 키값만 선택적으로 포함
vaultAuthRef: "vso-system/common-vault-auth"
3. extraEnv를 통한 시크릿 주입
Helm 차트에서 제공하는 extraEnv 또는 envFrom 옵션을 활용하여, VSO가 생성한 시크릿 내부의 값을 애플리케이션 환경변수로 연결합니다.
# values.yaml (TO-BE)
extraEnv:
DB_POSTGRESDB_PASSWORD:
valueFrom:
secretKeyRef:
name: n8n-db-secret # 위에서 지정한 destination.name 참조
key: postgrepass # 생성된 Secret 내부의 데이터 키
이렇게 구성하면 Helm 차트는 VaultStaticSecret가 생성한 Secret의 값을 이용하여 배포 합니다.
Step 5. Cleanup: 기존 AVP 설정 삭제 및 플러그인 제거
VSO를 통한 시크릿 동기화가 정상적으로 작동하는 것을 확인했다면, 이제 기존에 사용하던 AVP 관련 설정들을 제거해야 합니다.
5-1. 매니페스트 어노테이션 삭제
Argo CD가 해당 애플리케이션을 배포할 때 더 이상 AVP 플러그인을 호출하지 않도록 기존에 사용하던 avp 어노테이션을 제거합니다.
5-2. Argo CD ConfigMap/Sidecar 정리
- 환경변수 삭제: Argo CD CM/Secret에 등록했던 AVP_AUTH_TYPE, VAULT_ADDR 등을 삭제합니다.
- 컨테이너 제거: argocd-repo-server Deployment에서 argocd-vault-plugin 사이드카 컨테이너 설정을 제거합니다.
5-3. Vault 권한 회수
기존에 Argo CD(repo-server)가 볼트에 접근하기 위해 사용했던 전용 Token이나 AppRole이 있다면 이를 삭제합니다. 이제는 각 클러스터의 VSO가 개별적으로 인증하므로, Argo CD가 볼트에 직접 접근할 권한을 유지할 필요가 없습니다.
4. 전환 후기 및 결론
Argo CD 3.1 업그레이드 대응이라는 트러블슈팅으로 시작된 마이그레이션이었지만, 결과적으로 GitOps 파이프라인의 보안과 운영 안정성을 한 차원 높이는 전환점이 되었습니다.
가장 크게 체감하는 변화는 운영 피로도의 감소입니다. 기존에는 Vault의 값이 변경될 때마다 Argo CD에서 수동으로 Hard Refresh를 트리거하고 파드를 재시작해야 했으나, VSO 도입 이후에는 시크릿 갱신부터 파드의 롤링 재시작까지 전 과정이 매끄럽게 자동화되었습니다.
인프라 관리 측면에서도 이점을 얻었습니다. 시크릿 주입 실패 시 거대한 Argo CD 로그를 뒤지던 과거와 달리, 이제는 쿠버네티스 리소스(VaultStaticSecret)의 상태(Status)와 이벤트만으로 즉각적인 원인 파악과 트러블슈팅이 가능해졌습니다. 무엇보다 중앙의 Argo CD가 쥐고 있던 거대한 타겟 클러스터 시크릿 접근 권한을 각 클러스터 단위로 분산시킴으로써, 멀티 클러스터 환경에서의 보안 확장성을 확보한 것이 아키텍처 관점에서의 가장 완벽한 수확입니다.
별도의 오퍼레이터를 추가로 운영해야 한다는 초기 부담감은 있었으나, 역할 분리와 관리 측면에서 주는 이점이 이를 충분히 상쇄합니다. 장기적인 운영 안정성과 최신 쿠버네티스 생태계와의 철학적 호환성을 고려한다면, 매니페스트 렌더링 방식에서 VSO와 같은 오퍼레이터 기반 동기화 방식의 전환은 선택이 아닌 필수라고 생각합니다.