crowdsecurity/crowdsec · critical
failed to derive key: %w
Error message
failed to derive key: %w
What it means
deriveKey expands the master secret into a 32-byte AES-256 key with HKDF-SHA256. This error means hkdf.Read failed while extracting the key — practically only when the input secret is empty/nil or the hash stream errors, which should never occur with a validated secret. Both sealCookieV0 and openCookieV0Bytes depend on it.
Source
Thrown at pkg/appsec/challenge/crypto.go:93
// cookiePlaintextFixedHeaderLen is the size of the fixed-layout portion of
// the plaintext header that precedes the protobuf envelope:
//
// not_after_be8 (8) + flags_byte (1) + reason_len_be (2) = 11
//
// followed by reason_bytes (variable, 0..MaxAllowlistReasonLen).
const cookiePlaintextFixedHeaderLen = 8 + 1 + 2
// cookieFlagAllowlisted marks a cookie minted by GrantChallengeCookie
// (operator allowlist bypass) rather than by a real challenge submission.
const cookieFlagAllowlisted byte = 0x01
func deriveKey(secret []byte) ([]byte, error) {
h := hkdf.New(sha256.New, secret, nil, []byte(hkdfInfo))
key := make([]byte, 32) // AES-256
if _, err := h.Read(key); err != nil {
return nil, fmt.Errorf("failed to derive key: %w", err)
}
return key, nil
}
// sealCookieV0 produces a v0 cookie sealed under the long-lived master
// cookie key. notAfter is the unix-seconds expiration; flags carries the
// allowlist bit set by GrantChallengeCookie (0 for normal cookies);
// reason is the operator-supplied allowlist reason (empty for normal
// cookies). All three are prepended to the marshaled proto BEFORE
// encryption so they are both confidential (not observable from the wire)
// and authenticated (any tamper attempt invalidates the GCM tag).
//
// Returns ErrAllowlistReasonSize if reason exceeds MaxAllowlistReasonLen.
func sealCookieV0(envelope *pb.ChallengeCookie, masterCookieKey []byte, notAfter int64, flags byte, reason string, aad []byte, maxCookieLen int) (string, error) {
if maxCookieLen <= 0 {
maxCookieLen = MaxCookieLen
}View on GitHub (pinned to 909b515798)
Solutions
- Ensure the runtime is built via NewChallengeRuntime with a valid master secret (>= 32 bytes) or its random fallback.
- Check that BuildOptions was called and its error handled before constructing the runtime.
- Log the wrapped hkdf error; an empty HKDF secret input is the usual root cause.
Example fix
// before
opts, _ := challenge.BuildOptions(cfg, logger)
rt, err := challenge.NewChallengeRuntime(opts...)
// after
opts, err := challenge.BuildOptions(cfg, logger)
if err != nil {
return fmt.Errorf("challenge config: %w", err)
}
rt, err := challenge.NewChallengeRuntime(opts...) Defensive patterns
Strategy: validation
Validate before calling
if len(masterSecret) == 0 {
return errors.New("master secret is empty; configure master_secret or use NewChallengeRuntime")
} Try / catch
if err := errors.Is(err, hkdfErrFamily); err != nil {
// construct runtime via the standard constructor instead of manual keys
} Prevention
- Always build the runtime through NewChallengeRuntime/BuildOptions, never with hand-made keys.
- Verify ParseConfiguredSecret succeeded before installing a secret.
- Add a startup smoke test that seals and opens a round-trip cookie.
When it happens
Trigger: Calling sealCookieV0 or openCookieV0Bytes (via openCookie/ValidCookie) with an empty or nil master cookie key, e.g. a ChallengeRuntime whose keyring was never initialized with a valid master secret.
Common situations: A mis-built runtime where WithMasterSecret was never applied; a keyring initialized from an unvalidated empty secret; programmatic use of the challenge package without going through BuildOptions.
Related errors
- cookie expired
- unknown cookie version
- empty master secret
- generate PoW salt: %w
- failed to seal challenge cookie: %w
AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06).
Data as JSON: /api/errors/dfb58194750eee3a.
Report an issue: GitHub.