[쿠버네티스 패턴] 25장. Secure Configuration 패턴

bocopile·2026년 1월 17일

쿠버네티스 패턴

목록 보기
23/28
post-thumbnail

1. 개념 요약

Secure Configuration 패턴은 Kubernetes에서 자격 증명(credentials)을 최대한 안전하게 유지하기 위한 모범 사례를 다룹니다.

어떤 실제 애플리케이션도 고립되어 존재하지 않습니다. 클라우드 제공업체의 부가 서비스, 다른 마이크로서비스, 데이터베이스 등 외부 시스템과 연결해야 합니다.

이러한 연결에는 대부분 인증(Authentication)이 필요하며, 사용자 이름과 비밀번호 또는 보안 토큰과 같은 자격 증명을 전송해야 합니다.

이 민감한 정보는 애플리케이션 가까이에 안전하게(securely) 저장되어야 합니다.

2. 문제 상황 (Problem)

"Kubernetes Secret은 정말 안전한가? 어떤 상황에서 문제가 되는가?"

Kubernetes Secret의 한계

20장 "Configuration Resource"에서 배웠듯이, Secret 리소스는 이름과 달리 암호화되지 않고 Base64 인코딩만 적용됩니다.

Kubernetes는 Secret 내용에 대한 접근을 제한하기 위해 최선을 다하지만,
여전히 여러 보안 과제가 존재합니다.

핵심 보안 과제

과제설명위험도
GitOps 환경Secret을 원격 Git 저장소에 저장해야 하는가? 저장한다면 암호화 없이 저장할 수 없음[높음]
복호화 시점Git에 암호화되어 커밋된 경우, 클러스터로 들어가는 과정에서 어디서 복호화되는가?[중간]
클러스터 관리자 신뢰클러스터 내부에 암호화되어 저장되어도, 클러스터 관리자는 모든 데이터에 접근 가능[높음]
멀티테넌시여러 팀이 같은 클러스터를 사용할 때 Secret 격리 문제[중간]

클러스터 관리자 신뢰 문제

RBAC 규칙으로 Kubernetes 리소스에 대한 접근을 세밀하게 제어할 수 있습니다.

하지만 최소한 한 명은 클러스터의 모든 데이터에 접근할 수 있습니다
바로 클러스터 관리자
입니다.

실제 예시: 금융 서비스 회사에서 AWS EKS를 사용하는 경우, AWS 계정 관리자와 Kubernetes 클러스터 관리자 모두 민감한 데이터베이스 자격 증명에 접근할 수 있습니다.

PCI-DSS 규정 준수를 위해서는 이러한 접근을 최소화해야 합니다.

3. 솔루션 (Solution)

"Secret을 애플리케이션 코드 변경 없이 안전하게 관리하려면 어떤 접근 방식이 있는가?"

가장 단순한 해결책은 애플리케이션 내부에서 직접 암호화된 정보를 복호화하는 것입니다.

하지만 이 방식은 비즈니스 로직과 보안 로직을 결합시키는 문제가 있습니다.
Kubernetes에서는 더 투명한 방법들이 있습니다.

두 가지 주요 접근 방식

접근 방식설명결과물
Out-of-Cluster EncryptionKubernetes 외부에 암호화된 설정 정보를 저장하고, 클러스터 진입 시점 또는 Operator에 의해 Secret으로 변환Kubernetes Secret 생성
Centralized Secret Management클라우드 제공업체(AWS, Azure, GCP) 또는 자체 Vault 서비스를 활용하여 민감한 데이터 저장다양한 방식으로 Pod에 전달

2026년 트렌드 패러다임 변화

2026년 현재, Secret 관리의 정의가 바뀌고 있습니다.
단순히 데이터베이스 비밀번호를 암호화하는 것이 아니라,
멀티 클라우드 환경에서 수천 개의 임시 자격 증명 라이프사이클을 관리하는 것으로 확장되었습니다.

관점과거 (2020-2023)현재 (2024-2026)
자격 증명 수명Static Secrets (장기 자격 증명)Dynamic Secrets / JIT Access (단기, 필요 시 발급)
인증 질문"What is the secret?""Who is asking?" (Identity-Based Access)
표준 도구다양한 도구 혼재External Secrets Operator가 사실상 표준
SOPS 활용CLI 도구만sops-secrets-operator CRD 지원

핵심 변화: 애플리케이션이 Kubernetes ServiceAccount 또는 클라우드 IAM Identity로 인증하면, SMS가 해당 워크로드에 맞는 최소 권한의 단기 자격 증명을 발급합니다.

4. Out-of-Cluster Encryption

[요약]

  • 핵심 아이디어: 암호화된 Secret → Git 저장 → 클러스터에서 복호화 → Kubernetes Secret 생성
  • 적용 대상: GitOps 환경, CI/CD 파이프라인, 소스 코드와 Secret 함께 관리
  • 대표 도구: Sealed Secrets, External Secrets Operator, SOPS

