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>
56 lines
1.6 KiB
Go
56 lines
1.6 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
saCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
|
saTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" //nolint:gosec // path, not a secret
|
|
)
|
|
|
|
// k8sBearerRT adds this pod's ServiceAccount bearer to every request. k3s serves
|
|
// its OIDC discovery/JWKS over the cluster CA and requires an AUTHENTICATED
|
|
// request (anonymous is 401), so the JWKS fetch must carry a token — ADR-0011.
|
|
type k8sBearerRT struct {
|
|
token string
|
|
base http.RoundTripper
|
|
}
|
|
|
|
func (b k8sBearerRT) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
r.Header.Set("Authorization", "Bearer "+b.token)
|
|
return b.base.RoundTrip(r)
|
|
}
|
|
|
|
// k8sOIDCClient builds an HTTP client that can reach the in-cluster k8s OIDC
|
|
// discovery + JWKS: it trusts the cluster CA and carries this pod's SA bearer.
|
|
// Returns an error (not running in a pod, files unreadable) so the caller can
|
|
// skip the k8s issuer without disabling other auth.
|
|
func k8sOIDCClient() (*http.Client, error) {
|
|
ca, err := os.ReadFile(saCAFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read cluster CA: %w", err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(ca) {
|
|
return nil, fmt.Errorf("parse cluster CA: no certs in %s", saCAFile)
|
|
}
|
|
tok, err := os.ReadFile(saTokenFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read SA token: %w", err)
|
|
}
|
|
return &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
Transport: k8sBearerRT{
|
|
token: strings.TrimSpace(string(tok)),
|
|
base: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}},
|
|
},
|
|
}, nil
|
|
}
|