sops instructions

This commit is contained in:
John Lancaster
2026-07-04 00:30:15 -05:00
parent dccdc15851
commit 3f767dfc43
4 changed files with 99 additions and 18 deletions
@@ -0,0 +1,72 @@
---
description: "Use when working with SOPS in this repo: editing secrets, adding multiline values, changing recipients, or wiring sops-nix consumers. Captures repo conventions, safe commands, and validation habits."
applyTo: ".sops.yaml, keys/secrets.yaml, modules/hosts/**/secrets.yaml, modules/hosts/**/*.nix, modules/programs/sops.nix, modules/features/**/*.nix, modules/services/**/*.nix"
---
# Using SOPS In This Repo
This repo treats SOPS files as the source of encrypted runtime material, and Nix code as the wiring that exposes those secrets to services. Keep those roles separate: edit secret values with SOPS, consume them through sops-nix paths, and keep recipient policy scoped to the machines or operators that actually need access.
## Mental Model
- Secrets live in encrypted YAML files; Nix modules should refer to secret paths, not decrypted values.
- Host-only credentials belong in `modules/hosts/<host>/secrets.yaml`.
- Shared credentials belong in `keys/secrets.yaml` only when multiple hosts intentionally consume the same material.
- `.sops.yaml` controls who can decrypt each file; keep those recipient sets narrow and explicit.
- Secret key names are an interface. Rename them only when every consumer changes in the same edit.
## Editing Secrets
- Use SOPS tooling for all value changes: `sops <file>`, `edit-secrets`, or `sops set ...`.
- Do not hand-edit `ENC[...]` payloads. That bypasses SOPS and breaks the encrypted document's integrity metadata.
- Keep plaintext out of tracked files. Temporary plaintext files are acceptable only as local working inputs and should be removed after use.
- Prefer `SOPS_EDITOR=nvim sops <file>` for interactive edits when editor choice matters.
For large or generated values, avoid putting the secret directly in shell history:
```bash
sops set --value-file <secrets-file> '["parent"]["key"]' /path/to/plaintext-value
```
If using inline `sops set`, the value argument must be valid JSON.
## Multiline Values
Use YAML literal blocks for private keys, certificates, provisioner keys, and other values where newlines matter:
```yaml
janus:
ssh_user_ca_key: |-
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
```
- Use `|-` when the final trailing newline should be stripped.
- Use `|` when the consuming program expects the final trailing newline.
- Avoid folded style (`>`) for keys and certs because it rewrites line breaks.
## Wiring Secrets Into Nix
- Consume secrets through `config.sops.secrets."<key>".path` or `config.sops.placeholder` where templating requires it.
- Do not read decrypted secret contents during Nix evaluation.
- Set `owner`, `group`, and `mode` explicitly for non-root services.
- Use `path` or `sops.templates` when a service needs a specific file layout instead of copying secret contents into the store or tracked files.
- If moving a secret between shared and host-local files, update `sops.defaultSopsFile`, `mysops.hostSecretFile`, and every affected `sops.secrets` declaration together.
## Recipient And Rotation Practices
- Add dedicated `.sops.yaml` `path_regex` rules for host-local files before broad fallback rules.
- Keep broad fallback rules conservative; do not expand them to grant casual access to every YAML or JSON file.
- Run `sops updatekeys <file>` after changing recipients so data is rewrapped for the new key set.
- Rotate or migrate in order: update recipients, re-encrypt affected files, update consumers, then validate affected hosts.
## Validation Habits
After SOPS or sops-nix changes, check the contract rather than only the syntax:
1. The expected secrets file is still selected by `sops.defaultSopsFile` or `mysops.hostSecretFile`.
2. `.sops.yaml` has a specific rule for any host-local `secrets.yaml` touched.
3. Secret names in YAML still match the `sops.secrets."<key>"` declarations.
4. Services that need restart or reload behavior have `restartUnits` or `reloadUnits` set.
5. Affected host evaluation/build catches missing secret keys before deployment.
+21 -3
View File
@@ -13,7 +13,6 @@ STEPPATH=/tmp/janus-step-ca-bootstrap/step step ca init --name Janus --dns janus
Insert generated runtime CA material into `modules/hosts/janus/secrets.yaml` under `janus`:
- `/tmp/janus-step-ca-bootstrap/ca_password.txt` -> `ca_password`
- `/tmp/janus-step-ca-bootstrap/step/certs/intermediate_ca.crt` -> `intermediate_ca_crt`
- `/tmp/janus-step-ca-bootstrap/step/secrets/intermediate_ca_key` -> `intermediate_ca_key`
- `/tmp/janus-step-ca-bootstrap/step/secrets/ssh_host_ca_key` -> `ssh_host_ca_key`
- `/tmp/janus-step-ca-bootstrap/step/secrets/ssh_user_ca_key` -> `ssh_user_ca_key`
@@ -27,14 +26,33 @@ If rotating provisioner password, also set:
Secret source-of-truth after this split:
- `modules/hosts/janus/secrets.yaml`: Janus runtime CA secrets (`ca_password`, `intermediate_ca_crt`, `intermediate_ca_key`, `ssh_host_ca_key`, `ssh_user_ca_key`, `admin_provisioner_encrypted_key`)
- `modules/hosts/janus/secrets.yaml`: Janus runtime CA secrets (`ca_password`, `intermediate_ca_key`, `ssh_host_ca_key`, `ssh_user_ca_key`, `admin_provisioner_encrypted_key`)
- `keys/secrets.yaml`: shared Janus provisioner secret (`janus.admin_jwk`) consumed by `step-ssh-host` across hosts
Then update public artifacts in repo from generated output:
- `modules/hosts/janus/root_ca.crt` from `/tmp/janus-step-ca-bootstrap/step/certs/root_ca.crt`
- `modules/hosts/janus/intermediate_ca.crt` from `/tmp/janus-step-ca-bootstrap/step/certs/intermediate_ca.crt` (public certificate; intentionally committed, not stored in SOPS)
- `modules/hosts/janus/fingerprint` from:
```shell
step certificate fingerprint /tmp/janus-step-ca-bootstrap/step/certs/root_ca.crt
```
- `modules/hosts/janus/ssh_user_ca.pub` from `/tmp/janus-step-ca-bootstrap/step/certs/ssh_user_ca_key.pub`
- `modules/hosts/janus/ssh_user_ca.pub` from `/tmp/janus-step-ca-bootstrap/step/certs/ssh_user_ca_key.pub`
## Back up the root CA key offline
The root CA private key is **not** deployed and is never needed by the running CA.
It is only used to sign/rotate intermediates. Store it offline (e.g. 1Password)
before wiping the bootstrap directory, otherwise intermediate rotation becomes
impossible and any future rotation forces a full root rotation (redistributing
`root_ca.crt` + `fingerprint` to every client).
Back up, then wipe the bootstrap material:
- `/tmp/janus-step-ca-bootstrap/step/secrets/root_ca_key` -> offline secret store
- `/tmp/janus-step-ca-bootstrap/ca_password.txt` -> offline secret store (root/intermediate key password)
```shell
shred -u /tmp/janus-step-ca-bootstrap/step/secrets/root_ca_key 2>/dev/null || true
rm -rf /tmp/janus-step-ca-bootstrap
```
+1 -1
View File
@@ -28,6 +28,7 @@ in
};
step-ca = {
rootCertPath = ./root_ca.crt;
intermediateCertPath = ./intermediate_ca.crt;
dnsNames = [
"${hostname}.john-stream.com"
"192.168.1.244"
@@ -35,7 +36,6 @@ in
secrets = {
sopsFile = ./secrets.yaml;
caPassword = "janus/ca_password";
intermediateCrt = "janus/intermediate_ca_crt";
intermediateKey = "janus/intermediate_ca_key";
sshHostCaKey = "janus/ssh_host_ca_key";
sshUserCaKey = "janus/ssh_user_ca_key";
+5 -14
View File
@@ -7,7 +7,6 @@
caPort = 443;
caPasswordPath = (lib.getAttr cfg.secrets.caPassword config.sops.secrets).path;
intermediateCrtPath = (lib.getAttr cfg.secrets.intermediateCrt config.sops.secrets).path;
intermediateKeyPath = (lib.getAttr cfg.secrets.intermediateKey config.sops.secrets).path;
sshHostCaKeyPath = (lib.getAttr cfg.secrets.sshHostCaKey config.sops.secrets).path;
sshUserCaKeyPath = (lib.getAttr cfg.secrets.sshUserCaKey config.sops.secrets).path;
@@ -16,7 +15,7 @@
renderedStepCaConfig = builtins.toJSON {
root = cfg.rootCertPath;
crt = intermediateCrtPath;
crt = cfg.intermediateCertPath;
key = intermediateKeyPath;
address = "${caAddress}:${toString caPort}";
dnsNames = cfg.dnsNames;
@@ -87,6 +86,10 @@
description = "Path to the Step CA root certificate served by this host.";
type = lib.types.path;
};
intermediateCertPath = lib.mkOption {
description = "Path to the Step CA intermediate certificate served by this host. This is public material and does not need to be stored in SOPS.";
type = lib.types.path;
};
dnsNames = lib.mkOption {
description = "DNS names and IP SANs advertised by this Step CA instance.";
type = with lib.types; listOf str;
@@ -100,10 +103,6 @@
description = "SOPS key for the Step CA intermediate password.";
type = lib.types.str;
};
intermediateCrt = lib.mkOption {
description = "SOPS key for the Step CA intermediate certificate.";
type = lib.types.str;
};
intermediateKey = lib.mkOption {
description = "SOPS key for the Step CA intermediate private key.";
type = lib.types.str;
@@ -132,13 +131,6 @@
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.intermediateCrt}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.intermediateKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
@@ -184,7 +176,6 @@
intermediatePasswordFile = caPasswordPath;
};
# Keep modules/services/step-ca/ca.json as reference-only; runtime config comes from SOPS template.
environment.etc."smallstep/ca.json".source =
lib.mkForce config.sops.templates."step-ca-config".path;
systemd.services.step-ca.restartTriggers =