클러스터 외부에서 비밀 데이터를 가져와 Kubernetes Secret으로 변환하는 방식입니다.
2026년 기준 가장 널리 사용되는 세 가지 프로젝트를 살펴보겠습니다.

(1) Sealed Secrets

개념

Sealed Secrets는 Bitnami에서 2017년에 소개한 Kubernetes 애드온으로,
암호화된 데이터를 CRD(CustomResourceDefinition) SealedSecret에 저장합니다.

백그라운드에서 Operator가 이러한 리소스를 모니터링하고,
각 SealedSecret에 대해 복호화된 내용으로 Kubernetes Secret을 생성합니다.

아키텍처

암호화 방식

Secret은 AES-256-GCM으로 대칭 암호화되어 세션 키로 사용되고,
세션 키는 RSA-OAEP로 비대칭 암호화됩니다.
이는 TLS가 사용하는 것과 동일한 설정입니다.

┌─────────────────────────────────────────────────────────┐
│                    암호화 계층 구조                         │
├─────────────────────────────────────────────────────────┤
│  Secret Data  ──▶  AES-256-GCM (세션키)  ──▶  암호문       │
│  Session Key  ──▶  RSA-OAEP (공개키)     ──▶  암호화된 키   │
└─────────────────────────────────────────────────────────┘

세 가지 Scope

SealedSecret을 생성할 때 선택할 수 있는 세 가지 범위가 있습니다

Scope설명사용 사례
Strict (기본값)namespace와 name이 고정됨. 원본 Secret과 동일한 namespace, 동일한 name으로만 생성 가능단일 환경, 엄격한 보안 요구
Namespace-wide동일 namespace 내에서 다른 name으로 적용 가능같은 namespace에서 여러 배포
Cluster-widenamespace와 name 모두 변경 가능멀티 환경 배포, 재사용

Scope 관련 Annotation

AnnotationValue설명
sealedsecrets.bitnami.com/namespace-wide"true"namespace-wide scope 활성화
sealedsecrets.bitnami.com/cluster-wide"true"cluster-wide scope 활성화

예제: SealedSecret 생성

Step 1. 원본 Secret 생성

# mysecret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
data:
  username: YWRtaW4=        # admin
  password: czNjcjN0cEBzcw== # s3cr3tp@ss

Step 2. kubeseal로 암호화

# cluster-wide scope로 SealedSecret 생성
kubeseal --scope cluster-wide -f mysecret.yaml -o yaml > sealedsecret.yaml

Step 3. 생성된 SealedSecret

# sealedsecret.yaml - Git에 안전하게 저장 가능
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  annotations:
    sealedsecrets.bitnami.com/cluster-wide: "true"  # (1) cluster-wide scope
  name: db-credentials
spec:
  encryptedData:
    password: AgCrKIF2gA7tSR/gqw+FH6cEV...wPWWkHJbo=  # (2) 암호화된 값
    username: AgAmvgFQBBNPlt9Gmx...0DNHJpDIMUGgwaQroXT+o=
  • (1) cluster-wide annotation으로 어떤 namespace, 어떤 name으로도 적용 가능
  • (2) 각 값이 개별적으로 암호화됨

Step 4. 클러스터에 적용

kubectl apply -f sealedsecret.yaml

# Operator가 자동으로 Secret 생성
kubectl get secrets
# NAME              TYPE     DATA   AGE
# db-credentials    Opaque   2      5s

실제 운영 예시

시나리오: DevOps 팀이 GitOps를 사용하여 여러 환경(dev, staging, prod)에 동일한 애플리케이션을 배포합니다.

# 1. 각 환경별 Secret 준비
cat > dev-secret.yaml << EOF
apiVersion: v1
kind: Secret
metadata:
  name: api-keys
  namespace: dev
data:
  API_KEY: ZGV2LWFwaS1rZXk=
EOF

# 2. namespace-wide scope로 암호화 (같은 namespace 내 다른 이름 허용)
kubeseal --scope namespace-wide \\
  --controller-namespace kube-system \\
  -f dev-secret.yaml > dev-sealedsecret.yaml

# 3. Git에 커밋
git add dev-sealedsecret.yaml
git commit -m "Add encrypted API keys for dev environment"
git push

[주의] 주의사항

  • Private Key 백업 필수: Operator를 제거하면 복호화 불가능
  • Key Rotation: 정기적인 키 교체 필요
  • 서버 사이드 컴포넌트: Operator가 항상 실행되어야 함
# Private Key 백업
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key \\
  -o yaml > sealed-secrets-master-key.yaml

(2) External Secrets Operator

개념

External Secrets Operator는 다양한 외부 SMS(Secret Management System)와 통합되는 Kubernetes Operator입니다.

