infiniflow/ragflow · critical

checkpoint HMAC key: invalid base64 in CHECKPOINT_HMAC_KEY:

Error message

checkpoint HMAC key: invalid base64 in CHECKPOINT_HMAC_KEY: {err}

What it means

loadCheckpointHMACKey runs at package initialization (var checkpointHMACKey = loadCheckpointHMACKey()) and panics when the CHECKPOINT_HMAC_KEY env var is set but is not valid standard base64. The key (32 bytes, base64-encoded) is used to HMAC checkpoint payloads; treating malformed input as a fatal startup error prevents silently running with a bogus key that would break checkpoint integrity verification.

Source

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

// ---- Checkpoint integrity (HMAC) ----

const (
	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)

View on GitHub (pinned to 554fb1133a)

Solutions

  1. Generate a correct value: openssl rand -base64 32 and set CHECKPOINT_HMAC_KEY to its output
  2. Strip any trailing newline/whitespace: export CHECKPOINT_HMAC_KEY="$(printf %s "$CHECKPOINT_HMAC_KEY")"
  3. If the secret is urlsafe base64, re-encode it to standard base64 (replace - with +, _ with /) or store it decoded
  4. Verify with: echo -n "$CHECKPOINT_HMAC_KEY" | base64 -d | wc -c # must print 32

Example fix

# before
export CHECKPOINT_HMAC_KEY="a1b2c3..."      # raw hex -> panic: invalid base64

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

Strategy: validation

Validate before calling

if v := os.Getenv("CHECKPOINT_HMAC_KEY"); v != "" {
    if _, err := base64.StdEncoding.DecodeString(strings.TrimSpace(v)); err != nil {
        return fmt.Errorf("CHECKPOINT_HMAC_KEY must be standard base64")
    }
}

Prevention

When it happens

Trigger: Setting CHECKPOINT_HMAC_KEY to a raw (non-base64) 32-byte string, a hex-encoded key, a base64 URL-safe-encoded value using -/_ instead of +//, or a value with trailing whitespace/newline or a typo — any input base64.StdEncoding.DecodeString rejects. The panic fires at process startup, before serving.

Common situations: Operators paste `openssl rand -hex 32` output (hex, not base64) into the env var; a shell here-doc or YAML block scalar appends a newline; a secret manager injects urlsafe-base64; copy-paste drops or mangles trailing '=' padding.

Related errors


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