infiniflow/ragflow · critical

checkpoint HMAC key: CHECKPOINT_HMAC_KEY must decode to exac

Error message

checkpoint HMAC key: CHECKPOINT_HMAC_KEY must decode to exactly 32 bytes, got %d

What it means

The sibling check in loadCheckpointHMACKey: the env value decoded as base64 successfully, but the decoded byte length is not exactly 32. The HMAC construction expects a 256-bit key, so any other length is a fatal configuration error at package-init time. Common causes are keys generated with a different byte length, or double-encoding (base64 of a base64 string).

Source

Thrown at internal/harness/core/interrupt.go:231

	hmacLen    = 32
	envHMACKey = "CHECKPOINT_HMAC_KEY"
)

// checkpointHMACKey reads the HMAC key from the CHECKPOINT_HMAC_KEY env var
// (base64-encoded, 32 bytes). If unset, a random key is generated per startup
// with a log warning — this is safe for single-process in-memory usage but
// will BREAK checkpoint resume across process restarts. Production deployments
// MUST set CHECKPOINT_HMAC_KEY to a stable base64-encoded 32-byte secret.
var checkpointHMACKey = loadCheckpointHMACKey()

func loadCheckpointHMACKey() []byte {
	if env := common.GetEnv(envHMACKey); env != "" {
		k, err := base64.StdEncoding.DecodeString(env)
		if err != nil {
			panic("checkpoint HMAC key: invalid base64 in " + envHMACKey + ": " + err.Error())
		}
		if len(k) != 32 {
			panic("checkpoint HMAC key: " + envHMACKey + " must decode to exactly 32 bytes, got " + fmt.Sprintf("%d", len(k)))
		}
		return k
	}
	k := make([]byte, 32)
	if _, err := rand.Read(k); err != nil {
		panic("failed to generate checkpoint HMAC key: " + err.Error())
	}
	common.Warn("checkpoint HMAC env not set — using random per-process key; checkpoint resume across restarts will fail", zap.String("env", envHMACKey))
	return k
}

func computeCheckpointHMAC(payload []byte) []byte {
	mac := hmac.New(sha256.New, checkpointHMACKey)
	mac.Write(payload)
	return mac.Sum(nil)
}

func loadCheckpoint(store CheckPointStore, ctx context.Context, cid string) (context.Context, *runContext, *ResumeInfo, error) {

View on GitHub (pinned to 554fb1133a)

Solutions

  1. Regenerate with exactly 32 bytes: openssl rand -base64 32, then export it as CHECKPOINT_HMAC_KEY
  2. Read the 'got N' in the panic message to see how far off the length is; ~44 bytes suggests double-encoding — encode the raw secret once
  3. Keep the same 32-byte key across restarts and all replicas; do not rotate per pod
  4. Verify: echo -n "$CHECKPOINT_HMAC_KEY" | base64 -d | wc -c # must print 32

Example fix

# before
export CHECKPOINT_HMAC_KEY="$(openssl rand -base64 16)"  # panic: must decode to exactly 32 bytes, got 16

# after
export CHECKPOINT_HMAC_KEY="$(openssl rand -base64 32)"
Defensive patterns

Strategy: validation

Validate before calling

k, err := base64.StdEncoding.DecodeString(strings.TrimSpace(v))
if err != nil || len(k) != 32 {
    return fmt.Errorf("CHECKPOINT_HMAC_KEY must be base64 of exactly 32 bytes")
}

Prevention

When it happens

Trigger: Setting CHECKPOINT_HMAC_KEY to base64 of 16, 24, or 64 bytes (e.g. openssl rand -base64 16), base64-encoding an already-base64 string (yields ~44+ decoded bytes), or otherwise supplying a correctly encoded key of the wrong length. Panics at startup with the actual decoded length in the message.

Common situations: Operator generates a '64-char' or 16-byte secret by habit; secret-management tooling enforces its own key size (e.g. 128-bit) incompatible with the 32-byte requirement; the decoded length reported in the panic message identifies the mismatch.

Related errors


AI-assisted analysis of infiniflow/ragflow@554fb1133a (2026-08-15). Data as JSON: /api/errors/6856e612cdb50432. Report an issue: GitHub.