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==
Tá 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:
- RBAC obmedzení prístupu ku
kubectl get secret. - Šifrovaní etcd at rest (v managed K8s je to väčšinou zapnuté automaticky).
- 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:
- 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. - Záloha súkromného kľúča v externom trezore (Vault, 1Password, šifrovaný USB).
- 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:
- Nový klaster (alebo reinstalovaný operátor) generuje nový kľúčový pár.
- Všetky SealedSecrets v Gite sú nepoužiteľné — treba re-zapečatiť.
- 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:
- CRD konfigurácia (
SecretStore,ExternalSecret) je v Gite — automaticky zálohovaná. - Externý backend (Vault, AWS SM) má vlastnú backup stratégiu — tú treba riešiť na úrovni backendu.
- 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.