Managing secrets securely in Kubernetes environments presents one of the most critical challenges in cloud-native infrastructure. While Kubernetes provides native Secret objects, storing these secrets in Git repositories or managing them across multiple clusters requires additional tooling. This comprehensive guide explores two leading solutions: Sealed Secrets and External Secrets Operator, providing enterprise-grade implementation patterns for production environments.

Kubernetes Secrets Management: Sealed Secrets vs External Secrets Operator

The Secrets Management Challenge

Traditional Kubernetes Secrets face several critical limitations in enterprise environments:

Native Secrets Limitations

Storage Security: Base64 encoding provides no encryption

  • Secrets stored in etcd require encryption at rest configuration
  • Git repository storage exposes sensitive data
  • Version control history retains deleted secrets
  • Access control relies solely on RBAC

Operational Complexity: Manual secret management doesn’t scale

  • Secret rotation requires manual updates across clusters
  • No centralized secret lifecycle management
  • Difficult audit trail maintenance
  • Cross-environment synchronization challenges

Sealed Secrets Architecture

Sealed Secrets, developed by Bitnami, uses asymmetric cryptography to encrypt secrets that can only be decrypted by the cluster controller.

Core Components

# Sealed Secrets Controller Architecture
apiVersion: v1
kind: Namespace
metadata:
  name: sealed-secrets
  labels:
    app.kubernetes.io/name: sealed-secrets
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sealed-secrets-controller
  namespace: sealed-secrets
spec:
  replicas: 1
  selector:
    matchLabels:
      name: sealed-secrets-controller
  template:
    metadata:
      labels:
        name: sealed-secrets-controller
    spec:
      serviceAccountName: sealed-secrets-controller
      containers:
      - name: sealed-secrets-controller
        image: quay.io/bitnami/sealed-secrets-controller:v0.24.0
        command:
        - controller
        args:
        - --key-renew-period=720h
        - --key-rotation-enabled=true
        - --update-status=true
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 8081
          name: metrics
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          limits:
            cpu: 500m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 128Mi
        securityContext:
          runAsNonRoot: true
          runAsUser: 1001
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL

RBAC Configuration

# Sealed Secrets Service Account and Permissions
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sealed-secrets-controller
  namespace: sealed-secrets
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sealed-secrets-controller
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - get
  - list
  - create
  - update
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - bitnami.com
  resources:
  - sealedsecrets
  verbs:
  - get
  - list
  - watch
  - update
- apiGroups:
  - bitnami.com
  resources:
  - sealedsecrets/status
  verbs:
  - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sealed-secrets-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: sealed-secrets-controller
subjects:
- kind: ServiceAccount
  name: sealed-secrets-controller
  namespace: sealed-secrets
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sealed-secrets-key-admin
  namespace: sealed-secrets
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - get
  - list
  - create
  - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sealed-secrets-controller
  namespace: sealed-secrets
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: sealed-secrets-key-admin
subjects:
- kind: ServiceAccount
  name: sealed-secrets-controller
  namespace: sealed-secrets

Creating and Using Sealed Secrets

#!/bin/bash
# Sealed Secrets Creation and Management Script

set -euo pipefail

# Configuration
NAMESPACE="${NAMESPACE:-default}"
SECRET_NAME="${SECRET_NAME:-app-secrets}"
CONTROLLER_NAME="${CONTROLLER_NAME:-sealed-secrets-controller}"
CONTROLLER_NAMESPACE="${CONTROLLER_NAMESPACE:-sealed-secrets}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log() {
    echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"
}

error() {
    echo -e "${RED}[ERROR]${NC} $*" >&2
}

warn() {
    echo -e "${YELLOW}[WARN]${NC} $*"
}

# Install kubeseal CLI
install_kubeseal() {
    log "Installing kubeseal CLI..."

    local version="0.24.0"
    local os="linux"
    local arch="amd64"

    curl -L "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${version}/kubeseal-${version}-${os}-${arch}.tar.gz" \
        -o /tmp/kubeseal.tar.gz

    tar xfz /tmp/kubeseal.tar.gz -C /tmp/
    sudo mv /tmp/kubeseal /usr/local/bin/
    sudo chmod +x /usr/local/bin/kubeseal
    rm /tmp/kubeseal.tar.gz

    log "kubeseal version: $(kubeseal --version)"
}

