neondatabase/neon · error
path is neither a directory or a file
Error message
path is neither a directory or a file
What it means
JwtAuth::from_key_path stats the configured path; if it exists but is neither a regular file nor a directory (character device, FIFO, socket, block device), it bails with this message. Ed25519 decoding keys can only be loaded from a single PEM file or a directory of PEM files.
Source
Thrown at libs/utils/src/auth.rs:168
pub fn from_key_path(key_path: &Utf8Path) -> Result<Self> {
let metadata = key_path.metadata()?;
let decoding_keys = if metadata.is_dir() {
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.View on GitHub (pinned to 8f60b04da4)
Solutions
- Point the config at a real Ed25519 public key PEM file or a directory of .pem files
- Generate a keypair if needed: openssl genpkey -algorithm ed25519, then publish the public part with openssl pkey -pubout
- Re-check the resolved path for typos or incomplete template expansion
Example fix
# before --public-key-path=/dev/null # after openssl pkey -in ed25519.pem -pubout -out /etc/neon/public.pem --public-key-path=/etc/neon/public.pem
Defensive patterns
Strategy: validation
Validate before calling
fn is_loadable_key_path(p: &camino::Utf8Path) -> bool {
match p.metadata() {
Ok(m) => m.is_file() || m.is_dir(),
Err(_) => false,
}
}
// run before JwtAuth::from_key_path
anyhow::ensure!(is_loadable_key_path(&key_path), "key path must be a regular file or directory"); Type guard
fn is_bad_key_path_error(err: &anyhow::Error) -> bool {
err.to_string().contains("neither a directory or a file")
} Try / catch
match JwtAuth::from_key_path(&key_path) {
Err(e) if e.to_string().contains("neither a directory or a file") => {
eprintln!("auth key path {key_path} is not a regular file or directory");
std::process::exit(1);
}
other => other?,
} Prevention
- Validate the key path type during config load, before startup
- Never substitute device nodes (/dev/null) for key files
- Lint configs in CI with existence and file-type checks
When it happens
Trigger: Configuring the JWT public key path (e.g. a safekeeper/pageserver auth config pointing at --public-key-path) with a special file such as /dev/null, a unix socket, or a named pipe.
Common situations: Using /dev/null to 'disable' auth in scripts; config templating resolving to a device node; Docker bind-mounting a socket over the key path.
Related errors
- Safekeeper set up for auth but no private key specified
- Configured for JWT auth with zero decoding keys. All JWT gat
- could not parse config file: {}
- could not open config file at path: {}
- failed to verify authorization token
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/d4473b0a6452dc55.
Report an issue: GitHub.