neondatabase/neon · critical
no private key found in {}
Error message
no private key found in {} What it means
load_private_key read the key file and rustls_pemfile::private_key parsed it without an I/O or PEM-decoding error, but found no PEM section it recognizes as a private key. rustls-pemfile only accepts 'RSA PRIVATE KEY' (PKCS#1), 'PRIVATE KEY' (PKCS#8), 'EC PRIVATE KEY' (SEC1), and encrypted PKCS#8 sections; a file containing only certificates, an OpenSSH-format key, or a DER/binary blob yields None, which becomes this error naming the file path.
Source
Thrown at libs/http-utils/src/tls_certs.rs:37
let mut reader = std::io::Cursor::new(&cert_data);
let cert_chain = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.context(format!("failed parsing certificate from file {filename:?}"))?;
Ok(cert_chain)
}
pub async fn load_private_key(filename: &Utf8Path) -> anyhow::Result<PrivateKeyDer<'static>> {
let key_data = tokio::fs::read(filename)
.await
.context(format!("failed reading private key file {filename:?}"))?;
let mut reader = std::io::Cursor::new(&key_data);
let key = rustls_pemfile::private_key(&mut reader)
.context(format!("failed parsing private key from file {filename:?}"))?;
key.ok_or(anyhow::anyhow!(
"no private key found in {}",
filename.as_str(),
))
}
pub async fn load_certified_key(
key_filename: &Utf8Path,
cert_filename: &Utf8Path,
) -> anyhow::Result<CertifiedKey> {
let cert_chain = load_cert_chain(cert_filename).await?;
let key = load_private_key(key_filename).await?;
let key = rustls::crypto::ring::default_provider()
.key_provider
.load_private_key(key)?;
let certified_key = CertifiedKey::new(cert_chain, key);
certified_key.keys_match()?;View on GitHub (pinned to 8f60b04da4)
Solutions
- Verify the file actually contains a private key section: grep 'PRIVATE KEY' /path/to/key (expect '-----BEGIN ... PRIVATE KEY-----')
- If key/cert paths were swapped, point the key setting at the real key file and the cert setting at the chain
- Regenerate or convert the key into a supported format: 'openssl genpkey -algorithm RSA' / '-algorithm EC' (PKCS#8), or 'openssl pkcs8 -topk8 -nocrypt -in old.key -out key.pem'
- Check the secret/volume actually delivered bytes: 'wc -c' and 'head -1' on the file
- For encrypted keys ('ENCRYPTED PRIVATE KEY'), decrypt first with 'openssl pkey -in key.enc -out key.pem'
Example fix
# before: key was made with ssh-keygen (OpenSSH format, unsupported) # -----BEGIN OPENSSH PRIVATE KEY----- ssh-keygen -t ed25519 -f tls.key # after: generate a PKCS#8 key rustls-pemfile can load openssl genpkey -algorithm RSA -out tls.key # or convert an existing PEM key to unencrypted PKCS#8 openssl pkcs8 -topk8 -nocrypt -in tls_old.key -out tls.key
Defensive patterns
Strategy: validation
Validate before calling
use tokio::io::AsyncReadExt;
/// Fails with an actionable message before server start if the file has no
/// PEM section rustls-pemfile recognizes as a private key.
pub async fn ensure_private_key_pem(path: &camino::Utf8Path) -> anyhow::Result<()> {
let mut data = String::new();
tokio::fs::File::open(path)
.await?
.read_to_string(&mut data)
.await?;
let has_key = data
.lines()
.any(|l| l.starts_with("-----BEGIN") && l.contains("PRIVATE KEY-----"));
anyhow::ensure!(
has_key,
"{path} has no `-----BEGIN ... PRIVATE KEY-----` section \
(supported: PKCS#1 RSA, PKCS#8, SEC1 EC; OpenSSH keys are NOT supported)"
);
Ok(())
} Try / catch
match tls_certs::load_certified_key(&key_path, &cert_path).await {
Ok(k) => k,
Err(e) if format!("{e:#}").contains("no private key found") => {
// Refuse to start with a precise hint instead of crashing later on TLS accept
anyhow::bail!(
"TLS key {key_path} unusable: {e:#}. \
Check key/cert paths are not swapped and the key is PKCS#8/PKCS#1/SEC1 PEM, \
not OpenSSH or DER."
)
}
Err(e) => return Err(e),
} Prevention
- Run 'grep PRIVATE KEY <keyfile>' in deployment scripts before starting the service
- Always generate TLS keys with openssl (genpkey/pkcs8), never ssh-keygen
- Name secret files explicitly (tls.key / tls.crt) and assert their first line in CI for mounted secrets
- Keep key and cert paths adjacent in config so they are swapped together or not at all
When it happens
Trigger: Pointing the TLS key setting at the certificate file (paths swapped) so the file has only 'CERTIFICATE' sections; using an ssh-keygen ed25519 key ('-----BEGIN OPENSSH PRIVATE KEY-----') which rustls-pemfile cannot parse; an empty or truncated file after a failed secret mount; a raw DER key not wrapped in base64 PEM armor.
Common situations: Kubernetes/Docker secrets mounted empty or as a directory at startup; cert-manager or vault output where the key and cert variable names were swapped; ops staff generating TLS keys with ssh-keygen instead of openssl; encrypted keys whose PEM section the reader skips without a password provider.
Related errors
- pageserver connection information should be provided
- safekeeper connstrings should be provided
- tenant id should be provided
- timeline id should be provided
- shard {shard_index} missing from pageserver_connection_info
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/aeb4a27d9a198914.
Report an issue: GitHub.