Kim Seogyu
Backend

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

GitOps에서 Secrets를 안전하게 관리하는 방법. Sealed Secrets, External Secrets Operator, SOPS의 동작 원리와 선택 기준을 다룹니다.

Published 2026년 1월 5일9 min read1,785 words

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 히스토리에 한 번 들어가면 영구히 노출됩니다.

딜레마의 본질

flowchart TB
    subgraph Problem [GitOps Secrets 딜레마]
        SSOT[Git = Single Source of Truth]
        Secret[Secrets도 Git에 있어야?]
        Risk[평문 노출 위험]
    end
    
    SSOT --> Secret
    Secret --> Risk
    
    subgraph Solutions [해결 방법]
        Encrypted[암호화해서 Git에 저장]
        External[외부 저장소 참조]
    end
    
    Risk --> Encrypted
    Risk --> External
    
    Encrypted --> SealedSecrets[Sealed Secrets]
    Encrypted --> SOPS[SOPS]
    External --> ESO[External Secrets Operator]
    External --> CSI[Secrets Store CSI Driver]

해결책 1: Sealed Secrets

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

동작 원리

sequenceDiagram
    participant Dev as 개발자
    participant CLI as kubeseal CLI
    participant Controller as Sealed Secrets Controller
    participant K8s as Kubernetes API
    
    Note over Controller: RSA 키 쌍 보유 (private/public)
    
    Dev->>CLI: 원본 Secret YAML 제공
    CLI->>Controller: 공개키 요청
    Controller-->>CLI: 공개키 반환
    CLI->>CLI: 공개키로 암호화
    CLI->>Dev: SealedSecret YAML 생성
    
    Dev->>K8s: git push → GitOps → SealedSecret 적용
    K8s->>Controller: SealedSecret 감지
    Controller->>Controller: 비밀키로 복호화
    Controller->>K8s: 일반 Secret 생성

설치

# 컨트롤러 설치
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으로 동기화합니다.

동작 원리

flowchart LR
    subgraph External [외부 Secret 저장소]
        AWS[AWS Secrets Manager]
        Vault[HashiCorp Vault]
        GCP[GCP Secret Manager]
        Azure[Azure Key Vault]
    end
    
    subgraph Cluster [Kubernetes Cluster]
        ESO[External Secrets\nOperator]
        ExtSecret[ExternalSecret CR]
        K8sSecret[Kubernetes Secret]
    end
    
    ExtSecret -->|참조| ESO
    ESO -->|API 호출| AWS & Vault & GCP & Azure
    ESO -->|생성/동기화| K8sSecret

핵심 포인트: 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 파일의 값만 선택적으로 암호화합니다.

동작 원리

flowchart LR
    subgraph Encryption [암호화 키]
        AGE[age key]
        AWS_KMS[AWS KMS]
        GCP_KMS[GCP KMS]
        Azure_KMS[Azure Key Vault]
        PGP[PGP]
    end
    
    subgraph Files [파일]
        Plain[평문 YAML]
        Encrypted[암호화된 YAML\n키는 평문, 값만 암호화]
    end
    
    Plain -->|sops -e| Encrypted
    Encrypted -->|sops -d| Plain
    
    Encryption --> Plain

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

# 암호화 후 (읽기 쉬움!)
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

전략 선택 가이드

flowchart TB
    Start[Secrets 관리 전략 선택] --> Q1{외부 비밀 관리 시스템\n이미 사용 중?}
    
    Q1 -->|Yes| ESO[External Secrets Operator]
    Q1 -->|No| Q2{멀티 클러스터?}
    
    Q2 -->|Yes| Q3{중앙 비밀 저장소\n도입 가능?}
    Q3 -->|Yes| ESO
    Q3 -->|No| SOPS[SOPS + age/KMS]
    
    Q2 -->|No| Q4{단순함 우선?}
    Q4 -->|Yes| SealedSecrets[Sealed Secrets]
    Q4 -->|No| SOPS

비교 표

관점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