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.
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 stringView on GitHub (pinned to 565fd254d0)
Solutions
- Set oidc.issuer to the full issuer base URL, e.g. https://idp.example.com/realms/main
- Use the 'issuer' field from your IdP's /.well-known/openid-configuration, not an endpoint URL
- Ensure the scheme is http or https and the URL parses cleanly
- 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
- Copy the issuer verbatim from the IdP's /.well-known/openid-configuration
- Never use an endpoint URL (token/authorize) as the issuer
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
- extracting ID token
- authenticated principal is not in any allowed group
- oidc_client_secret and oidc_client_secret_path are mutually
- oidc.client_id is required when oidc.issuer is set
- oidc.client_secret or oidc.client_secret_path is required wh
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/ed9c9e96796d4e6b.
Report an issue: GitHub.