Backend

GitOps 심화 시리즈 #5: Secrets Management - Git에 비밀을 안전하게 저장하기

2026-01-059 min read

GitOps 심화 시리즈 #5: Secrets Management - Git에 비밀을 안전하게 저장하기

시리즈 개요

#주제핵심 내용
1GitOps 개요철학과 원칙, Push vs Pull 배포, Reconciliation
2ArgoCD Deep Dive아키텍처, Application CRD, Sync 전략
3Flux CD & GitOps Toolkit컨트롤러 아키텍처, GitRepository, Kustomization
4환경별 설정 관리Kustomize vs Helm, 전략 선택 기준
5Secrets ManagementSealed Secrets, External Secrets, SOPS
6CI/CD 파이프라인 통합Image Updater, Progressive Delivery

GitOps에서 Secrets의 딜레마

GitOps의 핵심 원칙은 Git을 Single Source of Truth로 삼는 것입니다. 하지만 Kubernetes Secret을 Git에 저장하면?

# ⚠️ 절대 이렇게 하면 안 됩니다!
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=        # base64는 암호화가 아님!
  password: c3VwZXJzZWNyZXQ=  # 누구나 디코딩 가능
# 즉시 평문 노출
echo "c3VwZXJzZWNyZXQ=" | base64 -d
# 출력: supersecret

[!CAUTION] Base64는 인코딩이지 암호화가 아닙니다. Git 히스토리에 한 번 들어가면 영구히 노출됩니다.

딜레마의 본질


해결책 1: Sealed Secrets

Sealed Secrets는 Bitnami에서 개발한 Kubernetes 컨트롤러로, 클러스터 내에서만 복호화 가능한 암호화된 Secret을 Git에 저장합니다.

동작 원리

설치

# 컨트롤러 설치
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
  -n kube-system

# CLI 설치 (macOS)
brew install kubeseal

사용법

# 1. 원본 Secret 생성 (적용하지 않음!)
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=supersecret \
  --dry-run=client -o yaml > secret.yaml

# 2. Sealed Secret으로 암호화
kubeseal --format yaml < secret.yaml > sealed-secret.yaml

# 3. 원본 삭제, Sealed Secret만 Git에 커밋
rm secret.yaml
git add sealed-secret.yaml
git commit -m "Add encrypted database credentials"

생성된 SealedSecret

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
  namespace: default
spec:
  encryptedData:
    username: AgBy8BQ...암호화된_데이터...==
    password: AgCtr2I...암호화된_데이터...==
  template:
    metadata:
      name: db-credentials
      namespace: default
    type: Opaque

Scope 설정

SealedSecret은 어떤 조건에서 복호화를 허용할지 범위(scope)를 지정할 수 있습니다:

# strict (기본): 동일한 namespace + name만 허용
kubeseal --scope strict

# namespace-wide: 동일 namespace 내 다른 이름 허용
kubeseal --scope namespace-wide

# cluster-wide: 모든 namespace에서 사용 가능
kubeseal --scope cluster-wide
Scope이름 변경네임스페이스 변경보안 수준
strict높음
namespace-wide중간
cluster-wide낮음

키 관리

[!WARNING] Sealed Secrets 컨트롤러의 비밀키가 유출되면 모든 SealedSecret이 복호화됩니다. 키 백업이 필수입니다.

# 키 백업
kubectl get secret -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > sealed-secrets-key-backup.yaml

# 키 복원 (재해 복구 시)
kubectl apply -f sealed-secrets-key-backup.yaml
kubectl delete pod -n kube-system -l app.kubernetes.io/name=sealed-secrets

한계

  1. 클러스터 종속: 다른 클러스터에서는 복호화 불가
  2. 키 로테이션 복잡: 키 변경 시 모든 SealedSecret 재암호화 필요
  3. 단일 실패점: 컨트롤러 장애 시 Secret 생성 불가

해결책 2: External Secrets Operator (ESO)

