Kubernetes secrets: External Secrets Operator vs Sealed Secrets

Coding.nl
8 min leestijd
Inhoud van dit artikel
  1. Waarom een gewoon Secret niet genoeg is
  2. Sealed Secrets: versleuteld in Git
  3. External Secrets Operator: de bron staat buiten het cluster
  4. Hele bestanden opbouwen met templates
  5. Rotatie: wat ESO wel en niet doet
  6. De vergelijking
  7. Wanneer wat
  8. Inpassen in GitOps met Argo CD
  9. Overige maatregelen die je niet overslaat

Een Kubernetes Secret is standaard alleen base64-gecodeerd, niet versleuteld, en hoort dus niet in Git. De twee gangbare oplossingen zijn External Secrets Operator (ESO), dat secrets ophaalt uit een externe secret manager, en Sealed Secrets, dat versleutelde secrets in Git zet die alleen je cluster kan ontsleutelen. Voor de meeste webshops en Laravel-platformen met meer dan één omgeving kiezen wij ESO; Sealed Secrets is prima voor één klein cluster zonder externe secret store.

Waarom een gewoon Secret niet genoeg is

Wat veel teams verrast: dit is geen versleuteling.

bash
kubectl get secret shop-db -n shop -o jsonpath='{.data.password}' | base64 -d

Iedereen met leesrechten op Secrets in die namespace ziet het wachtwoord. Wie het YAML-bestand in Git zet, publiceert het wachtwoord aan iedereen met toegang tot de repository, inclusief de historie.

Voor een webshop gaat het om gevoelige waarden:

  • Databasewachtwoorden voor MySQL of MariaDB.
  • De Magento-encryptiesleutel (crypt/key) waarmee onder meer betaalgegevens en integratietokens versleuteld zijn.
  • Laravel APP_KEY.
  • API-sleutels van betaalproviders, ERP-koppelingen, mailproviders en zoekdiensten.
  • Credentials voor object storage waar pub/media of Laravel-uploads staan.

Met GitOps, waarbij Argo CD je cluster gelijk houdt aan een Git-repository, wil je alles in Git. Behalve de geheimen zelf. Daar zijn de twee tools voor.

Sealed Secrets: versleuteld in Git

Sealed Secrets (bitnami-labs/sealed-secrets) werkt met asymmetrische encryptie. Een controller in je cluster heeft een private key. Jij versleutelt lokaal met de publieke sleutel via kubeseal. Het resultaat, een SealedSecret, mag in Git. Alleen de controller kan het terugzetten naar een gewoon Secret.

bash
kubectl create secret generic shop-db -n shop \
  --from-literal=password='hier-het-echte-wachtwoord' \
  --dry-run=client -o yaml \
  | kubeseal --format yaml > k8s/shop/shop-db.sealed.yaml

Het resultaat ziet er zo uit:

yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: shop-db
  namespace: shop
spec:
  encryptedData:
    password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...

Sterke punten:

  • Geen externe dienst nodig. Alles zit in je cluster en je Git-repo.
  • Eenvoudig te begrijpen en snel op te zetten.
  • Een versleuteld secret is gebonden aan naam en namespace. Kopiëren naar een andere namespace werkt niet zonder herversleutelen.

Zwakke punten in de praktijk:

  • De private key is je single point of failure. Ben je het cluster kwijt zonder backup van de sleutels, dan kun je je SealedSecrets niet meer ontsleutelen. De controller vernieuwt zijn sleutel periodiek; oude sleutels moet je bewaren.
  • Per cluster versleutelen. Drie omgevingen (test, acceptatie, productie) betekent drie keer versleutelen, of één gedeelde sleutel, wat het voordeel deels tenietdoet.
  • Rotatie is handwerk. Wachtwoord wijzigen betekent opnieuw kubeseal, committen en syncen.
  • Geen centrale bron. Het secret leeft in Git, niet in een systeem met audit log en toegangsbeleid.

Een punt van aandacht sinds 2025: Broadcom heeft de gratis Bitnami-catalogus grotendeels naar een legacy-repository verplaatst die niet meer wordt bijgewerkt. Sealed Secrets is daarop een uitzondering en wordt nog onderhouden, maar controleer welk image en welke tag je precies trekt. Een image uit bitnamilegacy krijgt geen updates meer.

External Secrets Operator: de bron staat buiten het cluster

ESO draait het model om. Het secret staat niet in Git, ook niet versleuteld. Het staat in een secret manager: AWS Secrets Manager, Google Secret Manager, Azure Key Vault, HashiCorp Vault, 1Password, Bitwarden, Infisical en nog een lange rij andere providers.

In Git zet je alleen een verwijzing: haal dit secret op uit die store en maak er een Kubernetes Secret van. Die verwijzing bevat geen geheimen.