Sealed Secrets와의 핵심 차이점은 암호화된 데이터 저장소를 직접 관리하지 않고 외부 SMS에 의존한다는 점입니다.

이를 통해 클라우드 SMS의 모든 기능(키 자동 교체, 전용 UI, 감사 로그 등)을 활용할 수 있습니다.

아키텍처

핵심 CRD 두 가지

CRD역할관리 주체
SecretStore외부 SMS 연결 설정 (타입, 인증 정보)Platform/Security 팀
ExternalSecret어떤 Secret을 가져올지 정의, SecretStore 참조Application 팀

예제: AWS Secrets Manager 연동

Step 1. SecretStore 설정

# secret-store-aws.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: secret-store-aws
  namespace: production
spec:
  provider:
    aws:                              # (1) AWS Secrets Manager 사용
      service: SecretsManager
      region: us-east-1
      auth:
        secretRef:
          accessKeyIDSecretRef:       # (2) AWS 접근 키 참조
            name: awssm-secret
            key: access-key
          secretAccessKeySecretRef:
            name: awssm-secret
            key: secret-access-key
  • (1) AWS Secrets Manager를 프로바이더로 설정
  • (2) AWS 인증에 필요한 키가 담긴 Secret 참조 (이 Secret은 별도로 안전하게 생성해야 함)

Step 2. ExternalSecret 정의

# external-secret-db.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 1h                 # (1) 1시간마다 동기화
  secretStoreRef:                     # (2) SecretStore 참조
    name: secret-store-aws
    kind: SecretStore
  target:
    name: db-credentials-secrets      # (3) 생성될 Secret 이름
    creationPolicy: Owner
  data:
    - secretKey: username             # Secret의 key 이름
      remoteRef:
        key: cluster/db-username      # (4) AWS Secrets Manager의 경로
    - secretKey: password
      remoteRef:
        key: cluster/db-password
  • (1) refreshInterval로 자동 동기화 주기 설정 (키 자동 교체 지원)
  • (2) 앞서 정의한 SecretStore 참조
  • (3) 생성될 Kubernetes Secret 이름
  • (4) AWS Secrets Manager에서 가져올 Secret 경로

Step 3. 결과 확인

# ExternalSecret 상태 확인
kubectl get externalsecret db-credentials -n production
# NAME             STORE              REFRESH INTERVAL   STATUS
# db-credentials   secret-store-aws   1h                 SecretSynced

# 생성된 Secret 확인
kubectl get secret db-credentials-secrets -n production -o yaml

지원되는 프로바이더 (2025년 기준)

프로바이더서비스
AWSSecrets Manager, Parameter Store
AzureKey Vault
GCPSecret Manager
HashiCorpVault
IBMSecrets Manager
OracleVault
Kubernetes다른 클러스터의 Secret

실제 운영 예시

시나리오: 마이크로서비스 아키텍처에서 각 서비스가 필요한 Secret만 접근하도록 제한합니다.

# ClusterSecretStore - 클러스터 전체에서 공유
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "<https://vault.company.com:8200>"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "external-secrets"
---
# 각 팀은 자신의 namespace에서 ExternalSecret만 관리
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: payment-service-secrets
  namespace: payments
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: payment-credentials
  data:
    - secretKey: STRIPE_API_KEY
      remoteRef:
        key: payments/stripe
        property: api_key
    - secretKey: STRIPE_WEBHOOK_SECRET
      remoteRef:
        key: payments/stripe
        property: webhook_secret

[장점] External Secrets의 장점

  • 관심사 분리: Secret 관리와 애플리케이션 배포 분리
  • 키 자동 교체 지원: SMS의 rotation 기능 활용
  • 중앙 집중식 관리: 모든 Secret을 한 곳에서 관리
  • 감사 로그: SMS의 접근 로그로 누가 언제 접근했는지 추적

(3) SOPS (Secret OPerationS)

개념

SOPS는 Mozilla에서 개발한 순수 클라이언트 사이드 솔루션입니다.

Kubernetes에 특화되지 않았지만, YAML이나 JSON 파일을 암호화하고 복호화하여
소스 코드 저장소에 안전하게 저장할 수 있습니다.

값(value)만 암호화하고 키(key)는 그대로 유지하는 것이 특징입니다.

암호화 방식

SOPS는 다양한 암호화 방식을 지원합니다:

방식설명사용 사례
age로컬에 저장된 키로 비대칭 암호화개인/소규모 팀
AWS KMSAWS의 키 관리 서비스AWS 환경
GCP KMSGoogle의 키 관리 서비스GCP 환경
Azure Key VaultAzure의 키 관리 서비스Azure 환경
HashiCorp Vault자체 호스팅 가능한 SMS온프레미스/하이브리드

아키텍처

[참고] SMS vs KMS 개념 정리