# Fetch public key from controller
fetch_public_key() {
    log "Fetching public key from sealed-secrets controller..."

    kubeseal --fetch-cert \
        --controller-name="${CONTROLLER_NAME}" \
        --controller-namespace="${CONTROLLER_NAMESPACE}" \
        > /tmp/sealed-secrets-public-key.pem

    log "Public key saved to /tmp/sealed-secrets-public-key.pem"
}

# Create sealed secret from literal values
create_sealed_secret_literal() {
    local secret_name="$1"
    local namespace="$2"
    shift 2
    local literals=("$@")

    log "Creating sealed secret: ${secret_name} in namespace: ${namespace}"

    # Build kubectl command
    local cmd="kubectl create secret generic ${secret_name} --namespace=${namespace} --dry-run=client -o yaml"

    for literal in "${literals[@]}"; do
        cmd="${cmd} --from-literal=${literal}"
    done

    # Create and seal the secret
    eval "${cmd}" | kubeseal \
        --controller-name="${CONTROLLER_NAME}" \
        --controller-namespace="${CONTROLLER_NAMESPACE}" \
        --format=yaml \
        > "${secret_name}-sealed.yaml"

    log "Sealed secret created: ${secret_name}-sealed.yaml"
}

# Create sealed secret from file
create_sealed_secret_file() {
    local secret_name="$1"
    local namespace="$2"
    local file_path="$3"

    log "Creating sealed secret from file: ${file_path}"

    kubectl create secret generic "${secret_name}" \
        --namespace="${namespace}" \
        --from-file="${file_path}" \
        --dry-run=client -o yaml | \
    kubeseal \
        --controller-name="${CONTROLLER_NAME}" \
        --controller-namespace="${CONTROLLER_NAMESPACE}" \
        --format=yaml \
        > "${secret_name}-sealed.yaml"

    log "Sealed secret created: ${secret_name}-sealed.yaml"
}

# Seal existing secret
seal_existing_secret() {
    local secret_name="$1"
    local namespace="$2"

    log "Sealing existing secret: ${secret_name}"

    kubectl get secret "${secret_name}" \
        --namespace="${namespace}" \
        -o yaml | \
    kubeseal \
        --controller-name="${CONTROLLER_NAME}" \
        --controller-namespace="${CONTROLLER_NAMESPACE}" \
        --format=yaml \
        > "${secret_name}-sealed.yaml"

    log "Existing secret sealed: ${secret_name}-sealed.yaml"
}

# Rotate sealed secrets encryption key
rotate_encryption_key() {
    log "Rotating sealed secrets encryption key..."

    # Label old key for deletion
    kubectl label secret \
        -n "${CONTROLLER_NAMESPACE}" \
        -l sealedsecrets.bitnami.com/sealed-secrets-key=active \
        sealedsecrets.bitnami.com/sealed-secrets-key=old \
        --overwrite

    # Restart controller to generate new key
    kubectl rollout restart deployment/"${CONTROLLER_NAME}" \
        -n "${CONTROLLER_NAMESPACE}"

    # Wait for rollout
    kubectl rollout status deployment/"${CONTROLLER_NAME}" \
        -n "${CONTROLLER_NAMESPACE}" \
        --timeout=300s

    log "Encryption key rotated successfully"
}

# Backup encryption keys
backup_encryption_keys() {
    local backup_dir="${1:-./sealed-secrets-backup}"

    log "Backing up sealed secrets encryption keys to: ${backup_dir}"

    mkdir -p "${backup_dir}"

    kubectl get secret \
        -n "${CONTROLLER_NAMESPACE}" \
        -l sealedsecrets.bitnami.com/sealed-secrets-key \
        -o yaml \
        > "${backup_dir}/sealed-secrets-keys-$(date +%Y%m%d-%H%M%S).yaml"

    log "Encryption keys backed up successfully"
}

# Verify sealed secret
verify_sealed_secret() {
    local sealed_secret_file="$1"

    log "Verifying sealed secret: ${sealed_secret_file}"

    if ! kubectl apply --dry-run=server -f "${sealed_secret_file}"; then
        error "Sealed secret validation failed"
        return 1
    fi

    log "Sealed secret is valid"
}

