juanfont/headscale · critical

creating OIDC provider from issuer config

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.

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.

Appendix: 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)