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

  1. Provide a positive TTL (e.g. 1h) or omit ExpirationTTL entirely for a non-expiring token.
  2. Audit scripts for inverted duration arithmetic and clamp TTLs to >= 0 before the API call.
  3. 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

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


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