8 Commits
Author SHA1 Message Date
mathiasandClaude Opus 5 09bde6df8a fix(health): /healthz answers now, and /readyz takes a degraded pod out
CD / Deploy via GitOps (push) Successful in 5s
CD / Lint / Test / Vet (push) Successful in 30s
CD / Build & Import (push) Successful in 15s
Closes #57. Consumes mcp-chassis v0.6.0 (mcp-chassis#4).

Two halves of one six-day outage. The chassis fix alone would not have fixed
this one.

/HEALTHZ WAS A CONSTANT

newHealthzHandler marshalled its body ONCE at startup and served those bytes for
the process lifetime. On 2026-09-14 it reported jwt.status=degraded for six days
after Authentik had recovered, because the answer was computed before Authentik
was up and could never change. A health endpoint that cannot change its answer
is not a health endpoint.

It now evaluates readiness per request. That is also what drives recovery: the
chassis resolves a pending issuer on demand, so every probe is a retry.

/READYZ IS NEW, AND IT IS THE ACTUAL CONTROL

Through all three occurrences the pod was 1/1 Running and readiness-green while
401ing every OAuth caller. k8s had no signal and kept routing to it. /readyz
answers 503 while JWT auth is degraded, so the pod leaves the Service instead of
serving guaranteed failures, and the kubelet's probe cadence becomes the retry
loop — no background goroutine, no context to own.

/healthz deliberately stays 200. The process is alive; a liveness probe must not
restart it, because restarting does not make the IdP come back sooner.

A server with no issuer configured is always ready. Making readiness depend on an
issuer it was never given would pull every static-token-only MCP out of its
Service — asserted, not assumed.

THE RETRY-WITHOUT-THE-K8S-ISSUER FALLBACK IS GONE

It existed because one unreachable issuer used to fail the whole constructor and
take Authentik auth down with it. Since v0.6.0 each issuer resolves lazily and
independently, so a down issuer degrades only itself. The warning it logged —
"multi-issuer init failed; retrying without the k8s issuer" — was the first line
of today's incident; it no longer has a reason to exist.

A constructor error now means a config error, which exits non-zero rather than
warning and continuing.

TESTS

healthz_live_test.go, written first and red (newReadyzHandler undefined, and
newHealthzHandler's signature could not express a state that changes). The
sharpest assertion is that readiness is evaluated TWICE for two requests — a
handler that caches cannot pass it.

The existing table test is ported, not weakened: same three cases, same expected
statuses, only the mechanism by which state reaches the handler changed.

Verified: task check EXIT=0 (lint, vet, race). The lint failure it surfaced was
pre-existing and is fixed in the preceding commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:06:15 +02:00
mathiasandClaude Opus 4.8 240c3ec081 feat(auth): trust the k8s cluster OIDC issuer for ServiceAccount tokens (ADR-0011 D1)
CD / Lint / Test / Vet (push) Successful in 17s
CD / Build & Import (push) Successful in 22s
CD / Deploy via GitOps (push) Successful in 3s
Additive: gitea-mcp now validates JWTs from a LIST of issuers — the existing
Authentik issuer (DEX_ISSUER_URL) AND, when K8S_ISSUER_URL is set, the in-cluster
k8s OIDC issuer for audience-bound ServiceAccount tokens. Lets in-cluster pods
authenticate with kubelet-rotated projected SA tokens instead of a static bearer.

- config: K8S_ISSUER_URL + K8S_MCP_AUDIENCE.
- cmd/gitea-mcp/k8soidc.go: HTTP client that fetches the k8s OIDC discovery/JWKS
  with the cluster CA + this pod's SA bearer (k3s requires an authed fetch;
  anonymous is 401).
- main.go: build the issuer list; switch NewJWTValidator -> NewMultiJWTValidator.
  The k8s issuer is best-effort — if its client can't be built (not in a pod) or
  its discovery is unreachable at startup, it is DROPPED and we fall back so
  Authentik/static auth is never taken down. Smoke-tested: off-pod it logs the
  skip and starts static-only; static-bearer /mcp returns 400 (auth passed), not 401.
- bump mcp-chassis v0.3.0 -> v0.5.0 (multi-issuer + per-issuer HTTPClient).

