Skip to content

Use SOPS-backed secrets

Use a SOPS-backed Secret when Kix should own the Kubernetes Secret but its values must remain encrypted in the repository and Nix store. Kix evaluates the encrypted file into the cluster artifact, then runs sops --decrypt immediately before it applies the Secret.

This guide uses an Age identity. Install SOPS and Age first, and make sure the machine that runs kix deploy can access the identity.

Generate an identity and print its recipient:

❱ age-keygen -o age-key.txt
❱ age-keygen -y age-key.txt

Keep age-key.txt outside the repository. Your deployment environment needs the identity, while developers only need the recipient to encrypt changes.

Create the file with the keys your package expects:

credentials.enc.yaml
username: application-user
password: replace-with-a-generated-password

Encrypt it in place. The shell variable keeps the full recipient out of later commands:

❱ recipient=$(age-keygen -y age-key.txt)
❱ sops encrypt --age "$recipient" --in-place credentials.enc.yaml

Open the file before committing it and confirm that the values have become ENC[...] entries. Commit the encrypted file, but not age-key.txt.

Pass the encrypted file to scope.mkSecret and declare its keys explicitly:

how-to/platform/sops-secret-app.nix (L15–L22)
credentials = scope.mkSecret {
name = "${scope.instanceName}-credentials";
keys = [
"username"
"password"
];
source = ./sops-credentials.enc.yaml;
};

View source on GitHub ↗

Kix cannot derive the key names from encrypted values, so keys is required. It also lets the out helpers catch misspelled keys during evaluation.

This validation checks how the package uses the declared keys, not whether the encrypted file contains them. If the file omits a declared key, Kix can apply the Secret successfully and the workload fails when it tries to read the key. By contrast, the deploy-time readiness check for scope.mkSecretRef verifies that every declared key exists in the live Secret.

Add hash to catch an encrypted file that changed without the declaration changing:

package.nix
credentials = scope.mkSecret {
name = "credentials";
source = ./credentials.enc.yaml;
keys = [
"username"
"password"
];
hash = "sha256-<base64>";
};

Kix records this value in the kix.run/content-hash annotation. During deployment, it hashes the decrypted data and stops if the hash differs. On the first attempt, use the computed hash from the error message to fill in the declaration.

Consume the Secret in the same way as any other Kix-managed Secret:

how-to/platform/sops-secret-app.nix (L26–L47)
deployment = scope.mkDeployment {
name = scope.instanceName;
spec = {
replicas = 1;
selector.matchLabels = scope.selectorLabels;
template.spec.containers = [
{
name = "app";
image = "busybox:1.36";
command = [
"sh"
"-c"
"sleep 3600"
];
env = self.credentials.out.mkEnv {
APP_USERNAME = "username";
APP_PASSWORD = "password";
};
}
];
};
};

View source on GitHub ↗

The reference from deployment to credentials also gives Kix the deploy ordering between them.

Render the cluster before deploying:

Run in kix-examples/ Output excerpt
❱ kix build how-to-platform-sops --output json
[
  {
    "kind": "Secret",
    "namespace": "sops-example",
    "name": "app-credentials",
    "declaredKeys": [
      "username",
      "password"
    ],
    "plaintextIncluded": false
  }
]

The output excerpt reports the declared keys and confirms that the rendered Secret has neither data nor stringData. The artifact contains a reference to the encrypted source, not the decrypted values.

Point SOPS at the identity on the deployment machine, then deploy normally:

Run in kix-examples/
❱ export SOPS_AGE_KEY_FILE=/secure/path/age-key.txt
❱ kix deploy how-to-platform-sops

Kix stops the deploy if SOPS is unavailable, the identity cannot decrypt the file, or the decrypted document is not a top-level key-value map.

Check that Kubernetes received the two declared keys without printing their values:

Run in kix-examples/
❱ kubectl get secret app-credentials -n sops-example
NAME              TYPE     DATA   AGE
app-credentials   Opaque   2      14s

Give the deployment identity to CI through its secret store or workload identity mechanism. If another controller should own the Kubernetes Secret, use an externally managed Secret instead.