crowdsecurity/crowdsec · error

ErrCookieExpired

ErrCookieExpired

Error message

%w: not_after=%d now=%d

What it means

openCookieV0Bytes rejects the cookie because its not_after timestamp is <= the current time, i.e. the challenge cookie has reached its expiration. It wraps ErrCookieExpired, which callers match with errors.Is to distinguish expiry from other malformed-cookie failures.

Source

Thrown at pkg/appsec/challenge/crypto.go:259

	if len(plaintext) < cookiePlaintextFixedHeaderLen {
		return nil, fmt.Errorf("%w: plaintext shorter than fixed header", ErrCookieMalformed)
	}

	notAfter := int64(binary.BigEndian.Uint64(plaintext[:8]))
	flags := plaintext[8]
	reasonLen := int(binary.BigEndian.Uint16(plaintext[9:11]))

	if reasonLen > MaxAllowlistReasonLen {
		return nil, fmt.Errorf("%w: reason_len=%d", ErrCookieMalformed, reasonLen)
	}

	if len(plaintext) < cookiePlaintextFixedHeaderLen+reasonLen {
		return nil, fmt.Errorf("%w: plaintext shorter than declared reason_len", ErrCookieMalformed)
	}

	if notAfter <= now.Unix() {
		return nil, fmt.Errorf("%w: not_after=%d now=%d", ErrCookieExpired, notAfter, now.Unix())
	}

	reasonStart := cookiePlaintextFixedHeaderLen
	reasonEnd := reasonStart + reasonLen
	reason := string(plaintext[reasonStart:reasonEnd])

	envelope := &pb.ChallengeCookie{}
	if err := proto.Unmarshal(plaintext[reasonEnd:], envelope); err != nil {
		return nil, fmt.Errorf("%w: %w", ErrCookiePayload, err)
	}

	return &CookieEnvelope{
		Envelope:        envelope,
		Allowlisted:     flags&cookieFlagAllowlisted != 0,
		AllowlistReason: reason,
		NotAfter:        notAfter,
	}, nil
}

View on GitHub (pinned to 909b515798)

Solutions

  1. Have the client obtain a fresh challenge cookie and retry
  2. Synchronize clocks (NTP) across all instances sharing the master_secret
  3. Increase the cookie validity window at issuance if legitimate lifetimes are being cut short
  4. Treat ErrCookieExpired as a normal flow: clear the expired cookie client-side and restart the challenge

Example fix

// before: generic handling
if err != nil { return err }
// after
if errors.Is(err, challenge.ErrCookieExpired) {
    return issueNewChallenge() // fresh cookie, not an error for the user
}
Defensive patterns

Strategy: try-catch

Validate before calling

if errors.Is(err, challenge.ErrCookieExpired) { /* refresh */ }

Type guard

func isCookieExpired(err error) bool { return errors.Is(err, challenge.ErrCookieExpired) }

Try / catch

env, err := rt.OpenCookie(raw, now)
if errors.Is(err, challenge.ErrCookieExpired) {
    clearClientCookie(w)
    return issueNewChallenge(w, r)
} else if err != nil {
    return err
}

Prevention

When it happens

Trigger: openCookie is called after the cookie's not_after timestamp has passed — the cookie was issued more than its validity window ago, or the verifier's clock is ahead of the issuer's.

Common situations: A browser or API client caches the challenge cookie past its TTL and replays it; clock skew between distributed CrowdSec instances behind a load balancer; long-lived sessions resuming after keyring rotation; tests replaying a recorded cookie.

Related errors


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/b37422abb63e6a1b. Report an issue: GitHub.