구분SMS (Secret Management System)KMS (Key Management System)
역할Secret을 저장하고 관리하는 서비스암호화 키를 관리하는 서비스
기능저장, 접근 제어, 자동 암호화, UI 제공키 생성, 저장, 교체, 접근 제어
데이터실제 민감한 데이터를 저장암호화 키만 저장 (데이터는 외부)
예시AWS Secrets Manager, VaultAWS KMS, GCP KMS, GnuPG keyserver
사용 패턴API로 Secret 조회키를 받아서 직접 암호화/복호화

예제: SOPS로 Secret 암호화

Step 1. age 키 생성

# age 키 페어 생성
$ age-keygen -o keys.txt
Public key: age1j49ugcg2rzyye07ksyvj5688m6hmv...

# 키 파일 확인
$ cat keys.txt
# created: 2024-01-15T10:30:00+09:00
# public key: age1j49ugcg2rzyye07ksyvj5688m6hmv...
AGE-SECRET-KEY-1QFNZ7...

Step 2. 원본 ConfigMap/Secret 준비

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name_unencrypted: db-auth    # (1) _unencrypted 접미사로 암호화 제외
data:
  # User and Password
  USER: "batman"
  PASSWORD: "r0b1n"
  • (1) _unencrypted 접미사를 붙이면 해당 필드는 암호화되지 않음 (복호화 시 접미사 자동 제거)

Step 3. SOPS로 암호화

$ sops --encrypt \\
    --age age1j49ugcg2rzyye07ksyvj5688m6hmv... \\
    configmap.yaml > configmap_encrypted.yaml

Step 4. 암호화된 결과

# configmap_encrypted.yaml
apiVersion: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]  # (1) 값 암호화
kind: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
metadata:
  name_unencrypted: db-auth    # (2) 암호화 제외됨
data:
  #ENC[AES256_GCM,data:...,iv:...,tag:...,type:comment]  # 주석도 암호화!
  USER: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
  PASSWORD: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
sops:                           # (3) 복호화에 필요한 메타데이터
  age:
    - recipient: age1j49ugcg2rzyye07ksyvj5688m6hmv...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2024-01-15T10:35:00Z"
  mac: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
  unencrypted_suffix: _unencrypted
  • (1) 모든 값이 ENC[...] 형태로 암호화됨
  • (2) _unencrypted 접미사가 붙은 필드는 평문 유지
  • (3) sops 섹션에 복호화에 필요한 메타데이터 포함

예제. SOPS로 복호화 및 적용

# 환경 변수로 키 파일 지정
$ export SOPS_AGE_KEY_FILE=keys.txt

# 복호화 후 바로 적용 (파이프라인)
$ sops --decrypt configmap_encrypted.yaml | kubectl apply -f -
configmap/db-auth created

# 또는 복호화된 내용 확인
$ sops --decrypt configmap_encrypted.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: db-auth                # _unencrypted 접미사 자동 제거
data:
  USER: "batman"
  PASSWORD: "r0b1n"

CI/CD 파이프라인 통합 예시

# .github/workflows/deploy.yaml
name: Deploy to Kubernetes
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install sops
        run: |
          curl -LO <https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64>
          chmod +x sops-v3.8.1.linux.amd64
          sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Deploy secrets
        run: |
          # AWS KMS를 사용하는 경우 자동으로 권한 인증
          sops --decrypt k8s/secrets.enc.yaml | kubectl apply -f -
          kubectl apply -f k8s/deployment.yaml

[비교] SOPS의 장점과 한계

장점한계
서버 사이드 컴포넌트 불필요클러스터에 적용 후에는 평문으로 저장됨
GitOps와 완벽 호환키 관리 책임이 사용자에게 있음
다양한 KMS 지원복호화 권한이 있는 곳에서만 적용 가능
설치/유지보수 간단자동 키 rotation 미지원

5. Centralized Secret Management

[요약]

  • 핵심 아이디어: 외부 SMS → 실시간 요청 → Pod 볼륨/환경변수로 주입 (etcd 저장 없음)
  • 적용 대상: 최고 수준 보안 요구, 자동 rotation 필요, 감사/규정 준수 환경
  • 대표 도구: Secrets Store CSI Driver, Vault Sidecar Agent Injection

클러스터 외부의 전문 SMS에 민감한 정보를 저장하고,
필요할 때 안전한 채널을 통해 요청하는 방식입니다.

클러스터 내부에 Secret이 영구 저장되지 않는 것이 핵심입니다.

(1) Secrets Store CSI Driver

개념

  • *CSI(Container Storage Interface)**는 스토리지 시스템을 컨테이너화된 애플리케이션에 노출하기 위한 Kubernetes API입니다.

Secrets Store CSI Driver는 다양한 중앙 집중식 SMS에 접근하여 일반 Kubernetes 볼륨처럼 마운트할 수 있게 해줍니다.

