* fix(security): encrypt session cookies + fail closed on invalid config
Batch 1 of security audit remediation (ranks 1, 2, 6).
- session.go: derive independent HMAC + AES-256 keys via stdlib HKDF-SHA256
and build the gorilla cookie store with both, so session cookies are now
encrypted, not merely signed. The single-key store previously left OIDC
access/refresh/ID tokens recoverable from raw cookie bytes. Cookie format
changes, so existing sessions are invalidated on deploy (one-time re-login).
- main.go: call config.Validate() at construction and error out on failure,
instead of silently substituting a public hardcoded encryption key for
empty/short keys (which allowed session forgery). The yaegi analyzer
passes via .traefik.yml testData.
- settings.go: isValidSecureURL permits plaintext HTTP for loopback hosts
only (RFC 8252); remote providers must still use HTTPS.
- tests: complete configs that did not satisfy Validate(); add regression
tests in security_audit_fixes_test.go.
Configs below documented minimums (rateLimit < 10, key < 32 chars) are now
rejected at startup (fail closed).
* fix(security): validate discovered OIDC endpoints + pin introspection host
Batch 2 of security audit remediation (ranks 3, 4).
- url_helpers.go: add validateDiscoveredEndpoint, an SSRF screen for endpoints
taken from the provider discovery document (jwks_uri, token, authorization,
revocation, end_session, introspection, registration). Blocks link-local
(cloud metadata 169.254.169.254), multicast, unspecified and private
addresses (unless allowPrivateIPAddresses); blocks loopback unless the
configured providerURL is itself loopback (dev/test). Cross-domain JWKS
hosts (e.g. Google) stay allowed. Add sameHost helper.
- main.go: updateMetadataEndpoints screens every discovered endpoint and
blanks any that fail (fail closed downstream). The introspection endpoint
carries the client secret via HTTP Basic, so it is additionally pinned to
the providerURL host to stop a poisoned discovery document exfiltrating the
secret to an attacker-controlled host.
- tests: regression tests for the SSRF guard and the host pin.
* fix(security): close open redirects + anchor excluded-URL matching
Batch 3 of security audit remediation (ranks 5, 14, 15).
- auth_flow.go: run the stored incoming path through normalizeLogoutPath
before using it as the post-login redirect, so //evil.com and /\evil.com
payloads become host-relative (open-redirect, rank 5).
- url_helpers.go: excluded-URL matching is anchored at a natural boundary
(exact, sub-path "/", or file extension "."), so excluding "/public" no
longer also bypasses auth on "/publicsecret"; "/favicon" still matches
"/favicon.ico" (rank 14).
- internal/utils: X-Forwarded-Host is sanitized (first value only; reject
CRLF/whitespace/multi-value) before building redirect URLs (rank 15).
- helpers.go: the logout redirect used when there is no provider end-session
endpoint is host-relative, never an absolute URL derived from the
client-controllable request host (logout open-redirect, rank 15).
- tests: update two logout cases that asserted the old absolute redirect;
add regression tests.
* fix(security): reject unverified Azure tokens; fix transport TLS reuse
Batch 4 of security audit remediation (ranks 7, 11).
- token_validation_rs.go: an Azure nonce-bearing access token that cannot be
cryptographically verified no longer returns "authenticated" when there is
no ID token to corroborate it; it refreshes (if possible) or forces
re-authentication instead of failing open (rank 7).
- http_client_pool.go: the at-limit transport-reuse path now takes the write
lock before mutating refCount (fixes a data race) and only reuses a
transport whose TLS settings (CA pool + InsecureSkipVerify) match the
caller's, never one with a different trust store; if none matches it returns
nil so the caller falls back to a verifying default transport (rank 11).
- tests: add a transport-pool TLS-isolation regression test.
* fix(security): stop logging templated header values (token leak)
Batch 5 of security audit remediation (rank 16).
middleware.go: templated downstream headers commonly carry the access token
(e.g. "Authorization: Bearer {{.AccessToken}}"). The debug log line printed
the full header value, leaking credentials into logs. Log the header name and
byte length instead.
* fix(security): cache-key collision, cache-config divergence, fleet cleanup
Batch 6 of security audit remediation (ranks 9, 10, 12).
- token_manager.go: detectTokenType keys its cache on a SHA-256 hash of the
full token instead of the first 32 chars (which are only the base64url JWT
header). Distinct tokens sharing alg+kid no longer collide and get
mis-classified (rank 10).
- cache_manager.go: the process-global cache manager is initialized once and
shared across plugin instances; it now logs a loud warning when a later
instance requests a different explicit Redis backend that is silently
ignored, surfacing the cross-instance state-isolation hazard (rank 9).
- singleton_resources.go / main.go / utilities.go: track a process-global live
instance count; the shared singleton-token-cleanup task is stopped only when
the LAST instance shuts down, so one instance's Close() (e.g. a config reload)
no longer kills cleanup for surviving instances (rank 12).
- tests: update TestDetectTokenTypeCaching for the new key; add regression tests.
* fix(security): bound introspection cache + cookie lifetime to config
Batch 7 of security audit remediation (ranks 8, 13).
- token_introspection.go: when requireTokenIntrospection is enabled, cap the
positive introspection-result cache at 30s (instead of 5m) so a token
revoked at the provider stops passing within ~30s, matching the operator's
near-real-time revocation expectation (rank 8).
- session.go: bind the cookie store's MaxAge to the configured sessionMaxAge,
so the cookie codec's cryptographic timestamp validity is no longer fixed at
gorilla's 30-day default; a stolen cookie is valid only for the configured
session lifetime (rank 13).
- tests: add a cookie-lifetime regression test.
* fix(security): low-severity hardening (cache, DoS caps, PKCE, throttle)
Batch 8 of security audit remediation — low severity
(ranks 24, 25, 27, 29, 31, 36, 37, 41, 45, 46, 49).
- universal_cache.go: updateLocalCache updates an existing key in place instead
of orphaning its LRU element and double-counting currentSize/currentMemory
(rank 36 — the only production-reachable bug in this batch).
- jwk.go / metadata_cache.go / token_introspection.go: bound response bodies
with io.LimitReader (1 MiB) to prevent memory exhaustion from a hostile or
buggy provider (ranks 24, 25).
- jwk.go: skip JWKs not usable for signature verification (use != sig, or
key_ops without "verify") when building the key set (rank 49).
- auth_flow.go: fail closed at the callback when PKCE is enabled but the code
verifier is missing, instead of silently dropping it (rank 27).
- utilities.go / main.go: match allowedUserDomains case-insensitively (rank 31).
- bearer_auth.go: a single success no longer wipes an active per-IP penalty;
the counter resets only when no penalty is in effect (rank 29).
- main.go: handle (not discard) the NewSessionManager error (rank 37).
- error_recovery.go: take a write lock in isServiceDegraded (it deletes from a
map); compare retryable-error substrings case-insensitively (ranks 45, 46).
- singleton_resources.go: bind the generic-cache cleanup goroutine to the
resource-manager shutdown channel so it cannot outlive its owner (rank 41).
- tests: update the bearer throttle test to the corrected penalty semantics.
* fix(security): header sanitization, issuer pinning, fail-closed paths
Batch 9 of security audit remediation (ranks 18, 19, 20, 21, 22, 30, 33, 34).
- middleware.go / bearer_auth.go: sanitize claim-derived values on the cookie
auth path before injecting them into downstream headers. Drop group/role and
identifier values containing control chars, bidi-override runes, or the
, ; = delimiters (a comma would inject phantom entries into X-User-Groups);
reject control/bidi/over-length in rendered templated header output (but
permit , ; = in free-form values such as a bearer token). The bearer path
already sanitized; the cookie path did not (ranks 33, 34).
- main.go / metadata_cache.go: pin the discovered issuer to the configured
provider host (sameHost) and refuse/never-cache a mismatch, so a poisoned
discovery document cannot redefine the JWT trust anchor (ranks 21, 22).
- token_introspection.go: when a distinct API audience is configured, fail
closed on a missing or mismatched introspection audience; aud parsed as
string-or-array per RFC 7662 (rank 19).
- logout.go: front-channel logout requires a matching issuer; an empty iss is
rejected (blocks unauthenticated forced-logout via a known sid) (rank 30).
- token_validation_rs.go: an opaque access token with no ID token and no
successful introspection fails closed (re-auth) instead of authenticating
(ranks 18, 20).
- tests: realistic same-host provider mocks; regression tests for the header
sanitization distinction and the fail-closed paths.
* chore(security): remove unwired dead code with latent footguns
Batch 10 of security audit remediation — delete confirmed-dead, unwired
subsystems (ranks 26, 35, 50). None had a production caller (grep-verified);
removal eliminates the latent footguns and ~2.1k lines of dead code.
- token_validator.go (deleted): an unused *TokenValidator whose validateJWT set
Valid=true with NO signature verification — a severe footgun if ever wired
(rank 50). The wired RS-aware validators are unaffected.
- security_monitoring.go (deleted): an unused *SecurityMonitor / ExtractClientIP
that trusted spoofable X-Forwarded-For / X-Real-IP. The live bearer throttle
uses clientIPForBearer (RemoteAddr-only), unchanged (rank 35).
- dynamic_client_registration.go: removed the RFC 7592 management methods
(Update/Read/DeleteClientRegistration) that dereferenced an attacker-
influenced RegistrationClientURI with the registration token attached and no
HTTPS/SSRF gate, and had no callers. The wired RFC 7591 RegisterClient and
credential-store helpers are kept (rank 26).
- tests: removed the tests covering the deleted code.
* chore: add Makefile with yaegi load validation
No Makefile existed. The new `yaegi-validate` target interprets the plugin
under the yaegi interpreter the same way Traefik loads it, catching yaegi-only
incompatibilities (unsupported stdlib symbols, reflection edge cases) that the
native `go build` / `go test` toolchain does not. Importing the plugin forces
yaegi to interpret every file plus its vendored deps; CreateConfig + New
exercise the instantiation path.
- cmd/yaegicheck/main.go: the load driver, marked //go:build ignore so it is
excluded from `go build ./...` (avoids VCS-stamping a main binary, which
fails in git-worktree layouts) yet is run explicitly by yaegi.
- Makefile: build / fmt / vet / lint / test / vendor / yaegi-validate / check
targets; `make check` runs vet + tests + yaegi-validate.
Verified: `make yaegi-validate` passes on this branch — the HKDF cookie
encryption, net-based endpoint validation, and claim sanitizers all interpret
and instantiate cleanly under yaegi.
* ci: bump workflow Go toolchain to 1.25; pin yaegi-validate to v0.16.1
Traefik v3.7.1 (the deployed version) is built with `go 1.25.0`, so the PR and
release workflows now use Go 1.25.x to match the toolchain Traefik uses.
Important distinction: the CI Go version is the build TOOLCHAIN. The plugin's
actual interpreter-compatibility ceiling is the yaegi version Traefik bundles
(v0.16.1, which declares go 1.21 and ships a ~Go 1.22 stdlib symbol surface),
NOT the CI Go version. That ceiling is enforced by `make yaegi-validate` plus
the go.mod language directive — e.g. it is why HKDF is hand-rolled with
hmac+sha256 rather than Go 1.24's crypto/hkdf, which yaegi v0.16.1 lacks.
Also pin Makefile YAEGI_VERSION to v0.16.1 (what Traefik v3.7.1 vendors) so
yaegi-validate exercises the real deployed interpreter instead of @latest,
which could pass on a newer yaegi that supports symbols the deployed one does
not.
* docs: align README/CONFIGURATION with branch behavior changes
- excludedURLs: documented as segment/extension-boundary matching (was
"prefix-matched") — "/public" no longer also matches "/publicsecret" (rank 14).
- Front-channel logout now requires a matching `iss`; requests without one are
rejected with 400 (rank 30).
- Add an "Upgrading from an earlier release" note: session cookies are now
AES-256 encrypted with lifetime tracking sessionMaxAge (one-time re-login on
upgrade), and invalid configuration (rateLimit < 10, key < 32 bytes, missing
callbackURL, non-HTTPS remote providerURL) now fails closed at startup.
* fix: remove staticcheck-flagged unused functions; wire staticcheck into make check
CI Static Analysis (standalone staticcheck) failed with U1000 "unused":
- dynamic_client_registration.go: deleteCredentialsFromStore — its only caller
was the RFC 7592 DeleteClientRegistration removed in the dead-code batch.
- token_test.go: createTestJWTSimple — its only callers were the TokenValidator
tests removed in the same batch.
Both confirmed to have zero remaining callers and removed. build / vet /
go test ./... / staticcheck ./... all green.
The pre-commit hook runs golangci-lint, but CI runs standalone staticcheck
(which flags U1000). Add a `staticcheck` Makefile target and include it in
`make check` so this class of finding is caught locally before push.
* fix(test): stabilize flaky TestWorkerPool_TaskPanic
tasksFailed is incremented in the worker's deferred recover(), which runs after the panicking task's own defer wg.Done(). wg.Wait() could therefore return before the failure was recorded, so reading the counter immediately raced and flaked on slow CI runners. Poll until the failure lands (2s budget) instead. Verified 200x plain + 50x under -race/GOMAXPROCS=1.
Adds optional extraAuthParams map[string]string config.
Extra params are appended to the authorization request but can never
override plugin-managed params (client_id, state, nonce, etc.).
Discloses the single anonymous adoption ping sent on first plugin
instantiation. Points users to the upstream README section for the
disclosure pattern and to the local telemetry.go for the inline
implementation.
* docs: bearer-token auth design spec
* docs: harden bearer-auth spec with security review findings
* feat(bearer): opt-in M2M bearer-token authentication
Adds an opt-in Authorization: Bearer <jwt> path for machine-to-machine
clients. Replaces and supersedes the broken approach in PR #93
(synthetic-session that omitted user_identifier and skipped ID-token
rejection / replay-protection-semantics / kid-pinning / etc.).
Design
Two auth entrypoints feed one shared post-auth pipeline:
cookie path ─┐
├── forwardAuthorized(rw, req, *principal)
bearer path ─┘ (roles/groups, header injection, security
headers, cookie strip, forward)
buildPrincipalFromSession and buildPrincipalFromBearerToken produce
the same `principal` value type. forwardAuthorized is session-agnostic
and runs the existing post-auth work; processAuthorizedRequest now
wraps it with the session-specific concerns (backchannel-logout,
dirty/Save). The cookie path's behaviour is byte-identical to before
this PR; the existing test suite passes unmodified.
Security hardening baked into the bearer path
- Audience MANDATORY. Startup fails when EnableBearerAuth=true and
Audience is empty.
- BearerIdentifierClaim defaults to "sub"; "email" is rejected at
startup to avoid the unverified-email spoofing footgun. Cookie
path's UserIdentifierClaim is unaffected and still defaults to
"email".
- ID tokens explicitly rejected via the existing detectTokenType
helper (nonce, typ=at+jwt, token_use, scope, aud-vs-clientID
heuristics); belt-and-braces nonce/token_use=id rejection on top.
- alg pinned to asymmetric allowlist (RS/PS/ES 256/384/512) BEFORE
JWKS fetch, blocking alg=none and alg=HS* probes from amplifying
into upstream calls.
- kid length capped at 256 bytes and charset-restricted before JWKS
fetch, blocking pathological-kid JWKS amplification.
- Multi-audience tokens require azp == clientID.
- iat upper-age bound (MaxTokenAgeSeconds, default 24h) bounds clock-
manipulation and forever-token abuse.
- Identifier sanitization: length cap, control-char + bidi-override
+ delimiter (, ; =) rejection.
- Per-IP failure throttle: configurable threshold/window/penalty;
returns 429 + Retry-After. Limits offline-guessing-style attacks
and protects the shared rate-limiter / JWKS endpoint.
- JTI replay marking suppressed via new internal verifyOpts
{skipReplayMarking} so the same bearer can be reused until exp;
the blacklist Get stays active so RevokeToken still terminates a
bearer token immediately. The existing exported VerifyToken
interface is unchanged so all mocks continue to work.
- Cookie wins by default when both bearer and cookie are present
(safer against browser/extension/proxy bearer injection).
Operator can flip via BearerOverridesCookie.
- Authorization header stripped on forward by default; also stripped
on excluded URLs so the token can't leak into health/metrics
downstream logs.
- Optional RFC 7662 introspection via existing
requireTokenIntrospection. Introspection-endpoint failure returns
503 (distinguishes infra from token rejection).
- 401s use RFC 6750 WWW-Authenticate hints (toggleable). Failure
reason is logged at debug; raw tokens are never logged.
Implementation
- principal.go: pure-data principal type and buildPrincipalFromSession.
- bearer_auth.go: alg/kid pin, classifier, identifier sanitization,
multi-aud azp gate, iat age check, per-IP failure tracker,
handleBearerRequest, buildPrincipalFromBearerToken.
- token_manager.go: VerifyToken now wraps a new verifyTokenWithOpts
that accepts internal-only verifyOpts. Existing callers, the
TokenVerifier interface, and all mocks unchanged.
- middleware.go: extracted forwardAuthorized from
processAuthorizedRequest; wired bearer detection after init wait
+ after bypass; excluded-URL Authorization strip when bearer
enabled.
- settings.go: ten new config fields with defaults applied in
CreateConfig.
- main.go: startup validation for audience + identifier-claim
guard; bearer failure tracker init.
Tests
- bearer_auth_test.go: table-driven helper tests for every new
component (parseBearerJOSEHeader, sanitizeBearerIdentifier,
resolveBearerIdentifier, enforceMultiAudienceAzp, enforceIatAge,
bearerFailureTracker, detectBearerToken). Integration tests
through ServeHTTP covering happy path, ID-token rejection,
alg=none rejection, oversized kid, multi-aud with/without azp,
iat-too-old, bidi identifier, replay (100x reuse), 429 throttle
trip, excluded-URL strip, roles gate, cookie-wins precedence,
BearerOverridesCookie, oversized token, malformed JWT,
feature-off pass-through. Startup validation for audience-
required and email-identifier-rejected.
- All existing tests pass unmodified (cookie-path regression).
- go vet clean. golangci-lint clean (0 issues). Race detector
clean on bearer tests.
Documentation
- README.md: bearer auth section with security highlights and
config snippet; doc link in the index.
- .traefik.yml: commented config block exposing every bearer knob.
- docs/CONFIGURATION.md: new subsection with full parameter table.
- docs/BEARER_AUTH.md: threat model, hardening matrix, failure
response table, operational guidance, known follow-ups.
- docs/superpowers/specs/2026-05-18-bearer-token-auth-design.md:
design spec + security-review hardening history.
* fix(cache): redact raw cache keys in debug logs (CodeQL go/clear-text-logging)
CodeQL flagged 9 high-severity alerts (go/clear-text-logging) where the
in-memory cache and the hybrid L1+L2 backend printed `key=%s` at debug.
Cache callers (token cache, blacklist, introspection cache) pass raw
access / refresh / id tokens as cache keys, so any debug-enabled
deployment would write them to log streams.
Pre-existing issue. CodeQL started flagging it on this PR because the
new bearer-auth path adds a data-flow source (req.Header.Get("Authorization"))
that reaches the existing logging sinks via the same cache. The cookie
path had the same risk but wasn't tracked as taint by CodeQL.
Fix: hash the key (SHA-256[:8] hex) before printing. Same approach the
bearer-auth logger uses for principal identifiers (spec §13). Doesn't
change cache semantics — same key still produces the same hash, so
debug correlation across log lines is preserved without exposing the
raw value.
Touches both affected packages:
- internal/cache/cache.go (2 sites: Set + LRU eviction)
- internal/cache/backends/hybrid.go (12 sites: L1/L2 read/write/fallback)
New helper `redactKey` colocated with each package (unexported,
package-local) keeps the change blast radius narrow. Tests green; lint
clean.
* docs(bearer): how to obtain bearer tokens from the OIDC provider
Adds a section walking operators through the OAuth 2.0 client_credentials
flow (RFC 6749 §4.4) and the JWT bearer assertion alternative (RFC 7523),
with a worked Auth0-shape curl example, a per-provider quick reference
(Auth0, Okta, Keycloak, Entra v2, Cognito, GitLab, Google), operational
notes (token TTL, caching, JWKS rotation, revocation, scope vs audience,
secret hygiene), and a three-line validation loop.
Most common operator confusion: "I enabled the feature but tokens get
401'd" — almost always missing or wrong audience. The new section makes
the audience-matching requirement loud, with per-provider parameter
names so people don't have to dig through IdP docs.
Locations:
- docs/BEARER_AUTH.md — full section under "Quick start"
- README.md — short snippet + deep link
revocation endpoints, joining the existing client_secret_post default.
Both are opt-in via the new clientAuthMethod config field. Closes#135.
private_key_jwt (RFC 7523 §2.2 / OpenID Connect Core §9)
========================================================
Plugin signs a short-lived JWT with a configured private key and presents
it as client_assertion. Use when the IdP enforces short secret TTLs or
requires secretless client auth (Microsoft Entra ID / Azure AD, Okta,
Auth0, Keycloak).
New Config fields:
clientAuthMethod (default: client_secret_post)
clientAssertionPrivateKey (inline PEM)
clientAssertionKeyPath (PEM file path; mutually exclusive)
clientAssertionKeyID (JWS kid header — required)
clientAssertionAlg (default: RS256; RS/PS/ES 256–512 supported)
PEM forms accepted: PKCS#8, PKCS#1, SEC1.
Assertion claims: iss=sub=clientID, aud=tokenURL, iat=now, exp=now+60s,
random 16-byte hex jti per request. ECDSA signatures are raw r||s per
RFC 7515 (not ASN.1).
client_secret_basic (RFC 6749 §2.3.1)
=====================================
Sends credentials in the Authorization: Basic header instead of the
body. Both halves are form-urlencoded individually before base64 — that
encoding step is required by the spec and is NOT what stdlib's
http.Request.SetBasicAuth does, so the plugin uses its own helper. The
form body omits client_id and client_secret on this path.
Wire-up
=======
Both methods are dispatched at the same two call sites:
helpers.go:exchangeTokens — auth_code + refresh_token grants
token_manager.go:RevokeTokenWithProvider — RFC 7009 revocation
Existing clientSecret deployments are unaffected — empty
clientAuthMethod maps to the historical client_secret_post behavior, and
clientAssertion remains nil unless the new fields are set.
Yaegi compatibility
===================
All required crypto/rsa, crypto/ecdsa, crypto/x509, encoding/pem and
crypto/sha256/384/512 symbols are exposed by the traefik/yaegi stdlib
symbol tables (RSA SignPKCS1v15 + SignPSS, ECDSA Sign,
ParsePKCS8/1PrivateKey, ParseECPrivateKey).
Tests (16 new)
==============
Algorithm-family coverage:
TestIssue135_SignerRSAFamily — RS256/384/512 + PS256/384/512
TestIssue135_SignerECDSAFamily — ES256/384/512, raw r||s shape
TestIssue135_SignerRejectsAlgKeyMismatch
TestIssue135_SignerJTIUniqueness — 50 sigs, all jti distinct
TestIssue135_SignerPEMVariants — PKCS#8, PKCS#1, SEC1
Config validation:
TestIssue135_ConfigValidation — full Validate() matrix
TestIssue135_ConfigKeyPathLoadsFile
Wire-up:
TestIssue135_AuthCodeExchangeUsesAssertion
TestIssue135_RefreshTokenUsesAssertion
TestIssue135_BackcompatClientSecretPath
TestIssue135_RevocationUsesAssertion
TestIssue135_BuildSignerFromInlineConfig
TestIssue135_BuildSignerDefaultsToRS256
TestIssue135_ClientSecretBasicAuth — Authorization header, no body creds
TestIssue135_ClientSecretBasicURLEncodesReservedChars — :, +, /, @, =, &
TestIssue135_ClientSecretBasicRevocation — revocation parity
Documentation
=============
README.md — required-row note + 5 optional rows + dedicated section
docs/CONFIGURATION.md — new Client Authentication section with three
method subsections, OpenSSL keygen snippet, RFC links
docs/index.html — 5 new config-table rows + Private Key JWT
explainer card
.traefik.yml + examples/complete-traefik-config.yaml — commented
opt-in example
Out of scope (deferred)
=======================
mTLS / tls_client_auth (RFC 8705) — separate change; requires per-call
http.Client with tls.Config.Certificates and conflicts with the current
pooled HTTP client architecture.
The redis.enableTLS / redis.tlsSkipVerify settings were accepted by the
config layer but silently dropped before reaching the connection pool, so
the plugin always dialed Redis in plaintext. This blocked TLS-only Redis
deployments such as AWS ElastiCache with in-transit encryption.
- Add EnableTLS, TLSSkipVerify, TLSServerName to backends.Config and
PoolConfig and forward them through universal_cache_singleton ->
backends.Config -> PoolConfig.
- In the connection pool, dial via tls.Dialer.DialContext (TLS 1.2
minimum) with SNI defaulting to the host part of the configured
Address when TLSServerName is empty, so ElastiCache cluster endpoints
validate out of the box. Plain dial path now also propagates ctx.
- Add regression tests covering successful TLS negotiation with skip-
verify, rejection of self-signed certs without skip-verify, rejection
of plain TCP servers when EnableTLS=true, and unaffected plaintext
behavior.
- Document maxRefreshTokenAgeSeconds (added in 1b6c861) and the implicit
SSE / WebSocket auth bypass (added in 684a990) in README.md,
docs/CONFIGURATION.md and docs/index.html.
- Add the missing redis.tlsSkipVerify row to docs/index.html and clarify
the redis.enableTLS description.
patch-release
* Allow internal IPs for OIDC configuration via extra flag.
Addresses issue #97
* Allow for internal IPs in OIDC configuration.
Addresses issue #97.
* feat: Add allowPrivateIPAddresses config option for internal networks
Adds a new configuration option `allowPrivateIPAddresses` that allows
OIDC provider URLs to use private IP addresses (10.x.x.x, 172.16-31.x.x,
192.168.x.x). This is useful for internal deployments where Keycloak or
other OIDC providers run on private networks without DNS resolution.
Security considerations:
- Loopback addresses (127.0.0.1, localhost, ::1) remain blocked
- Link-local addresses (169.254.x.x) remain blocked
- Default is false (secure by default)
Fixes#97
* feat: Support non-email user identifiers for Azure AD
Add userIdentifierClaim configuration option to support Azure AD users
without email addresses. This allows using alternative JWT claims like
"sub", "oid", "upn", or "preferred_username" for user identification.
- Default behavior uses "email" claim (backward compatible)
- Falls back to "sub" claim if configured claim is missing
- allowedUsers matches against the configured claim value
- allowedUserDomains only applies when using email-based identification
Fixes#95
* Race condition on traefik pod startup
When the plugin initializes and calls GetMetadataWithRecovery():
1. Checks cache first (if metadata is cached, returns immediately)
2. Creates a retry executor with startup-optimized settings (10 attempts, 1s delays)
3. Attempts to fetch metadata from the OIDC provider
4. If the fetch fails with a retryable error (connection refused, EOF, TLS/certificate errors, Traefik default cert), it waits and retries
5. After 10 attempts or on a non-retryable error, returns the error
This allows the plugin to handle the race condition where:
- Traefik initializes the plugin before routes are established
- Traefik serves its default certificate before loading real ones
- The OIDC provider pod isn't fully ready yet
Fixes issue #90
* Race condition on traefik pod startup
When the plugin initializes and calls GetMetadataWithRecovery():
1. Checks cache first (if metadata is cached, returns immediately)
2. Creates a retry executor with startup-optimized settings (10 attempts, 1s delays)
3. Attempts to fetch metadata from the OIDC provider
4. If the fetch fails with a retryable error (connection refused, EOF, TLS/certificate errors, Traefik default cert), it waits and retries
5. After 10 attempts or on a non-retryable error, returns the error
This allows the plugin to handle the race condition where:
- Traefik initializes the plugin before routes are established
- Traefik serves its default certificate before loading real ones
- The OIDC provider pod isn't fully ready yet
Fixes issue #90
* Headers too big and 431 responses
Added new option `minimalHeaders` to reduce the size of forwarded headers from the auth middleware to backend services.
- When minimalHeaders: false (default): All headers are forwarded as before
- X-Forwarded-User (always set)
- X-Auth-Request-Redirect
- X-Auth-Request-User
- X-Auth-Request-Token (the large ID token)
- X-User-Groups, X-User-Roles (if configured)
- When minimalHeaders: true: Reduces header overhead
- X-Forwarded-User (always set)
- X-User-Groups, X-User-Roles (still forwarded if configured)
- Custom templated headers (still processed)
- Skipped: X-Auth-Request-Token, X-Auth-Request-User, X-Auth-Request-Redirect
Fixes issues #64 and #86
* Add redis support for distributed caching
* Move towards the self-provided Redis connection pool and RESP protocol implementation.
Official redis client library won't work with yaegi.
* fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.
* fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.
* fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.
* fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.
* fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.
* ... and another all nighter.
* fixup! ... and another all nighter.
* fixup! fixup! ... and another all nighter.
* fixup! fixup! fixup! ... and another all nighter.
* Resolve issue #85 by adding ability to set custom claims in JWT tokens
* Remove redundant validation in auth middleware ( issue #89 )
* Add ability to set cookie prefix for session cookies ( #87 )
* fixup! Add ability to set cookie prefix for session cookies ( #87 )
* Add ability to set cookie max age - issue #91
* Potential fix for code scanning alert no. 10: Size computation for allocation may overflow
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* fixup! Merge main into 0.8.0-redis: resolve conflicts
---------
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* Add sharded cache and prevention of CPU spikes / locks
* Add dynamic client registration with oidc provider
* Fix race condition introduced during the sharded cache implementation.
* Add page for traefikoidc.
* Add ability to disable replay protection. - This is useful for runs with multiple traefik replicas to avoid false positives and tokens re-creation.
* Enhance the CI/CD pipelines
* Increase test coverage.
* Update vendored dependencies.
* Update behaviour on forceHTTPS as per issue #82
* Speed improvements.
After introduction of introspection the plugin became significantly slower.
This commit introduces several optimizations to bring the speed back up.
* Add relevant documentation and tests.
* Automatic discovery of the scopes.
Issue #61 raised very valid concerns about users configuring scopes that are not supported by the provider.
This change introduces automatic discovery of supported scopes by fetching the provider's discovery document and filtering out unsupported scopes.
Before:
User configures: scopes: ["openid", "profile", "email", "offline_access"]
Self-hosted GitLab: "The requested scope is invalid, unknown, or malformed"
Authentication: ❌ FAILS
After:
User configures: scopes: ["openid", "profile", "email", "offline_access"]
Middleware checks discovery doc → offline_access not supported
Automatically filters to: ["openid", "profile", "email"]
Authentication: ✅ SUCCEEDS
* Resolves issue #74 by enabling user to specify expected audience in the configuration.
* Fix flaky tests.
* Fix bug affecting Azure OIDC authentication ( and most likely others )
* Fixes issue #51
* Ensure that appended roles are unique. Update the documentation.
* Improvements targetting possible memory usage spikes.
* Additional fixes and cleanup
* Refactoring code to fix the issues identified by the users.
* Modernize run
* Fieldalignment
* Multiple changes to improve performance and reduce complexity.
- Optimise the errors and recovery.
- Deduplicate code in metadata cache.
- Remove unused performance monitoring code.
- Simplify session management and settings handling.
* Fix claims issue.
* Add ability to overwrite the default scopes in the settings file
* Well.. that escalated quickly.
Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ).
* Bugfix #51: Ensures that user provided scopes overrides work.
* fixup! Bugfix #51: Ensures that user provided scopes overrides work.
* fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work.
* Abstract the provider logic into a separate package.
* Additional micro fixes and cleanups.
* Simplify all the things.
* fixup! Simplify all the things.
* fixup! fixup! Simplify all the things.
* fixup! fixup! fixup! Simplify all the things.
* fixup! fixup! fixup! fixup! Simplify all the things.
* ...
* Cleanup tests.
* fixup! Cleanup tests.
* fixup! fixup! fixup! Cleanup tests.
* fixup! fixup! fixup! fixup! Cleanup tests.
* fixup! fixup! fixup! fixup! fixup! Cleanup tests.
* Issue #53: Fix CSRF token handling in reverse proxy
1. ✅ HTTPS Detection Fixed (session.go:723)
- Now uses X-Forwarded-Proto header instead of r.URL.Scheme
- Properly detects HTTPS in reverse proxy environments
2. ✅ SameSite Cookie Attribute Fixed
- Removed automatic SameSiteStrictMode for HTTPS (would break OAuth)
- Keeps SameSiteLaxMode to allow OAuth callbacks from external domains
- Only uses Strict for AJAX requests which don't involve OAuth redirects
3. ✅ Cookie Domain Handling Fixed
- Now respects X-Forwarded-Host header for cookie domain
- Ensures cookies are set for the public domain, not internal proxy domain
4. ✅ EnhanceSessionSecurity Properly Integrated
- Function is now actually called during session save
- Applies security enhancements without breaking OAuth flow
Why Issue #53 Failed Before:
1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back)
2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail
3. Cookie domain might have been wrong (internal vs public domain)
Why It Works Now:
1. Cookies are properly marked Secure for HTTPS
2. Uses SameSite=Lax to allow OAuth provider callbacks
3. Cookie domain uses public domain from X-Forwarded-Host
4. CSRF token persists through the entire OAuth flow
* Next set of enhancements together with memory usage improvements.
* Memory leak fixes and optimisations.
* CSRF and Cookie Domain fixes
* fixup! CSRF and Cookie Domain fixes
* Metadata cache leak fix + profiling
* fixup! Metadata cache leak fix + profiling
* Memory leaks hunting, part 1337.
* Further pursue of perfection.
* fixup! Further pursue of perfection.
* fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.
* Clear race conditions
* fixup! Clear race conditions
* Weekend fun with memory leaks
* Splitting code into multiple files with reasonable testing coverage.
```
ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements
ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements
ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements
github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements
ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements
ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements
ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements
ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements
ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements
ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements
ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements
ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements
ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements
````
* fixup! Splitting code into multiple files with reasonable testing coverage.
* fixup! fixup! Splitting code into multiple files with reasonable testing coverage.
* Weekend fun with further optimisations.
* fixup! Weekend fun with further optimisations.
* fixup! fixup! Weekend fun with further optimisations.
* fixup! fixup! fixup! Weekend fun with further optimisations.
* fixup! fixup! fixup! fixup! Weekend fun with further optimisations.
* fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations.
* Pre-release cleanup.
* Enhance test coverage.
* fixup! Enhance test coverage.
* fixup! fixup! Enhance test coverage.
* fixup! fixup! fixup! Enhance test coverage.