# 2026-07-13 -- unattended git push from the jumphost (SEC-005)

The jumphost clone could not `git push`: every attempt died with
`could not read Username for 'https://git.baldurkeep.com'`. The branch sat **12 commits
ahead of origin for two days** as a result -- a real data-loss exposure, since all of the
D-112 / OPNsense edge work existed only on this box. Fixed by giving git a stored
credential. Operator ruled the posture; recorded as **SEC-005**.

## What was configured

    git config --global credential.helper store     # -> ~/.git-credentials, mode 600

The token itself was written by the OPERATOR via a hidden prompt
(`getpass` + `/dev/tty`), URL-encoded, file created 0600 from `os.open` so it is never
briefly world-readable. **The token was never read into agent context** -- verification is
by *using* the credential (push succeeds / file survives), never by printing it. Per the
CLAUDE.md secrets rule.

Result: `f5fd4f5..425d434` pushed; `git ls-remote` confirms origin HEAD == local HEAD.

## Two GitBucket behaviours worth knowing (both cost us a cycle)

**1. The git username is NOT the login you type into the web UI.**
The operator signs in to GitBucket with `jesse.austin@neumatrix.com`. That address is
rejected for git transport. Measured against the real endpoints:

| username | `/api/v3/user` | `info/refs?service=git-receive-pack` | verdict |
|---|---|---|---|
| `jesse.austin@neumatrix.com` | 401 | 401 | rejected |
| **`jesse.austin`** | 401 | **200** | **CAN PUSH** |
| `JANeumatrix` (the git `user.name`) | 401 | 401 | rejected |

Note the whole API column is 401: **GitBucket does not accept PAT basic-auth on
`/api/v3/`, but does accept it for git transport.** So an API probe is the WRONG way to
validate this credential -- it produces a false negative. Validate against
`info/refs?service=git-receive-pack`, which is what push actually uses.

**2. `credential.helper store` DELETES the credential when the server rejects it.**
git calls `credential reject` on a 401, and the store helper erases the matching line.
The first (bad-username) push therefore emptied `~/.git-credentials` to 0 bytes, and the
follow-up diagnostics were all authenticating with an empty string -- which looked like a
corrupt/unparseable file and sent the investigation the wrong way for a beat. If the file
mysteriously empties, the credential was rejected; it did not fail to write.

Consequence for tooling: **validate before writing.** The helper script now confirms push
rights against GitBucket and only then persists the token, so a bad entry cannot silently
wipe the store.

## Security posture (SEC-005)

A long-lived, account-wide PAT now sits in plaintext on the jumphost, readable by anything
running as `jessea123`. This is the accepted cost of unattended push, ruled by the operator.
Mitigations in place: dedicated token (not a reused personal one), mode 600, and a recorded
revoke path. GitBucket has no per-repo token scoping, so least-privilege is not achievable
here -- the control is rotation, not scope.

**Revoke/rotate:** GitBucket -> Account Settings -> Applications. Do it at v1 close, or
immediately if the jumphost is shared, rebuilt, or its disk leaves the premises.

## Revert

    git config --global --unset credential.helper
    rm -f ~/.git-credentials

Then revoke the token in GitBucket. Pushes go back to requiring interactive auth.