핵심 차이점: 일반 Secret 볼륨과 달리 etcd에 아무것도 저장되지 않고, 외부 Vault에서 직접 가져옵니다.

지원 SMS (2025년 기준)

  • AWS Secrets Manager
  • Azure Key Vault
  • GCP Secret Manager
  • HashiCorp Vault

아키텍처

설정 단계

관리자 작업

  1. Secrets Store CSI Driver 설치 (cluster-admin 권한 필요)
  2. SMS 프로바이더 플러그인 설치
  3. 접근 규칙 및 정책 구성 (ServiceAccount와 SMS 역할 매핑)

개발자 작업

  1. SecretProviderClass 정의
  2. Pod에서 CSI 볼륨 마운트

예제. HashiCorp Vault 연동

Step 1: SecretProviderClass 정의

# secret-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-database
  namespace: production
spec:
  provider: vault                      # (1) 프로바이더 타입
  parameters:
    vaultAddress: "<http://vault.default:8200>"  # (2) Vault 주소
    roleName: "database"               # (3) Vault 인증 역할
    objects: |
      - objectName: "database-password"  # (4) 마운트될 파일 이름
        secretPath: "secret/data/database-creds"  # (5) Vault 경로
        secretKey: "password"           # (6) Vault Secret의 키
      - objectName: "database-username"
        secretPath: "secret/data/database-creds"
        secretKey: "username"
  • (1) azure, gcp, aws, vault 중 선택
  • (2) Vault 서비스 URL
  • (3) Kubernetes ServiceAccount가 매핑될 Vault 역할
  • (4) Pod 내 파일 이름
  • (5) Vault에서 Secret이 저장된 경로
  • (6) 해당 경로의 어떤 키를 가져올지

Step 2. Pod에서 CSI 볼륨 마운트

# pod-with-vault-secrets.yaml
apiVersion: v1
kind: Pod
metadata:
  name: webapp
  namespace: production
spec:
  serviceAccountName: vault-access-sa   # (1) Vault 인증에 사용될 SA
  containers:
    - name: app
      image: myapp:latest
      volumeMounts:
        - name: secrets-store
          mountPath: "/secrets-store"   # (2) Secret이 마운트될 경로
          readOnly: true
      env:
        - name: DB_PASSWORD_FILE
          value: "/secrets-store/database-password"
  volumes:
    - name: secrets-store
      csi:                              # (3) CSI 드라이버 사용
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "vault-database"  # (4) SecretProviderClass 참조
  • (1) 이 ServiceAccount는 Vault 측에서 database 역할에 매핑되어 있어야 함
  • (2) 애플리케이션은 이 경로에서 파일을 읽음
  • (3) CSI 드라이버 지정
  • (4) 앞서 정의한 SecretProviderClass 참조

Step 3. Vault 측 설정 (참고)

# Vault에서 Kubernetes 인증 활성화
vault auth enable kubernetes

# Kubernetes 인증 설정
vault write auth/kubernetes/config \\
    kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"

# 정책 생성
vault policy write database-policy - <<EOF
path "secret/data/database-creds" {
  capabilities = ["read"]
}
EOF

# 역할 생성 (ServiceAccount와 매핑)
vault write auth/kubernetes/role/database \\
    bound_service_account_names=vault-access-sa \\
    bound_service_account_namespaces=production \\
    policies=database-policy \\
    ttl=1h

Step 4. 결과 확인

# Pod 내부에서 Secret 파일 확인
kubectl exec -it webapp -n production -- ls -la /secrets-store/
# -r--r--r-- 1 root root 16 Jan 15 10:00 database-password
# -r--r--r-- 1 root root 8  Jan 15 10:00 database-username

kubectl exec -it webapp -n production -- cat /secrets-store/database-password
# s3cr3tp@ss

[비교] CSI Driver의 장단점

장점단점
etcd에 민감 정보 저장 안 함설정이 복잡함
SMS의 모든 기능 활용 가능더 많은 컴포넌트 = 더 많은 장애 포인트
볼륨 마운트로 투명한 접근트러블슈팅 어려움
ServiceAccount 기반 세밀한 접근 제어SMS 장애 시 Pod 시작 불가

(2) Vault Sidecar Agent Injection

개념

애플리케이션이 proprietary 클라이언트 라이브러리 없이 외부 SMS에 접근할 수 있는 또 다른 방법입니다.

Mutating Webhook을 활용하여 특정 annotation이 있는 Pod에 자동으로 Vault Sidecar를 주입합니다.

두 가지 패턴을 활용합니다

  • Init Container: Pod 시작 전 Secret을 가져와 공유 볼륨에 복사
  • Sidecar: 지속적으로 Secret을 동기화하여 자동 rotation 지원

아키텍처

예제. Vault Sidecar Injection

Step 1. Vault Injector 설치 (Helm)

