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

  1. curl the discovery endpoint from the headscale host: curl -v https://issuer.example.com/.well-known/openid-configuration
  2. Fix the issuer URL to exactly the value the provider documents (scheme, host, path, no trailing slash)
  3. Install/trust the issuer's CA certificate if it uses a private PKI
  4. Check the oidc.timeout relevant egress/firewall rules and DNS resolution
  5. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/6f286f77fdda1c3c. Report an issue: GitHub.