feat: Phase 1 — per-installation credential generation, eliminate hardcoded passwords
Generate unique random passwords at first boot for Bitcoin RPC, all database
services (mempool, btcpay, immich, penpot, mysql-root), and Fedimint gateway.
Credentials stored in /var/lib/archipelago/secrets/ with 600 permissions.
Scripts: first-boot-containers.sh, deploy-to-target.sh, deploy-bitcoin-knots.sh,
container-doctor.sh all read from secrets files instead of hardcoded values.
Rust backend: new bitcoin_rpc module reads password from secrets file, env var,
or dev fallback. All .basic_auth() calls and container config strings now use
the shared credential reader instead of hardcoded "archipelago123".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:39:52 +00:00
|
|
|
//! Shared Bitcoin RPC credential management.
|
|
|
|
|
//! Reads credentials from the per-installation secrets file, falling back to
|
|
|
|
|
//! environment variables, then a dev-only default.
|
|
|
|
|
|
|
|
|
|
use tokio::sync::OnceCell;
|
|
|
|
|
use tracing::debug;
|
|
|
|
|
|
|
|
|
|
const SECRETS_PATH: &str = "/var/lib/archipelago/secrets/bitcoin-rpc-password";
|
|
|
|
|
const DEFAULT_USER: &str = "archipelago";
|
|
|
|
|
|
|
|
|
|
static CACHED_PASSWORD: OnceCell<String> = OnceCell::const_new();
|
|
|
|
|
|
|
|
|
|
/// Read the Bitcoin RPC password from the secrets file, env var, or dev fallback.
|
|
|
|
|
async fn read_password() -> String {
|
|
|
|
|
// 1. Try secrets file (production path)
|
|
|
|
|
if let Ok(pass) = tokio::fs::read_to_string(SECRETS_PATH).await {
|
|
|
|
|
let pass = pass.trim().to_string();
|
|
|
|
|
if !pass.is_empty() {
|
|
|
|
|
debug!("Bitcoin RPC password loaded from secrets file");
|
|
|
|
|
return pass;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Try environment variable
|
|
|
|
|
if let Ok(pass) = std::env::var("BITCOIN_RPC_PASSWORD") {
|
|
|
|
|
if !pass.is_empty() {
|
|
|
|
|
debug!("Bitcoin RPC password loaded from env var");
|
|
|
|
|
return pass;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 22:41:23 +00:00
|
|
|
// 3. Generate a random password and persist it (first-boot provisioning)
|
|
|
|
|
let random_pass = generate_random_password();
|
|
|
|
|
if let Some(parent) = std::path::Path::new(SECRETS_PATH).parent() {
|
|
|
|
|
let _ = tokio::fs::create_dir_all(parent).await;
|
|
|
|
|
}
|
|
|
|
|
match tokio::fs::write(SECRETS_PATH, &random_pass).await {
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
// Restrict permissions to owner-only
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
let _ = std::fs::set_permissions(SECRETS_PATH, std::fs::Permissions::from_mode(0o600));
|
|
|
|
|
}
|
|
|
|
|
debug!("Bitcoin RPC password: generated and saved to secrets file");
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!("Failed to save generated Bitcoin RPC password: {} — using ephemeral", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
random_pass
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate a cryptographically random password for Bitcoin RPC (32 hex chars).
|
|
|
|
|
fn generate_random_password() -> String {
|
|
|
|
|
let bytes: [u8; 16] = rand::random();
|
|
|
|
|
hex::encode(bytes)
|
feat: Phase 1 — per-installation credential generation, eliminate hardcoded passwords
Generate unique random passwords at first boot for Bitcoin RPC, all database
services (mempool, btcpay, immich, penpot, mysql-root), and Fedimint gateway.
Credentials stored in /var/lib/archipelago/secrets/ with 600 permissions.
Scripts: first-boot-containers.sh, deploy-to-target.sh, deploy-bitcoin-knots.sh,
container-doctor.sh all read from secrets files instead of hardcoded values.
Rust backend: new bitcoin_rpc module reads password from secrets file, env var,
or dev fallback. All .basic_auth() calls and container config strings now use
the shared credential reader instead of hardcoded "archipelago123".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:39:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get Bitcoin RPC credentials (user, password). Cached after first call.
|
|
|
|
|
pub async fn bitcoin_rpc_credentials() -> (String, String) {
|
|
|
|
|
let pass = CACHED_PASSWORD
|
|
|
|
|
.get_or_init(|| async { read_password().await })
|
|
|
|
|
.await;
|
|
|
|
|
(DEFAULT_USER.to_string(), pass.clone())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the Bitcoin RPC password as a plain string (for config generation).
|
|
|
|
|
pub async fn bitcoin_rpc_password() -> String {
|
|
|
|
|
let (_, pass) = bitcoin_rpc_credentials().await;
|
|
|
|
|
pass
|
|
|
|
|
}
|