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.

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.

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