🟡 Intermediate

SOPS, External Secrets Operator a Sealed Secrets — GitOps-friendly secret management

Kubernetes Secret je jedným z najčastejšie zneužívaných objektov v celom K8s ekosystéme — nie preto, že by bol nebezpečný sám o sebe, ale preto, že väčšina tímov ho nesprávne chápe, a následne nesprávne spravuje. Výsledok: databázové heslá a API kľúče v plain texte v Git repozitári, na Slacku, v CI logoch.

Tento článok porovnáva tri produkčne overené prístupy k správe secretov v Kubernetes: SOPS (pôvodne Mozilla, od 2023 komunitný getsops org), Sealed Secrets (Bitnami, acq. VMware/Broadcom) a External Secrets Operator (ESO). Každý rieši rovnaký problém iným spôsobom, s inými kompromismi. Cieľom je, aby ste po prečítaní vedeli, ktorý nástroj patrí do vášho stacku — a prečo.


Problém: base64 nie je šifrovanie

Keď vytvoríte Kubernetes Secret:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
data:
  password: c3VwZXJzZWNyZXRwYXNzd29yZA==

c3VwZXJzZWNyZXRwYXNzd29yZA== hodnota nie je šifrovaná. Je to base64-enkódovaný reťazec supersecretpassword. Ktokoľvek, kto má prístup k YAML súboru, môže v sekundách získať heslo:

echo "c3VwZXJzZWNyZXRwYXNzd29yZA==" | base64 -d
# supersecretpassword

Kubernetes Secret nie je bezpečnostná vrstva — je to len konvencia. Skutočná bezpečnosť závisí na:

  1. RBAC obmedzení prístupu ku kubectl get secret.
  2. Šifrovaní etcd at rest (v managed K8s je to väčšinou zapnuté automaticky).
  3. Nikdy necommitovať plain Secret YAML do Gitu.

Bod 3 je práve tam, kde väčšina tímov zlyháva.

Čo sa stane po commite do Gitu

Git je distribuovaný a trvalý. Ak commitnete Secret YAML s plain hodnotami (alebo base64, čo je to isté), stane sa toto:

  • Hodnota ostáva v git histórií navždy, aj po zmazaní súboru.
  • Každý klon repozitára obsahuje citlivé dáta.
  • GitHub/GitLab secret scanning môže (ale nemusí) zachytiť únik.
  • CI/CD systémy (Jenkins, GitHub Actions, GitLab CI) klonoujú repozitár — secret je vystavený aj tam.
  • Pri zmene prístupu k repozitáru (nový developer, externý kontributor) okamžite dostanú prístup ku všetkým historickým secretom.

Jeden commit s DB heslom = rotation celého hesla + audit kto mal prístup k repozitáru. To je drahé.

Tri filozofie riešenia

┌──────────────────────────────────────────────────────────────────────┐
│  SOPS                │  Sealed Secrets      │  External Secrets      │
│  "Encrypt in Git"    │  "Seal for cluster"  │  "Sync from external"  │
│                      │                      │                        │
│  Secret je v Gite,   │  Secret je v Gite,   │  Secret NIE JE v Gite │
│  ale zašifrovaný     │  zapečatený pre 1    │  — žije v externom    │
│  (age/GPG/KMS)       │  konkrétny klaster   │  systéme (Vault, SM)  │
└──────────────────────────────────────────────────────────────────────┘

Prehľad troch nástrojov

SOPS (pôvodne Mozilla, teraz getsops komunita)

SOPS (Secrets OPerationS) je CLI nástroj, ktorý šifruje hodnoty v YAML/JSON/ENV/INI súboroch. Kľúče zostanú plain text, šifrujú sa iba hodnoty. Výsledný súbor môžete commitnúť do Gitu. Pôvodne vyvinutý v Mozille, od 2023 je projekt spravovaný komunitou v github.com/getsops/sops.

Podporované backend kľúče:

  • age — moderný, jednoduchý, odporúčaný pre small/medium tímy
  • GPG — starší štandard, stále bežne používaný
  • AWS KMS — pre tímy na AWS
  • GCP KMS — pre GCP
  • Azure Key Vault — pre Azure
  • HashiCorp Vault Transit — pre on-premise Vault

SOPS je agnostický voči Kubernetes — šifruje súbory, nie K8s objekty. Dešifrovanie prebieha buď lokálne (developer), v CI/CD pipeline, alebo natívne cez FluxCD.

Sealed Secrets (Bitnami)

Sealed Secrets je Kubernetes operator od Bitnami (teraz Broadcom). Princíp je jednoduchý: operátor generuje asymetrický kľúčový pár. CLI nástroj kubeseal zašifruje váš Secret pomocou verejného kľúča klastra — vznikne SealedSecret CRD. Tento objekt môžete bezpečne commitnúť do Gitu. Operátor v klastri ho rozbalí pomocou súkromného kľúča a vytvorí natívny Kubernetes Secret.

SealedSecret je viazaný na konkrétny klaster a (voliteľne) na konkrétny namespace a meno Secretu. Nemôžete ho "rozlepiť" bez prístupu k súkromnému kľúču toho klastra.

External Secrets Operator (ESO)

External Secrets Operator je CNCF projekt, ktorý synchronizuje secrety z externých systémov do Kubernetes Secret objektov. Sekret nežije v Gite vôbec — žije v Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Bitwarden, 1Password, GitLab, Infisical atď.

ESO definuje CRD SecretStore (alebo ClusterSecretStore) ako konfiguráciu backendu, a ExternalSecret ako mapovanie konkrétnych hodnôt. Operátor pravidelne synchronizuje hodnoty a automaticky rotuje secrety v K8s pri zmene v externom systéme.


SOPS — hlboký ponor

Inštalácia

# SOPS 3.9+ — Linux AMD64
curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
chmod +x sops-v3.9.4.linux.amd64
mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops

# age — generátor kľúčov
# Debian/Ubuntu
apt install age
# alebo z release
curl -LO https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-amd64.tar.gz
tar xz < age-v1.2.1-linux-amd64.tar.gz
mv age/age age/age-keygen /usr/local/bin/

