JuliusBrussee/caveman · error

awscreds: credential expiry

Error message

awscreds: %s credential expiry: %w

What it means

credentialsFromJSON passes the parsed Expiration field to parseExpiry; if that cannot produce a valid time, the credential document's expiry is malformed and this wrapped error is returned. The library throws it because credentials without a parsable expiration cannot be safely cached or refreshed.

Solutions

  1. Inspect the Expiration field in the endpoint's raw response; it must be an RFC3339 timestamp like 2026-01-02T15:04:05Z.
  2. If using a custom/mock metadata endpoint, fix it to emit a valid RFC3339 Expiration.
  3. If the body looks corrupted, check for proxy interference and retry against the real metadata service.
  4. Update the library if the service changed its timestamp format (rare AWS-side format changes).

Example fix

// before (mock IMDS response)
{"AccessKeyID":"AKIA...","SecretAccessKey":"...","Token":"...","Expiration":"tomorrow"}
// after
{"AccessKeyID":"AKIA...","SecretAccessKey":"...","Token":"...","Expiration":"2026-09-21T00:00:00Z"}
Defensive patterns

Strategy: validation

Validate before calling

func hasRFC3339Expiration(body []byte) bool {
    var p struct{ Expiration string `json:"Expiration"` }
    if json.Unmarshal(body, &p) != nil { return false }
    _, err := time.Parse(time.RFC3339, p.Expiration)
    return err == nil
}

Try / catch

creds, err := provider.Credentials(ctx)
var expiryErr *time.ParseError
if errors.As(err, &expiryErr) {
    return fmt.Errorf("metadata endpoint sent bad Expiration (%q): check mock/proxy", expiryErr.Value)
}

Prevention

When it happens

Trigger: fromContainer or fromIMDS receives credential JSON whose Expiration is missing, empty, not RFC3339/expected ISO8601 format, or zero-valued.

Common situations: A fake/placeholder metadata endpoint in local development returning incomplete JSON; a hand-rolled mock ECS agent with a wrong date format; an intercepted or corrupted response body; unusual custom AWS_EC2_METADATA_SERVICE_ENDPOINT implementations.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/bcc129c8b6ae0e9e. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/awscreds/awscreds.go:628

		return nil, fmt.Errorf("awscreds: read %s response: %w", what, err)
	}
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return nil, fmt.Errorf("awscreds: %s: http %d", what, resp.StatusCode)
	}
	return body, nil
}

func credentialsFromJSON(body []byte, source string) (*result, error) {
	var parsed credentialJSON
	if err := json.Unmarshal(body, &parsed); err != nil {
		return nil, fmt.Errorf("awscreds: %s returned an unparseable response", source)
	}
	if parsed.Code != "" && !strings.EqualFold(parsed.Code, "Success") {
		return nil, fmt.Errorf("awscreds: %s returned code %q", source, parsed.Code)
	}
	expires, err := parseExpiry(parsed.Expiration)
	if err != nil {
		return nil, fmt.Errorf("awscreds: %s credential expiry: %w", source, err)
	}
	return &result{
		creds: awssig.Credentials{
			AccessKeyID:     strings.TrimSpace(parsed.AccessKeyID),
			SecretAccessKey: strings.TrimSpace(parsed.SecretAccessKey),
			SessionToken:    strings.TrimSpace(parsed.Token),
		},
		expires: expires,
		source:  source,
	}, nil
}

// parseExpiry maps an absent expiry to the zero time, which means "never
// refresh" to the cache.
func parseExpiry(raw string) (time.Time, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return time.Time{}, nil

View on GitHub (pinned to 3ee70a1026)