hashicorp/nomad · error
ErrInvalidKeyIDHeader
ErrInvalidKeyIDHeader
Error message
%w; must be one of: "x5t", "x5t#S256"
What it means
hashKeyID computes the key-ID header (x5t or x5t#S256) for the client-assertion JWT. It returns ErrInvalidKeyIDHeader wrapped when the configured header type is neither "x5t" nor "x5t#S256". The comment notes this is normally validated at upsert, so hitting it means an unvalidated value reached the builder.
Source
Thrown at lib/auth/oidc/client_assertion.go:205
}
// hashKeyID derives a "certificate thumbprint" that the OIDC provider uses
// to find the certificate to verify the private key JWT signature.
// https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.7
func hashKeyID(cert *x509.Certificate, header structs.OIDCClientAssertionKeyIDHeader) (string, error) {
var hasher hash.Hash
switch header {
case structs.OIDCClientAssertionHeaderX5t:
if fips140.Enabled() {
return "", errors.New("x5t assertion headers use SHA-1, which is forbidden in FIPS-140 mode")
}
hasher = sha1.New()
case structs.OIDCClientAssertionHeaderX5tS256:
hasher = sha256.New()
default:
// this should be validated long before here, at upsert
return "", fmt.Errorf(`%w; must be one of: "x5t", "x5t#S256"`, structs.ErrInvalidKeyIDHeader)
}
hasher.Write(cert.Raw)
hashed := hasher.Sum(nil)
return base64.RawURLEncoding.EncodeToString(hashed), nil
}
// newlineHeaders allows flexible copy-paste of a one-line key/cert PEM
// by adding newlines around "----BEGIN.*-----" and
// "-----END.*(KEY|CERTIFICATE)-----"
// it's okay to have extra whitespace, but it's imperative that there be
// at least one newline between the header/footer and the content.
func newlineHeaders(bts []byte) []byte {
cp := bytes.Clone(bts)
cp = bytes.TrimSpace(cp)
cp = bytes.ReplaceAll(cp, []byte("-----BEGIN"), []byte("\n-----BEGIN"))
cp = bytes.ReplaceAll(cp, []byte("-----END"), []byte("\n-----END"))
// key may be "PRIVATE KEY" or "RSA PRIVATE KEY", so just look for "KEY"
cp = bytes.ReplaceAll(cp, []byte("KEY-----"), []byte("KEY-----\n"))View on GitHub (pinned to 482b49bf1a)
Solutions
- Set the key-ID header to exactly "x5t" or "x5t#S256" (re-upsert the OIDC client assertion config)
- If stored state has a bad value, update it via the API/CLI so upsert validation runs again
- Check for tooling or scripts writing the field directly to the store and route them through the validated API
Example fix
// before "key_id_header": "x5t-S256" // after "key_id_header": "x5t#S256"
Defensive patterns
Strategy: validation
Validate before calling
var validKeyIDHeaders = map[string]bool{"x5t": true, "x5t#S256": true}
if !validKeyIDHeaders[keyIDHeader] {
return fmt.Errorf("key_id_header %q invalid; must be x5t or x5t#S256", keyIDHeader)
} Type guard
func isValidKeyIDHeader(h string) bool {
return h == "x5t" || h == "x5t#S256"
} Try / catch
// wrap and inspect via errors.Is
if err != nil {
if errors.Is(err, structs.ErrInvalidKeyIDHeader) {
// fix config and re-upsert the OIDC client
}
return err
} Prevention
- Always set key-ID headers through the validated upsert API, not direct state writes
- Whitelist "x5t" and "x5t#S256" in any tooling that writes this field
- Watch for typo variants like "x5t-S256" in config templates
- Add a config lint/CI check asserting allowed header values
When it happens
Trigger: BuildClientAssertionJWT calls hashKeyID with an OIDC client assertion key-ID header value that is not "x5t" or "x5t#S256" — typically a value written directly to state/store bypassing upsert validation, or an API client typo.
Common situations: Hand-edited Nomad ACL/OIDC objects in state; older clients or tooling writing unsupported header names; schema/version drift after an upgrade where validation rules changed; typo like "x5t-S256".
Related errors
- missing Audience
- PrivateKey is required for `private_key` KeySource
- unknown OIDC KeySource %q
- invalid config: %w
- %w; key header for key ID must be %q
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/2ac685b9e5cc68da.
Report an issue: GitHub.