juanfont/headscale · error · errOidcMutuallyExclusive
oidc_client_secret and oidc_client_secret_path are mutually
Error message
oidc_client_secret and oidc_client_secret_path are mutually exclusive
What it means
errOidcMutuallyExclusive is an unexported config validation error in hscontrol/types/config.go:35 returned at config.go:1189 when both oidc_client_secret and oidc_client_secret_path are set at once. Headscale refuses the ambiguity of two secret sources; exactly one must be provided (see also errOIDCClientSecretRequired for the neither-set case).
Source
Thrown at hscontrol/types/config.go:35
"github.com/rs/zerolog"
"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"
)
View on GitHub (pinned to 565fd254d0)
Solutions
- Keep exactly one source: remove oidc_client_secret and keep oidc_client_secret_path (preferred for secrets)
- Or remove oidc_client_secret_path and keep the inline value only in non-production setups
- Check environment-variable overrides (viper merges env) for a second definition
- Restart headscale after correcting the config
Example fix
# before oidc: client_secret: my-secret client_secret_path: /run/secrets/oidc # after oidc: client_secret_path: /run/secrets/oidc
Defensive patterns
Strategy: validation
Validate before calling
if cfg.OIDC.ClientSecret != "" && cfg.OIDC.ClientSecretPath != "" {
return errors.New("set only one of client_secret / client_secret_path")
} Type guard
null
Try / catch
null
Prevention
- Prefer file-based secrets and delete inline keys when migrating
- Lint configs in CI for double-defined OIDC secret keys
When it happens
Trigger: A config file (or environment overrides) setting both oidc.client_secret and oidc.client_secret_path; templated configs that inject the inline secret while also mounting a secret file path.
Common situations: Migrating from inline secrets to file-based secrets and forgetting to remove the old key; Helm/Nix templates defaulting both keys; environment variable plus config file both defining the secret.
Related errors
- oidc.client_secret or oidc.client_secret_path is required wh
- extracting ID token
- authenticated principal is not in any allowed group
- oidc.issuer must be a valid http(s) URL
- oidc.client_id is required when oidc.issuer is set
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/734cbf1cb5bd34ff.
Report an issue: GitHub.