Sealed Secrets
Kubernetes에서 암호화된 비밀 관리 - GitOps 환경에서 안전한 시크릿 저장을 위한 Bitnami 오픈소스 도구
Kubernetes에서 암호화된 비밀 관리 - GitOps 환경에서 안전한 시크릿 저장을 위한 Bitnami 오픈소스 도구
Sealed Secrets는 Bitnami에서 개발한 Kubernetes용 시크릿 암호화 도구입니다. GitOps 환경에서 가장 큰 과제인 "비밀 정보를 Git에 어떻게 안전하게 저장할 것인가"라는 문제를 해결합니다. 일반 Kubernetes Secret은 base64 인코딩만 되어 있어 실제로는 평문이나 다름없지만, Sealed Secrets를 사용하면 공개키로 암호화되어 클러스터 내의 Sealed Secrets Controller만 복호화할 수 있습니다.
작동 방식은 비대칭 암호화(공개키/비공개키)를 기반으로 합니다. 클러스터에 설치된 Controller가 비공개키를 보유하고, 개발자는 kubeseal CLI 도구를 통해 공개키로 Secret을 암호화하여 SealedSecret 리소스를 생성합니다. 이 SealedSecret은 Git에 안전하게 저장할 수 있으며, 클러스터에 배포되면 Controller가 자동으로 복호화하여 실제 Secret을 생성합니다.
Sealed Secrets의 가장 큰 장점은 기존 Kubernetes 워크플로우와의 완벽한 통합입니다. ArgoCD, Flux 같은 GitOps 도구와 함께 사용하면 인프라 전체를 코드로 관리할 수 있습니다. 또한 namespace별, 클러스터별로 스코프를 지정할 수 있어 다중 환경 관리에도 유연합니다. 암호화된 SealedSecret은 특정 클러스터, 특정 namespace에서만 복호화되도록 제한할 수 있습니다.
운영 시 주의할 점은 비공개키 관리입니다. Controller의 비공개키가 유출되면 모든 SealedSecret이 복호화될 수 있으므로, 키 로테이션 정책과 백업 전략이 필수입니다. 또한 SealedSecret은 생성 시점의 공개키로 암호화되므로, 키 로테이션 후에는 기존 SealedSecret을 재암호화해야 합니다. 대규모 환경에서는 HashiCorp Vault나 External Secrets Operator와의 조합도 고려할 만합니다.
# Sealed Secrets Controller 설치 (Helm 사용)
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm repo update
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace kube-system \
--set fullnameOverride=sealed-secrets-controller
# kubeseal CLI 설치 (macOS)
brew install kubeseal
# kubeseal CLI 설치 (Linux)
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/kubeseal-0.24.5-linux-amd64.tar.gz
tar -xvzf kubeseal-0.24.5-linux-amd64.tar.gz
sudo install -m 755 kubeseal /usr/local/bin/kubeseal
# 공개키 가져오기 (오프라인 사용 시)
kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
> sealed-secrets-pub.pem
# 1. 먼저 일반 Secret YAML 생성
cat > my-secret.yaml << EOF
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: production
type: Opaque
data:
username: YWRtaW4= # base64 encoded: admin
password: c3VwZXJzZWNyZXQ= # base64 encoded: supersecret
EOF
# 2. kubeseal로 암호화 (클러스터에 연결된 상태)
kubeseal --format yaml < my-secret.yaml > sealed-secret.yaml
# 3. 또는 공개키 파일 직접 지정 (오프라인 환경)
kubeseal --format yaml \
--cert sealed-secrets-pub.pem \
< my-secret.yaml > sealed-secret.yaml
# 4. 생성된 SealedSecret 확인
cat sealed-secret.yaml
# sealed-secret.yaml - Git에 안전하게 저장 가능
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: database-credentials
namespace: production
spec:
encryptedData:
username: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...
password: AgBvLh7fH9/WTBu+ZqMC2nvDpAJJSzAR...
template:
metadata:
name: database-credentials
namespace: production
type: Opaque
---
# 다른 스코프 옵션들
# strict (기본값): namespace와 name 둘 다 일치해야 복호화
# namespace-wide: 같은 namespace 내 어떤 이름으로도 복호화 가능
# cluster-wide: 클러스터 전체에서 복호화 가능
# namespace-wide 스코프로 암호화
# kubeseal --scope namespace-wide --format yaml < my-secret.yaml
# cluster-wide 스코프로 암호화
# kubeseal --scope cluster-wide --format yaml < my-secret.yaml
# .github/workflows/seal-secrets.yml
name: Seal Secrets
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- development
- staging
- production
jobs:
seal-secret:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubeseal
run: |
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/kubeseal-0.24.5-linux-amd64.tar.gz
tar -xvzf kubeseal-0.24.5-linux-amd64.tar.gz
sudo install -m 755 kubeseal /usr/local/bin/kubeseal
- name: Seal secrets for environment
run: |
CERT_FILE="certs/${{ inputs.environment }}-sealed-secrets.pem"
# 환경별 시크릿 암호화
for secret_file in secrets/${{ inputs.environment }}/*.yaml; do
output_file="manifests/${{ inputs.environment }}/$(basename $secret_file .yaml)-sealed.yaml"
kubeseal --format yaml \
--cert "$CERT_FILE" \
< "$secret_file" > "$output_file"
done
- name: Commit sealed secrets
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add manifests/
git commit -m "Seal secrets for ${{ inputs.environment }}"
git push
import subprocess
import yaml
import json
from pathlib import Path
from datetime import datetime, timedelta
from kubernetes import client, config
class SealedSecretsManager:
def __init__(self, kubeconfig_path: str = None):
if kubeconfig_path:
config.load_kube_config(config_file=kubeconfig_path)
else:
config.load_incluster_config()
self.custom_api = client.CustomObjectsApi()
self.core_api = client.CoreV1Api()
def list_sealed_secrets(self, namespace: str = None):
"""클러스터의 모든 SealedSecret 목록 조회"""
if namespace:
secrets = self.custom_api.list_namespaced_custom_object(
group="bitnami.com",
version="v1alpha1",
namespace=namespace,
plural="sealedsecrets"
)
else:
secrets = self.custom_api.list_cluster_custom_object(
group="bitnami.com",
version="v1alpha1",
plural="sealedsecrets"
)
return secrets.get('items', [])
def verify_sealed_secret(self, sealed_secret_path: str) -> bool:
"""SealedSecret이 현재 Controller 키로 복호화 가능한지 검증"""
try:
result = subprocess.run(
['kubeseal', '--validate', '--format', 'yaml'],
input=Path(sealed_secret_path).read_bytes(),
capture_output=True
)
return result.returncode == 0
except Exception as e:
print(f"Validation failed: {e}")
return False
def get_controller_key_age(self, namespace: str = "kube-system"):
"""Controller의 현재 키 생성 시점 확인"""
secrets = self.core_api.list_namespaced_secret(
namespace=namespace,
label_selector="sealedsecrets.bitnami.com/sealed-secrets-key=active"
)
if secrets.items:
key_secret = secrets.items[0]
creation_time = key_secret.metadata.creation_timestamp
age = datetime.now(creation_time.tzinfo) - creation_time
return {
"name": key_secret.metadata.name,
"created": creation_time.isoformat(),
"age_days": age.days
}
return None
def backup_controller_keys(self, output_dir: str):
"""Controller 키 백업 (재해복구용)"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
# 모든 키 시크릿 백업
secrets = self.core_api.list_namespaced_secret(
namespace="kube-system",
label_selector="sealedsecrets.bitnami.com/sealed-secrets-key"
)
for secret in secrets.items:
backup_file = Path(output_dir) / f"{secret.metadata.name}.yaml"
secret_dict = client.ApiClient().sanitize_for_serialization(secret)
with open(backup_file, 'w') as f:
yaml.dump(secret_dict, f)
print(f"Backed up: {backup_file}")
def rotate_and_reseal(self, manifest_dir: str, new_cert_path: str):
"""키 로테이션 후 모든 SealedSecret 재암호화"""
sealed_files = list(Path(manifest_dir).rglob("*-sealed.yaml"))
for sealed_file in sealed_files:
# 원본 Secret 파일 찾기
original_file = str(sealed_file).replace("-sealed.yaml", ".yaml")
if not Path(original_file).exists():
print(f"Warning: Original secret not found for {sealed_file}")
continue
# 새 인증서로 재암호화
result = subprocess.run(
['kubeseal', '--format', 'yaml', '--cert', new_cert_path],
input=Path(original_file).read_bytes(),
capture_output=True
)
if result.returncode == 0:
sealed_file.write_bytes(result.stdout)
print(f"Re-sealed: {sealed_file}")
else:
print(f"Failed to re-seal: {sealed_file}")
# 사용 예시
if __name__ == "__main__":
manager = SealedSecretsManager()
# 모든 SealedSecret 조회
secrets = manager.list_sealed_secrets()
for secret in secrets:
name = secret['metadata']['name']
ns = secret['metadata']['namespace']
print(f"SealedSecret: {ns}/{name}")
# 키 나이 확인 (30일 이상이면 로테이션 권장)
key_info = manager.get_controller_key_age()
if key_info and key_info['age_days'] > 30:
print(f"Warning: Key is {key_info['age_days']} days old. Consider rotation.")