Twee resources:

  • SecretStore of ClusterSecretStore: hoe ESO bij de externe store komt.
  • ExternalSecret: welk secret, welke sleutels, welk Kubernetes Secret als resultaat.

Een ClusterSecretStore voor AWS Secrets Manager, met authenticatie via een service account (IRSA of Pod Identity op EKS):

yaml
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: aws-secrets
spec:
  provider:
    aws:
      service: SecretsManager
      region: eu-central-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets
            namespace: external-secrets

En een ExternalSecret voor de databasegegevens van een Laravel-app:

yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: app-db
  namespace: platform
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: aws-secrets
  target:
    name: app-db
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: prod/platform/database
        property: password
    - secretKey: APP_KEY
      remoteRef:
        key: prod/platform/app
        property: app_key

ESO maakt een gewoon Secret app-db aan en ververst het elk uur. Je Deployment gebruikt het zoals elk ander Secret:

yaml
envFrom:
  - secretRef:
      name: app-db

Let op de API-versie. Sinds ESO v0.17 is external-secrets.io/v1 de API. Veel tutorials gebruiken nog v1beta1; die voorbeelden moet je omzetten. De huidige releases zitten inmiddels in de 2.x-reeks.

Hele bestanden opbouwen met templates

Niet elke applicatie leest losse environment variables. Soms wil je een compleet configuratiebestand. ESO heeft daar templates voor:

yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: shop-env
  namespace: shop
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: aws-secrets
  target:
    name: shop-env
    template:
      engineVersion: v2
      data:
        DB_HOST: "{{ .host }}"
        DB_USER: "{{ .username }}"
        DB_PASSWORD: "{{ .password }}"
        MAGENTO_CRYPT_KEY: "{{ .crypt_key }}"
  dataFrom:
    - extract:
        key: prod/shop/magento

dataFrom.extract haalt alle velden van een JSON-secret op. Het template bepaalt wat er in het Kubernetes Secret komt.

Voor Magento werkt dit goed in combinatie met een app/etc/env.php in het image die zijn gevoelige waarden via getenv() leest. Zo blijft env.php zelf vrij van geheimen en kan hetzelfde image op elke omgeving draaien. Wat je verder met de encryptiesleutel moet, lees je in Magento encryptiesleutel en env.php: secrets beheren.

Rotatie: wat ESO wel en niet doet

ESO ververst het Kubernetes Secret op het refreshInterval. Wijzig je het wachtwoord in AWS Secrets Manager, dan staat het binnen dat interval in je cluster.

Wat ESO niet doet: je pods herstarten. Een PHP-FPM-container die zijn environment bij het opstarten heeft gelezen, blijft het oude wachtwoord gebruiken. Twee oplossingen:

  • Stakater Reloader. Een kleine controller die Deployments herstart als een gekoppeld Secret verandert:
yaml
metadata:
  annotations:
    secret.reloader.stakater.com/reload: "app-db"
  • Secrets als bestand mounten en ze in de applicatie bij gebruik lezen. Kubernetes werkt gemounte Secrets na verloop van tijd bij, maar niet als je subPath gebruikt.

Bij databasewachtwoorden roteer je in twee stappen: eerst een tweede gebruiker of tweede wachtwoord toevoegen, dan de applicatie omzetten, dan het oude intrekken. Anders heb je een venster waarin pods met het oude wachtwoord niet meer verbinden.

De vergelijking

Sealed SecretsExternal Secrets Operator
Waar staat het geheimVersleuteld in GitIn een externe secret manager
Externe dienst nodigNeeJa
Meerdere clustersPer cluster versleutelenZelfde store, aparte paden per omgeving
RotatieHandmatig opnieuw versleutelenWijzigen in de store, ESO synchroniseert
Audit logGit-historieAudit log van de secret manager
Grootste risicoVerlies van de private keyToegang tot de store verkeerd ingericht
KostenAlleen je clusterKosten van de secret manager per secret en per API-call

Over die kosten: cloud secret managers rekenen meestal per secret per maand en per aantal API-calls. Een te kort refreshInterval bij veel secrets loopt dan op. Kijk op de prijspagina van je provider en kies een interval dat past, een uur is voor de meeste waarden ruim voldoende.

Wanneer wat

Kies Sealed Secrets als:

  • Je één cluster hebt en dat zo blijft.
  • Je geen cloud secret manager hebt en die ook niet wilt beheren.
  • Het aantal secrets klein is en ze zelden veranderen.

Kies External Secrets Operator als:

  • Je meerdere omgevingen of clusters hebt.
  • Je al in een cloud draait met een eigen secret manager, of Vault gebruikt.
  • Je wilt roteren zonder Git-commits.
  • Je een audit trail nodig hebt van wie wanneer welk secret heeft gelezen of gewijzigd.