helm repo add hashicorp <https://helm.releases.hashicorp.com>
helm install vault hashicorp/vault \\
    --set "injector.enabled=true" \\
    --set "server.dev.enabled=true"  # 개발용

Step 2. Pod에 Annotation 추가

apiVersion: v1
kind: Pod
metadata:
  name: webapp
  annotations:
    # (1) Vault Agent 주입 활성화
    vault.hashicorp.com/agent-inject: "true"

    # (2) 사용할 Vault 역할
    vault.hashicorp.com/role: "webapp"

    # (3) Secret 마운트 경로와 Vault 경로 매핑
    vault.hashicorp.com/agent-inject-secret-database: "secret/data/database"

    # (4) 템플릿으로 원하는 형식 지정 (선택)
    vault.hashicorp.com/agent-inject-template-database: |
      {{- with secret "secret/data/database" -}}
      DB_USER={{ .Data.data.username }}
      DB_PASS={{ .Data.data.password }}
      {{- end }}
spec:
  serviceAccountName: webapp-sa
  containers:
    - name: app
      image: myapp:latest
      # Secret은 /vault/secrets/database에 자동 마운트됨

Step 3. 자동 주입 결과

# Pod 확인 - Sidecar가 자동 추가됨
kubectl get pod webapp -o jsonpath='{.spec.containers[*].name}'
# app vault-agent

# 주입된 볼륨 확인
kubectl get pod webapp -o jsonpath='{.spec.volumes[*].name}'
# vault-secrets

Step 4. 애플리케이션에서 사용

# Python 애플리케이션 예시
import os

def get_db_credentials():
    # Vault Sidecar가 동기화한 파일 읽기
    with open('/vault/secrets/database', 'r') as f:
        content = f.read()

    # 파싱 (템플릿 사용 시)
    creds = {}
    for line in content.strip().split('\\n'):
        key, value = line.split('=')
        creds[key] = value

    return creds['DB_USER'], creds['DB_PASS']

[비교] Vault Sidecar의 장단점

장점단점
CSI Driver보다 설정 간단보안 annotation이 앱 배포에 포함됨 (관심사 혼합)
SMS 직접 접근 차단Pod당 추가 컨테이너 리소스 소모
자동 Secret rotationVault 의존성
애플리케이션 코드 변경 불필요

6. Kubernetes v1.35 운영 관점 보강

[요약]

  • 기존 기능: Encryption at Rest, Immutable Secrets, Audit Logging, Pod Security Standards
  • v1.35 신규: Pod Certificates (mTLS), Image Pull Credentials Verification, CSI SA Tokens 분리
  • 적용 효과: etcd 암호화, 변경 방지, 접근 추적, 런타임 보안 강화
  • 권장 사항: 모든 프로덕션 클러스터에 기본 적용

1. Encryption at Rest (etcd 암호화)

Kubernetes 1.13+부터 지원되는 etcd 저장 데이터 암호화입니다.

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      # 가장 먼저 나온 provider가 암호화에 사용됨
      - aescbc:
          keys:
            - name: key1
              secret: <BASE64_ENCODED_32_BYTE_KEY>
      - identity: {}  # 기존 암호화되지 않은 데이터 읽기용
# API Server 설정에 추가
kube-apiserver --encryption-provider-config=/etc/kubernetes/encryption-config.yaml

# 기존 Secret 재암호화
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

2. Immutable Secrets

Kubernetes 1.21+에서 GA된 변경 불가능한 Secret입니다.

apiVersion: v1
kind: Secret
metadata:
  name: api-credentials
type: Opaque
data:
  API_KEY: c2VjcmV0LWtleQ==
immutable: true  # [O] 수정/삭제 시 새로 생성해야 함

장점:

  • kubelet의 Secret watch 부하 감소
  • 실수로 인한 변경 방지
  • 대규모 클러스터에서 성능 향상

3. Audit Logging 설정

Secret 접근을 추적하기 위한 감사 로그 설정입니다.

# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Secret 읽기 요청은 상세 로깅
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["secrets"]
    verbs: ["get", "list", "watch"]

  # Secret 생성/수정/삭제는 메타데이터만
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]
    verbs: ["create", "update", "delete", "patch"]
# API Server 설정
kube-apiserver \\
  --audit-policy-file=/etc/kubernetes/audit-policy.yaml \\
  --audit-log-path=/var/log/kubernetes/audit.log \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=10

4. Zero Trust Secret 전달 구조

구현 요소:

  • Workload Identity: Pod의 ServiceAccount를 클라우드 IAM과 연결
  • Short-lived credentials: 짧은 TTL의 토큰 사용
  • Just-in-time access: 필요할 때만 Secret 요청
  • Automatic rotation: 자동 갱신 메커니즘

5. Pod Security Standards와 Secret

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: secrets
          mountPath: /secrets
          readOnly: true  # [O] Secret 볼륨은 항상 readOnly

