Registry Credential Vault Model: src-vault, Multi-Recipient sops, and Actor-Scoped Access

Accepted

ontoref
The provisioning registry (zot OCI) became the primary coordination hub for domain and mode artifacts (ADR-016 consequences, migration 0015). This introduced registry credentials as a first-class conce

Context

The provisioning registry (zot OCI) became the primary coordination hub for domain and mode artifacts (ADR-016 consequences, migration 0015). This introduced registry credentials as a first-class concern: every project that pushes or pulls artifacts needs credentials, and those credentials must be distributed across actors (developers, CI pipelines, the ontoref daemon, AI agents, ops tooling) with different access levels. The naive model — environment variables or ambient ~/.docker/config.json — has three compounding failure modes: (1) credentials are ambient and unscoped, so an operation against project A can use credentials that belong to project B; (2) there is no distribution mechanism — adding a new team member or CI key requires manual redistribution of a shared secret; (3) there is no audit trail for credential access or changes. A supply-chain attack via misconfigured registry namespace endpoints is the concrete risk: if the daemon proxied registry calls, any actor with MCP access could redirect them to an arbitrary endpoint using ambient credentials.

Decision

Registry credentials are managed through a per-project src-vault stored as an OCI artifact in the same ZOT registry it protects. The src-vault uses sops with age multi-recipient encryption: each actor role has its own age keypair, and credential files are encrypted for the exact set of recipients that need access. The vault backend (restic or kopia) handles versioning and local copies; the ZOT registry handles distribution. Each project has a local access.sops.yaml in ~/.config/ontoref/vaults/<project-slug>/ containing three fields: zot_username, zot_password, and vault_key. All three are encrypted by the actor's master age private key (.kage), which lives at an external path the actor controls (hardware key, encrypted disk, or declared path in config.ncl) — never inside the vault directory. At operation time, sops decrypts access.sops.yaml in memory: zot_username and zot_password are used to pull the src-vault OCI artifact via a DOCKER_CONFIG tmpdir that is deleted immediately after; vault_key is passed as RESTIC_PASSWORD or KOPIA_PASSWORD env var for the duration of the vault operation and never written to disk. No plaintext credential of any kind persists beyond the operation scope. Credential resolution runs exclusively in the ontoref CLI — the daemon is structurally excluded. All oras invocations use an isolated DOCKER_CONFIG tmpdir; no ambient ~/.docker/config.json is consulted. Access logs are appended to a jsonl file stored as a layer in the same src-vault OCI artifact and mirrored locally in ~/.config/ontoref/vaults/<project-slug>/logs/access.jsonl.

Constraints

  • Hard The credentials required to pull the src-vault OCI artifact from ZOT must be stored outside the vault itself — each src-vault has its own access credentials, held in the actor's local .kage, not inside any vault it protects
  • Hard RegistryEntry must not use credential_env — only credential_sops or credential_oidc are valid credential reference fields
  • Hard Every *.sops.yaml credential file must include at minimum the admin and the bound_actor recipients for its access class
  • Hard Any domain or mode that pushes or pulls from a registry must declare uses_registry referencing the RegistryEntry id in manifest.ncl
  • Soft All sops operations on registry credential files must go through ontoref secrets — direct sops invocations bypass lock state and audit log
  • Hard Every oras invocation must use DOCKER_CONFIG pointing to a tmpdir containing only the credential for the target registry endpoint — no ambient ~/.docker/config.json
  • Hard vault_key must never be written to disk in plaintext — it is decrypted from access.sops.yaml into RESTIC_PASSWORD or KOPIA_PASSWORD for the duration of the operation only
  • Soft All vault snapshot operations must use vault-backend.nu — direct restic or kopia invocations are not permitted in recipes or modules
  • Hard Every push of src-vault/<project>:latest to ZOT must produce a cosign signature; every pull must verify the signature before the artifact is trusted

Alternatives considered

  • Environment variables (REGISTRY_TOKEN, DOCKER_CONFIG) per CI jobrejected: Visible in process list, inherited by subprocesses, logged by CI systems, cannot be scoped to a specific registry endpoint. No revocation without rotating all consumers. The primary attack surface for supply-chain credential leakage.
  • Shared age keyring — one private key distributed to all developersrejected: Revocation requires rotating the shared key and redistributing to all remaining members — coordination cost scales with team size. No per-actor audit trail. A leaked key compromises all actors simultaneously.
  • HashiCorp Vault or similar external secrets managerrejected: Introduces a network dependency for every credential resolution. Requires operating a separate service with its own HA, backup, and auth model. The OCI registry is already the coordination hub — using it as the vault distribution backend reuses existing infrastructure and auth.
  • Daemon resolves credentials on behalf of CLI actorsrejected: The daemon is a long-lived process accessible to multiple actors (developer, agent, CI via MCP). Giving it credential resolution capability means any actor with daemon access can trigger registry operations using credentials they do not personally hold. The structural exclusion of the daemon from credential resolution is a load-bearing architectural property.
  • Single credential file per registry (no RO/RW split)rejected: A single credential with RW access distributed to read-only actors (cdci, ontoref, agent) violates least-privilege. A compromised CI pipeline with RW credentials can push malicious artifacts. The RO/RW split means a compromised read-only credential cannot alter the artifact namespace.

Related ADRs

ADR-005 · ADR-012 · ADR-016

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.