Newer
Older
openstack-caracal-dc-dc / docs / changelog-20260713-git-credential-store.md

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.