neondatabase/neon · error

Configured for JWT auth with zero decoding keys. All JWT gat

Error message

Configured for JWT auth with zero decoding keys. All JWT gated requests would be rejected.

What it means

from_key_path accepts a directory to support key rotation, but if that directory contains no loadable regular files the constructor bails. With zero decoding keys every JWT-gated request would be rejected, so the library fails fast at startup instead of running in an always-deny state.

Source

Thrown at libs/utils/src/auth.rs:171

            let mut keys = Vec::new();
            for entry in fs::read_dir(key_path)? {
                let path = entry?.path();
                if !path.is_file() {
                    // Ignore directories (don't recurse)
                    continue;
                }
                let public_key = fs::read(path)?;
                keys.push(DecodingKey::from_ed_pem(&public_key)?);
            }
            keys
        } else if metadata.is_file() {
            let public_key = fs::read(key_path)?;
            vec![DecodingKey::from_ed_pem(&public_key)?]
        } else {
            anyhow::bail!("path is neither a directory or a file")
        };
        if decoding_keys.is_empty() {
            anyhow::bail!(
                "Configured for JWT auth with zero decoding keys. All JWT gated requests would be rejected."
            );
        }
        Ok(Self::new(decoding_keys))
    }

    pub fn from_key(key: String) -> Result<Self> {
        Ok(Self::new(vec![DecodingKey::from_ed_pem(key.as_bytes())?]))
    }

    /// Attempt to decode the token with the internal decoding keys.
    ///
    /// The function tries the stored decoding keys in succession,
    /// and returns the first yielding a successful result.
    /// If there is no working decoding key, it returns the last error.
    pub fn decode<D: DeserializeOwned>(
        &self,
        token: &str,

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Ensure at least one Ed25519 public key PEM exists in the directory
  2. Check the deployment/mount: kubectl describe pod, verify secret name and mountPath
  3. Add a provisioning step that writes keys before the service starts

Example fix

# before: secret mounted but empty
volumes:
  - name: jwt-keys
    secret:
      secretName: jwt-keys-wrong-name
# after
kubectl create secret generic jwt-keys --from-file=public.pem=./public.pem
# files land at /etc/neon/keys/public.pem
Defensive patterns

Strategy: validation

Validate before calling

fn key_dir_has_pem(dir: &camino::Utf8Path) -> bool {
    std::fs::read_dir(dir)
        .map(|entries| entries.filter_map(Result::ok).any(|e| e.path().is_file()))
        .unwrap_or(false)
}

// run before JwtAuth::from_key_path
anyhow::ensure!(key_dir_has_pem(&key_path), "JWT key directory is empty");

Type guard

fn is_zero_keys_error(err: &anyhow::Error) -> bool {
    err.to_string().contains("zero decoding keys")
}

Try / catch

match JwtAuth::from_key_path(&key_path) {
    Err(e) if e.to_string().contains("zero decoding keys") => {
        eprintln!("key directory {key_path} contains no PEM files; provision keys first");
        std::process::exit(1);
    }
    other => other?,
}

Prevention

When it happens

Trigger: The configured JWT key directory exists but is empty: a Kubernetes secret volume that was never populated, a failed/renamed secret mount, or an automation step that skipped key provisioning.

Common situations: k8s secret mounted empty due to name mismatch or missing items; key rotation removed old files before new ones landed; fresh environments missing the provisioning step.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/b25f442b11b03f02. Report an issue: GitHub.