Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Secrets Management

Catallaxy uses a three-layer model for secrets: stores, managed secrets, and projections.

The Model

Stores          → Where encrypted values are stored (one SOPS file per store)
Managed Secrets → Source-of-truth keys that reference a store
Projections     → How managed secrets map to K8s Secrets per cluster/namespace

Setup

1. Install SOPS and age

SOPS is included in the devshell. For age keys:

# Generate an age key (or use a YubiKey)
age-keygen -o ~/.config/sops/age/keys.txt

# For YubiKey:
age-plugin-yubikey  # Follow prompts to set up

2. Configure .sops.yaml

Create .sops.yaml at the repo root with creation rules matching your store paths:

creation_rules:
  # Prod secrets — encrypt with your age key
  - path_regex: secrets/homelab\.prod/.*\.enc\.yaml$
    age: age1...your-public-key...

  # Local dev secrets
  - path_regex: secrets/homelab\.local/.*\.enc\.yaml$
    age: age1...your-public-key...

3. Declare stores and managed secrets

In your lab config:

# Stores — each becomes one SOPS file
lab.secrets.stores.cloud-creds = { backend = "sops"; };

# Managed secrets — source-of-truth keys
lab.secrets.managed = {
  do-token = {
    store = "cloud-creds";
    keys.token = { };         # Manual entry via secrets edit
  };
  cf-token = {
    store = "cloud-creds";
    keys.token = { };
  };
  db-password = {
    store = "cloud-creds";
    keys.password = {
      generator = "alphanumeric";  # Auto-generated
      length = 32;
    };
  };
};

4. Generate and edit secrets

# Generate SOPS files (creates placeholders, auto-generates where configured)
cata secrets generate

# Edit a store to fill in manual values
cata secrets edit cloud-creds

# List all secrets and their status
cata secrets list

Projections

Projections map managed secrets into Kubernetes Secrets with optional transforms:

# In a cluster config:
secrets.projections.cloudflare-api-token = {
  source = "cf-token";           # From lab.secrets.managed
  namespace = "cert-manager";    # Target K8s namespace
  phase = "operators";           # Injected before this phase deploys
  keys.api-token.from = "token"; # Key mapping: K8s key ← managed key
};

secrets.projections.db-credentials = {
  source = "db-password";
  namespace = "forgejo";
  phase = "databases";
  keys = {
    password.from = "password";
    credentials = {
      from = "password";
      transform = "json-wrap";   # Wraps as {"password": "value"}
      jsonKey = "password";
    };
  };
};

Transforms

TransformDescription
none (default)Passthrough — value as-is
base64Base64-encode the value
json-wrapWrap as JSON: {"<jsonKey>": "<value>"}

Phase Ordering

Projections must be in a phase that runs before or during the phase of components that reference them. A Nix assertion validates this at build time:

Projection 'my-secret' is in phase 'infrastructure' (order 30)
but namespace 'my-ns' has components in phase 'operators' (order 10).
The secret won't exist when the component deploys.

The projection-ref lint check also validates that secretKeyRef names match declared projection names.

Per-Environment Keys

Use different age keys per environment in .sops.yaml:

creation_rules:
  - path_regex: secrets/.*\.prod/.*
    age: age1...prod-yubikey...

  - path_regex: secrets/.*\.staging/.*
    age: age1...staging-key...

  - path_regex: secrets/.*\.local/.*
    age: age1...dev-key...

YubiKey Workflow

With age-plugin-yubikey, SOPS prompts for the YubiKey PIN during decryption. During lab up, secrets are decrypted once (during the ensure-secrets step) and cached in memory for all subsequent cluster deployments. This avoids repeated PIN prompts when Crossplane provisioning creates long gaps between cluster deploys.

CLI Commands

cata secrets generate          # Generate SOPS files for all stores
cata secrets edit <store>      # Decrypt, edit, re-encrypt a store
cata secrets decrypt <store>   # Decrypt to stdout
cata secrets rotate <store>    # Rotate encryption keys
cata secrets list              # Show stores, managed secrets, and projections