🔒 보안

External Secrets

External Secrets Operator

AWS Secrets Manager, HashiCorp Vault, Azure Key Vault 등 외부 비밀 저장소와 Kubernetes를 연동하여 Secret 리소스를 자동으로 동기화하는 Kubernetes 오퍼레이터입니다.

📖 상세 설명

External Secrets Operator(ESO)는 Kubernetes 환경에서 외부 비밀 관리 시스템과의 통합을 자동화하는 오픈소스 프로젝트입니다. CNCF Sandbox 프로젝트로, 기업 환경에서 중앙 집중화된 시크릿 관리를 Kubernetes 네이티브 방식으로 연동할 수 있게 해줍니다.

ESO의 핵심 개념은 ExternalSecret CRD(Custom Resource Definition)입니다. 이 리소스를 정의하면 ESO 컨트롤러가 외부 저장소에서 시크릿을 가져와 Kubernetes Secret으로 자동 생성하고, 주기적으로 동기화합니다. 이를 통해 GitOps 워크플로우에서 시크릿 값을 직접 노출하지 않고도 선언적으로 관리할 수 있습니다.

지원하는 백엔드로는 AWS Secrets Manager, AWS Parameter Store, HashiCorp Vault, Google Secret Manager, Azure Key Vault, IBM Cloud Secrets Manager, CyberArk Conjur, Doppler, 1Password 등이 있습니다. SecretStore 또는 ClusterSecretStore 리소스를 통해 백엔드 연결을 구성하며, 네임스페이스 단위 또는 클러스터 전체에서 재사용할 수 있습니다.

보안 모범 사례로는 최소 권한 원칙을 적용한 IAM 정책 설정, 주기적인 시크릿 로테이션 구성, 멀티 테넌트 환경에서의 SecretStore 분리 등이 있습니다. 또한 PushSecret 기능을 활용하면 Kubernetes Secret을 외부 저장소로 내보낼 수도 있어 양방향 동기화가 가능합니다.

💻 코드 예제

External Secrets 구성 예제 (YAML/Python)

# SecretStore 정의 - AWS Secrets Manager 연동
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-store
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

---
# ExternalSecret 정의 - 외부 시크릿을 Kubernetes Secret으로 동기화
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: production
spec:
  refreshInterval: 1h  # 동기화 주기
  secretStoreRef:
    name: aws-secrets-store
    kind: SecretStore
  target:
    name: db-secret  # 생성될 Kubernetes Secret 이름
    creationPolicy: Owner
    template:
      type: Opaque
      data:
        # 템플릿을 사용한 값 변환
        connection-string: |
          postgresql://{{ .username }}:{{ .password }}@{{ .host }}:5432/{{ .database }}
  data:
    - secretKey: username
      remoteRef:
        key: prod/database/credentials
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database/credentials
        property: password
    - secretKey: host
      remoteRef:
        key: prod/database/credentials
        property: host
    - secretKey: database
      remoteRef:
        key: prod/database/credentials
        property: database

Python으로 External Secrets 상태 모니터링

from kubernetes import client, config
from kubernetes.client.rest import ApiException
import json

# Kubernetes 설정 로드
config.load_incluster_config()  # 클러스터 내부에서 실행 시
# config.load_kube_config()  # 로컬에서 실행 시

# Custom Objects API 클라이언트
api = client.CustomObjectsApi()

def get_external_secret_status(namespace: str, name: str) -> dict:
    """ExternalSecret 리소스의 동기화 상태 확인"""
    try:
        external_secret = api.get_namespaced_custom_object(
            group="external-secrets.io",
            version="v1beta1",
            namespace=namespace,
            plural="externalsecrets",
            name=name
        )

        status = external_secret.get("status", {})
        conditions = status.get("conditions", [])

        # SecretSynced 조건 확인
        for condition in conditions:
            if condition["type"] == "Ready":
                return {
                    "name": name,
                    "namespace": namespace,
                    "synced": condition["status"] == "True",
                    "reason": condition.get("reason", "Unknown"),
                    "message": condition.get("message", ""),
                    "last_sync": status.get("refreshTime")
                }

        return {"name": name, "synced": False, "reason": "NoCondition"}

    except ApiException as e:
        return {"name": name, "error": str(e)}