Een eerlijke kanttekening over ESO. In augustus 2025 pauzeerden de maintainers alle releases omdat het team te klein was. Daarna zijn er nieuwe maintainers bijgekomen en lopen de releases weer: in oktober 2026 verschenen meerdere 2.x-versies kort na elkaar. Dat incident laat zien dat je bij elke open-source component naar het onderhoud moet kijken, net als bij ingress-nginx dat in 2026 stopte.

Inpassen in GitOps met Argo CD

Met Argo CD wil je dat de SecretStore bestaat voordat de ExternalSecrets worden aangemaakt, en dat Secrets bestaan voordat de applicatie start. Dat regel je met sync waves:

yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: app-db
  namespace: platform
  annotations:
    argocd.argoproj.io/sync-wave: "-1"

De ExternalSecret in wave -1, de migratie-Job in wave 0, de Deployments daarna. Hoe je die volgorde voor migraties opbouwt, beschrijven we in Argo CD sync waves voor migraties.

Argo CD beheert alleen de ExternalSecret, niet het Secret dat ESO aanmaakt; dat staat immers niet in Git. Gebruik creationPolicy: Owner. Dan is het Secret eigendom van de ExternalSecret en ruimt Kubernetes het op zodra Argo CD de ExternalSecret verwijdert.

Overige maatregelen die je niet overslaat

ESO of Sealed Secrets lost het Git-probleem op. Het Secret in je cluster blijft een Secret. Zorg daarom ook voor:

  • Encryptie at rest in etcd. Bij managed Kubernetes staat dat vaak standaard aan of is het een vinkje. Bij k3s of Talos op eigen servers moet je het zelf configureren.
  • RBAC. Applicatie-service-accounts krijgen geen get of list op Secrets. Mensen alleen waar nodig.
  • Aparte stores per omgeving. Productie-secrets in een eigen pad of eigen account, met een eigen ClusterSecretStore. Een testcluster kan dan niet bij productie.
  • Policy-checks. Met Kyverno kun je afdwingen dat niemand een gewoon Secret met data in Git zet.

Het bredere plaatje, inclusief image-scanning met Trivy en policies met Kyverno, staat op Secrets en security in Kubernetes. Voor het Laravel-deel van de stack zie Laravel op Kubernetes.

Twijfel je welke aanpak bij jouw platform past? Neem contact op. We zeggen het ook als een eenvoudiger opzet volstaat.

Veelgestelde vragen

Zijn Kubernetes Secrets versleuteld?+

Standaard alleen base64-gecodeerd, wat geen versleuteling is. Of ze in etcd versleuteld worden opgeslagen hangt af van je clusterconfiguratie. Bij managed Kubernetes is dat vaak geregeld; bij een zelf beheerd cluster moet je het zelf instellen.

Kan ik Sealed Secrets en External Secrets Operator samen gebruiken?+

Technisch wel, ze bijten elkaar niet. Het maakt je setup alleen lastiger te begrijpen. Kies er één als standaard en gebruik de ander hooguit tijdelijk tijdens een migratie.

Herstart ESO mijn pods als een secret verandert?+

Nee. ESO werkt alleen het Kubernetes Secret bij. Voor een herstart gebruik je een tool als Stakater Reloader, of je leest secrets als gemount bestand in plaats van als environment variable.

Welke API-versie moet ik gebruiken voor ExternalSecret?+

external-secrets.io/v1. Die is sinds ESO v0.17 de API-versie. Voorbeelden met v1beta1 zijn verouderd en moet je omzetten voordat je naar een actuele release gaat.

Wat als ik de sleutel van Sealed Secrets kwijtraak?+

Dan kun je de bestaande SealedSecrets niet meer ontsleutelen. Je moet alle secrets opnieuw aanmaken en versleutelen met een nieuwe sleutel. Maak daarom een backup van de sleutel-Secrets in de namespace van de controller en bewaar die buiten het cluster.

Ruthger Idema

Geschreven door Ruthger Idema

Eigenaar van Coding.nl. 15+ jaar e-commerce met Magento, Shopify en Laravel, samen met het vaste team in Emmen.

Wil je dit voor jouw shop laten uitzoeken? Plan een half uur

Een half uur via video met de mensen die het werk ook echt doen. Kies hieronder een moment dat jou past.

  • Direct met het vaste team, geen tussenlagen
  • Concrete feedback op je huidige situatie of plannen
  • Eerlijk advies, ook als het antwoord "niet doen" is
  • Indicatie van kosten en tijdlijn
Liever bellen? 0591-239 299
Beschikbaar deze week

Plan een gratis Tech Check

Vrijblijvend jouw uitdagingen bespreken

Of bel direct: 0591-239 299