crowdsecurity/crowdsec · critical

generate challenge nonce: %w

Error message

generate challenge nonce: %w

What it means

generateChallengeNonce reads 16 bytes of crypto/rand entropy and hex-encodes them as the per-challenge nonce `r` used to derive the challenge signing secret. The error wraps a crypto/rand read failure, meaning the OS entropy source could not supply bytes. It is thrown as an error (not a panic) so only the current request fails instead of the whole WAF.

Source

Thrown at pkg/appsec/challenge/ticket.go:60

)

// ticketAgeBackstop is a loose ceiling on accepted submission age in
// verifyChallenge. The actual freshness gate is the keyring live
// window (rotationInterval × maxLiveEpochs); this is a separate ceiling
// that protects against operators configuring an unusually wide live
// window. Loose enough not to interfere with real submissions on slow
// clients (high-difficulty PoW) but tight enough to bound replay
// surface in pathological configurations.
const ticketAgeBackstop = 20 * time.Minute

// generateChallengeNonce returns a fresh 16-byte random per-challenge nonce
// (`r`) as hex. `r` keys single-use bookkeeping (spent_set.go) and seeds the
// per-challenge secret `s = HMAC(K_epoch, r)`. Error (not panic) on entropy
// failure so only the current request fails.
func generateChallengeNonce() (string, error) {
	buf := make([]byte, 16)
	if _, err := crand.Read(buf); err != nil {
		return "", fmt.Errorf("generate challenge nonce: %w", err)
	}

	return hex.EncodeToString(buf), nil
}

// deriveChallengeSecret computes the per-challenge signing secret
// `s = HMAC(K_epoch, r)` (hex). `s` is never transmitted; client and server
// derive it independently from the same per-epoch key.
func deriveChallengeSecret(signKey []byte, r string) string {
	return hmacSHA256Hex(signKey, []byte(r))
}

// epochForTimestamp converts a nanosecond UnixNano string (the format used in
// challenge.go's ts) into the keyring's epoch identifier. Uses the same
// rotation interval as the keyring so two instances always agree.
func (c *ChallengeRuntime) epochForTimestamp(ts string) int64 {
	tsVal, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || tsVal <= 0 {

View on GitHub (pinned to 909b515798)

Solutions

  1. Verify the kernel random source works: run `head -c 16 /dev/urandom | xxd` on the host running crowdsec
  2. Check container seccomp/apparmor profiles allow the getrandom(2) syscall
  3. Restart the host/container to restore the entropy subsystem
  4. Update to a newer Go/runtime version if the failure is a known getrandom bug on your platform

Example fix

// before
nonce, err := generateChallengeNonce()
if err != nil { panic(err) }
// after
nonce, err := generateChallengeNonce()
if err != nil {
    log.Errorf("challenge nonce unavailable: %v", err)
    http.Error(w, "internal error", http.StatusInternalServerError)
    return
}
Defensive patterns

Strategy: try-catch

Validate before calling

null

Try / catch

nonce, err := generateChallengeNonce()
if err != nil {
    log.Errorf("challenge nonce unavailable: %v", err)
    // fail only this request
    return err
}

Prevention

When it happens

Trigger: Calling generateChallengeNonce (directly in tests, or via GetChallengePage/freshChallenge) when crand.Read fails — typically when the OS random source (/dev/urandom, getrandom syscall) is unavailable or returns an error.

Common situations: Hardened containers with restricted syscalls blocking getrandom, exotic sandboxes/seccomp profiles, or heavily degraded kernel entropy conditions. Very rare on normal Linux systems.

Related errors


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