{"record":{"id":"b3e4213ea0d9d5c7","repo":"kubernetes/kops","slug":"attested-document-expired-at-s","errorCode":null,"errorMessage":"attested document expired at %s","messagePattern":"attested document expired at (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"upup/pkg/fi/cloudup/azure/attest.go","lineNumber":324,"sourceCode":"\t\treturn nil, fmt.Errorf(\"attested document createdOn %s is too far in the future\", data.TimeStamp.CreatedOn)\n\t}\n\toldestAllowedCreatedOn := now.Add(-(attestedDocumentMaxAge + attestedDocumentMaxClockSkew))\n\tif createdOn.Before(oldestAllowedCreatedOn) {\n\t\treturn nil, fmt.Errorf(\"attested document createdOn %s is older than allowed freshness window of %s plus %s clock skew\", data.TimeStamp.CreatedOn, attestedDocumentMaxAge, attestedDocumentMaxClockSkew)\n\t}\n\tklog.V(4).Infof(\"Attested document createdOn is fresh (createdOn=%s now=%s)\", createdOn.Format(time.RFC3339), now.Format(time.RFC3339))\n\n\t// Verify the attested document has not expired and has a coherent lifetime.\n\tif data.TimeStamp.ExpiresOn != \"\" {\n\t\texpiresOn, err := time.Parse(attestedDocumentTimeFormat, data.TimeStamp.ExpiresOn)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing attested document expiration: %w\", err)\n\t\t}\n\t\tif expiresOn.Before(createdOn) {\n\t\t\treturn nil, fmt.Errorf(\"attested document expiresOn %s is before createdOn %s\", data.TimeStamp.ExpiresOn, data.TimeStamp.CreatedOn)\n\t\t}\n\t\tif expiresOn.Before(now.Add(-attestedDocumentMaxClockSkew)) {\n\t\t\treturn nil, fmt.Errorf(\"attested document expired at %s\", data.TimeStamp.ExpiresOn)\n\t\t}\n\t\tklog.V(4).Infof(\"Attested document not expired (expiresOn=%s)\", expiresOn.Format(time.RFC3339))\n\t}\n\n\treturn &data, nil\n}\n\n// intermediateCertPoolWithCaches performs a cached lookup against the supplied positive and\n// negative TTL caches, invoking fetch on a miss. Tests inject their own stores and fetchers.\nfunc intermediateCertPoolWithCaches(signer *x509.Certificate, fetch func(*x509.Certificate) (*x509.CertPool, error), positive, negative expirationcache.Store) (*x509.CertPool, error) {\n\tif signer == nil {\n\t\treturn nil, fmt.Errorf(\"signer certificate is required\")\n\t}\n\n\tkeyStr := intermediateCacheKeyForSigner(signer)\n\n\t// Positive cache wins over negative: a successful later fetch overwrites any stale negative entry,\n\t// which expires on its own shorter TTL.","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/upup/pkg/fi/cloudup/azure/attest.go#L306-L342","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst doc = cachedAttestedDocument // may be expired\nparsed, err := parseAndValidateAttestedDocumentContent(doc)\n// after\nif cachedAttestedDocumentExpired(cachedAttestedDocument) {\n    cachedAttestedDocument = fetchFreshAttestedDocument(ctx)\n}\nparsed, err := parseAndValidateAttestedDocumentContent(cachedAttestedDocument)","handlingStrategy":"validation","validationCode":"// Before verification, reject obviously stale documents\nexp, err := time.Parse(\"2006-01-02 15:04:05.999999999\", doc.TimeStamp.ExpiresOn)\nif err != nil || time.Until(exp) <= 0 {\n    doc = fetchFreshAttestedDocument(ctx) // re-attest instead of verifying a stale doc\n}","typeGuard":"func isAttestedDocumentCurrent(expiryStr string, skew time.Duration) bool {\n    exp, err := time.Parse(\"2006-01-02 15:04:05.999999999\", expiryStr)\n    return err == nil && exp.After(time.Now().Add(-skew))\n}","tryCatchPattern":"doc, err := verifyAttestedDocumentWithRootAndFetcher(ctx, raw)\nif err != nil && strings.Contains(err.Error(), \"expired at\") {\n    // re-fetch a fresh attested document and retry once\n    doc, err = verifyAttestedDocumentWithRootAndFetcher(ctx, fetchFreshRaw(ctx))\n}\nif err != nil { return fmt.Errorf(\"attestation verification failed: %w\", err) }","preventionTips":["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"],"tags":["azure","attestation","expired-document","clock-skew"],"backgroundTag":"attested-document-expired","analyzedSha":"4c8573c808a73d578c5eadc86d410646ea0b0d73","analyzedAt":"2026-09-05T04:13:19.212Z","contentChangedAt":"2026-09-05T04:13:19.212Z","schemaVersion":2},"datasetVersion":"2026-09-12T07:17:12.445Z"}