Skip to content

Secret Management

Kubernetes Secrets are only base64-encoded, so they can’t live in Git as-is. The convention KSail is built around is to encrypt them at rest with SOPS: you commit *.enc.yaml files whose secret values are ciphertext, and a GitOps engine (Flux or ArgoCD) decrypts them transparently on the way into the cluster. Keys stay out of Git; the repository stays the source of truth.

This guide covers the recommended layout, the ksail workload cipher commands that manage it, and how SOPS hands off to a runtime secret store for production.

Encrypted files follow a naming convention — *.enc.yaml — so they’re easy to spot and to target. A repo-root .sops.yaml declares creation_rules that tell SOPS what to encrypt and with which key:

.sops.yaml
creation_rules:
# Local environment — its own age recipient
- path_regex: k8s/clusters/local/.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1localrecipient...
# Production — a separate key, so a leaked local key can't decrypt prod
- path_regex: k8s/clusters/prod/.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1prodrecipient...
  • path_regex scopes a rule to a set of paths — typically one rule per environment, each under its per-cluster overlay (clusters/local/…, clusters/prod/…).
  • encrypted_regex: ^(data|stringData)$ encrypts only the Secret values — keys, names, and other metadata stay readable, so diffs and reviews still make sense.
  • age is the recipient public key. Using a separate key per environment keeps blast radius small: a compromised local key can’t unlock production secrets.

ksail workload cipher wraps SOPS so the same toolchain ships in the binary. It reads .sops.yaml, so you don’t pass recipients on the command line — the matching creation_rule picks the key.

Terminal window
ksail workload cipher encrypt k8s/clusters/local/secret.enc.yaml # encrypt in place
ksail workload cipher edit k8s/clusters/local/secret.enc.yaml # decrypt → $EDITOR → re-encrypt
ksail workload cipher decrypt k8s/clusters/local/secret.enc.yaml # decrypt to stdout
ksail workload cipher rotate k8s/clusters/local/secret.enc.yaml # re-key with a fresh data key

A typical first secret: write a normal manifest, then encrypt it.

Terminal window
kubectl create secret generic my-app \
--from-literal=token=s3cr3t \
--dry-run=client -o yaml > k8s/clusters/local/secret.enc.yaml
ksail workload cipher encrypt k8s/clusters/local/secret.enc.yaml

Commit the result — only data/stringData are ciphertext — then push and reconcile as usual.

Decryption is transparent during GitOps sync — there’s no manual plugin step. When a GitOps engine is active and SOPS is enabled, KSail provisions the age private key into the cluster and wires it into the engine; encrypted files in your source directory are decrypted automatically as they’re applied.

  • Flux — KSail creates a sops-age Secret in flux-system and Flux Kustomizations reference it via spec.decryption.secretRef.
  • ArgoCD — KSail creates the sops-age Secret in argocd and installs a Config Management Plugin sidecar that decrypts manifests before render. See ArgoCD ApplicationSet — SOPS Age Integration.

Configure resolution and behavior via spec.cluster.sops in ksail.yaml — see Declarative Configuration.

The age public recipient lives in .sops.yaml; the private key never enters the repository.

  • Locally, place it at ~/.config/sops/age/keys.txt (ksail workload cipher import AGE-SECRET-KEY-1… drops a key there for you).
  • In CI, provide it as a secret — e.g. an SOPS_AGE_KEY variable written to ~/.config/sops/age/keys.txt before push/reconcile — so the pipeline can decrypt when it needs to. See CI/CD Integration.

SOPS is the right tool for at-rest secrets in Git, but it isn’t a full secret-management plane: it doesn’t rotate credentials on its own or distribute them across many workloads. The recommended production pattern treats SOPS as a bootstrap seed:

  • Commit a small set of SOPS-encrypted secrets that seed a runtime secret store (a vault) — for example, the store’s root credential.
  • Run an operator (such as External Secrets Operator) that reads from that store and syncs/rotates the actual Secrets in-cluster. Day-to-day secrets then live in the store, not in Git.
  • Bootstrap-critical values — a cloud token a controller needs at startup, before the store is reachable — can instead be delivered through Flux post-build ${var} substitution from the bootstrap/ layer.

This keeps Git authoritative for what exists while the runtime store owns rotation and distribution. See the Reference Architecture for how this fits an end-to-end platform.

  1. Project Structure & GitOps Layout → — where *.enc.yaml files and .sops.yaml rules sit in a layered repository.

  2. Multi-Environment Workflows → — driving a key-per-environment setup with one repository and a config per cluster.

  3. Reference Architecture → — the SOPS-seeds-a-vault pattern in a complete platform.

ksail workload cipher