sops instructions
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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";
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user