# Example usage
main() {
    log "Sealed Secrets Management Script"

    # Ensure kubeseal is installed
    if ! command -v kubeseal &> /dev/null; then
        install_kubeseal
    fi

    # Fetch public key
    fetch_public_key

    # Example: Create sealed secret from literals
    create_sealed_secret_literal \
        "database-credentials" \
        "production" \
        "username=admin" \
        "password=securePassword123"

    # Example: Create sealed secret from file
    # create_sealed_secret_file "tls-cert" "production" "./tls.crt"

    # Verify created sealed secret
    verify_sealed_secret "database-credentials-sealed.yaml"

    log "Sealed secrets management completed"
}

# Run if executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

Sealed Secret Example

# Example Sealed Secret Resource
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: database-credentials
  namespace: production
  annotations:
    sealedsecrets.bitnami.com/cluster-wide: "true"
    sealedsecrets.bitnami.com/namespace-wide: "false"
spec:
  encryptedData:
    username: AgBvXR7ZT8... (encrypted data)
    password: AgCqT5YnM2... (encrypted data)
  template:
    metadata:
      name: database-credentials
      namespace: production
      labels:
        app: myapp
        component: database
    type: Opaque

External Secrets Operator Architecture

External Secrets Operator synchronizes secrets from external secret management systems into Kubernetes.

Controller Deployment

# External Secrets Operator Installation
apiVersion: v1
kind: Namespace
metadata:
  name: external-secrets
  labels:
    app.kubernetes.io/name: external-secrets
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-secrets
  namespace: external-secrets
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: external-secrets
  template:
    metadata:
      labels:
        app.kubernetes.io/name: external-secrets
    spec:
      serviceAccountName: external-secrets
      containers:
      - name: external-secrets
        image: ghcr.io/external-secrets/external-secrets:v0.9.9
        args:
        - --concurrent=5
        - --enable-leader-election
        - --loglevel=info
        - --metrics-addr=:8080
        ports:
        - containerPort: 8080
          name: metrics
          protocol: TCP
        - containerPort: 8081
          name: healthz
          protocol: TCP
        livenessProbe:
          httpGet:
            path: /healthz
            port: healthz
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /readyz
            port: healthz
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          limits:
            cpu: 200m
            memory: 256Mi
          requests:
            cpu: 100m
            memory: 128Mi
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-secrets-webhook
  namespace: external-secrets
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: external-secrets-webhook
  template:
    metadata:
      labels:
        app.kubernetes.io/name: external-secrets-webhook
    spec:
      serviceAccountName: external-secrets-webhook
      containers:
      - name: webhook
        image: ghcr.io/external-secrets/external-secrets:v0.9.9
        args:
        - webhook
        - --port=10250
        - --dns-name=external-secrets-webhook.external-secrets.svc
        - --cert-dir=/tmp/certs
        - --check-interval=5m
        - --loglevel=info
        ports:
        - containerPort: 10250
          name: webhook
          protocol: TCP
        - containerPort: 8081
          name: healthz
          protocol: TCP
        livenessProbe:
          httpGet:
            path: /healthz
            port: healthz
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /readyz
            port: healthz
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          limits:
            cpu: 100m
            memory: 128Mi
          requests:
            cpu: 50m
            memory: 64Mi
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
        volumeMounts:
        - name: certs
          mountPath: /tmp/certs
          readOnly: false
      volumes:
      - name: certs
        emptyDir: {}

AWS Secrets Manager Integration

# SecretStore for AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa
---
# ClusterSecretStore for Multi-Namespace Access
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: aws-secrets-manager-global
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa
            namespace: external-secrets
---
# ExternalSecret Resource
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: database-credentials
    creationPolicy: Owner
    deletionPolicy: Retain
    template:
      type: Opaque
      metadata:
        labels:
          app: myapp
          component: database
      data:
        # Template the secret data
        connection-string: |
          postgresql://{{ .username }}:{{ .password }}@{{ .host }}:{{ .port }}/{{ .database }}
  dataFrom:
  - extract:
      key: prod/database/credentials
  # Or use individual data items
  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: port
    remoteRef:
      key: prod/database/credentials
      property: port
  - secretKey: database
    remoteRef:
      key: prod/database/credentials
      property: database

HashiCorp Vault Integration

# SecretStore for HashiCorp Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.company.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "external-secrets"
          serviceAccountRef:
            name: external-secrets-sa
---
# ExternalSecret for Vault KV Store
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: vault-secrets
  namespace: production
