juanfont/headscale · error · errOIDCIssuerInvalid

oidc.issuer must be a valid http(s) URL

Error message

oidc.issuer must be a valid http(s) URL

What it means

errOIDCIssuerInvalid is an unexported config validation error in hscontrol/types/config.go:36 returned at config.go:384 (wrapped with the offending value) when oidc.issuer is set but does not parse as a valid http(s) URL. The issuer URL is the base for OIDC discovery, so it must be absolute http or https.

Solutions

  1. Set oidc.issuer to the full issuer base URL, e.g. https://idp.example.com/realms/main
  2. Use the 'issuer' field from your IdP's /.well-known/openid-configuration, not an endpoint URL
  3. Ensure the scheme is http or https and the URL parses cleanly
  4. Restart headscale and confirm OIDC discovery completes

Example fix

# before
oidc:
  issuer: idp.example.com/realms/main

# after
oidc:
  issuer: https://idp.example.com/realms/main
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(issuer)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
    return errors.New("issuer must be a valid http(s) URL")
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Setting oidc.issuer to a bare hostname ('idp.example.com'), an ldaps:// or other scheme, a URL with spaces, or a value that fails url.Parse/httpscheme checks; copy-pasting the issuer with quotes or trailing characters.

Common situations: Forgetting the https:// scheme in the issuer; using the token endpoint URL instead of the issuer base URL; config templating injecting unescaped values.

Related errors


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

Appendix: source

Thrown at hscontrol/types/config.go:36

	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
	"go4.org/netipx"
	"tailscale.com/net/tsaddr"
	"tailscale.com/tailcfg"
	"tailscale.com/types/dnstype"
	"tailscale.com/util/set"
)

const (
	PKCEMethodPlain string = "plain"
	PKCEMethodS256  string = "S256"

	defaultNodeStoreBatchSize = 100
)

var (
	errOidcMutuallyExclusive     = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
	errOIDCIssuerInvalid         = errors.New("oidc.issuer must be a valid http(s) URL")
	errOIDCClientIDRequired      = errors.New("oidc.client_id is required when oidc.issuer is set")
	errOIDCClientSecretRequired  = errors.New("oidc.client_secret or oidc.client_secret_path is required when oidc.issuer is set")
	errServerURLSuffix           = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable")
	errServerURLSame             = errors.New("server_url cannot use the same domain as base_domain in a way that could make the DERP and headscale server unreachable")
	errInvalidPKCEMethod         = errors.New("pkce.method must be either 'plain' or 'S256'")
	errTrustedProxyZeroRange     = errors.New("0.0.0.0/0 and ::/0 are not allowed")
	ErrNoPrefixConfigured        = errors.New("no IPv4 or IPv6 prefix configured, minimum one prefix is required")
	ErrInvalidAllocationStrategy = errors.New("invalid prefix allocation strategy")
)

type IPAllocationStrategy string

const (
	IPAllocationStrategySequential IPAllocationStrategy = "sequential"
	IPAllocationStrategyRandom     IPAllocationStrategy = "random"
)

type PolicyMode string

View on GitHub (pinned to 565fd254d0)