-
feat: server-level GitHub machine credential for authenticated requests (#109)
ci/woodpecker/tag/docker Pipeline was successfulreleased this
2026-08-10 21:42:39 +10:00 Why
Anonymous GitHub is capped at 60 requests/hour and cannot read private repositories. A machine credential usable by a free (non-enterprise) account is needed to lift the request budget to ~5000/hr and to read private-repo release assets.
Builds on the background syncer (#108, now merged to
master); this diff is the auth changes only.How
- Add
internal/githubauth: a process-wide GitHub credential delivered via env/secret, applied by default to every outbound GitHub request (releases scan, ranged asset-header GETs, and the generic-github byte proxy for private assets). - Support two modes:
- PAT —
GITHUB_TOKENsent asAuthorization: Bearer <token>. - GitHub App —
GITHUB_APP_ID+GITHUB_APP_INSTALLATION_ID+ private key (GITHUB_APP_PRIVATE_KEYinline PEM orGITHUB_APP_PRIVATE_KEY_PATH). Mint a short-lived RS256 JWT with stdlibcrypto/rsa(no new dependency), exchange it atPOST /app/installations/{id}/access_tokensfor a ~1h installation token, cache it, and single-flight a refresh a few minutes before expiry.
- PAT —
- Inject at the two GitHub call paths: the rpm github provider header builder (releases + ranged fetches) and the generic provider
AuthHeaders(byte proxy, github.com hosts only; the pre-signedobjects.githubusercontent.comredirect deliberately gets no Authorization). - Honor precedence: a remote's own
username/passwordoverrides the server credential; no credential configured stays anonymous (current behavior). - Fail closed at startup on partial App configuration (e.g. App id without a private key); a token-and-App conflict is also rejected.
- Never persist the credential to the DB, return it from an API, or log it (token-exchange failures never echo the response body).
- Read config via the existing
getenvconvention; document PAT vs App setup, the free-account fine-grained PAT scopes (Contents:read + Metadata:read), precedence, and the rate-limit implication.
Rate limit
Authenticated requests share the syncer's single global limiter — no second limiter is added. A token raises the effective GitHub ceiling (~5000/hr vs ~60/hr), so the limiter defaults stay safe.
Tests
internal/githubauthandinternal/provider/{rpm,generic}:- PAT attaches the correct
Authorizationheader to releases + asset-header requests. - App mints a valid RS256 JWT (verified against the app public key), exchanges it at a mocked endpoint, reuses the cached token without re-exchanging, refreshes near expiry, and single-flights concurrent callers.
- Per-remote credential overrides the server credential (rpm + generic).
- No credential → no
Authorizationheader, requests still succeed anonymously. - ETag/304 flow still works with auth attached.
- The credential does not appear in a remote's serialized JSON.
- Config validation: no-config is anonymous; partial App config and token/App conflict both error.
Verified fail-before/pass-after for the injection tests.
gofmt -l,go build ./...,go vet ./...,go test ./...all clean (26 packages).Reviewed-on: #109
Co-authored-by: Ben Vincent ben@unkin.net
Co-committed-by: Ben Vincent ben@unkin.netDownloads
- Add