Credential value templates: one declared shape for every credential's non-secret half and verification #23

Closed
opened 2026-09-11 21:45:01 +01:00 by allod-agent · 1 comment
Member

Replace the three hand-named credential formats and the verify-probe enum with one declared shape: a credential's non-secret text is a template around one {secret} placeholder, and a target's verification is one command string, so every consumer stops carrying a copy of the vocabulary and adding a new kind of credential is a registry edit, not a code change in three repos.

Primary goals:

  • One shape for every credential — value.template is the non-secret text with exactly one {secret}; a credential with no value is the secret verbatim. credential-store-url and rclone-remote-stanza become templates; raw and raw-forgejo-token become the absent case. The rclone password still passes through rclone obscure, named as value.encode = "rclone-obscure", the only remaining list, because an encoder is code.
  • Verify is a line, not a type — each target carries verify, one command printed verbatim, prefixed with ssh <system> when the target is not the host. The four probe types become four example lines in the docs.
  • The schema lives in one checked place — the secrets flake exports lib.credentialEncodings and a credential-registry check that validates every group: template shape, encoder membership, verify strings, and no legacy field on a credential that carries the new one. allod secret declare, allod secret rotate, and archetypes read the flake's exports and carry no enum.
  • Rotation never decrypts — the non-secret half is declared, so rotating renders the template around the new secret. The parsers that lifted user and host out of the old ciphertext go away. One host-side allod secret migrate <name> does that lift exactly once per existing container credential and rewrites its registry entry.
  • Coexistence until the private registry has migrated — the check accepts a legacy credential (format plus verify.type objects) or a new one, never both on one credential. rotate-token keeps rotating legacy entries until allod/nexus#52 retires it; allod secret rotate refuses a legacy container entry by naming migrate.

Current state and specifics

Three consumers enumerate the vocabulary today and must agree by hand: scripts/rotate-token in nexus (validate_group_metadata, the prepare_item and encryption switches, print_group_verification), the deployment's private secrets flake check, and the Go port on allod/tools#207 (validCredentialFormat, validRegistryVerifyType, printVerification). allod/nexus#54 was the cost of one new credential kind: one word in each of three places. The last comment on allod/tools#197 proposed an optional format and a per-target verify line; this issue takes that to its end, since the two containers differ only in the text around the secret.

Registry credential, new shape:

{
  "credential": "forgejo-https-token-allod-dev",
  "secret_path": "secrets/forgejo-https-token-allod-dev.age",
  "value": { "template": "https://allod-agent:{secret}@forge.anarch.diy" },
  "targets": [
    { "system": "allod-dev", "deployed_path": "/root/.git-credentials",
      "verify": "sudo env HOME=/root GIT_TERMINAL_PROMPT=0 git ls-remote https://forge.anarch.diy/allod/tools.git HEAD" }
  ]
}

A plain value omits value. An rclone stanza is { "template": "[shared]\ntype = ...\nhost = ...\nuser = ...\npass = {secret}\nexplicit_tls = true\n", "encode": "rclone-obscure" }. local_auth_refresh stays structured; its source check becomes "the source template is a git credential URL" instead of "format is credential-store-url". Group fields (service, account, ui_token_name, registry_alias, rotation_strategy) are unchanged; the registry's name is allod/secrets#22 and is not changed here.

Ordering: the secrets template's check and exports first; then the tools commands and the archetypes check, which read the exports; then the private registry migrates under a handoff issue; then legacy acceptance is removed from the check and rotate-token retires under allod/nexus#52.

Scope

In: the credential-registry check and lib.credentialEncodings in the public secrets template, with its example group migrated; allod secret declare, rotate, and the new host-only migrate in allod/tools on the new shape, superseding allod/tools#206 and #207; the archetypes local-auth-refresh check accepting a template source. Out, tracked elsewhere: the private registry migration and private validator (handoff issue filed when this lands); retiring rotate-token (allod/nexus#52); renaming the registry (allod/secrets#22); removing legacy acceptance from the check once no registry carries format (follow-up issue filed at handoff).

Refs allod/tools#197, allod/tools#196, allod/nexus#54.