spec:
  refreshInterval: 15m
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: vault-secrets
    creationPolicy: Owner
    template:
      engineVersion: v2
      data:
        config.json: |
          {
            "api_key": "{{ .api_key }}",
            "api_secret": "{{ .api_secret }}",
            "webhook_url": "{{ .webhook_url }}"
          }
  data:
  - secretKey: api_key
    remoteRef:
      key: prod/app/config
      property: api_key
  - secretKey: api_secret
    remoteRef:
      key: prod/app/config
      property: api_secret
  - secretKey: webhook_url
    remoteRef:
      key: prod/app/config
      property: webhook_url
---
# Dynamic Secret Example (Database Credentials)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: dynamic-db-credentials
  namespace: production
spec:
  refreshInterval: 5m  # Short interval for dynamic credentials
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: dynamic-db-credentials
    creationPolicy: Owner
    deletionPolicy: Delete
  data:
  - secretKey: username
    remoteRef:
      key: database/creds/app-role
      property: username
  - secretKey: password
    remoteRef:
      key: database/creds/app-role
      property: password

Google Secret Manager Integration

# SecretStore for Google Secret Manager
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: gcpsm-secret-store
spec:
  provider:
    gcpsm:
      projectID: "my-gcp-project"
      auth:
        workloadIdentity:
          clusterLocation: us-central1
          clusterName: production-cluster
          serviceAccountRef:
            name: external-secrets-sa
            namespace: external-secrets
---
# ExternalSecret for GCP Secret Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: gcp-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: gcpsm-secret-store
    kind: ClusterSecretStore
  target:
    name: gcp-secrets
    creationPolicy: Owner
  data:
  - secretKey: api-key
    remoteRef:
      key: prod-api-key
      version: latest
  - secretKey: database-password
    remoteRef:
      key: prod-database-password
      version: "1"  # Specific version

Azure Key Vault Integration

# SecretStore for Azure Key Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: azure-keyvault
  namespace: production
spec:
  provider:
    azurekv:
      vaultUrl: "https://my-vault.vault.azure.net"
      tenantId: "tenant-id"
      authType: WorkloadIdentity
      serviceAccountRef:
        name: external-secrets-sa
---
# ExternalSecret for Azure Key Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: azure-secrets
  namespace: production
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: azure-keyvault
    kind: SecretStore
  target:
    name: azure-secrets
    creationPolicy: Owner
  data:
  - secretKey: connection-string
    remoteRef:
      key: database-connection-string
  - secretKey: storage-account-key
    remoteRef:
      key: storage-account-key
  - secretKey: certificate
    remoteRef:
      key: tls-certificate

Advanced Configuration Patterns

PushSecret for Bidirectional Sync

# PushSecret - Push Kubernetes Secrets to External Provider
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
  name: push-secret-example
  namespace: production
spec:
  refreshInterval: 10m
  secretStoreRefs:
  - name: aws-secrets-manager
    kind: SecretStore
  selector:
    secret:
      name: local-secret
  data:
  - match:
      secretKey: username
      remoteRef:
        remoteKey: prod/app/credentials
        property: username
  - match:
      secretKey: password
      remoteRef:
        remoteKey: prod/app/credentials
        property: password

Secret Rotation Automation

# Automated Secret Rotation Configuration
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: rotated-credentials
  namespace: production
  annotations:
    external-secrets.io/rotation-strategy: "recreate"
spec:
  refreshInterval: 5m
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: rotated-credentials
    creationPolicy: Owner
    deletionPolicy: Delete
    template:
      type: Opaque
      metadata:
        annotations:
          reloader.stakater.com/match: "true"
  data:
  - secretKey: credentials
    remoteRef:
      key: dynamic/database/creds/app
---
# Reloader Deployment for Automatic Pod Restarts
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-with-rotation
  namespace: production
  annotations:
    reloader.stakater.com/search: "true"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: myapp:latest
        envFrom:
        - secretRef:
            name: rotated-credentials

Monitoring and Observability

Prometheus Metrics

# ServiceMonitor for Sealed Secrets
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: sealed-secrets-controller
  namespace: sealed-secrets
spec:
  selector:
    matchLabels:
      name: sealed-secrets-controller
  endpoints:
  - port: metrics
    interval: 30s
    path: /metrics
---
# ServiceMonitor for External Secrets
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: external-secrets
  namespace: external-secrets
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: external-secrets
  endpoints:
  - port: metrics
    interval: 30s
    path: /metrics

Grafana Dashboard

