hashicorp/nomad · error
token expiration TTL '%s' should not be negative
Error message
token expiration TTL '%s' should not be negative
What it means
ACLToken validation (create path, existing == nil) rejects a token whose ExpirationTTL is negative with 'token expiration TTL %q should not be negative'. TTLs are parsed to time.Duration; a negative value can never represent a valid time-til-expiry, so Nomad fails the request before computing ExpirationTime.
Source
Thrown at nomad/structs/acl.go:787
case ACLClientToken:
if len(a.Policies) == 0 && len(a.Roles) == 0 {
mErr.Errors = append(mErr.Errors, errors.New("client token missing policies or roles"))
}
case ACLManagementToken:
if len(a.Policies) != 0 || len(a.Roles) != 0 {
mErr.Errors = append(mErr.Errors, errors.New("management token cannot be associated with policies or roles"))
}
default:
mErr.Errors = append(mErr.Errors, errors.New("token type must be client or management"))
}
// There are different validation rules depending on whether the ACL token
// is being created or updated.
switch existing {
case nil:
if a.ExpirationTTL < 0 {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("token expiration TTL '%s' should not be negative", a.ExpirationTTL))
}
if a.ExpirationTime != nil && !a.ExpirationTime.IsZero() {
if a.CreateTime.After(*a.ExpirationTime) {
mErr.Errors = append(mErr.Errors, errors.New("expiration time cannot be before create time"))
}
// Create a time duration which details the time-til-expiry, so we can
// check this against the regions max and min values.
expiresIn := a.ExpirationTime.Sub(a.CreateTime)
if expiresIn > maxTTL {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("expiration time cannot be more than %s in the future (was %s)",
maxTTL, expiresIn))
} else if expiresIn < minTTL {
mErr.Errors = append(mErr.Errors,View on GitHub (pinned to 482b49bf1a)
Solutions
- Provide a positive TTL (e.g. 1h) or omit ExpirationTTL entirely for a non-expiring token.
- Audit scripts for inverted duration arithmetic and clamp TTLs to >= 0 before the API call.
- If 'no expiration' was intended, ensure the field is zero/nil rather than a sentinel negative value.
Example fix
// before token.ExpirationTTL = -1 * time.Hour // sentinel for 'no expiry' // after var token.ExpirationTTL time.Duration // leave zero for non-expiring token
Defensive patterns
Strategy: validation
Validate before calling
func validateExpirationTTL(ttl time.Duration) error {
if ttl < 0 {
return fmt.Errorf("expiration TTL %s is negative", ttl)
}
return nil
} Type guard
func ttlUsable(d *time.Duration) bool {
return d == nil || *d >= 0
} Try / catch
if err := token.Validate(nil); err != nil {
if strings.Contains(err.Error(), "should not be negative") {
return fmt.Errorf("fix TTL (no sentinel negatives): %w", err)
}
return err
} Prevention
- Never use negative durations as 'unset' sentinels; use zero value or omit the field.
- Guard duration arithmetic: compute TTL as end.Sub(start) and assert >= 0.
- Type-check config loaders so bare '-1' strings do not parse into TTL fields.
- Add pre-submit validation of all token TTL inputs.
When it happens
Trigger: Creating an ACL token (`nomad acl token create -ttl ...` or the ACL token API) with a negative ExpirationTTL, e.g. -5m, often from arithmetic or a misconfigured variable.
Common situations: Scripts computing TTL as (start - end) accidentally inverted; config files with placeholder values like '-1' meaning 'unset' that get parsed as a duration; templating bugs injecting a minus sign.
Related errors
- expiration time cannot be more than %s in the future (was %s
- expiration time cannot be less than %s in the future (was %s
- token name too long
- client token missing policies or roles
- management token cannot be associated with policies or roles
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/340e61cef88a5525.
Report an issue: GitHub.