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
- Re-fetch a fresh attested document from the attestation endpoint instead of reusing a cached one
- Ensure the machine's clock is synchronized (NTP/chrony) so expiresOn comparisons succeed
- Retry the verification after the clock skew window; if it persists, investigate the attestation endpoint response
- 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
- Always fetch a fresh attested document per verification; never persist it beyond its expiresOn
- Keep node clocks synchronized with NTP/chrony
- Cache only until expiry minus clock-skew margin
- Generate future-dated expiresOn in test fixtures
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
- decoding PKCS7 signature: %w
- parsing PKCS7 signature: %w
- verifying PKCS7 signature: %w
- PKCS7 signer certificate not found
- validating PKCS7 signer SAN: %w
AI-assisted analysis of kubernetes/kops@4c8573c808 (2026-09-05).
Data as JSON: /api/errors/b3e4213ea0d9d5c7.
Report an issue: GitHub.