{
  "dashboard": {
    "title": "Secrets Management Overview",
    "panels": [
      {
        "title": "External Secrets Sync Status",
        "targets": [
          {
            "expr": "externalsecret_sync_calls_total",
            "legendFormat": "{{namespace}}/{{name}} - {{status}}"
          }
        ]
      },
      {
        "title": "Secret Sync Errors",
        "targets": [
          {
            "expr": "rate(externalsecret_sync_calls_error[5m])",
            "legendFormat": "{{namespace}}/{{name}}"
          }
        ]
      },
      {
        "title": "Sealed Secrets Controller Status",
        "targets": [
          {
            "expr": "sealed_secrets_controller_unseal_requests_total",
            "legendFormat": "Unseal Requests"
          }
        ]
      }
    ]
  }
}

Alert Rules

# PrometheusRule for Secrets Monitoring
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: secrets-management-alerts
  namespace: monitoring
spec:
  groups:
  - name: external-secrets
    interval: 30s
    rules:
    - alert: ExternalSecretSyncFailure
      expr: |
        externalsecret_sync_calls_error > 0
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "External Secret sync failing"
        description: "External Secret {{ $labels.namespace }}/{{ $labels.name }} has failed to sync for 5 minutes"

    - alert: ExternalSecretNotReady
      expr: |
        externalsecret_status_condition{condition="Ready",status="False"} > 0
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "External Secret not ready"
        description: "External Secret {{ $labels.namespace }}/{{ $labels.name }} is not in Ready state"

    - alert: SecretStoreNotReady
      expr: |
        secretstore_status_condition{condition="Ready",status="False"} > 0
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "Secret Store not ready"
        description: "Secret Store {{ $labels.namespace }}/{{ $labels.name }} is not in Ready state"

  - name: sealed-secrets
    interval: 30s
    rules:
    - alert: SealedSecretsControllerDown
      expr: |
        up{job="sealed-secrets-controller"} == 0
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "Sealed Secrets Controller is down"
        description: "The Sealed Secrets Controller has been down for 5 minutes"

    - alert: HighUnsealErrorRate
      expr: |
        rate(sealed_secrets_controller_unseal_errors_total[5m]) > 0.1
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "High unseal error rate"
        description: "Sealed Secrets Controller is experiencing high unseal error rate"

Security Best Practices

RBAC for Secrets Access

# Restricted RBAC for Secrets Management
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secrets-operator
  namespace: production
rules:
# Allow reading SecretStores and ExternalSecrets
- apiGroups: ["external-secrets.io"]
  resources: ["secretstores", "externalsecrets"]
  verbs: ["get", "list", "watch"]
# Allow creating and updating Secrets
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["create", "update", "patch"]
# Deny deletion of secrets
# - apiGroups: [""]
#   resources: ["secrets"]
#   verbs: ["delete"]
---
# Role for Application Access to Secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-secrets-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["app-secrets", "database-credentials"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-secrets-reader
  namespace: production
subjects:
- kind: ServiceAccount
  name: myapp
  namespace: production
roleRef:
  kind: Role
  name: app-secrets-reader
  apiGroup: rbac.authorization.k8s.io

Network Policies

# Network Policy for External Secrets Controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: external-secrets-controller
  namespace: external-secrets
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: external-secrets
  policyTypes:
  - Ingress
  - Egress
  ingress:
  # Allow metrics scraping
  - from:
    - namespaceSelector:
        matchLabels:
          name: monitoring
    ports:
    - protocol: TCP
      port: 8080
  egress:
  # Allow DNS
  - to:
    - namespaceSelector:
        matchLabels:
          name: kube-system
    ports:
    - protocol: UDP
      port: 53
  # Allow Kubernetes API access
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          component: apiserver
    ports:
    - protocol: TCP
      port: 443
  # Allow external secret provider access (AWS, Vault, etc.)
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 443

Comparison and Selection Criteria

Feature Comparison Matrix

FeatureSealed SecretsExternal Secrets Operator
StorageGit repository (encrypted)External secret management system
Secret SourceKubernetes cluster onlyMultiple providers (AWS, GCP, Azure, Vault)
EncryptionAsymmetric (RSA)Provider-specific
GitOps FriendlyExcellentGood (requires external system)
Secret RotationManual re-encryptionAutomatic sync
Multi-ClusterSeparate keys per clusterCentralized secret source
ComplexityLowMedium to High
DependenciesNoneExternal secret management system
CostFreeDepends on provider
Backup/RecoveryGit historyProvider-dependent

Decision Framework

