{ id = "0016", slug = "registry-credential-vault", description = "Add sops vault config to project.ncl and credential_sops fields to registry entries (ADR-017). Enables per-project multi-recipient age-encrypted credential vaults stored as OCI artifacts in ZOT, with isolated DOCKER_CONFIG for every oras invocation.", check = { tag = "Grep", paths = [".ontoref/project.ncl"], pattern = "sops", }, instructions = m%" ## Registry credential vault (migration 0016) Implements ADR-017: per-project sops multi-recipient vaults stored as OCI artifacts in ZOT. Credentials are never in env vars, never in ambient docker config, never plaintext on disk. Every oras invocation runs with an isolated DOCKER_CONFIG. ### Reference material - **Helper contract**: `reflection/modules/secrets.nu` header docstring — defines `resolve-vault-access` (Layer 0), `resolve-registry-credential` (Layer 2), `assert-actor-authorized`, plus the 13 named errors callers must handle. - **Working example**: ontoref's own `.ontoref/project.ncl` and `.ontology/manifest.ncl` — copy this shape into your project. - **CLI surface**: `ore secrets {bootstrap|sync|push|open|close|status|audit}` and `ore vault {status|snapshots|check}` (P2 dispatcher wireup). ### Step 1 — Set the global master key path (one-time per developer) In `~/.config/ontoref/config.ncl`: vault = { master_key_path = "/path/to/your/.kage", backend = 'restic, }, This becomes the default for all projects. A project may override it in its own `.ontoref/project.ncl::sops.master_key_path` if it requires a different key. ### Step 2 — Add the sops block to .ontoref/project.ncl Inside the `make_project { ... }` call: sops = { enabled = true, vault_id = "", vault_backend = 'restic, registry_endpoint = "", actor_key_bindings = { developer = "developer", ci = "cdci", agent = "ontoref", admin = "admin", }, # master_key_path absent → resolves from ~/.config/ontoref/config.ncl::vault.master_key_path }, ### Step 3 — Add registry_provides + credential_sops to .ontology/manifest.ncl registry_provides = m.make_registry_provides { participant = "", registries = m.make_registries_config { default = "primary", registries = [ m.make_registry_entry { id = "primary", endpoint = "", role = 'primary, tls = true, namespaces = { own = ["domains//", "modes//"], prefixes = ["domains//", "modes//"], }, credential_sops = "registry/ro.sops.yaml", # RO — relative to src-vault root credential_sops_rw = "registry/rw.sops.yaml", # RW — relative to src-vault root }, ], }, }, NOTE: `credential_sops` paths are relative to the src-vault root (`~/.config/ontoref/vaults//src-vault/`), NOT to the project root. ### Step 4 — Declare uses_registry in domains/modes that call oras Any domain or mode NCL file that pushes or pulls artifacts must add: uses_registry = "", # id from make_registry_entry Enables impact analysis on `ore secrets close` to find affected services. ### Step 5 — Bootstrap the vault (admin only, one-time per project) export SOPS_AGE_RECIPIENTS="age1,age1,age1,..." ore secrets bootstrap This creates `~/.config/ontoref/vaults//access.sops.yaml`, initializes the local restic/kopia repo, and (next step) pushes the first src-vault artifact. ### Step 6 — Push the bootstrapped vault to ZOT export COSIGN_KEY_PATH=/path/to/cosign.key # required by ADR-017 ore secrets push The artifact is signed with cosign — unsigned vault pushes are rejected by the `src-vault-cosign-signed` hard constraint. ### Step 7 — Non-admin actors: sync the vault ore secrets sync Pulls `src-vault/:latest` from ZOT and decrypts access.sops.yaml. ### Step 8 — Verify ADR-017 constraints ore secrets audit Runs all checks: bootstrap-credentials, no-credential-env, multi-recipient mandatory. Failures here block downstream operations. ### Step 9 — Verify visible state ore secrets status # vault dir + last access entry + master_key resolution ore vault status # backend + repo presence + last snapshot Both should report a healthy state with no errors. "%, }