kubernetes/kops · error

attested document expired at %s

Error message

attested document expired at %s

What it means

parseAndValidateAttestedDocumentContent validates Azure attested documents returned by the IMDS/attestation endpoint. After parsing the expiresOn timestamp it checks whether the document is still valid at the current time, allowing a small clock skew (attestedDocumentMaxClockSkew). If expiresOn is older than now minus that skew, the document is considered expired and this error is thrown, because the attested evidence can no longer be trusted.

Source

Thrown at upup/pkg/fi/cloudup/azure/attest.go:324

		return nil, fmt.Errorf("attested document createdOn %s is too far in the future", data.TimeStamp.CreatedOn)
	}
	oldestAllowedCreatedOn := now.Add(-(attestedDocumentMaxAge + attestedDocumentMaxClockSkew))
	if createdOn.Before(oldestAllowedCreatedOn) {
		return nil, fmt.Errorf("attested document createdOn %s is older than allowed freshness window of %s plus %s clock skew", data.TimeStamp.CreatedOn, attestedDocumentMaxAge, attestedDocumentMaxClockSkew)
	}
	klog.V(4).Infof("Attested document createdOn is fresh (createdOn=%s now=%s)", createdOn.Format(time.RFC3339), now.Format(time.RFC3339))

	// Verify the attested document has not expired and has a coherent lifetime.
	if data.TimeStamp.ExpiresOn != "" {
		expiresOn, err := time.Parse(attestedDocumentTimeFormat, data.TimeStamp.ExpiresOn)
		if err != nil {
			return nil, fmt.Errorf("parsing attested document expiration: %w", err)
		}
		if expiresOn.Before(createdOn) {
			return nil, fmt.Errorf("attested document expiresOn %s is before createdOn %s", data.TimeStamp.ExpiresOn, data.TimeStamp.CreatedOn)
		}
		if expiresOn.Before(now.Add(-attestedDocumentMaxClockSkew)) {
			return nil, fmt.Errorf("attested document expired at %s", data.TimeStamp.ExpiresOn)
		}
		klog.V(4).Infof("Attested document not expired (expiresOn=%s)", expiresOn.Format(time.RFC3339))
	}

	return &data, nil
}

// intermediateCertPoolWithCaches performs a cached lookup against the supplied positive and
// negative TTL caches, invoking fetch on a miss. Tests inject their own stores and fetchers.
func intermediateCertPoolWithCaches(signer *x509.Certificate, fetch func(*x509.Certificate) (*x509.CertPool, error), positive, negative expirationcache.Store) (*x509.CertPool, error) {
	if signer == nil {
		return nil, fmt.Errorf("signer certificate is required")
	}

	keyStr := intermediateCacheKeyForSigner(signer)

	// Positive cache wins over negative: a successful later fetch overwrites any stale negative entry,
	// which expires on its own shorter TTL.

View on GitHub (pinned to 4c8573c808)

Solutions

  1. Re-fetch a fresh attested document from the attestation endpoint instead of reusing a cached one
  2. Ensure the machine's clock is synchronized (NTP/chrony) so expiresOn comparisons succeed
  3. Retry the verification after the clock skew window; if it persists, investigate the attestation endpoint response
  4. Update test fixtures to generate expiresOn in the future relative to the test run time

Example fix

// before
const doc = cachedAttestedDocument // may be expired
parsed, err := parseAndValidateAttestedDocumentContent(doc)
// after
if cachedAttestedDocumentExpired(cachedAttestedDocument) {
    cachedAttestedDocument = fetchFreshAttestedDocument(ctx)
}
parsed, err := parseAndValidateAttestedDocumentContent(cachedAttestedDocument)
Defensive patterns

Strategy: validation

Validate before calling

// Before verification, reject obviously stale documents
exp, err := time.Parse("2006-01-02 15:04:05.999999999", doc.TimeStamp.ExpiresOn)
if err != nil || time.Until(exp) <= 0 {
    doc = fetchFreshAttestedDocument(ctx) // re-attest instead of verifying a stale doc
}

Type guard

func isAttestedDocumentCurrent(expiryStr string, skew time.Duration) bool {
    exp, err := time.Parse("2006-01-02 15:04:05.999999999", expiryStr)
    return err == nil && exp.After(time.Now().Add(-skew))
}

Try / catch

doc, err := verifyAttestedDocumentWithRootAndFetcher(ctx, raw)
if err != nil && strings.Contains(err.Error(), "expired at") {
    // re-fetch a fresh attested document and retry once
    doc, err = verifyAttestedDocumentWithRootAndFetcher(ctx, fetchFreshRaw(ctx))
}
if err != nil { return fmt.Errorf("attestation verification failed: %w", err) }

Prevention

When it happens

Trigger: Calling parseAndValidateAttestedDocumentContent (directly or via verifyAttestedDocumentWithRootAndFetcher) when the attested document's TimeStamp.ExpiresOn is in the past beyond the allowed clock skew — e.g. cached or replayed attestation responses, or a stale document supplied by tests.

Common situations: Node with skewed system clock receiving old attestation responses; reusing a cached attested document past its expiry; Azure IMDS returning an already-expired document; test fixtures with hard-coded past timestamps.

Related errors


AI-assisted analysis of kubernetes/kops@4c8573c808 (2026-09-05). Data as JSON: /api/errors/b3e4213ea0d9d5c7. Report an issue: GitHub.