Documentation
¶
Overview ¶
Command wardyn-git-helper is the git credential helper for Wardyn-governed agent sandboxes. It implements the git-credential protocol (get/store/erase) as a thin broker client: on "get" it resolves a stored Personal Access Token matched by host (Azure DevOps / GitLab / a plain GitHub PAT via WARDYN_GIT_PAT_GRANTS, username resolved by the broker), calls the proxy's local mint route, then prints the credentials to stdout for git to consume. The credential never touches disk or argv — stdout only, consumed ephemerally.
It does NOT serve the GitHub App lane for a BROKERED run — one whose repos the git broker serves (WARDYN_GIT_BROKER_REPOS non-empty). There, agent-run points each granted repo at the proxy's /wardyn/gh/ route, which mints the repo-scoped installation token SERVER-SIDE, applies the per-repo allowlist, and parses every git-receive-pack ref against the run's branch namespace. A GitHub host is therefore REFUSED here (see resolveGrantForHost) so the token never enters the sandbox at all — it used to be printed to stdout, and a push carrying it straight to github.com:443 is an opaque tunnel no pkt-line parser can inspect. A run with a github_token grant but NO brokered repos has no broker route and no injected deny, so it still mints here, unchanged; so does a GitHub host with no App grant at all, falling through to a git_pat grant.
The helper is configured GLOBALLY (git config --system credential.helper), so for any host it does NOT broker (no matching grant) it emits NOTHING and exits 0, letting git fall through to its normal behavior. It must never break unrelated git auth.
Invariants:
- The brokered credential is never persisted to disk, env, or args — it is written to stdout only, for git to consume ephemerally (security invariant 1). It is never logged.
- Requests to the proxy use a direct transport (Proxy: nil) — the proxy URL is a known on-segment address, not subject to HTTP_PROXY env.
- If no grant is configured for the host (a github host absent from WARDYN_GIT_PAT_GRANTS, or any other host absent from it) the helper exits 0 with no output so git falls through to its normal prompting. This is intentional: runs without grants must not be blocked. A github host on a BROKERED run (WARDYN_GIT_BROKER_REPOS non-empty) takes the same silent-exit path, by refusal rather than by absence.
Caller authentication (in-sandbox token-exfiltration hardening):
The sandbox shares one kernel and one uid (agent). Without a check, ANY
in-sandbox process that speaks git's credential protocol — a sub-process, a
snooping `wardyn attach` shell, a careless tool — could invoke this helper
and have a LIVE GitHub token streamed to it on stdout. To raise the bar, the
helper requires the caller to PRESENT a per-run secret before it emits a
token:
- The canonical per-run secret lives in a FILE that is owned by the agent
uid and mode 0400 (provisioned by agent-run; see deploy/images/*/agent-run).
Its path is passed to the helper via the git credential.helper config
(`--secret-file <path>`). The helper reads it as the EXPECTED value.
- The caller PRESENTS the secret via the WARDYN_GIT_HELPER_SECRET
environment variable. agent-run exports it for its descendants (the agent
process and its git invocations), so it is process-scoped — it is NOT in
the container-wide sandbox env, so a separate attach exec or any
non-descendant process does not inherit it.
- Before minting, the helper constant-time-compares the presented value
against the expected file content. A mismatch (or no presented value)
fails CLOSED: no token is emitted, but git is NOT errored (we return
success with empty output, exactly like the no-grant case) so the helper
never breaks git itself.
- If no secret file is provisioned for the run (the file is absent/empty —
e.g. an interactive run that never goes through agent-run, or a
deployment that has not wired the gate), the helper preserves the legacy
behaviour and mints, so legitimate git is never blocked.
RESIDUAL (documented honestly): this secret binds a caller going through
THIS BINARY — it stops a caller that speaks git's credential protocol
(a sub-process, a snooping attach shell) by routing through
wardyn-git-helper itself. It does NOT bind the credential at its source:
the proxy's local mint route (POST /wardyn/v1/credentials/mint) is itself
unauthenticated — like every other /wardyn/... local route, it trusts
anything that can reach it as "the sandbox" — so a caller willing to skip
this binary, read the grant id straight out of the container-wide
WARDYN_GIT_PAT_GRANTS env (unlike WARDYN_GIT_HELPER_SECRET, that one is
NOT process-scoped), and POST the route directly is not bound at all.
Within the "goes through this binary" set, the secret further raises the
bar from "any in-sandbox process" to "code executing AS the agent uid": a
process running as the agent user can still read the 0400 secret file (or
read WARDYN_GIT_HELPER_SECRET from a descendant's /proc/<pid>/environ, or
simply be a descendant of agent-run) and thereby obtain the token via the
binary too. Closing either gap requires authenticating the CALLER at the
proxy's mint route itself (e.g. SPIFFE-attested mint, or a per-invocation
nonce), which is future work. Interactive runs ARE gated: their idle
main process is agent-run too, and its --idle path calls
provision_git_helper_secret exactly as task mode does (see
deploy/images/common/agent-run-lib.sh). What differs is inheritance —
`wardyn attach` is a FRESH exec, not a descendant of that process tree,
so it never inherits WARDYN_GIT_HELPER_SECRET and this helper refuses it
until the human presents the secret themselves. That refusal is a real
gate, not a wall: the 0400 file is agent-readable, which is the same
residual described above.
The secret is never logged.
Usage (set by git credential helper config):
git config credential.helper \ '/usr/local/bin/wardyn-git-helper --secret-file /home/agent/.wardyn/git-helper.secret'
git invokes: wardyn-git-helper [--secret-file <path>] get|store|erase