6. v1.35 신규 보안 기능 (2025년 말 기준)

Kubernetes 1.32~1.35 사이에 Secret 관리와 관련된 중요한 기능들이 안정화되거나 베타로 승격되었습니다.

기능상태KEP설명
Pod Certificates for mTLSBetaKEP-4317Pod 간 mTLS를 위한 X.509 인증서 자동 발급. PodCertificateRequest API 제공
Image Pull Credentials VerificationBetaKEP-2535캐시된 이미지도 레지스트리 인증 재검증. imagePullCredentialsVerificationPolicy 설정
CSI ServiceAccount Tokens via SecretsAlphaKEP-5538CSI 드라이버의 SA 토큰을 volumeContext에서 분리하여 별도 secrets 필드로 관리

Pod Certificates for mTLS (KEP-4317)

Pod 간 통신에 mTLS를 적용하려면 각 Pod에 X.509 인증서가 필요합니다. 기존에는 cert-manager 같은 외부 도구가 필수였지만, v1.35부터 네이티브 지원이 가능해졌습니다.

# v1.35+ PodCertificate 볼륨 소스 예시 (Beta)
apiVersion: v1
kind: Pod
metadata:
  name: mtls-app
spec:
  containers:
    - name: app
      image: myapp:latest
      volumeMounts:
        - name: pod-cert
          mountPath: /etc/certs
          readOnly: true
  volumes:
    - name: pod-cert
      podCertificate:           # 신규 볼륨 타입
        issuerRef:
          name: cluster-issuer
          kind: ClusterIssuer
        dnsNames:
          - myapp.default.svc

Image Pull Credentials Verification (KEP-2535)

기존에는 이미지가 한 번 pull되어 캐시되면, 다른 Pod가 인증 없이 해당 이미지를 사용할 수 있었습니다. v1.35에서는 이를 방지합니다.

# kubelet 설정
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
imagePullCredentialsVerificationPolicy: Always  # Never | IfNotPresent | Always
  • Always: 캐시된 이미지도 매번 인증 재검증
  • IfNotPresent: 캐시에 없을 때만 인증
  • Never: 인증 검증 안 함 (기본값, 하위 호환)

7. 패턴 선택 가이드

"우리 조직과 환경에는 어떤 패턴이 가장 적합한가?"

의사결정 플로우차트

상황별 추천

상황추천 솔루션이유
소규모 팀, GitOps 시작SOPS설치 없음, 즉시 사용 가능
멀티 클러스터 환경External Secrets중앙 SMS로 일관된 관리
최고 수준 보안 요구CSI Driveretcd에 저장 안 함
개발자 편의성 우선Vault Sidecarannotation만 추가하면 됨
단일 클러스터, Git 저장 필요Sealed Secrets간단하고 검증됨
클라우드 네이티브External Secrets + 클라우드 SMS클라우드 기능 최대 활용

비교 표

기능Sealed SecretsExternal SecretsSOPSCSI DriverVault Sidecar
복잡도낮음중간낮음높음중간
서버 컴포넌트필요필요불필요필요필요
Git 친화적O-OXX
자동 rotationXOXOO
etcd 저장Secret 생성Secret 생성Secret 생성XX
SMS 연동XOKMS만OO
멀티 클러스터제한적OO--

8. 보안 경고

중요: 아무리 안전하게 Secret을 구성하더라도, 악의적인 의도를 가진 사람이 클러스터와 컨테이너에 대한 완전한 root 접근 권한을 가지고 있다면 해당 데이터에 접근할 방법은 항상 존재합니다. 이 패턴은 이러한 종류의 공격을 최대한 어렵게 만드는 것이 목표입니다.

Defense in Depth 원칙 적용

  1. 네트워크 계층: NetworkPolicy로 Pod 간 통신 제한
  2. 인증 계층: RBAC으로 Secret 접근 제어
  3. 저장 계층: Encryption at Rest 활성화
  4. 런타임 계층: Pod Security Standards 적용
  5. 감사 계층: Audit Logging으로 접근 추적

9. 실습: 전체 워크플로우

Sealed Secrets 실습

# 1. Sealed Secrets 설치
kubectl apply -f <https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/controller.yaml>

# 2. kubeseal CLI 설치 (macOS)
brew install kubeseal

# 3. 테스트 Secret 생성
kubectl create secret generic test-secret \\
    --from-literal=username=admin \\
    --from-literal=password=supersecret \\
    --dry-run=client -o yaml > test-secret.yaml

# 4. SealedSecret으로 변환
kubeseal -f test-secret.yaml -o yaml > sealed-secret.yaml

# 5. Git에 커밋 (안전!)
cat sealed-secret.yaml
git add sealed-secret.yaml
git commit -m "Add encrypted test secret"

# 6. 클러스터에 적용
kubectl apply -f sealed-secret.yaml