Choose Sealed Secrets when:

  • GitOps workflow is primary requirement
  • Self-contained solution preferred
  • No existing secret management infrastructure
  • Cost is a primary concern
  • Secrets don’t require frequent rotation
  • Multi-cluster secrets are cluster-specific

Choose External Secrets Operator when:

  • Existing secret management system (Vault, AWS Secrets Manager)
  • Dynamic secret generation required
  • Frequent secret rotation needed
  • Centralized secret management across multiple clusters
  • Compliance requires dedicated secret management
  • Complex secret templating and transformation needed

Production Implementation Guide

Migration Strategy

#!/bin/bash
# Migration from Sealed Secrets to External Secrets

set -euo pipefail

migrate_sealed_to_external() {
    local namespace="$1"
    local sealed_secret="$2"
    local secret_store="$3"
    local remote_path="$4"

    echo "Migrating sealed secret: ${sealed_secret}"

    # 1. Extract the unsealed secret
    kubectl get secret "${sealed_secret}" -n "${namespace}" -o json > "/tmp/${sealed_secret}.json"

    # 2. Upload to external provider (example: AWS Secrets Manager)
    local secret_data=$(kubectl get secret "${sealed_secret}" -n "${namespace}" -o json | \
        jq -r '.data | map_values(@base64d) | to_entries | map({key: .key, value: .value}) | from_entries')

    aws secretsmanager create-secret \
        --name "${remote_path}" \
        --secret-string "${secret_data}" \
        --region us-east-1

    # 3. Create ExternalSecret resource
    cat <<EOF | kubectl apply -f -
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: ${sealed_secret}
  namespace: ${namespace}
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: ${secret_store}
    kind: SecretStore
  target:
    name: ${sealed_secret}
    creationPolicy: Owner
  dataFrom:
  - extract:
      key: ${remote_path}
EOF

    echo "Migration completed for: ${sealed_secret}"
}

# Example usage
# migrate_sealed_to_external "production" "app-secrets" "aws-secrets-manager" "prod/app/secrets"

Disaster Recovery

# Backup CronJob for External Secrets
apiVersion: batch/v1
kind: CronJob
metadata:
  name: external-secrets-backup
  namespace: external-secrets
spec:
  schedule: "0 2 * * *"  # Daily at 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: backup-sa
          containers:
          - name: backup
            image: bitnami/kubectl:latest
            command:
            - /bin/sh
            - -c
            - |
              #!/bin/sh
              set -e

              BACKUP_DIR="/backup/external-secrets-$(date +%Y%m%d)"
              mkdir -p "$BACKUP_DIR"

              # Backup all ExternalSecret resources
              for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
                kubectl get externalsecrets -n "$ns" -o yaml > "$BACKUP_DIR/externalsecrets-$ns.yaml" 2>/dev/null || true
                kubectl get secretstores -n "$ns" -o yaml > "$BACKUP_DIR/secretstores-$ns.yaml" 2>/dev/null || true
              done

              # Backup ClusterSecretStores
              kubectl get clustersecretstores -o yaml > "$BACKUP_DIR/clustersecretstores.yaml"

              # Upload to S3
              aws s3 sync "$BACKUP_DIR" "s3://my-backup-bucket/external-secrets/"

              # Cleanup old backups (keep 30 days)
              find /backup -type d -mtime +30 -exec rm -rf {} +
            volumeMounts:
            - name: backup
              mountPath: /backup
          volumes:
          - name: backup
            persistentVolumeClaim:
              claimName: backup-pvc
          restartPolicy: OnFailure

Conclusion

Both Sealed Secrets and External Secrets Operator provide robust solutions for Kubernetes secrets management, each with distinct advantages for different use cases. Sealed Secrets excels in GitOps-centric workflows with its simplicity and self-contained architecture, while External Secrets Operator shines in enterprise environments requiring integration with existing secret management infrastructure and dynamic secret rotation.

The choice between these solutions should be based on your organization’s existing infrastructure, compliance requirements, operational complexity tolerance, and secret lifecycle management needs. Many organizations successfully implement both solutions, using Sealed Secrets for application configuration and External Secrets Operator for dynamic credentials and sensitive secrets requiring frequent rotation.

Implementing proper monitoring, backup strategies, and security controls remains critical regardless of which solution you choose, ensuring that your secrets management infrastructure provides the security, reliability, and operational efficiency required for production Kubernetes environments.