Generovanie age kľúčov

# Vygenerovanie nového kľúčového páru
age-keygen -o ~/.config/sops/age/keys.txt

# Výstup (príklad — nikdy nezdieľajte secret key!):
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# 
# AGE-SECRET-KEY-1QJEP8PW8...

# Prezrite si verejný kľúč
cat ~/.config/sops/age/keys.txt | grep "public key"

Verejný kľúč (age1...) môžete zdieľať v .sops.yaml v repozitári — je to bezpečné. Súkromný kľúč (AGE-SECRET-KEY-1...) musí zostať tajný — uchováva sa lokálne alebo v CI secrets.

.sops.yaml — pravidlá šifrovania

.sops.yaml je konfiguračný súbor v koreni repozitára, ktorý definuje, kto môže dešifrovať a ktoré súbory sú šifrované:

# .sops.yaml — v koreni repozitára
creation_rules:
  # Produkčné secrety — vyžadujú dva age kľúče (dev + CI)
  - path_regex: secrets/production/.*\.yaml$
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
      age1lgg...ci_key_here...
    # Voliteľne aj AWS KMS pre produkciu
    # kms: arn:aws:kms:eu-west-1:123456789:key/mrk-abc123

  # Staging — len developer kľúč
  - path_regex: secrets/staging/.*\.yaml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

  # Shared secrets — viacero príjemcov
  - path_regex: secrets/shared/.*\.yaml$
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
      age1dev2...another_developer...,
      age1dev3...another_developer...

  # Helm values so secretmi
  - path_regex: helm/.*\.secrets\.yaml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Každé pravidlo môže kombnovať viacero kľúčových typov — SOPS zašifruje data encryption key (DEK) pre každého príjemcu zvlášť.

Šifrovanie a editovanie súborov

# Zašifrovanie existujúceho YAML súboru
sops --encrypt secrets/production/db.yaml > secrets/production/db.enc.yaml

# Alebo in-place (prepisuje súbor)
sops --encrypt --in-place secrets/production/db.yaml

# Editovanie zašifrovaného súboru (otvorí editor, dešifruje do pamäte)
sops secrets/production/db.yaml

# Dešifrovanie do stdout (pre CI pipeline)
sops --decrypt secrets/production/db.yaml

# Dešifrovanie do súboru
sops --decrypt secrets/production/db.yaml > /tmp/db-plain.yaml

# Zobrazenie konkrétnej hodnoty
sops --decrypt --extract '["db"]["password"]' secrets/production/db.yaml

SOPS-zašifrovaný súbor — príklad

Vstupný súbor (secrets/production/db.yaml pred šifrovaním):

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
stringData:
  host: postgres.internal
  port: "5432"
  database: myapp_prod
  username: myapp
  password: sup3rS3cretP4ssw0rd!
  ssl_cert: |
    -----BEGIN CERTIFICATE-----
    MIIBkTCB+wIJ...
    -----END CERTIFICATE-----

Po sops --encrypt --in-place:

apiVersion: v1
kind: Secret
metadata:
    name: db-credentials
    namespace: production