External Secrets Operator는 외부 비밀 관리 시스템(AWS Secrets Manager, HashiCorp Vault 등)에서 Secret을 가져와 Kubernetes Secret으로 동기화합니다.

동작 원리

핵심 포인트: Git에는 **ExternalSecret (참조 정보)**만 저장하고, 실제 비밀 값은 외부 저장소에 있습니다.

설치

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

SecretStore & ClusterSecretStore

먼저 외부 저장소와의 연결을 설정합니다:

# ClusterSecretStore (클러스터 전역)
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: aws-secrets-manager
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa
            namespace: external-secrets
# AWS IAM Role for Service Account (IRSA) 설정 필요
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-secrets-sa
  namespace: external-secrets
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/external-secrets-role

ExternalSecret

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  # 동기화 주기
  refreshInterval: 1h
  
  # SecretStore 참조
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  
  # 생성할 Secret 설정
  target:
    name: db-credentials
    creationPolicy: Owner
  
  # 데이터 매핑
  data:
    - secretKey: username      # K8s Secret의 키
      remoteRef:
        key: prod/database     # AWS Secrets Manager의 경로
        property: username     # JSON 내 속성
    
    - secretKey: password
      remoteRef:
        key: prod/database
        property: password

전체 Secret 가져오기

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: all-db-credentials
spec:
  # ...
  dataFrom:
    - extract:
        key: prod/database  # JSON 전체를 가져와서 각 키를 Secret 데이터로

지원하는 Provider

Provider설명
AWS Secrets ManagerAWS 네이티브
AWS Parameter StoreSSM 파라미터
HashiCorp Vault온프레미스/클라우드
GCP Secret ManagerGCP 네이티브
Azure Key VaultAzure 네이티브
1Password팀 비밀 공유
DopplerSaaS 비밀 관리

장점과 단점

장점:

  • Git에 비밀 값이 전혀 없음
  • 자동 로테이션 지원
  • 중앙 집중식 비밀 관리

단점:

  • 외부 서비스 의존성
  • 네트워크 지연
  • 추가 비용 (AWS Secrets Manager 등)

해결책 3: SOPS (Secrets OPerationS)

SOPS는 Mozilla에서 개발한 파일 레벨 암호화 도구입니다. YAML, JSON, ENV 파일의 값만 선택적으로 암호화합니다.

동작 원리

특징: 키는 평문, 값만 암호화

# 암호화 후 (읽기 쉬움!)
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
stringData:
  username: ENC[AES256_GCM,data:6FLiRc8=,iv:...,tag:...,type:str]
  password: ENC[AES256_GCM,data:HdNqmY3WUr8=,iv:...,tag:...,type:str]
sops:
  age:
    - recipient: age1...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2024-01-15T10:00:00Z"
  mac: ENC[AES256_GCM,...]
  version: 3.8.1

[!TIP] Git diff가 의미있습니다. 어떤 키가 변경되었는지 바로 알 수 있습니다.

Age 키로 사용하기 (권장)

# age 설치
brew install age

# 키 쌍 생성
age-keygen -o key.txt
# Public key: age1abc...
# Private key 저장됨

# SOPS 설치
brew install sops

# .sops.yaml 설정 (레포지토리 루트)
cat > .sops.yaml << EOF
creation_rules:
  - path_regex: .*secrets.*\.yaml$
    age: age1abc...  # 공개키
EOF

암호화/복호화

# 암호화
sops -e secrets.yaml > secrets.enc.yaml

# 복호화
sops -d secrets.enc.yaml > secrets.yaml

# 제자리 편집 (복호화 → 편집 → 저장 시 재암호화)
sops secrets.enc.yaml

Flux와 SOPS 통합

Flux는 SOPS를 네이티브로 지원합니다:

# 복호화 키를 Secret으로 저장
apiVersion: v1
kind: Secret
metadata:
  name: sops-age
  namespace: flux-system
