juanfont/headscale · critical
creating OIDC provider from issuer config: %w
Error message
creating OIDC provider from issuer config: %w
What it means
Returned by NewAuthProviderOIDC when oidc.NewProvider fails to discover the OIDC issuer's well-known configuration (fetching {issuer}/.well-known/openid-configuration and validating it). This runs at startup with the caller's bounded context, so an unreachable or misbehaving issuer fails fast rather than hanging.
Source
Thrown at hscontrol/oidc.go:92
// steps. It is a bounded [expirable.LRU] keyed by OIDC state, evicting oldest
// entries to keep the cache footprint constant under attack.
authCache *expirable.LRU[string, AuthInfo]
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
}
func NewAuthProviderOIDC(
ctx context.Context,
h *Headscale,
serverURL string,
cfg *types.OIDCConfig,
) (*AuthProviderOIDC, error) {
// Use the caller's context (bounded, see app.go) so a slow or unreachable
// issuer fails discovery within the timeout instead of hanging startup.
oidcProvider, err := oidc.NewProvider(ctx, cfg.Issuer)
if err != nil {
return nil, fmt.Errorf("creating OIDC provider from issuer config: %w", err)
}
oauth2Config := &oauth2.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Endpoint: oidcProvider.Endpoint(),
RedirectURL: strings.TrimSuffix(serverURL, "/") + "/oidc/callback",
Scopes: cfg.Scope,
}
authCache := expirable.NewLRU[string, AuthInfo](
authCacheMaxEntries,
nil,
authCacheExpiration,
)
return &AuthProviderOIDC{
h: h,View on GitHub (pinned to 565fd254d0)
Solutions
- curl the discovery endpoint from the headscale host: curl -v https://issuer.example.com/.well-known/openid-configuration
- Fix the issuer URL to exactly the value the provider documents (scheme, host, path, no trailing slash)
- Install/trust the issuer's CA certificate if it uses a private PKI
- Check the oidc.timeout relevant egress/firewall rules and DNS resolution
- Verify the discovery document's 'issuer' field matches the configured issuer string exactly
Example fix
# before oidc: issuer: https://sso.example.com/oauth2/ # after oidc: issuer: https://sso.example.com/oauth2
Defensive patterns
Strategy: retry
Validate before calling
discURL := strings.TrimSuffix(cfg.Issuer, "/") + "/.well-known/openid-configuration"
resp, err := http.Get(discURL) // with timeout
if err != nil || resp.StatusCode != 200 {
return fmt.Errorf("OIDC issuer discovery unreachable at %s: %v", discURL, err)
} Try / catch
oidcProvider, err := oidc.NewProvider(ctx, cfg.Issuer)
if err != nil {
// startup error: log issuer URL and wrapped cause, fix config/network, restart
return nil, fmt.Errorf("creating OIDC provider from issuer config: %w", err)
} Prevention
- Smoke-test the discovery endpoint from the headscale host before enabling OIDC
- Pin the issuer URL exactly as documented by the provider, and trust its CA in the host store
- Keep OIDC issuer dependencies in retry-backed startup probes so transient provider outages don't strand deployments
When it happens
Trigger: oidc.issuer URL wrong or not reachable (DNS failure, TLS error, timeout); the issuer's discovery document is invalid or serves HTML (e.g. behind an identity provider login page); clock skew breaking TLS validation; the issuer does not implement discovery.
Common situations: Typo in the oidc.issuer config value; self-signed issuer certificate without the CA trusted by headscale; network egress blocked from the headscale host; provider outage; issuer URL with a trailing path mismatch (discovery requires exact issuer-string match).
Related errors
- errOIDCIssuerInvalid
- trusted_proxies[%d] %q: %w
- test pattern is required as first argument or use --test fla
- auth request rejected
- STUN address not set
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/6f286f77fdda1c3c.
Report an issue: GitHub.