def list_all_external_secrets() -> list:
    """클러스터 내 모든 ExternalSecret 상태 조회"""
    results = []

    external_secrets = api.list_cluster_custom_object(
        group="external-secrets.io",
        version="v1beta1",
        plural="externalsecrets"
    )

    for es in external_secrets.get("items", []):
        metadata = es["metadata"]
        status = get_external_secret_status(
            namespace=metadata["namespace"],
            name=metadata["name"]
        )
        results.append(status)

    return results

def check_secret_sync_health() -> dict:
    """전체 ExternalSecret 동기화 상태 헬스체크"""
    all_secrets = list_all_external_secrets()

    total = len(all_secrets)
    synced = sum(1 for s in all_secrets if s.get("synced", False))
    failed = [s for s in all_secrets if not s.get("synced", False)]

    return {
        "total": total,
        "synced": synced,
        "failed_count": len(failed),
        "failed_secrets": failed,
        "health": "healthy" if len(failed) == 0 else "degraded"
    }

# 사용 예시
if __name__ == "__main__":
    # 특정 ExternalSecret 상태 확인
    status = get_external_secret_status("production", "database-credentials")
    print(f"동기화 상태: {status}")

    # 전체 헬스체크
    health = check_secret_sync_health()
    print(f"전체 상태: {health['synced']}/{health['total']} 동기화됨")

    if health["failed_secrets"]:
        print("동기화 실패한 시크릿:")
        for secret in health["failed_secrets"]:
            print(f"  - {secret['namespace']}/{secret['name']}: {secret.get('reason')}")

🗣️ 실무에서 이렇게 말해요

데브옵스 엔지니어 "현재 Helm 차트에 시크릿이 하드코딩되어 있어서 GitOps 적용이 어렵습니다. External Secrets Operator 도입하면 어떨까요?"
보안팀 "좋은 접근이네요. 중앙 Vault에서 관리하는 시크릿을 ESO로 가져오면 감사 로그도 일원화됩니다. SecretStore는 네임스페이스별로 분리해주세요."
SRE 팀장 "refreshInterval은 프로덕션은 1시간, 개발은 5분으로 다르게 설정하고, 동기화 실패 시 알림도 구성해주세요."
면접관 "Kubernetes에서 시크릿 관리 전략을 어떻게 수립하시나요?"
지원자 "GitOps 환경에서는 External Secrets Operator를 활용합니다. Git에는 ExternalSecret CRD만 저장하고, 실제 값은 Vault나 AWS Secrets Manager에서 관리해요. 시크릿 로테이션 시에도 ESO가 자동 동기화해주어 파드 재시작 없이 반영할 수 있습니다. 멀티 테넌트 환경에서는 ClusterSecretStore로 백엔드 연결을 중앙화하고, RBAC으로 네임스페이스별 접근을 제어합니다."
리뷰어 "ExternalSecret에서 creationPolicy가 Owner로 되어 있는데, 이 ExternalSecret이 삭제되면 생성된 Secret도 같이 삭제됩니다. 의도된 건가요?"
작성자 "네, GitOps로 관리되어서 ExternalSecret이 삭제되면 시크릿도 정리되어야 합니다. 다만 중요한 시크릿은 Orphan으로 바꿔서 수동 삭제가 필요하도록 하겠습니다."

⚠️ 주의사항

  • 인증 정보 보안: SecretStore에 설정하는 백엔드 인증 정보(IAM 역할, 토큰 등)도 민감 정보입니다. IRSA(IAM Roles for Service Accounts)나 Workload Identity를 사용하여 장기 자격 증명 저장을 피하세요.
  • 동기화 실패 대응: 외부 저장소 접근 불가 시 기존 Secret은 유지되지만 업데이트가 중단됩니다. 동기화 상태를 모니터링하고 alerts를 설정해 장애에 대응하세요.
  • 시크릿 로테이션 시점: refreshInterval이 지나야 새 값이 반영됩니다. 즉각 반영이 필요하면 ExternalSecret 리소스에 annotation을 업데이트하거나, 시크릿 버저닝을 활용하세요.

📚 더 배우기