{"record":{"id":"720fd34f03c10c36","repo":"neondatabase/neon","slug":"safekeeper-set-up-for-auth-but-no-private-key-spec","errorCode":null,"errorMessage":"Safekeeper set up for auth but no private key specified","messagePattern":"Safekeeper set up for auth but no private key specified","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"control_plane/src/storage_controller.rs","lineNumber":636,"sourceCode":"\n        if let Some(lag) = self.config.max_secondary_lag_bytes.as_ref() {\n            args.push(format!(\"--max-secondary-lag-bytes={lag}\"))\n        }\n\n        if let Some(threshold) = self.config.long_reconcile_threshold {\n            args.push(format!(\n                \"--long-reconcile-threshold={}\",\n                humantime::Duration::from(threshold)\n            ))\n        }\n\n        args.push(format!(\n            \"--neon-local-repo-dir={}\",\n            self.env.base_data_dir.display()\n        ));\n\n        if self.env.safekeepers.iter().any(|sk| sk.auth_enabled) && self.private_key.is_none() {\n            anyhow::bail!(\"Safekeeper set up for auth but no private key specified\");\n        }\n\n        if self.config.timelines_onto_safekeepers {\n            args.push(\"--timelines-onto-safekeepers\".to_string());\n        }\n\n        // neon_local is used in test environments where we often have less than 3 safekeepers.\n        if self.config.timeline_safekeeper_count.is_some() || self.env.safekeepers.len() < 3 {\n            let sk_cnt = self\n                .config\n                .timeline_safekeeper_count\n                .unwrap_or(self.env.safekeepers.len());\n\n            args.push(format!(\"--timeline-safekeeper-count={sk_cnt}\"));\n        }\n\n        if let Some(duration) = self.config.shard_split_request_timeout {\n            args.push(format!(","sourceCodeStart":618,"sourceCodeEnd":654,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/control_plane/src/storage_controller.rs#L618-L654","documentation":"storcon talks to safekeepers over their HTTP API. When any safekeeper in the local env has auth enabled, storcon must sign JWTs for those requests, which requires a private key; if the storage controller was configured without one, argument construction bails before the process is even started.","triggerScenarios":"The env config enables safekeeper auth (auth_enabled on any safekeeper) while the storage controller has no private key configured (missing private_key in config or on the CLI).","commonSituations":"Turning on JWT auth for safekeepers in a local env but forgetting to provision the storcon key pair, or reusing an old env config after auth support was introduced.","solutions":["Generate a JWT key pair and configure the storage controller's private key (config private_key / CLI key option)","Or disable safekeeper auth in the env config for local development"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"let auth_used = env.safekeepers.iter().any(|sk| sk.auth_enabled);\nanyhow::ensure!(\n    !auth_used || private_key.is_some(),\n    \"safekeeper auth enabled but the storage controller has no private key\"\n);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Provision the JWT key pair whenever safekeeper auth is enabled","Keep auth disabled for purely local runs","Add a config lint that fails early on this inconsistency"],"tags":["rust","storage-controller","safekeeper","auth","jwt","config"],"backgroundTag":"missing-auth-credentials","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}