diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index c3eda09b..7e750282 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -115,6 +115,13 @@ impl RpcHandler { check_bitcoin_implementation_conflict(package_id).await?; let repaired_bitcoin_conf = if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") { + // Materialise the RPC password file before any install path + // runs. The orchestrator path resolves secret_env from + // /var/lib/archipelago/secrets/bitcoin-rpc-password at start + // time; if the file is missing, bitcoind exits within ms. + // bitcoin_rpc_credentials() generates + persists on first + // call (OnceCell-cached), so this is idempotent. + let _ = crate::bitcoin_rpc::bitcoin_rpc_credentials().await; ensure_bitcoin_rpc_bindings().await? } else { false diff --git a/core/container/src/manifest.rs b/core/container/src/manifest.rs index a4c8c5ec..123bc82e 100644 --- a/core/container/src/manifest.rs +++ b/core/container/src/manifest.rs @@ -619,6 +619,14 @@ impl ContainerConfig { let mut out = Vec::with_capacity(self.secret_env.len()); for e in &self.secret_env { let v = provider.read(&e.secret_file)?; + // An empty secret produces e.g. `-rpcpassword=` and crashes + // the container on auth before logs are useful. Fail loud. + if v.trim().is_empty() { + return Err(ManifestError::Invalid(format!( + "secret_env {} resolved to empty value (file: {})", + e.key, e.secret_file + ))); + } out.push(format!("{}={}", e.key, v)); } Ok(out) @@ -1051,6 +1059,36 @@ app: assert_eq!(out[1], "FM_GATEWAY_PASSWORD=supersecret2"); } + #[test] + fn resolve_secret_env_rejects_empty_value() { + let c = ContainerConfig { + image: Some("x:latest".to_string()), + image_signature: None, + pull_policy: "if-not-present".to_string(), + build: None, + network: None, + custom_args: vec![], + entrypoint: None, + derived_env: vec![], + secret_env: vec![SecretEnv { + key: "BITCOIN_RPC_PASS".to_string(), + secret_file: "bitcoin-rpc-password".to_string(), + }], + data_uid: None, + }; + let p = MapSecretsProvider { + data: HashMap::from([("bitcoin-rpc-password".to_string(), " \n".to_string())]), + }; + let err = c.resolve_secret_env(&p).unwrap_err(); + match err { + ManifestError::Invalid(msg) => assert!( + msg.contains("BITCOIN_RPC_PASS") && msg.contains("bitcoin-rpc-password"), + "msg should name the env key + file: {msg}" + ), + other => panic!("expected Invalid, got {other:?}"), + } + } + #[test] fn parse_every_real_manifest() { let app_manifests = list_repo_manifests();