stringData:
  age.agekey: |
    # created: 2024-01-15
    # public key: age1abc...
    AGE-SECRET-KEY-1...

---
# Kustomization에서 복호화 활성화
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: my-app
  namespace: flux-system
spec:
  # ...
  decryption:
    provider: sops
    secretRef:
      name: sops-age

ArgoCD와 SOPS

ArgoCD는 플러그인을 통해 SOPS를 지원합니다:

# argocd-cm ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  configManagementPlugins: |
    - name: kustomize-sops
      generate:
        command: ["bash", "-c"]
        args: ["kustomize build . | sops -d /dev/stdin"]

AWS KMS와 함께 사용

# .sops.yaml
creation_rules:
  - path_regex: .*prod.*secrets.*\.yaml$
    kms: arn:aws:kms:ap-northeast-2:123456789:key/abc-def
    
  - path_regex: .*dev.*secrets.*\.yaml$
    kms: arn:aws:kms:ap-northeast-2:123456789:key/xyz-123
# 환경별 다른 KMS 키 사용
sops -e --kms arn:aws:kms:... secrets.yaml

전략 선택 가이드

비교 표

관점Sealed SecretsESOSOPS
Git에 저장되는 것암호화된 SealedSecret참조(ExternalSecret)암호화된 파일
복호화 위치클러스터 내외부 저장소클러스터/로컬
멀티 클러스터❌ (각 클러스터 키 다름)✅ (중앙 저장소)✅ (같은 키 공유)
비밀 로테이션수동자동 (refreshInterval)수동
외부 의존성없음있음 (Vault, AWS 등)선택적 (age vs KMS)
Flux 지원✅ (네이티브)
ArgoCD 지원✅ (플러그인)
학습 곡선낮음중간중간

권장 시나리오

상황권장 솔루션
단일 클러스터, 빠른 시작Sealed Secrets
AWS/GCP/Azure 사용, 중앙 관리External Secrets Operator
멀티 클러스터, Flux 사용SOPS + age
기존 Vault 인프라ESO + Vault
규제 요구사항 (감사 로그 필요)ESO + AWS Secrets Manager

보안 모범 사례

1. 최소 권한 원칙

# ESO ServiceAccount에 필요한 권한만 부여
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": [
        "arn:aws:secretsmanager:*:*:secret:prod/*"
      ]
    }
  ]
}

2. 네임스페이스 격리

# SecretStore를 네임스페이스별로 분리
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: team-a-secrets
  namespace: team-a
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      # team-a 전용 IAM Role
      auth:
        jwt:
          serviceAccountRef:
            name: team-a-eso-sa

3. 자동 로테이션

# ESO: 1시간마다 동기화
spec:
  refreshInterval: 1h

# AWS Secrets Manager: 자동 로테이션 활성화
# Lambda 함수가 주기적으로 비밀 값 변경

4. 감사 로깅

# AWS CloudTrail에서 Secret 접근 로그 확인
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue

정리

솔루션핵심 개념장점단점
Sealed Secrets클러스터 키로 암호화단순, 외부 의존성 없음클러스터 종속, 키 관리
ESO외부 저장소 참조중앙 관리, 자동 로테이션외부 의존성, 비용
SOPS파일 레벨 암호화Git diff 가능, 유연함수동 로테이션

다음 편 예고

6편: CI/CD 파이프라인 통합에서는 다음을 다룹니다:

  • GitOps에서 CI와 CD의 분리
  • ArgoCD Image Updater
  • Flux Image Automation
  • Progressive Delivery (Argo Rollouts, Flagger)
  • 프로덕션 GitOps 워크플로우

참고 자료

Share

Related Articles

Comments

이 블로그는 제가 알고 있는 것들을 잊지 않기 위해 기록하는 공간입니다.
직접 작성한 글도 있고, AI의 도움을 받아 정리한 글도 있습니다.
정확하지 않은 내용이 있을 수 있으니 참고용으로 봐주세요.

© 2026 Seogyu Kim