CCE

Arroyo System Secrets Configuration

Contents

This CCE module configures SOPS + age secret management for the Arroyo System. Secrets are encrypted to your Yubikey (via age-plugin-yubikey), a backup age keypair, and each host's SSH ed25519 key (converted to an age recipient with ssh-to-age).

The .sops.yaml recipient rules are generated from the NixOS Host Definitions in systems.org: each host heading declares :ARROYO_SYSTEM_ROLE: and :ARROYO_AGE_RECIPIENT:, and the Lua generators below query the Arroyo DB to emit YAML anchors and per-role key_groups.

Replace the placeholder yubikey and backup recipients below with your own (on your personal branch). The template branch carries <YOUR_*> placeholders.

Static recipient keys

These are your personal decryption identities:

  • rrix_tpm_vc — a age-plugin-tpm identity sealed in the TPM 2.0 of virtuous-cassette (per-machine, no PIN/tap)

  • rrix_backup — a plain age keypair whose private half is stored offline (paper + password manager), the emergency fallback

Host age recipients (derived from SSH host keys) are generated dynamically below. When a YubiKey 5 is available, add a rrix_yubikey recipient here for portable hardware-backed editing — the .sops.yaml key_groups below already reference it.

yaml#+name: sops_static_keys
  - &rrix_tpm_vc    age1tag1q2sxwd40w7hlhpqx0dmy24cclxr0la9q0cf75434wnmw6czjw9c6q3fcw7p
#  - &rrix_yubikey   age1yubikey1<YOUR_YUBIKEY_RECIPIENT>
  - &rrix_backup    age1cpjqjf6v3wdkxxupdzma2zzn0az34k0npylc0zq0m3vetn2la57q0qw6f0
  - &nixflix        age1gu5w2m2ngdernhqaelk4cteu2tnuq5nxk8ux3a6wlzqyc0hwnd6qtucd8q

.sops.yaml

yaml:tangle ~/nix/.sops.yaml:noweb yes:comments none
# Generated from systems.org by arroyo-system/secrets.org.
# Do not edit by hand — edit the org source and re-tangle.
keys:
<<sops_static_keys()>>
<<arroyo_age_recipients_yaml()>>

creation_rules:
  - path_regex: secrets/nixflix\.yaml$
    key_groups:
    - age: [ *rrix_tpm_vc, *rrix_backup, *nixflix ]
<<arroyo_sops_hosts_yaml()>>

Generated host recipients

arroyo.age_recipients_all() queries the DB for every host heading with :ARROYO_AGE_RECIPIENT: and returns host (slugified title), recipient, and role. This block emits one YAML anchor per host.

lua#+name: arroyo_age_recipients_yaml:eval arroyo:results raw
local hosts = arroyo.age_recipients_all()
local lines = {}
for _, h in ipairs(hosts) do
  table.insert(lines, string.format("  - &%s %s", h.host, h.recipient))
end
return table.concat(lines, "\n")

Generated creation_rules: rules

This block groups hosts by role and emits one path_regex / key_groups rule per role. Each role's secrets are encrypted to all personal identities (TPM, yubikey, backup) + every host of that role. A final catch-all rule encrypts unscoped secret files to just the personal identities.

lua#+name: arroyo_sops_hosts_yaml:eval arroyo:results raw
local all = arroyo.age_recipients_all()
local roles = {}
local role_order = {}
for _, h in ipairs(all) do
  if not roles[h.role] then
    roles[h.role] = {}
    table.insert(role_order, h.role)
  end
  table.insert(roles[h.role], h)
end
-- local identities = { "*rrix_tpm_vc", "*rrix_yubikey", "*rrix_backup" }
local identities = { "*rrix_tpm_vc", "*rrix_backup" }
local lines = {}
for _, role in ipairs(role_order) do
  local refs = {}
  for _, id in ipairs(identities) do table.insert(refs, id) end
  for _, h in ipairs(roles[role]) do table.insert(refs, "*" .. h.host) end
  table.insert(lines, string.format(
    "  - path_regex: secrets/%s\\.yaml$\n    key_groups:\n    - age: [ %s ]",
    role, table.concat(refs, ", ")))
end
-- table.insert(lines, "  - path_regex: secrets/([^/]+)\\.yaml$\n    key_groups:\n    - age: [ *rrix_tpm_vc, *rrix_yubikey, *rrix_backup ]")
table.insert(lines, "  - path_regex: secrets/([^/]+)\\.yaml$\n    key_groups:\n    - age: [ *rrix_tpm_vc, *rrix_backup ]")
return table.concat(lines, "\n")

lib/secrets.nix — host recipient attrset

A Nix attrset mapping host slugs to their age recipients, for programmatic use by other modules if needed.

lua#+name: arroyo_secrets_nix:eval arroyo:results raw
local all = arroyo.age_recipients_all()
local lines = { "{", "  hosts = {" }
for _, h in ipairs(all) do
  table.insert(lines, string.format("    %s = %q;", h.host, h.recipient))
end
table.insert(lines, "  };")
table.insert(lines, "}")
return table.concat(lines, "\n")
nix:tangle ~/nix/lib/secrets.nix:noweb yes
<<arroyo_secrets_nix()>>

SOPS-Nix Flake Input

The SOPS secret management module imports sops-nix from the flake.

nix:tangle ~/nix/snippets/sops-nix-input.nix
sops-nix = {
  url = "github:Mic92/sops-nix";
  inputs.nixpkgs.follows = "nixpkgs";
};