{ id = "0018", slug = "recipient-routing", description = "Adopt per-file recipient routing for tenant isolation (ADR-019). Optional: only projects requiring multi-tenant credential separation need to apply. Adds recipient_groups + recipient_rules to project.ncl::sops; bootstrap then generates /.sops.yaml from declarations and sops encrypts each file with the union of declared groups.", # Adoption is opt-in. Projects without multi-tenant needs MAY skip. # Mark as 'applied' when the project explicitly chose a path: # - Path A (single-team / legacy): project.ncl has sops but no recipient_groups # - Path B/C: project.ncl declares both recipient_groups and recipient_rules # Either path satisfies the check below. check = { tag = "Grep", paths = [".ontoref/project.ncl"], pattern = "sops", }, instructions = m%" ## Per-file recipient routing for tenant isolation (migration 0018, ADR-019) This migration is **OPTIONAL**. Apply only if your project requires multi-tenant credential separation, AI-agent restricted access, or developer/admin role mixing that the single-recipient-set model from migration 0016 cannot express. ### Decide your path ┌────────────────────────────────────────────────────────────────────┐ │ Will multiple actors (clients/agents/teams) operate this project? │ └─────────────────────────────────┬──────────────────────────────────┘ │ ┌─────── No ──────┴─────── Yes ──────────────┐ ▼ ▼ ┌──────────────────────────┐ ┌──────────────────────────────────┐ │ Path A — Single team │ │ Will those actors need DISTINCT │ │ Migration 0016 sufficient│ │ credential sets per actor? │ │ — no further action │ └──────────────────┬───────────────┘ └──────────────────────────┘ │ ┌─────── No ────────────┴──── Yes ──────┐ ▼ ▼ ┌─────────────────────────────────┐ ┌────────────────────────┐ │ Stay with migration 0016 │ │ Path B/C: this 0018 │ │ Re-evaluate when needs change │ │ Apply steps below │ └──────────────────────────────────┘ └────────────────────────┘ ### Step 1 — Templates Three templates ship under install/resources/templates/sops/: single-team/ — Path A reference (no migration needed) multi-tenant/ — Path B reference (clients/services/teams isolated) agent-first/ — Path C reference (AI agent has narrow read-only access) Pick one, copy its project.ncl.snippet and manifest.ncl.snippet into the matching files in your project. Adapt placeholders to your tenancy. ### Step 2 — Declare groups and rules Inside .ontoref/project.ncl::sops, add: recipient_groups = { admin = [\"age1...\"], # admins (full access) ops = [\"age1...\"], # ops/observability clientA = [\"age1clientA-lead...\"], # one group per client/team/agent clientB = [\"age1clientB-lead...\"], agents = [\"age1agent...\"], # AI agents (typically RO) }, recipient_rules = [ { path = \"access\\\\.sops\\\\.yaml$\", groups = [\"admin\", \"ops\"] }, { path = \"registry/clientA-.*\\\\.sops\\\\.yaml$\", groups = [\"admin\", \"clientA\"] }, { path = \"registry/clientB-.*\\\\.sops\\\\.yaml$\", groups = [\"admin\", \"clientB\"] }, { path = \"registry/agent-readonly\\\\.sops\\\\.yaml$\", groups = [\"admin\", \"agents\"] }, ], ### Step 3 — Add per-tenant RegistryEntry items in manifest.ncl For each rule, declare a corresponding RegistryEntry whose credential_sops matches the rule's path pattern: m.make_registry_entry { id = \"clientA\", endpoint = \"\", credential_sops = \"registry/clientA-ro.sops.yaml\", credential_sops_rw = \"registry/clientA-rw.sops.yaml\", namespaces = { own = [\"domains/clientA/\", \"modes/clientA/\"], prefixes = [\"domains/clientA/\"], }, } ### Step 4 — Bootstrap or migrate NEW PROJECT ore secrets bootstrap (generates /.sops.yaml + access.sops.yaml, skips default registry/ro|rw.sops.yaml — operator populates per-tenant files via 'ore secrets open') EXISTING PROJECT ore secrets rekey (regenerates /.sops.yaml from new declarations, re-encrypts every existing *.sops.yaml with the matching rule) ### Step 5 — Verify ore secrets describe # see groups, rules, recipients per file ore secrets audit # 6 checks pass: # bootstrap-credentials OK # no-credential-env OK # multi-recipient-mandatory OK # recipient-routing-coherent OK # recipient-routing-coverage OK # no-multi-vault OK ### What does NOT change - vault_id remains one per project (multi-vault is explicitly out of scope) - Master .kage continues per-developer - Restic/Kopia repo and OCI artifact remain unique per project - Vault lock semantics unchanged - All ADR-017 invariants preserved ### When to escalate to multi-vault (not implemented) - Hard isolation: separate master keys per environment (prod vs staging) - Hard isolation: separate restic repos to prevent any cross-pull visibility - Compliance grade: regulator requires environments cannot share vault contents even when encrypted When such a case appears, capture it in a new ADR superseding/extending ADR-019 before adopting multi-vault. ### References ADR-019 decision + 6 checkable constraints reflection/qa.ncl::credential-vault-templates three-path overview install/resources/templates/sops/ ready-to-copy snippets reflection/migrations/0016 Layer 0/1/2 prerequisites "%, }