Refs infra ADR-0011; enables retiring the in-cluster static bearer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:16:39 +02:00
mathiasandClaude Opus 4.8 72ba89be63 feat(auth): adopt chassis v0.3.0 — auth audit logs + 503 on Dex outage (#6, #9)
CD / Build & Import (push) Successful in 22s
CD / Lint / Test / Vet (push) Successful in 7s
CD / Deploy via GitOps (push) Has been skipped
Bumps mcp-chassis to v0.3.0, which adds structured audit logging on every auth
rejection and returns 503 temporarily_unavailable (not a silent 401) when Dex is
unreachable at validation time. Wires slog.SetDefault so those audit lines flow
through gitea-mcp's JSON handler.

Together with the earlier /healthz jwt-status reporting and startup degradation
warning, this closes #6 (Dex-down is now observable and distinct from a bad
token) and #9 (auth failures are audit-logged: reason, IP, token type, hashed
fingerprint — never the raw token).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:17:39 +02:00
mathiasandClaude Opus 4.8 3329ff3088 fix(deps): migrate mcp-chassis to git.d-ma.be path — unblock CD build (#74)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 22s
CD / Deploy via GitOps (push) Successful in 4s
The image build's `go mod download` failed on the stale
gitea.d-ma.be/mathias/mcp-chassis import (the gitea→git rename; the server no
longer serves a matching go-import meta tag). This is the latent #74 breakage
flagged for gitea-mcp, triggered by the first clean rebuild since the rename
(the #42 push). Point at git.d-ma.be/mathias/mcp-chassis v0.2.0 + go mod tidy.

Unblocks deploying the #42 create_project_from_template fix. gitea-mcp's own
module path stays gitea.d-ma.be (main module, not fetched — separate cleanup).
Refs #74, #42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:06:35 +02:00
mathiasandClaude Opus 4.7 658f4ba84f feat(auth): migrate to gitea.d-ma.be/mathias/mcp-chassis v0.1.0
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Failing after 2s
CD / Deploy via GitOps (push) Has been skipped
First real port of the MCP chassis library — abort-criterion check for
spike S3 of the 2026-05 homelab architecture review.

Changes:
- Drop internal/auth/jwt.go (~79 LOC) — chassis provides JWTValidator
  with identical signature.
- Drop internal/auth/bearer.go (~42 LOC) — chassis BearerMiddleware
  has the same static-or-JWT semantics plus an optional WWW-Authenticate
  resource_metadata challenge (consumed via new resourceMetadataURL arg).
- Drop internal/auth/bearer_test.go — same scenarios are covered in
  the chassis bearer_test.go now.
- main.go: import chassis as `chassisauth`, build resourceMetadataURL
  only when both DexIssuerURL + MCPResourceURL are set, replace the
  inline /.well-known/oauth-protected-resource handler with the chassis
  ProtectedResourceHandler.

internal/auth/caller.go (oauth2-proxy header → context) stays — chassis
out-of-scope.

Net LOC change: -~150 LOC duplicated infra + a 5-LOC import.
go.mod gains gitea.d-ma.be/mathias/mcp-chassis v0.1.0 (jwx/v2 + testify
already transitive, no new top-level deps).

Verifies abort criterion: one PR, one binary's worth of port, task check
green (lint + test + vet + govulncheck clean). Per the S3 spike spec,
this clears the chassis to continue. Next port: hyperguild/ingestion
(brain-mcp), filed as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:25:23 +02:00
mathias 91be18c100 feat(auth): JWT-or-static middleware + /.well-known/oauth-protected-resource (issue #5)
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
- internal/auth/jwt.go: JWTValidator via lestrrat-go/jwx/v2, JWKS auto-refresh
- internal/auth/bearer.go: replace Gitea PAT validation with JWT->static->default chain
- internal/gitea/client.go: always use service PAT; remove TokenFromContext lookup
- internal/config/config.go: add DexIssuerURL, MCPAudience, MCPResourceURL, StaticToken
- cmd/gitea-mcp/main.go: wire validator, fix /.well-known to return real AS list
- bearer_test.go: rewrite for new API
2026-05-12 11:30:52 +02:00
mathias 4274b48ea5 feat(gitea): default-branch lru cache
Shared LRU avoids repeated Gitea calls for default-branch resolution;
the simple stdlib map alternative would race on concurrent access without
a mutex per entry, which is more code than the LRU.
2026-05-04 23:06:06 +02:00
mathiasandClaude Sonnet 4.6 d399a216c1 feat(config): env-var loading
Add internal/config package with Config struct and Load() function.
Reads GITEA_BASE_URL, GITEA_API_TOKEN, GITEA_MCP_ALLOWED_OWNERS,
GITEA_MCP_ORIGIN_ALLOWLIST, GITEA_MCP_PORT with sensible defaults.
Wire cfg.Port into main.go. TDD: tests written first, then impl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:19:31 +02:00