Replace the three hand-named credential formats and the verify-probe enum with one declared shape: a credential's non-secret text is a template around one `{secret}` placeholder, and a target's verification is one command string, so every consumer stops carrying a copy of the vocabulary and adding a new kind of credential is a registry edit, not a code change in three repos. Primary goals: - **One shape for every credential** — `value.template` is the non-secret text with exactly one `{secret}`; a credential with no `value` is the secret verbatim. `credential-store-url` and `rclone-remote-stanza` become templates; `raw` and `raw-forgejo-token` become the absent case. The rclone password still passes through `rclone obscure`, named as `value.encode = "rclone-obscure"`, the only remaining list, because an encoder is code. - **Verify is a line, not a type** — each target carries `verify`, one command printed verbatim, prefixed with `ssh <system>` when the target is not the host. The four probe types become four example lines in the docs. - **The schema lives in one checked place** — the secrets flake exports `lib.credentialEncodings` and a `credential-registry` check that validates every group: template shape, encoder membership, verify strings, and no legacy field on a credential that carries the new one. `allod secret declare`, `allod secret rotate`, and archetypes read the flake's exports and carry no enum. - **Rotation never decrypts** — the non-secret half is declared, so rotating renders the template around the new secret. The parsers that lifted user and host out of the old ciphertext go away. One host-side `allod secret migrate <name>` does that lift exactly once per existing container credential and rewrites its registry entry. - **Coexistence until the private registry has migrated** — the check accepts a legacy credential (`format` plus `verify.type` objects) or a new one, never both on one credential. `rotate-token` keeps rotating legacy entries until allod/nexus#52 retires it; `allod secret rotate` refuses a legacy container entry by naming `migrate`. ### Current state and specifics Three consumers enumerate the vocabulary today and must agree by hand: `scripts/rotate-token` in nexus (`validate_group_metadata`, the `prepare_item` and encryption switches, `print_group_verification`), the deployment's private secrets flake check, and the Go port on allod/tools#207 (`validCredentialFormat`, `validRegistryVerifyType`, `printVerification`). allod/nexus#54 was the cost of one new credential kind: one word in each of three places. The last comment on allod/tools#197 proposed an optional format and a per-target verify line; this issue takes that to its end, since the two containers differ only in the text around the secret. Registry credential, new shape: ```json { "credential": "forgejo-https-token-allod-dev", "secret_path": "secrets/forgejo-https-token-allod-dev.age", "value": { "template": "https://allod-agent:{secret}@forge.anarch.diy" }, "targets": [ { "system": "allod-dev", "deployed_path": "/root/.git-credentials", "verify": "sudo env HOME=/root GIT_TERMINAL_PROMPT=0 git ls-remote https://forge.anarch.diy/allod/tools.git HEAD" } ] } ``` A plain value omits `value`. An rclone stanza is `{ "template": "[shared]\ntype = ...\nhost = ...\nuser = ...\npass = {secret}\nexplicit_tls = true\n", "encode": "rclone-obscure" }`. `local_auth_refresh` stays structured; its source check becomes "the source template is a git credential URL" instead of "format is credential-store-url". Group fields (`service`, `account`, `ui_token_name`, `registry_alias`, `rotation_strategy`) are unchanged; the registry's name is allod/secrets#22 and is not changed here. Ordering: the secrets template's check and exports first; then the tools commands and the archetypes check, which read the exports; then the private registry migrates under a handoff issue; then legacy acceptance is removed from the check and `rotate-token` retires under allod/nexus#52. ### Scope In: the `credential-registry` check and `lib.credentialEncodings` in the public secrets template, with its example group migrated; `allod secret declare`, `rotate`, and the new host-only `migrate` in allod/tools on the new shape, superseding allod/tools#206 and #207; the archetypes local-auth-refresh check accepting a template source. Out, tracked elsewhere: the private registry migration and private validator (handoff issue filed when this lands); retiring `rotate-token` (allod/nexus#52); renaming the registry (allod/secrets#22); removing legacy acceptance from the check once no registry carries `format` (follow-up issue filed at handoff). Refs allod/tools#197, allod/tools#196, allod/nexus#54.
Author
Member

The common credential shape stands; the private-side reconciliation needs a narrower cutover order, not another registry field.

  • A credential named by local_auth_refresh stays legacy until allod/nexus#52 gives refresh-local-auth a new-shape implementation or retires that dependency. The migration command must refuse that credential while the legacy refresh path still requires format and structured verify metadata.
  • allod secret create must render value.template around the candidate secret just like rotate; otherwise a newly declared templated credential would encrypt the bare secret.
  • Before migration writes registry text derived from decrypted bytes, it must prove byte-for-byte round-trip identity against the original plaintext, require the canonical container template shape with exactly one placeholder, and reject any extracted secret that remains elsewhere in the template.
  • The archetypes legacy-or-template predicate can land before the public example changes, so the safe merge order is archetypes compatibility, then the secrets contract/example, then tools last.

The private handoff will also require the deployment flake to re-export lib.credentialEncodings and its validator to accept legacy-or-new entries before any credential migration runs. These are sequencing constraints only; the JSON credential contract in this issue is unchanged.

The common credential shape stands; the private-side reconciliation needs a narrower cutover order, not another registry field. - A credential named by `local_auth_refresh` stays legacy until allod/nexus#52 gives refresh-local-auth a new-shape implementation or retires that dependency. The migration command must refuse that credential while the legacy refresh path still requires `format` and structured verify metadata. - `allod secret create` must render `value.template` around the candidate secret just like `rotate`; otherwise a newly declared templated credential would encrypt the bare secret. - Before migration writes registry text derived from decrypted bytes, it must prove byte-for-byte round-trip identity against the original plaintext, require the canonical container template shape with exactly one placeholder, and reject any extracted secret that remains elsewhere in the template. - The archetypes legacy-or-template predicate can land before the public example changes, so the safe merge order is archetypes compatibility, then the secrets contract/example, then tools last. The private handoff will also require the deployment flake to re-export `lib.credentialEncodings` and its validator to accept legacy-or-new entries before any credential migration runs. These are sequencing constraints only; the JSON credential contract in this issue is unchanged.
vnprc closed this issue 2026-09-12 02:24:16 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
allod/secrets#23
No description provided.