juanfont/headscale · error · errOIDCClientIDRequired

oidc.client_id is required when oidc.issuer is set

Error message

oidc.client_id is required when oidc.issuer is set

What it means

errOIDCClientIDRequired is an unexported config validation error in hscontrol/types/config.go:37 returned at config.go:388 when oidc.issuer is configured but oidc.client_id is empty. An OIDC client cannot authenticate to the IdP without its client ID, so headscale fails config validation at startup.

Source

Thrown at hscontrol/types/config.go:37

	"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)

Solutions

  1. Create an OIDC client in your IdP and set its ID under oidc.client_id
  2. Check YAML indentation so client_id sits inside the oidc: block at the right level
  3. Ensure no environment override sets oidc.client_id to an empty string
  4. Restart headscale after adding the value

Example fix

# before
oidc:
  issuer: https://idp.example.com

# after
oidc:
  issuer: https://idp.example.com
  client_id: headscale
Defensive patterns

Strategy: validation

Validate before calling

if cfg.OIDC.Issuer != "" && cfg.OIDC.ClientID == "" {
    return errors.New("oidc.client_id required when issuer is set")
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Enabling the oidc section with an issuer but omitting client_id; YAML indentation placing client_id outside the oidc block; templating that leaves client_id as an empty string.

Common situations: First-time OIDC setup where the operator configures the issuer before creating the IdP client; env-var overrides clearing the value; config refactor breaking key nesting.

Related errors


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