# 7. 생성된 Secret 확인
kubectl get secret test-secret -o jsonpath='{.data.password}' | base64 -d
# supersecret

SOPS + age 실습 (완전 로컬)

# 1. age 설치
# macOS
brew install age

# Linux
sudo apt install age  # 또는 snap install age

# 2. age 키 페어 생성
age-keygen -o keys.txt
# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 3. 공개키 확인 및 저장
AGE_PUBLIC_KEY=$(grep "public key:" keys.txt | cut -d: -f2 | tr -d ' ')
echo $AGE_PUBLIC_KEY

# 4. SOPS 설치
# macOS
brew install sops

# Linux
curl -LO <https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64>
chmod +x sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops

# 5. Secret YAML 생성
cat > db-secret.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  username: admin
  password: supersecretpassword
EOF

# 6. SOPS로 암호화
sops --encrypt --age $AGE_PUBLIC_KEY db-secret.yaml > db-secret.enc.yaml

# 7. 암호화된 파일 확인 (값만 암호화됨)
cat db-secret.enc.yaml

# 8. Git에 안전하게 저장
git add db-secret.enc.yaml
git commit -m "Add encrypted database secret"

# 9. 복호화 후 클러스터에 적용
export SOPS_AGE_KEY_FILE=./keys.txt
sops --decrypt db-secret.enc.yaml | kubectl apply -f -

# 10. 적용 확인
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d

HashiCorp Vault 실습 (On-Premise)

# 1. Vault 설치 (개발 모드 - 테스트용)
# Helm으로 설치
helm repo add hashicorp <https://helm.releases.hashicorp.com>
helm install vault hashicorp/vault --set "server.dev.enabled=true"

# 2. Vault Pod 확인
kubectl get pods -l app.kubernetes.io/name=vault

# 3. Vault에 접속
kubectl exec -it vault-0 -- /bin/sh

# 4. (Vault 내부) Secret 저장
vault kv put secret/database username="admin" password="dbpassword123"

# 5. (Vault 내부) Secret 확인
vault kv get secret/database

# 6. (Vault 내부) Kubernetes 인증 활성화
vault auth enable kubernetes

vault write auth/kubernetes/config \\
    kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

# 7. (Vault 내부) 정책 생성
vault policy write database-policy - <<EOF
path "secret/data/database" {
  capabilities = ["read"]
}
EOF

# 8. (Vault 내부) Role 생성 (ServiceAccount 매핑)
vault write auth/kubernetes/role/database \\
    bound_service_account_names=webapp-sa \\
    bound_service_account_namespaces=default \\
    policies=database-policy \\
    ttl=1h

# 9. (Vault 외부) ServiceAccount 생성
kubectl create serviceaccount webapp-sa

# 10. (Vault 외부) Vault Agent Injector로 Secret 주입 테스트
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: webapp
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "database"
    vault.hashicorp.com/agent-inject-secret-database: "secret/data/database"
spec:
  serviceAccountName: webapp-sa
  containers:
    - name: app
      image: nginx:alpine
      command: ["sleep", "infinity"]
EOF

# 11. Secret 주입 확인
kubectl exec webapp -- cat /vault/secrets/database

정리 (Cleanup)

# Sealed Secrets 정리
kubectl delete sealedsecret test-secret
kubectl delete -f <https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/controller.yaml>

# SOPS 정리
kubectl delete secret db-credentials
rm -f keys.txt db-secret.yaml db-secret.enc.yaml

# Vault 정리
kubectl delete pod webapp
kubectl delete serviceaccount webapp-sa
helm uninstall vault

10. 결론

[최종 요약]

  • 핵심 메시지: Kubernetes Secret ≠ 암호화. 상황에 맞는 도구 선택과 Defense in Depth 적용이 핵심
  • 선택 기준: Git 저장 필요 → Out-of-Cluster / 최고 보안 → CSI Driver / 편의성 → Vault Sidecar
  • 필수 조치: Encryption at Rest + RBAC + Audit Logging + Pod Security Standards

Secure Configuration 패턴은 Secret 탈취의 난이도를 높이고, 보안 책임을 명확하게 분리하기 위한 현실적인 접근법입니다.

핵심 포인트

  1. Kubernetes Secret은 암호화가 아니라 인코딩입니다
  2. 상황에 맞는 적절한 도구 선택이 중요합니다
  3. Defense in Depth 원칙을 적용하세요
  4. 자동화된 rotation을 고려하세요
  5. 감사 로그로 접근을 추적하세요

기술은 계속 발전하지만, 여기서 소개한 기법들(클라이언트 측 암호화, Secret 동기화, 볼륨 프로젝션, 사이드카 주입)은 보편적이며 앞으로도 새로운 솔루션의 기반이 될 것입니다.

11. 출처 (References)

도서

공식 문서

프로젝트

클라우드 SMS

profile
DevOps Engineer

0개의 댓글