130 lines
4.5 KiB
Text
130 lines
4.5 KiB
Text
|
|
{
|
||
|
|
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 = "<your-project-slug>",
|
||
|
|
vault_backend = 'restic,
|
||
|
|
registry_endpoint = "<your-zot-host>",
|
||
|
|
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 = "<your-project-slug>",
|
||
|
|
registries = m.make_registries_config {
|
||
|
|
default = "primary",
|
||
|
|
registries = [
|
||
|
|
m.make_registry_entry {
|
||
|
|
id = "primary",
|
||
|
|
endpoint = "<your-zot-host>",
|
||
|
|
role = 'primary,
|
||
|
|
tls = true,
|
||
|
|
namespaces = {
|
||
|
|
own = ["domains/<your-slug>/", "modes/<your-slug>/"],
|
||
|
|
prefixes = ["domains/<your-slug>/", "modes/<your-slug>/"],
|
||
|
|
},
|
||
|
|
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/<vault_id>/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 = "<registry-entry-id>", # 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<developer>,age1<ci>,age1<admin>,..."
|
||
|
|
ore secrets bootstrap
|
||
|
|
|
||
|
|
This creates `~/.config/ontoref/vaults/<vault_id>/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/<vault_id>: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.
|
||
|
|
"%,
|
||
|
|
}
|