hashicorp/nomad · error

cannot update expiration time

Error message

cannot update expiration time

What it means

Nomad ACL tokens support an expiration TTL and, once created, a computed expiration time. During token update, the Validate/merge logic compares the incoming token against the existing one and rejects any mutation of expiration fields, because expiry is immutable after creation.

Source

Thrown at nomad/structs/acl.go:819

					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,
					fmt.Errorf("expiration time cannot be less than %s in the future (was %s)",
						minTTL, expiresIn))
			}
		}
	default:
		if existing.Global != a.Global {
			mErr.Errors = append(mErr.Errors, errors.New("cannot toggle global mode"))
		}
		if existing.ExpirationTTL != a.ExpirationTTL {
			mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
		}
		if a.ExpirationTime != nil {
			if !existing.ExpirationTime.Equal(*a.ExpirationTime) {
				mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
			}
		}

	}

	return mErr.ErrorOrNil()
}

// HasExpirationTime checks whether the ACL token has an expiration time value
// set.
func (a *ACLToken) HasExpirationTime() bool {
	if a == nil || a.ExpirationTime == nil {
		return false
	}
	return !a.ExpirationTime.IsZero()
}

// IsExpired compares the ACLToken.ExpirationTime against the passed t to

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Remove the ExpirationTime (and ExpirationTTL) fields from the update request so only mutable fields are changed
  2. Create a new token with the desired expiration instead of updating the existing one
  3. If you must keep sending the full struct, send ExpirationTime exactly as returned by the server (same instant, equal time comparison)

Example fix

// before
token.ExpirationTime = time.Now().Add(24 * time.Hour)
client.ACLTokens().Update(token, nil)
// after
newToken := &structs.ACLToken{Name: token.Name, Policies: token.Policies, ExpirationTTL: 24 * time.Hour}
client.ACLTokens().Create(newToken, nil)
Defensive patterns

Strategy: validation

Validate before calling

func canUpdate(existing, incoming *structs.ACLToken) error {
  if incoming.ExpirationTime != nil && existing.ExpirationTime != nil &&
    !existing.ExpirationTime.Equal(*incoming.ExpirationTime) {
    return errors.New("expiration is immutable: create a new token instead")
  }
  return nil
}

Prevention

When it happens

Trigger: Calling the ACL token update API (ACL.UpsertTokens / token Update RPC) with a token whose ExpirationTime differs from the stored token's ExpirationTime, e.g. copying an old token struct and altering ExpirationTime, or round-tripping a token where the server recomputed the time.

Common situations: Operators trying to extend or shorten a token's life by editing its expiration via the update endpoint; client code that copies a GET'd token, mutates expiration fields, and PUTs it back; tooling that normalizes timestamps and introduces a slight time drift so the times no longer compare equal.

Related errors


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