hashicorp/nomad · error

invalid MaxTokenTTL value

Error message

invalid MaxTokenTTL value '%s' (should be between %s and %s)

What it means

ACLAuthMethod.Validate() rejects MaxTokenTTL when it is not within [minTTL, maxTTL], the bounds supplied by the server (system defaults or agent config). Both the error message and bounds are formatted with time.Duration.String().

Solutions

  1. Set MaxTokenTTL between the server's min and max (check agent config: audit the ACL token TTL defaults, e.g. 1m–24h).
  2. Read the error message bounds and align the value to them.
  3. Set MaxTokenTTL explicitly instead of relying on zero-value; use time.Duration constants in Go.
  4. If a longer TTL is genuinely needed, raise the server-side max token TTL config, then re-submit.

Example fix

// before
am.MaxTokenTTL = 0 // "no expiry"
// after
am.MaxTokenTTL = 24 * time.Hour
Defensive patterns

Strategy: validation

Validate before calling

minTTL, maxTTL := 1*time.Minute, 24*time.Hour // confirm with server config
if am.MaxTokenTTL < minTTL || am.MaxTokenTTL > maxTTL {
	return fmt.Errorf("MaxTokenTTL %s outside [%s, %s]", am.MaxTokenTTL, minTTL, maxTTL)
}

Try / catch

if err := am.Validate(minTTL, maxTTL); err != nil {
	if strings.Contains(err.Error(), "invalid MaxTokenTTL") {
		// parse bounds from message and clamp before retry
	}
	return err
}

Prevention

When it happens

Trigger: Upserting an ACL auth method whose MaxTokenTTL is below the server minimum or above the server maximum (e.g. 1s when min is 1m, or 0/8760h when the configured max is 24h). Also triggered when minTTL > MaxTokenTTL with a zero MaxTokenTTL where a nonzero is required.

Common situations: Setting MaxTokenTTL: "0s" expecting 'no expiry'; guessing TTLs in a config where the operator changed the server's default/max ACL token TTLs; unit mistakes like "2h" vs "2m".

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/c39a1f81695809ee. Report an issue: GitHub.

Appendix: source

Thrown at nomad/structs/acl.go:1448

		mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
	}

	if !slices.Contains([]string{ACLAuthMethodTokenLocalityLocal, ACLAuthMethodTokenLocalityGlobal}, a.TokenLocality) {
		mErr.Errors = append(
			mErr.Errors, fmt.Errorf("invalid token locality '%s'", a.TokenLocality))
	}

	if !slices.Contains(ValidACLAuthMethodTypes, a.Type) {
		mErr.Errors = append(
			mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
	}

	if err := a.Config.Validate(a.Type); err != nil {
		mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid config: %w", err))
	}

	if minTTL > a.MaxTokenTTL || a.MaxTokenTTL > maxTTL {
		mErr.Errors = append(mErr.Errors, fmt.Errorf(
			"invalid MaxTokenTTL value '%s' (should be between %s and %s)",
			a.MaxTokenTTL.String(), minTTL.String(), maxTTL.String()))
	}

	return mErr.ErrorOrNil()
}

// Sanitize returns a copy of the ACLAuthMethod with any secrets redacted
func (a *ACLAuthMethod) Sanitize() *ACLAuthMethod {
	if a == nil || a.Config == nil {
		return a
	}
	// copy to ensure we do not mutate a pointer pulled directly out of state.
	clean := a.Copy()
	// clean nested structs here, so it's obvious what all is being cleaned
	// in one spot, rather than follow a stack of sanitization calls.
	if clean.Config.OIDCClientSecret != "" {
		clean.Config.OIDCClientSecret = "redacted"

View on GitHub (pinned to 482b49bf1a)