ward-kdl: ops forgejo-key - read the forgejo token from k3s external-secrets (kubectl), strictly single-key #386

Closed
opened 2026-06-26 16:20:18 +00:00 by coilysiren · 2 comments
Owner

Goal

Give ward-kdl a chained-command read for the forgejo API token that goes through kubectl into the k3s external-secrets mirror, not AWS SSM - so reading the token needs only tailnet + kubectl, never a 2FA-gated AWS call. Strictly limited to the forgejo key and nothing else.

Why

Today the token is read via ward-kdl ops aws ssm get-parameter --name /forgejo/api-token ... (see agentic-os/scripts/git-credential-forgejo-ssm.sh and scripts/docker-credential-forgejo-ssm). That needs AWS auth -> 2FA. When the 2FA device is absent, Kai is locked out of her own git token. But external-secrets already mirrors /forgejo/api-token into a k8s Secret (forgejo-runner-secrets, key api-token, namespace forgejo, see infrastructure/deploy/forgejo-runner.yml) using the cluster's static, non-2FA aws-credentials. So the token is already reachable over the tailnet - we just need a ward-kdl verb that reads it, scoped to only that key.

Dependency

This depends on cli-guard #173 (the sealed clause that blocks trailing caller args). With sealed, the verb below is strictly single-key in pure KDL - no hand-written gated Go needed. Carry this after that lands (or rebase on it).

What to build

Add an exec-dialect verb that reads exactly the forgejo secret, sealed so no caller args can widen it:

wrap ward-kdl ops forgejo-key {
    exec kubectl
    can run read {
        argv get secret forgejo-runner-secrets -n forgejo -o "go-template={{.data.api-token | base64decode}}"
        sealed
        describe "read ONLY the forgejo api-token from the k3s external-secrets mirror, decoded"
    }
}
  • argv pins the full kubectl command; sealed (cli-guard #173) forbids any trailing caller token, so the verb cannot reach another secret or namespace.
  • The go-template ... base64decode emits the decoded token on stdout, matching the old SSM --output text behavior so the credential helpers can swap one command for another.
  • Place the guardfile next to the existing exec guardfiles (cmd/ward-kdl/), mirroring ward-kdl.kubectl.guardfile.kdl. Add the matching docs/ward-kdl/ entry.

Repoint the credential helpers (cross-repo: agentic-os)

Update agentic-os/scripts/git-credential-forgejo-ssm.sh and scripts/docker-credential-forgejo-ssm to fetch via ward ops forgejo-key read, falling back to the SSM path when kubectl/tailnet is unavailable (so an off-tailnet host still works). Keep both strictly on the forgejo token. File/carry that half in the agentic-os repo and link it.

Acceptance

  • ward ops forgejo-key read returns the decoded forgejo api-token over the tailnet with no AWS call and no 2FA.
  • It cannot read any other secret or namespace - a pivot attempt (extra secret name, different -n) is refused by sealed, with a test.
  • The grant audit-logs through ward like other ops verbs.
  • Credential helpers repointed (kubectl-first, SSM-fallback), still strictly forgejo-only.
  • Docs updated; deliberately single-key for now (broader "cache all SSM in k3s" is infrastructure#417).

Files

cmd/ward-kdl/ (new guardfile), cmd/ward-kdl/ward-kdl.kubectl.guardfile.kdl (reference), docs/ward-kdl/, cmd/ward/execassets/. Cross-repo: agentic-os/scripts/git-credential-forgejo-ssm.sh, agentic-os/scripts/docker-credential-forgejo-ssm.

When done

Run the gate, commit, push to canonical main, update docs/FEATURES.md if the ops surface is inventoried there, and link the agentic-os helper-repoint issue.

## Goal Give ward-kdl a chained-command read for the **forgejo API token** that goes through **kubectl into the k3s external-secrets mirror**, not AWS SSM - so reading the token needs only tailnet + kubectl, never a 2FA-gated AWS call. **Strictly limited to the forgejo key and nothing else.** ## Why Today the token is read via `ward-kdl ops aws ssm get-parameter --name /forgejo/api-token ...` (see `agentic-os/scripts/git-credential-forgejo-ssm.sh` and `scripts/docker-credential-forgejo-ssm`). That needs AWS auth -> 2FA. When the 2FA device is absent, Kai is locked out of her own git token. But external-secrets **already mirrors** `/forgejo/api-token` into a k8s Secret (`forgejo-runner-secrets`, key `api-token`, namespace `forgejo`, see `infrastructure/deploy/forgejo-runner.yml`) using the cluster's **static, non-2FA** `aws-credentials`. So the token is already reachable over the tailnet - we just need a ward-kdl verb that reads it, scoped to only that key. ## Dependency This depends on **cli-guard #173** (the `sealed` clause that blocks trailing caller args). With `sealed`, the verb below is strictly single-key in **pure KDL** - no hand-written gated Go needed. Carry this **after** that lands (or rebase on it). ## What to build Add an exec-dialect verb that reads exactly the forgejo secret, sealed so no caller args can widen it: ```kdl wrap ward-kdl ops forgejo-key { exec kubectl can run read { argv get secret forgejo-runner-secrets -n forgejo -o "go-template={{.data.api-token | base64decode}}" sealed describe "read ONLY the forgejo api-token from the k3s external-secrets mirror, decoded" } } ``` - `argv` pins the full kubectl command; `sealed` (cli-guard #173) forbids any trailing caller token, so the verb **cannot** reach another secret or namespace. - The `go-template ... base64decode` emits the **decoded** token on stdout, matching the old SSM `--output text` behavior so the credential helpers can swap one command for another. - Place the guardfile next to the existing exec guardfiles (`cmd/ward-kdl/`), mirroring `ward-kdl.kubectl.guardfile.kdl`. Add the matching `docs/ward-kdl/` entry. ## Repoint the credential helpers (cross-repo: agentic-os) Update `agentic-os/scripts/git-credential-forgejo-ssm.sh` and `scripts/docker-credential-forgejo-ssm` to fetch via `ward ops forgejo-key read`, **falling back to the SSM path** when kubectl/tailnet is unavailable (so an off-tailnet host still works). Keep both strictly on the forgejo token. File/carry that half in the agentic-os repo and link it. ## Acceptance - `ward ops forgejo-key read` returns the **decoded** forgejo api-token over the tailnet with **no AWS call and no 2FA**. - It **cannot** read any other secret or namespace - a pivot attempt (extra secret name, different `-n`) is refused by `sealed`, with a test. - The grant audit-logs through ward like other ops verbs. - Credential helpers repointed (kubectl-first, SSM-fallback), still strictly forgejo-only. - Docs updated; deliberately single-key for now (broader "cache all SSM in k3s" is `infrastructure#417`). ## Files `cmd/ward-kdl/` (new guardfile), `cmd/ward-kdl/ward-kdl.kubectl.guardfile.kdl` (reference), `docs/ward-kdl/`, `cmd/ward/execassets/`. Cross-repo: `agentic-os/scripts/git-credential-forgejo-ssm.sh`, `agentic-os/scripts/docker-credential-forgejo-ssm`. ## When done Run the gate, commit, push to canonical `main`, update `docs/FEATURES.md` if the ops surface is inventoried there, and link the agentic-os helper-repoint issue.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-386 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-01T04:40:31Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-ward-386` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-01T04:40:31Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - ward ops forgejo-key read landed on main: a sealed single-key kubectl reader for the Forgejo token, closes #386.

The core turned out smaller than the issue framed it. The named dependency (cli-guard #173, the sealed clause) has not landed anywhere, and cli-guard is read-only substrate in this container, so I could not add it. But sealed is just sugar for freeze-the-argv-and-forbid-trailing-tokens, and the pinned execverb already ships the primitives: an argv override, deny-when any-arg matches * for positionals, and a strict allow-flag allowlist for caller flags. Together those are a genuine seal, verified end to end - the clean call runs the exact argv, and every pivot vector (extra positional, -n, -o, --namespace) is refused before exec. When #173 ships upstream the two guards collapse to one sealed line.

Two things fought back. The go-template in the issue snippet (.data.api-token) is invalid - a hyphenated key needs index .data "api-token" - so I corrected it. And the 4000-char doc cap: both FEATURES.md and the surface index were already at the ceiling, so fitting a new surface meant trimming neighbouring bullets losslessly, which took a few rounds and a merge-conflict resolution against a concurrent kubectl-diff change on main.

Confidence is high on the ward side: build, vet, tests, and the full pre-commit suite pass, with a TestForgejoKeySealed regression guarding the seal. One honest follow-up is out of scope for this container - the agentic-os credential helpers (git-credential-forgejo-ssm.sh, docker-credential-forgejo-ssm) still need repointing to prefer ward ops forgejo-key read with an SSM fallback for off-tailnet hosts. That is a separate repo I was not granted here, so it is flagged in the commit body for a follow-up.

WARD-OUTCOME: done - ward ops forgejo-key read landed on main: a sealed single-key kubectl reader for the Forgejo token, closes #386. The core turned out smaller than the issue framed it. The named dependency (cli-guard #173, the sealed clause) has not landed anywhere, and cli-guard is read-only substrate in this container, so I could not add it. But sealed is just sugar for freeze-the-argv-and-forbid-trailing-tokens, and the pinned execverb already ships the primitives: an argv override, deny-when any-arg matches * for positionals, and a strict allow-flag allowlist for caller flags. Together those are a genuine seal, verified end to end - the clean call runs the exact argv, and every pivot vector (extra positional, -n, -o, --namespace) is refused before exec. When #173 ships upstream the two guards collapse to one sealed line. Two things fought back. The go-template in the issue snippet (.data.api-token) is invalid - a hyphenated key needs index .data "api-token" - so I corrected it. And the 4000-char doc cap: both FEATURES.md and the surface index were already at the ceiling, so fitting a new surface meant trimming neighbouring bullets losslessly, which took a few rounds and a merge-conflict resolution against a concurrent kubectl-diff change on main. Confidence is high on the ward side: build, vet, tests, and the full pre-commit suite pass, with a TestForgejoKeySealed regression guarding the seal. One honest follow-up is out of scope for this container - the agentic-os credential helpers (git-credential-forgejo-ssm.sh, docker-credential-forgejo-ssm) still need repointing to prefer ward ops forgejo-key read with an SSM fallback for off-tailnet hosts. That is a separate repo I was not granted here, so it is flagged in the commit body for a follow-up.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
coilyco-flight-deck/ward#386
No description provided.