type: Opaque
stringData:
    host: ENC[AES256_GCM,data:a2R...,tag:xyz==,type:str]
    port: ENC[AES256_GCM,data:bGQ...,tag:abc==,type:str]
    database: ENC[AES256_GCM,data:cHJ...,tag:def==,type:str]
    username: ENC[AES256_GCM,data:bXk...,tag:ghi==,type:str]
    password: ENC[AES256_GCM,data:c3V...,tag:jkl==,type:str]
    ssl_cert: ENC[AES256_GCM,data:LS0...,tag:mno==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age:
        - recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB...
            -----END AGE ENCRYPTED FILE-----
    lastmodified: "2026-04-17T10:00:00Z"
    mac: ENC[AES256_GCM,data:pqr...,tag:stu==,type:str]
    version: 3.9.4

Kľúče (name, namespace, type, stringData) sú čitateľné — môžete vidieť štruktúru bez dešifrovania. Hodnoty sú šifrované pomocou AES-256-GCM. Celý blok sops: obsahuje metadata a zašifrovaný DEK pre každého príjemcu.

Integrácia s Helm — helm-secrets plugin

helm-secrets je plugin, ktorý umožňuje Helm-u pracovať so SOPS-šifrovanými values súbormi:

# Inštalácia helm-secrets pluginu
helm plugin install https://github.com/jkroepke/helm-secrets

# Spustenie helm deploy so šifrovanými values
helm secrets upgrade myapp ./charts/myapp \
  -f values.yaml \
  -f secrets/production/myapp.secrets.yaml   # SOPS-šifrovaný súbor

# Plugin automaticky dešifruje secrets.yaml pred odovzdaním Helm-u
# a zmaže dočasný plain-text súbor po dokončení

Štruktúra repozitára:

charts/myapp/
├── values.yaml              # plain hodnoty — v Gite
└── values.production.yaml   # plain non-secret overrides — v Gite

secrets/production/
└── myapp.secrets.yaml       # SOPS-šifrované — v Gite (bezpečne)

FluxCD natívna SOPS podpora

FluxCD má zabudovanú podporu pre SOPS — netreba žiadny externý webhook ani init container. Flux dešifruje secrety pri aplikovaní manifestov.

Konfigurácia:

# Vytvorenie age key secret v klastri pre Flux
kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=$HOME/.config/sops/age/keys.txt

# Alebo cez flux CLI
flux create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=$HOME/.config/sops/age/keys.txt

Kustomization pre SOPS dešifrovanie:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: production-secrets
  namespace: flux-system
spec:
  interval: 10m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./clusters/production/secrets
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age   # secret s age kľúčom

Flux pri reconciliácii automaticky dešifruje všetky SOPS-šifrované súbory v danej ceste pomocou age kľúča a aplikuje ich do klastra.

CI/CD dešifrovanie

V GitHub Actions:

# .github/workflows/deploy.yaml
name: Deploy
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.9.4/sops-v3.9.4.linux.amd64
          chmod +x sops-v3.9.4.linux.amd64
          mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops

      - name: Configure age key
        env:
          SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
        run: |
          mkdir -p ~/.config/sops/age
          echo "$SOPS_AGE_KEY" > ~/.config/sops/age/keys.txt

      - name: Decrypt and apply secrets
        run: |
          sops --decrypt secrets/production/db.yaml | kubectl apply -f -
          # Alebo pomocou helm-secrets:
          # helm secrets upgrade myapp ./charts/myapp -f secrets/production/myapp.secrets.yaml

Sealed Secrets — hlboký ponor

Inštalácia — 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 \
  --version 2.16.2 \
  --set fullnameOverride=sealed-secrets-controller

Overenie:

kubectl get pods -n kube-system -l app.kubernetes.io/name=sealed-secrets
# sealed-secrets-controller-xxxxxxxxx-xxxxx   1/1   Running

kubectl get crd sealedsecrets.bitnami.com

kubeseal CLI

# Inštalácia kubeseal
curl -LO https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.3/kubeseal-0.27.3-linux-amd64.tar.gz
tar xzf kubeseal-0.27.3-linux-amd64.tar.gz kubeseal
mv kubeseal /usr/local/bin/

Vytvorenie SealedSecret

Workflow: vytvoríte bežný Secret, zapečatíte ho pomocou kubeseal, commitnete SealedSecret do Gitu.

# Krok 1 — vytvorenie plain Secret (NIKDY necommitovať!)
kubectl create secret generic db-credentials \
  --namespace production \
  --from-literal=username=myapp \
  --from-literal=password=sup3rS3cretP4ssw0rd! \
  --dry-run=client \
  -o yaml > /tmp/db-secret.yaml

# Krok 2 — zapečatenie
kubeseal \
  --controller-name=sealed-secrets-controller \
  --controller-namespace=kube-system \
  --format yaml \
  < /tmp/db-secret.yaml \
  > sealed-secrets/production/db-credentials.yaml

# Zmazanie plain textu
rm /tmp/db-secret.yaml

# Krok 3 — commit SealedSecret do Gitu (bezpečné!)
git add sealed-secrets/production/db-credentials.yaml
git commit -m "feat: add db-credentials sealed secret for production"

Výsledný SealedSecret YAML:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
  namespace: production
  annotations:
    sealedsecrets.bitnami.com/cluster-wide: "false"
spec:
  encryptedData:
    username: AgBy8hCpN3cU...zLk9wQ==
    password: AgCB3fDe4...xMmN8==
  template:
    metadata:
      name: db-credentials
      namespace: production
    type: Opaque

Operátor v klastri deteguje nový SealedSecret, rozbalí ho pomocou súkromného kľúča a vytvorí natívny Secret s rovnakým menom v rovnakom namespace.

Viazanosť na namespace a meno

SealedSecret je predvolene viazaný na konkrétny namespace a meno. Ak skopírujete SealedSecret do iného namespace, operátor ho odmietne dešifrovať.

Existujú tri režimy:

# Strict (default) — viazaný na namespace aj meno
kubeseal --scope strict ...

# Namespace-wide — viazaný len na namespace (meno môže byť rôzne)
kubeseal --scope namespace-wide ...

# Cluster-wide — dešifrovateľný kdekoľvek v klastri
kubeseal --scope cluster-wide ...

Odporúčanie: používajte strict pre produkciu. cluster-wide je vhodný len pre shared infrastructure secrets (napr. imagePullSecret pre registry).

Záloha master kľúča — kritické!

Sealed Secrets operátor generuje asymetrický kľúčový pár pri prvom spustení a ukladá ho ako K8s Secret v namespace kube-system. Ak stratíte tento kľúč, nie je možné dešifrovať žiadne SealedSecrets.

# Záloha master kľúča — uložte na bezpečné miesto (nie do Gitu!)
kubectl get secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > sealed-secrets-master-key-backup.yaml

# Obnova kľúča v novom klastri (pred inštaláciou operátora!)
kubectl apply -f sealed-secrets-master-key-backup.yaml
# Potom inštalovať operátor — bude používať existujúci kľúč

Zálohu master kľúča uložte do externého trezoru (Vault, LastPass, 1Password, šifrovaný USB), nie do rovnakého Gitu, kde máte SealedSecrets.

Rotácia cluster kľúčov

Operátor automaticky generuje nové kľúčové páry každých 30 dní (predvolené). Starý kľúč ostáva aktívny pre dešifrovanie existujúcich SealedSecrets. Nové SealedSecrets sú šifrované najnovším kľúčom.

# Prezrite si všetky kľúče
kubectl get secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key

# Manuálne zapečatenie všetkých SealedSecrets novým kľúčom (po rotácii)
# Odporúčam automatizovať v CI ako periodickom job-e
kubeseal --re-encrypt < old-sealed-secret.yaml > new-sealed-secret.yaml

Integrácia s GitHub Actions — automatické zapečatenie

# .github/workflows/seal-secrets.yaml
name: Seal Secrets
on:
  workflow_dispatch:
    inputs:
      secret_name:
        description: 'Secret name'
        required: true
      namespace:
        description: 'Target namespace'
        required: true
        default: 'production'

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

      - name: Configure kubectl
        uses: azure/k8s-set-context@v4
        with:
          kubeconfig: ${{ secrets.KUBECONFIG }}

      - name: Install kubeseal
        run: |
          curl -LO https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.3/kubeseal-0.27.3-linux-amd64.tar.gz
          tar xzf kubeseal-0.27.3-linux-amd64.tar.gz kubeseal
          mv kubeseal /usr/local/bin/

      - name: Seal secret from GitHub Secret
        env:
          SECRET_VALUE: ${{ secrets.DB_PASSWORD }}
        run: |
          kubectl create secret generic ${{ inputs.secret_name }} \
            --namespace ${{ inputs.namespace }} \
            --from-literal=password="$SECRET_VALUE" \
            --dry-run=client -o yaml \
          | kubeseal \
            --controller-name=sealed-secrets-controller \
            --controller-namespace=kube-system \
            --format yaml \
          > sealed-secrets/${{ inputs.namespace }}/${{ inputs.secret_name }}.yaml

      - name: Commit sealed secret
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add sealed-secrets/
          git commit -m "chore: seal ${{ inputs.secret_name }} for ${{ inputs.namespace }}"
          git push

External Secrets Operator — hlboký ponor

Inštalácia — Helm

# helm/eso-values.yaml
replicaCount: 2

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

serviceMonitor:
  enabled: true   # Prometheus monitoring

webhook:
  replicaCount: 2

certController:
  replicaCount: 2

# RBAC — ESO potrebuje rozsiahle oprávnenia
rbac:
  create: true

# Pod disruption budget pre HA
podDisruptionBudget:
  enabled: true
  minAvailable: 1
helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace \
  --version 0.12.1 \
  -f helm/eso-values.yaml

SecretStore — konfigurácia backendu

SecretStore je namespace-scoped CRD definujúci prístup k externému secret store. ClusterSecretStore je cluster-scoped varianta dostupná pre všetky namespacy.

SecretStore pre HashiCorp Vault s JWT auth (Kubernetes OIDC):

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"          # KV mount path
      version: "v2"           # KV v2
      namespace: "production" # Vault namespace (Enterprise feature)
      caBundle: |             # Vault CA cert (base64)
        LS0tLS1CRUdJTi...
      auth:
        jwt:
          path: "jwt"         # Vault auth method mount path
          role: "production-reader"
          kubernetesServiceAccountToken:
            serviceAccountRef:
              name: eso-vault-reader
            audiences:
              - "https://vault.internal"
            expirationSeconds: 600

Potrebný ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: eso-vault-reader
  namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: eso-vault-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["serviceaccounts/token"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: eso-vault-reader
  namespace: production
subjects:
  - kind: ServiceAccount
    name: external-secrets
    namespace: external-secrets
roleRef:
  kind: Role
  name: eso-vault-reader
  apiGroup: rbac.authorization.k8s.io

ClusterSecretStore — pre zdieľané infrastructure secrets:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: vault-cluster-backend
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        appRole:
          path: "approle"
          roleId: "production-cluster-role-id"
          secretRef:
            name: vault-approle-secret
            namespace: external-secrets
            key: roleSecretId

ExternalSecret — mapovanie hodnôt s templatingom

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 1h       # Ako často synchronizovať
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: db-credentials    # Názov výsledného K8s Secret
    creationPolicy: Owner   # ESO vlastní Secret (zmaže ho ak ExternalSecret zmizne)
    deletionPolicy: Retain  # Ale pri mazaní ExternalSecret Secret zostane
    template:               # Templating výsledného Secretu
      type: Opaque
      metadata:
        labels:
          app: myapp
          managed-by: external-secrets
      data:
        # Kombinácia viacerých hodnôt do jedného stringu
        DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@{{ .host }}:{{ .port }}/{{ .database }}?sslmode=require"
        # Priame mapovanie
        DB_HOST: "{{ .host }}"
        DB_PORT: "{{ .port }}"
  data:
    - secretKey: username      # Kľúč v K8s Secretu (pred templatingom)
      remoteRef:
        key: production/db     # Vault path
        property: username     # Vault key v rámci secret
    - secretKey: password
      remoteRef:
        key: production/db
        property: password
    - secretKey: host
      remoteRef:
        key: production/db
        property: host
    - secretKey: port
      remoteRef:
        key: production/db
        property: port
    - secretKey: database
      remoteRef:
        key: production/db
        property: database
  # Alternatíva — načítanie celého Vault secret-u naraz
  # dataFrom:
  #   - extract:
  #       key: production/db

Generátory — dynamické credentials

ESO podporuje aj generátory, ktoré vytvárajú secrety dynamicky. Najpraktickejší je Vault Dynamic Secrets cez generator:

apiVersion: generators.external-secrets.io/v1alpha1
kind: VaultDynamicSecret
metadata:
  name: mysql-dynamic-creds
  namespace: production
spec:
  path: "database/creds/myapp-role"   # Vault dynamic DB path
  method: GET
  provider:
    server: "https://vault.internal:8200"
    auth:
      jwt:
        path: "jwt"
        role: "production-reader"
        kubernetesServiceAccountToken:
          serviceAccountRef:
            name: eso-vault-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: mysql-dynamic-credentials
  namespace: production
spec:
  refreshInterval: 15m    # Lease TTL musí byť dlhší
  target:
    name: mysql-dynamic-credentials
  dataFrom:
    - sourceRef:
        generatorRef:
          apiVersion: generators.external-secrets.io/v1alpha1
          kind: VaultDynamicSecret
          name: mysql-dynamic-creds

Výsledok: ESO každých 15 minút požiada Vault o nové MySQL credentials (dočasný user s obmedzeným TTL), ktoré uloží do K8s Secret. Aplikácia vždy dostane čerstvé credentials s automatickou expiráciou.


Porovnávacia tabuľka

Kritérium SOPS Sealed Secrets External Secrets Operator
Zrelosť ✅ Vysoká (Mozilla, 8+ rokov) ✅ Vysoká (Bitnami/CNCF) ✅ Vysoká (CNCF Sandbox→Incubating)
Secret v Gite ✅ Áno (šifrovaný) ✅ Áno (zapečatený) ❌ Nie
Externý systém Voliteľné (KMS) ❌ Nie ✅ Povinné
Setup komplexita ⚠️ Stredná (key management) ✅ Nízka ⚠️ Vysoká (závisí na backend)
Key rotation Manuálna (sops updatekeys) ✅ Automatická (30d) ✅ Backend sa stará
Multi-cluster ⚠️ Rôzne kľúče per cluster ❌ Zložité (každý klaster má iný kľúč) ✅ Natívne (ClusterSecretStore)
Audit trail ❌ Len Git history ❌ Len Git history ✅ Vault/cloud provider audit log
Air-gap / offline ✅ Plná podpora ✅ Plná podpora ❌ Vyžaduje konektivitu
GitOps friendliness ✅ Natívne (Flux SOPS) ✅ CRD v Gite ✅ CRD v Gite (bez secretov)
Dynamic secrets ❌ Nie ❌ Nie ✅ Vault generators
Enterprise podpora Komunita Komunita ✅ External Secrets Inc.
Závislosť na K8s ❌ Nie (CLI tool) ✅ Vyžaduje operator ✅ Vyžaduje operator
Vendor lock-in ⚠️ KMS provider ❌ Žiadny ⚠️ Secret backend

Typické workflows

SOPS + FluxCD v produkcii

Najčistejší GitOps prístup pre tímy bez existujúceho secret managementu:

Developer workflow:
1. sops edit secrets/production/myapp.yaml   ← editor otvorí dešifrovaný súbor
2. Uloženie → SOPS automaticky zašifruje
3. git add + git commit + git push
4. Flux deteguje zmenu v Git, dešifruje secret pomocou age kľúča v klastri
5. K8s Secret je aktualizovaný → pod reštart (ak je nakonfigurovaný)

CI pipeline workflow:
1. GitHub Actions checkout
2. Nastavenie age kľúča z GitHub Secret
3. sops --decrypt | kubectl apply -f -
   alebo
   helm secrets upgrade ...

Výhody: minimálne externé závislosti, full offline support, jednoduché na pochopenie.

ESO + HashiCorp Vault v enterprise

Pre organizácie s existujúcim Vault:

Vault admin workflow:
1. vault kv put secret/production/myapp password=... username=...
2. Vault policy + JWT role pre K8s namespace

Platform engineer workflow:
1. ClusterSecretStore (raz pre cluster) → v Git ako CRD
2. SecretStore per namespace → v Git ako CRD
3. ExternalSecret per aplikácia → v Git ako CRD

Výsledok:
- V Gite: nula secretov, iba konfigurácia
- V Vault: všetky secrety s audit logom
- V K8s: automaticky synchronizované Secrets
- Rotácia: zmena v Vault → ESO synchronizuje do 1h (alebo podľa refreshInterval)

Sealed Secrets pre malé klastre

Pre jednoduchosť bez externých závislostí:

1. helm install sealed-secrets (5 minút)
2. kubeseal < plain-secret.yaml > sealed-secret.yaml
3. git add sealed-secret.yaml && git commit
4. kubectl apply -f sealed-secret.yaml (alebo ArgoCD/Flux)
5. Operátor vytvorí Secret

Vhodné pre:
- Jeden klaster (dev/staging/prod v jednom klastri)
- Malý tím (1-5 ľudí)
- Bez existujúceho Vault
- Nechcete spravovať age kľúče alebo externý systém

Integračné príklady

SOPS + age + FluxCD — kompletný setup

# 1. Generovanie age kľúča pre cluster (jednorazové)
age-keygen -o /tmp/cluster-age-key.txt
# Verejný kľúč uložte do .sops.yaml
# Súkromný kľúč uložte do K8s secret pre Flux

# 2. Secret pre Flux
kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=/tmp/cluster-age-key.txt
rm /tmp/cluster-age-key.txt

# 3. .sops.yaml v koreni repozitára
cat > .sops.yaml << 'EOF'
creation_rules:
  - path_regex: .*/secrets/.*\.yaml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
EOF

# 4. Vytvorenie a šifrovanie secretu
cat > secrets/production/app-secret.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: production
stringData:
  API_KEY: "my-super-secret-key"
  DB_PASSWORD: "another-secret"
EOF

sops --encrypt --in-place secrets/production/app-secret.yaml

# 5. Flux Kustomization s SOPS
cat > clusters/production/flux-kustomization.yaml << 'EOF'
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: production
  namespace: flux-system
spec:
  interval: 10m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./secrets/production
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age
EOF

git add .sops.yaml secrets/ clusters/
git commit -m "feat: configure SOPS+age encryption for production secrets"
git push

ESO + HashiCorp Vault s JWT auth — kompletný setup

# Strana Vault (konfigurácia JWT auth pre Kubernetes)

# 1. Aktivácia JWT auth v Vault
vault auth enable jwt

# 2. Konfigurácia JWT auth — OIDC discovery z K8s API servera
vault write auth/jwt/config \
  oidc_discovery_url="https://kubernetes.default.svc.cluster.local" \
  oidc_discovery_ca_pem="$(kubectl get configmap -n kube-system kube-root-ca.crt -o jsonpath='{.data.ca\.crt}')"

# 3. Vault policy pre čítanie production secrets
vault policy write production-reader - << 'EOF'
path "secret/data/production/*" {
  capabilities = ["read", "list"]
}
path "secret/metadata/production/*" {
  capabilities = ["read", "list"]
}
EOF

# 4. JWT role viazaná na K8s namespace a ServiceAccount
vault write auth/jwt/role/production-reader \
  role_type="jwt" \
  bound_audiences="https://vault.internal" \
  bound_subject="system:serviceaccount:production:eso-vault-reader" \
  user_claim="sub" \
  policies="production-reader" \
  ttl="1h"

# 5. Uloženie secretu do Vault
vault kv put secret/production/myapp \
  api_key="super-secret-api-key" \
  db_password="db-password-here"
# K8s strana — SecretStore + ExternalSecret

# secretstore-production.yaml
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: vault-production
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        jwt:
          path: "jwt"
          role: "production-reader"
          kubernetesServiceAccountToken:
            serviceAccountRef:
              name: eso-vault-reader
            audiences:
              - "https://vault.internal"
            expirationSeconds: 600
---
# externalsecret-myapp.yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: myapp-secrets
  namespace: production
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: vault-production
    kind: SecretStore
  target:
    name: myapp-secrets
    creationPolicy: Owner
    template:
      type: Opaque
      data:
        API_KEY: "{{ .api_key }}"
        DATABASE_URL: "postgresql://myapp:{{ .db_password }}@postgres.internal:5432/myapp_prod"
  dataFrom:
    - extract:
        key: production/myapp

Sealed Secrets + GitHub Actions — automatické zapečatenie

# .github/workflows/seal-and-deploy.yaml
name: Seal Secret and Deploy
on:
  workflow_dispatch:
    inputs:
      secret_name:
        description: 'Meno Secretu'
        required: true
      namespace:
        description: 'Namespace'
        required: true
        default: 'production'
      secret_key:
        description: 'Kľúč v Secretu'
        required: true
      secret_value_gh_secret:
        description: 'Názov GitHub Secret obsahujúceho hodnotu'
        required: true

jobs:
  seal-and-commit:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Configure kubeconfig
        run: echo "${{ secrets.KUBECONFIG }}" | base64 -d > /tmp/kubeconfig.yaml

      - name: Install kubeseal
        run: |
          curl -LO https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.3/kubeseal-0.27.3-linux-amd64.tar.gz
          tar xzf kubeseal-0.27.3-linux-amd64.tar.gz kubeseal
          mv kubeseal /usr/local/bin/

      - name: Seal and commit
        env:
          KUBECONFIG: /tmp/kubeconfig.yaml
          SECRET_VAL: ${{ secrets[inputs.secret_value_gh_secret] }}
        run: |
          # Vytvorenie plain Secret (len do pamäte, nikdy na disk)
          kubectl create secret generic "${{ inputs.secret_name }}" \
            --namespace "${{ inputs.namespace }}" \
            --from-literal="${{ inputs.secret_key }}=${SECRET_VAL}" \
            --dry-run=client -o yaml \
          | kubeseal \
            --controller-name=sealed-secrets-controller \
            --controller-namespace=kube-system \
            --format yaml \
          > "sealed-secrets/${{ inputs.namespace }}/${{ inputs.secret_name }}.yaml"

          git config user.name "GitHub Actions Bot"
          git config user.email "bot@github.com"
          git add "sealed-secrets/${{ inputs.namespace }}/${{ inputs.secret_name }}.yaml"
          git commit -m "chore: seal secret ${{ inputs.secret_name }} for ${{ inputs.namespace }}"
          git push

Rotačné stratégie

SOPS key rotation

Rotácia age kľúča v SOPS je manuálny proces — všetky šifrované súbory musia byť re-zašifrované novým kľúčom:

# 1. Generovanie nového age kľúča
age-keygen -o /tmp/new-age-key.txt
NEW_PUBLIC_KEY=$(grep "public key" /tmp/new-age-key.txt | awk '{print $NF}')

# 2. Aktualizácia .sops.yaml s novým kľúčom (pridajte nový, ponechajte starý počas rotácie)
# Editujte .sops.yaml a pridajte nový public key ku každému pravidlu

# 3. Re-šifrovanie všetkých súborov s oboma kľúčmi
find . -name "*.yaml" -path "*/secrets/*" | while read f; do
  sops updatekeys "$f" --yes
done

# 4. Commitnutie re-zašifrovaných súborov
git add -A
git commit -m "chore: rotate SOPS age key — re-encrypt all secrets"

# 5. Aktualizácia kľúča v Flux (K8s secret)
kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=/tmp/new-age-key.txt \
  --dry-run=client -o yaml | kubectl apply -f -

# 6. Uloženie nového kľúča na bezpečné miesto, zmazanie starého

# 7. Po overení — odstráňte starý kľúč z .sops.yaml a re-šifrujte znovu
find . -name "*.yaml" -path "*/secrets/*" | while read f; do
  sops updatekeys "$f" --yes
done
git add -A
git commit -m "chore: remove old SOPS key after rotation"

Sealed Secrets master key rotation

Operátor automaticky generuje nový kľúč každých 30 dní. Staré kľúče zostávajú aktívne pre dešifrovanie. Môžete zmeniť interval:

# Zmena rotačného intervalu (Helm values)
helm upgrade sealed-secrets sealed-secrets/sealed-secrets \
  --namespace kube-system \
  --set keyRenewPeriod=720h   # 30 dní (default)
  # alebo: 168h = 7 dní pre vyšší bezpečnostný level

# Manuálna rotácia kľúča (force regenerovanie mimo schedule-u)
# Ozajstná rotácia sa spúšťa reštartom controllera alebo vymazaním
# AKTÍVNEHO kľúč-secretu — controller automaticky vygeneruje nový:
kubectl delete secret -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key=active
# Controller zdetekuje chýbajúci aktívny kľúč a v ďalšom reconcile
# cycli vytvorí nový (staré kľúče zostávajú pre decrypt existujúcich SealedSecrets)
kubectl rollout restart -n kube-system deploy/sealed-secrets-controller

# Záloha NOVÉHO kľúča po rotácii
kubectl get secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  --sort-by=.metadata.creationTimestamp \
  -o yaml | tail -1 > new-master-key-backup.yaml

Po rotácii master kľúča odporúčam re-zapečatiť všetky SealedSecrets novým kľúčom (aj keď to nie je povinné):

# Re-zapečatenie všetkých SealedSecrets
for f in $(find . -name "*.yaml" -path "*/sealed-secrets/*"); do
  kubeseal --re-encrypt < "$f" > "${f}.new"
  mv "${f}.new" "$f"
done
git add -A && git commit -m "chore: re-seal all secrets with new cluster key"

ESO refresh intervals a rotácia

ESO synchronizuje secrety podľa refreshInterval. Pre citlivé secrety znížte interval, pre stabilné môžete zvýšiť:

spec:
  refreshInterval: 1h    # Štandard — každú hodinu
  # refreshInterval: 5m  # Pre dynamické DB credentials
  # refreshInterval: 24h # Pre dlhodobé API kľúče

Manuálna vynútená synchronizácia (bez čakania na interval):

# Anotácia spustí okamžitú synchronizáciu
kubectl annotate externalsecret db-credentials \
  --namespace production \
  force-sync=$(date +%s) \
  --overwrite

Backup a recovery

SOPS — čo keď stratíte age kľúč?

Ak stratíte súkromný age kľúč, nie je možné dešifrovať žiadne súbory šifrované pre daný kľúč. Toto je definitívna strata.

Prevencia:

  1. Viacero príjemcov v .sops.yaml — každý súbor je šifrovaný pre viacerých príjemcov (developer + CI kľúč). Strata jedného kľúča neznamená stratu dát.
  2. Záloha súkromného kľúča v externom trezore (Vault, 1Password, šifrovaný USB).
  3. KMS ako záloha — pridajte AWS KMS alebo Azure Key Vault ako ďalší príjemca. KMS nikdy „nestratíte".
# .sops.yaml — odporúčaná produkčná konfigurácia s viacerými príjemcami
creation_rules:
  - path_regex: secrets/production/.*\.yaml$
    age: >-
      age1developer1...,
      age1developer2...,
      age1ci_key...
    kms: arn:aws:kms:eu-west-1:123456789:key/mrk-abc123  # záloha

Sealed Secrets — strata master kľúča

Strata master kľúča operátora Sealed Secrets = nemožnosť dešifrovať žiadne SealedSecrets v klastri. Všetky secrety treba znovu vytvoriť a zapečatiť.

Recovery postup:

  1. Nový klaster (alebo reinstalovaný operátor) generuje nový kľúčový pár.
  2. Všetky SealedSecrets v Gite sú nepoužiteľné — treba re-zapečatiť.
  3. Originálne plain-text hodnoty musíte získať z iného zdroja (password manager, tím).

Preto je záloha master kľúča absolútne kritická:

# Pravidelná záloha master kľúča (cron job alebo CI)
#!/bin/bash
BACKUP_DATE=$(date +%Y%m%d)
kubectl get secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > "/secure-backup/sealed-secrets-master-key-${BACKUP_DATE}.yaml"

# Upload do šifrovaného S3 (Vault, 1Password atď.)
aws s3 cp \
  "/secure-backup/sealed-secrets-master-key-${BACKUP_DATE}.yaml" \
  "s3://secure-backups/sealed-secrets/" \
  --sse aws:kms

ESO — záloha konfigurácie

ESO samotné neukladá secrety — tie sú v externom systéme. Backup stratégia pre ESO:

  1. CRD konfigurácia (SecretStore, ExternalSecret) je v Gite — automaticky zálohovaná.
  2. Externý backend (Vault, AWS SM) má vlastnú backup stratégiu — tú treba riešiť na úrovni backendu.
  3. Vault snapshot:
# Záloha Vault
vault operator raft snapshot save /backup/vault-$(date +%Y%m%d).snap

# Obnova
vault operator raft snapshot restore /backup/vault-20260417.snap

Multi-tenancy

SOPS — per-namespace kľúče

Pre multi-tenant prostredie môžete definovať rôzne age kľúče per namespace/tím:

# .sops.yaml — multi-tenant
creation_rules:
  - path_regex: tenants/team-a/secrets/.*\.yaml$
    age: age1team_a_developer..., age1ci...

  - path_regex: tenants/team-b/secrets/.*\.yaml$
    age: age1team_b_developer..., age1ci...

  - path_regex: shared/secrets/.*\.yaml$
    age: age1platform_team..., age1ci...

Team A nemôže dešifrovať secrety Team B — každý má iný kľúč.

ESO — SecretStore vs ClusterSecretStore

ESO má elegantné riešenie multi-tenancy cez hierarchiu SecretStore:

# ClusterSecretStore — pre platform team (cluster-wide)
# Všetky namespacy môžu využívať tento store
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: vault-cluster
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        appRole:
          path: "approle"
          roleId: "cluster-reader"
          secretRef:
            name: vault-approle
            namespace: external-secrets
            key: roleSecretId
  # Obmedzenie prístupu len na špecifické Vault paths
  conditions:
    - namespaceSelector:
        matchLabels:
          tenant: "allowed"
# SecretStore — per-tenant (namespace-scoped)
# Team A má prístup len k ich Vault path
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: vault-team-a
  namespace: team-a
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        jwt:
          path: "jwt"
          role: "team-a-reader"   # Vault role s prístupom len na tenant/team-a/*
          kubernetesServiceAccountToken:
            serviceAccountRef:
              name: eso-team-a

Sealed Secrets multi-tenancy

Sealed Secrets má obmedzenejšiu multi-tenancy — operátor je jeden pre celý klaster a má prístup ku všetkým SealedSecrets. Tenancy je zabezpečená iba K8s RBAC (kto môže čítať SealedSecret CRD). Pre striktné izolácie použite radšej SOPS alebo ESO.


Dynamic secrets — pokročilý ESO usecase

Jednou z najsilnejších funkcií ESO v kombinácii s Vault je generovanie dynamických credentials — dočasných prihlasovacích údajov s automatickou expiráciou.

Vault Dynamic DB credentials

Namiesto statického MySQL hesla Vault vygeneruje per-aplikačný user s obmedzeným TTL:

# Konfigurácia Vault Database Secret Engine
vault secrets enable database

vault write database/config/myapp-mysql \
  plugin_name=mysql-database-plugin \
  connection_url="{{username}}:{{password}}@tcp(mysql.internal:3306)/" \
  allowed_roles="myapp-role" \
  username="vault-root" \
  password="vault-root-password"

vault write database/roles/myapp-role \
  db_name=myapp-mysql \
  creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO '{{name}}'@'%';" \
  default_ttl="1h" \
  max_ttl="24h"

ESO ExternalSecret s dynamickými credentials:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: mysql-dynamic
  namespace: production
spec:
  refreshInterval: 45m    # < TTL (1h)
  secretStoreRef:
    name: vault-production
    kind: SecretStore
  target:
    name: mysql-dynamic-credentials
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: database/creds/myapp-role
        property: username
    - secretKey: password
      remoteRef:
        key: database/creds/myapp-role
        property: password

Každých 45 minút ESO vytvorí nové MySQL credentials. Starý user automaticky expiruje po 1 hodine. Ak útočník získa credentials z K8s Secret, do hodiny budú neplatné.

AWS ECR a Azure ACR tokeny cez ESO generators

ESO podporuje short-lived registry tokeny pre pull imageov cez dedikované generators:

# AWS ECR — authorization token (platný ~12h)
apiVersion: generators.external-secrets.io/v1alpha1
kind: ECRAuthorizationToken
metadata:
  name: ecr-token
  namespace: production
spec:
  region: eu-west-1
  auth:
    jwt:
      serviceAccountRef:
        name: myapp-sa   # SA s IRSA anotáciou
---
# Azure Container Registry — AD token
apiVersion: generators.external-secrets.io/v1alpha1
kind: ACRAccessToken
metadata:
  name: acr-token
  namespace: production
spec:
  tenantId: "00000000-0000-0000-0000-000000000000"
  registry: "myregistry.azurecr.io"
  auth:
    workloadIdentity:
      serviceAccountRef:
        name: myapp-wi-sa

Tieto generators sa používajú cez ExternalSecret.spec.dataFrom.sourceRef.generatorRef — výsledný Secret obsahuje dockerconfigjson typu, ktorý možno odkazovať cez imagePullSecrets na pod-ochoch.


Decision tree — ktorý nástroj zvoliť?

Máte existujúci secret management systém (Vault, AWS SM, Azure KV)?
├── ÁNO → External Secrets Operator
│         Výhody: centrálny audit, dynamické credentials, bez secretov v Gite
│         Nevýhody: závislosť na externom systéme, komplexnejší setup
│
└── NIE
    │
    Potrebujete air-gap / offline support?
    ├── ÁNO → SOPS + age
    │         Výhody: jednoduché, offline, flexibilné, Flux natívne
    │         Nevýhody: key management, re-encryption pri rotácii
    │
    └── NIE
        │
        Je to jeden klaster bez multi-cluster požiadaviek?
        ├── ÁNO → Sealed Secrets
        │         Výhody: najjednoduchší setup, žiadne externé závislosti
        │         Nevýhody: master key záloha, zložité pri multi-cluster
        │
        └── NIE → SOPS + age (alebo zvážte investíciu do Vault + ESO)

Špeciálne scenáre:
- FluxCD v klastri → SOPS (natívna integrácia)
- ArgoCD v klastri → ESO alebo Sealed Secrets (ArgoCD nemá natívne SOPS)
- Compliance (SOC2, ISO27001) → ESO + Vault (audit trail)
- Malý tím, rýchly štart → Sealed Secrets
- Multi-tenant klaster → ESO alebo SOPS per-namespace kľúče

Anti-patterns — čomu sa vyhnúť

1. Commit .env súborov do Gitu

# NIKDY
echo "DB_PASSWORD=secret" >> .env
git add .env
git commit -m "add env file"

# Správne
echo ".env" >> .gitignore
# A použite SOPS, Sealed Secrets alebo ESO

Ak sa .env dostalo do Gitu, je potrebná okamžitá rotácia všetkých hodnôt a git filter-repo na vymazanie z histórie — a aj potom treba predpokladať, že credentials boli kompromitované.

2. Žiadna rotácia kľúčov

SOPS age kľúč bez dátumu expirácie a bez rotácie = jeden leaked kľúč a celá história secretov je kompromitovaná. Nastavte si rotáciu aspoň raz za 6 mesiacov.

# Monitorovanie veku SOPS kľúčov (pre age nemá built-in expiry — musíte sami)
# Uložte si dátum generovania kľúča a nastavte calendar reminder

3. Žiadna záloha master kľúča (Sealed Secrets)

# Pred každým upgrade Sealed Secrets operátora:
kubectl get secret -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > /secure-place/sealed-secrets-key-$(date +%Y%m%d).yaml

Toto je jedna z najčastejších príčin katastrofálnej straty dát pri Sealed Secrets.

4. SecretStore s príliš širokým RBAC

# ZLÉ — AppRole má prístup ku všetkým secretom
vault write auth/approle/role/myapp \
  policies="read-all-secrets"

# SPRÁVNE — minimálne oprávnenia
vault write auth/approle/role/myapp \
  policies="read-myapp-secrets-only"
  # kde policy: path "secret/data/myapp/*" { capabilities = ["read"] }

ESO ServiceAccount by mal mať prístup iba k secretom svojej aplikácie — nie ku ClusterSecretStore ak nepotrebuje cluster-wide prístup.

5. RefreshInterval príliš dlhý pre dynamické secrets

# ZLÉ — refresh po expiracii credentials
spec:
  refreshInterval: 2h
  # Vault DB credentials TTL: 1h → credentials expirujú pred refreshom!

# SPRÁVNE — refresh pred expiraciou
spec:
  refreshInterval: 45m
  # Vault DB credentials TTL: 1h → credentials sú obnovené 15 min pred expiraciou

6. Ignorovanie SealedSecret namespace bindingu

# ZLÉ — cluster-wide scope pre všetky secrety
kubeseal --scope cluster-wide ...

# Toto umožňuje použiť SealedSecret v akomkoľvek namespace
# → porušenie princípu least privilege

# SPRÁVNE — strict scope (default)
kubeseal --scope strict ...
# alebo explicitne:
kubeseal --scope namespace-wide ...  # ak potrebujete flexibilitu v rámci namespace

Záver

Neexistuje jedno správne riešenie pre secret management v Kubernetes — závisí od vašej organizácie, existujúcich nástrojov a bezpečnostných požiadaviek.

SOPS je najflexibilnejší a Flux-friendly prístup pre tímy, ktoré chcú secrety v Gite ale šifrované. Vyžaduje disciplínu pri správe kľúčov.

Sealed Secrets je najrýchlejší na setup — ideálny pre malé tímy a jednoduché prostredia. Limitácia je viazanosť na jeden klaster a nutnosť zálohovať master kľúč.

External Secrets Operator je správna voľba pre enterprise prostredia s existujúcim Vault alebo cloud secret managerom. Poskytuje centrálny audit trail, dynamické credentials a elegantné multi-tenancy. Vyžaduje viac infraštruktúry.

Jedno pravidlo platí pre všetky tri: nikdy plain Secret YAML do Gitu. Zvyšok je kompromis medzi komplexitou, bezpečnosťou a prevádzkovými nákladmi.


Ďalšie čítanie