From 27ff1d5b52e9ef29d91f9d4eace645ba15ae099a Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 1 May 2026 14:39:56 -0400 Subject: [PATCH] fix(install): generate bitcoin RPC password before orchestrator install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bitcoin containers were exiting in ms after start because the orchestrator install path skipped the credential-materialisation step the legacy path did. resolve_secret_env then failed to read /var/lib/archipelago/secrets/bitcoin-rpc-password, the container started with no password, and bitcoind crashed before logs were useful. Two changes: 1. install.rs — call bitcoin_rpc_credentials() for bitcoin/bitcoin-core/ bitcoin-knots before any install branch runs. The function generates + persists on first call (OnceCell-cached), so this is idempotent. 2. manifest.rs::resolve_secret_env — return ManifestError::Invalid when a resolved secret trims to empty, instead of silently producing `KEY=` env vars that crash auth. Adds a unit test for the empty-secret rejection. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/api/rpc/package/install.rs | 7 ++++ core/container/src/manifest.rs | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+) 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();