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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f273816405
commit
809a976960
@@ -77,6 +77,7 @@ impl RpcHandler {
|
||||
method: &str,
|
||||
params: &[serde_json::Value],
|
||||
) -> Result<T> {
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
let body = serde_json::json!({
|
||||
"jsonrpc": "1.0",
|
||||
"id": "archy",
|
||||
@@ -86,7 +87,7 @@ impl RpcHandler {
|
||||
|
||||
let resp = client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -193,12 +193,15 @@ impl RpcHandler {
|
||||
"--restart=unless-stopped", // Auto-restart policy
|
||||
];
|
||||
|
||||
// Read Bitcoin RPC password from secrets for container configs
|
||||
let rpc_pass = crate::bitcoin_rpc::bitcoin_rpc_password().await;
|
||||
|
||||
// App-specific configuration (should come from manifest)
|
||||
let (mut ports, mut volumes, env_vars, custom_command, mut custom_args) = {
|
||||
let mut allocator = self.port_allocator.lock().map_err(|e| {
|
||||
anyhow::anyhow!("Port allocator lock poisoned: {}", e)
|
||||
})?;
|
||||
get_app_config(package_id, &self.config.host_ip, &mut allocator)
|
||||
get_app_config(package_id, &self.config.host_ip, &mut allocator, &rpc_pass)
|
||||
};
|
||||
|
||||
// Fedimint Gateway: auto-detect LND and switch to lnd mode
|
||||
@@ -222,7 +225,7 @@ impl RpcHandler {
|
||||
"--network".to_string(), "bitcoin".to_string(),
|
||||
"--bitcoind-url".to_string(), format!("http://{}:8332", self.config.host_ip),
|
||||
"--bitcoind-username".to_string(), "archipelago".to_string(),
|
||||
"--bitcoind-password".to_string(), "archipelago123".to_string(),
|
||||
"--bitcoind-password".to_string(), rpc_pass.clone(),
|
||||
"lnd".to_string(),
|
||||
"--lnd-rpc-host".to_string(), format!("{}:10009", self.config.host_ip),
|
||||
"--lnd-tls-cert".to_string(), "/lnd/tls.cert".to_string(),
|
||||
@@ -305,16 +308,16 @@ impl RpcHandler {
|
||||
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
|
||||
let bitcoin_dir = "/var/lib/archipelago/bitcoin";
|
||||
let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
|
||||
let bitcoin_conf = "\
|
||||
let bitcoin_conf = format!("\
|
||||
server=1\n\
|
||||
prune=550\n\
|
||||
rpcuser=archipelago\n\
|
||||
rpcpassword=archipelago123\n\
|
||||
rpcpassword={}\n\
|
||||
rpcbind=0.0.0.0\n\
|
||||
rpcallowip=0.0.0.0/0\n\
|
||||
rpcport=8332\n\
|
||||
listen=1\n\
|
||||
printtoconsole=1\n";
|
||||
printtoconsole=1\n", rpc_pass);
|
||||
let _ = tokio::fs::create_dir_all(bitcoin_dir).await;
|
||||
let _ = tokio::fs::write(&conf_path, bitcoin_conf).await;
|
||||
info!("Created bitcoin.conf at {} with RPC + txindex enabled", conf_path);
|
||||
@@ -347,7 +350,7 @@ printtoconsole=1\n";
|
||||
run_args.push("--cpus=2");
|
||||
|
||||
// Health check definitions
|
||||
let health_args = get_health_check_args(package_id);
|
||||
let health_args = get_health_check_args(package_id, &rpc_pass);
|
||||
for arg in &health_args {
|
||||
run_args.push(arg);
|
||||
}
|
||||
@@ -1316,10 +1319,11 @@ fn is_readonly_compatible(app_id: &str) -> bool {
|
||||
|
||||
/// Get container health check arguments for podman run.
|
||||
/// Returns (health-cmd, interval, retries) args to append to run_args.
|
||||
fn get_health_check_args(app_id: &str) -> Vec<String> {
|
||||
fn get_health_check_args(app_id: &str, rpc_pass: &str) -> Vec<String> {
|
||||
let btc_health = format!("bitcoin-cli -rpcuser=archipelago -rpcpassword={} getblockchaininfo || exit 1", rpc_pass);
|
||||
let (cmd, interval, retries) = match app_id {
|
||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => (
|
||||
"bitcoin-cli -rpcuser=archipelago -rpcpassword=archipelago123 getblockchaininfo || exit 1",
|
||||
btc_health.as_str(),
|
||||
"30s", "3",
|
||||
),
|
||||
"lnd" => (
|
||||
@@ -1462,6 +1466,7 @@ fn get_app_config(
|
||||
app_id: &str,
|
||||
host_ip: &str,
|
||||
allocator: &mut PortAllocator,
|
||||
rpc_pass: &str,
|
||||
) -> (Vec<String>, Vec<String>, Vec<String>, Option<String>, Option<Vec<String>>) {
|
||||
match app_id {
|
||||
"homeassistant" | "home-assistant" => (
|
||||
@@ -1495,7 +1500,7 @@ fn get_app_config(
|
||||
"BTCPAY_CHAINS=btc".to_string(),
|
||||
format!("BTCPAY_BTCRPCURL=http://{}:8332", host_ip),
|
||||
"BTCPAY_BTCRPCUSER=archipelago".to_string(),
|
||||
"BTCPAY_BTCRPCPASSWORD=archipelago123".to_string(),
|
||||
format!("BTCPAY_BTCRPCPASSWORD={}", rpc_pass),
|
||||
"BTCPAY_POSTGRES=User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true".to_string(),
|
||||
],
|
||||
None,
|
||||
@@ -1519,7 +1524,7 @@ fn get_app_config(
|
||||
format!("CORE_RPC_HOST={}", host_ip),
|
||||
"CORE_RPC_PORT=8332".to_string(),
|
||||
"CORE_RPC_USERNAME=archipelago".to_string(),
|
||||
"CORE_RPC_PASSWORD=archipelago123".to_string(),
|
||||
format!("CORE_RPC_PASSWORD={}", rpc_pass),
|
||||
"DATABASE_ENABLED=true".to_string(),
|
||||
"DATABASE_HOST=archy-mempool-db".to_string(),
|
||||
"DATABASE_DATABASE=mempool".to_string(),
|
||||
@@ -1536,7 +1541,7 @@ fn get_app_config(
|
||||
vec!["50001:50001".to_string()],
|
||||
vec!["/var/lib/archipelago/electrumx:/data".to_string()],
|
||||
vec![
|
||||
format!("DAEMON_URL=http://archipelago:archipelago123@{}:8332/", bitcoin_host),
|
||||
format!("DAEMON_URL=http://archipelago:{}@{}:8332/", rpc_pass, bitcoin_host),
|
||||
"COIN=Bitcoin".to_string(),
|
||||
"DB_DIRECTORY=/data".to_string(),
|
||||
"SERVICES=tcp://:50001,rpc://0.0.0.0:8000".to_string(),
|
||||
@@ -1701,7 +1706,7 @@ fn get_app_config(
|
||||
vec![
|
||||
"FM_DATA_DIR=/data".to_string(),
|
||||
"FM_BITCOIND_USERNAME=archipelago".to_string(),
|
||||
"FM_BITCOIND_PASSWORD=archipelago123".to_string(),
|
||||
format!("FM_BITCOIND_PASSWORD={}", rpc_pass),
|
||||
"FM_BITCOIN_NETWORK=bitcoin".to_string(),
|
||||
"FM_BIND_P2P=0.0.0.0:8173".to_string(),
|
||||
"FM_BIND_API=0.0.0.0:8174".to_string(),
|
||||
@@ -1727,7 +1732,7 @@ fn get_app_config(
|
||||
"--network".to_string(), "bitcoin".to_string(),
|
||||
"--bitcoind-url".to_string(), format!("http://{}:8332", host_ip),
|
||||
"--bitcoind-username".to_string(), "archipelago".to_string(),
|
||||
"--bitcoind-password".to_string(), "archipelago123".to_string(),
|
||||
"--bitcoind-password".to_string(), rpc_pass.to_string(),
|
||||
"ldk".to_string(),
|
||||
"--ldk-lightning-port".to_string(), "9737".to_string(),
|
||||
"--ldk-alias".to_string(), "archipelago-gateway".to_string(),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//! 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Dev fallback (will only work on dev machines with default config)
|
||||
debug!("Bitcoin RPC password: using dev fallback");
|
||||
"archipelago123".to_string()
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
@@ -13,11 +13,9 @@ const ELECTRUMX_DATA_DIR: &str = "/var/lib/archipelago/electrumx";
|
||||
// Approximate final index size in bytes for mainnet (~55GB for ElectrumX full index)
|
||||
const ESTIMATED_FULL_INDEX_BYTES: f64 = 55_000_000_000.0;
|
||||
|
||||
/// Build Bitcoin RPC Basic auth header from env vars.
|
||||
/// Falls back to cookie auth file if env vars are not set.
|
||||
fn bitcoin_rpc_auth() -> String {
|
||||
let user = std::env::var("BITCOIN_RPC_USER").unwrap_or_else(|_| "archipelago".to_string());
|
||||
let pass = std::env::var("BITCOIN_RPC_PASSWORD").unwrap_or_else(|_| "archipelago123".to_string());
|
||||
/// Build Bitcoin RPC Basic auth header using shared credentials.
|
||||
async fn bitcoin_rpc_auth() -> String {
|
||||
let (user, pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
use base64::Engine;
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
|
||||
format!("Basic {}", encoded)
|
||||
@@ -120,7 +118,7 @@ async fn bitcoin_network_height() -> Result<u64> {
|
||||
let resp = client
|
||||
.post(BITCOIN_RPC_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", bitcoin_rpc_auth())
|
||||
.header("Authorization", bitcoin_rpc_auth().await)
|
||||
.body(body.to_string())
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -9,6 +9,7 @@ use tokio::signal;
|
||||
mod api;
|
||||
mod auth;
|
||||
mod backup;
|
||||
mod bitcoin_rpc;
|
||||
mod config;
|
||||
mod content_server;
|
||||
mod crash_recovery;
|
||||
|
||||
@@ -1301,6 +1301,8 @@ async fn handle_tx_relay_broadcast(
|
||||
}
|
||||
};
|
||||
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
|
||||
// Pre-flight: check if Bitcoin Core is reachable and synced
|
||||
let preflight_body = serde_json::json!({
|
||||
"jsonrpc": "1.0",
|
||||
@@ -1311,7 +1313,7 @@ async fn handle_tx_relay_broadcast(
|
||||
|
||||
match client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&preflight_body)
|
||||
.send()
|
||||
.await
|
||||
@@ -1364,7 +1366,7 @@ async fn handle_tx_relay_broadcast(
|
||||
|
||||
let txid = match client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
@@ -1522,9 +1524,10 @@ async fn check_tx_confirmations(client: &reqwest::Client, txid: &str) -> anyhow:
|
||||
"method": "gettransaction",
|
||||
"params": [txid]
|
||||
});
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
let resp = client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -602,12 +602,13 @@ struct BlockHeaderInfo {
|
||||
}
|
||||
|
||||
async fn bitcoin_rpc_getblockcount(client: &reqwest::Client) -> Result<u64> {
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
let body = serde_json::json!({
|
||||
"jsonrpc": "1.0", "id": "mesh", "method": "getblockcount", "params": []
|
||||
});
|
||||
let resp: BitcoinRpcResponse<u64> = client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
@@ -625,13 +626,14 @@ async fn bitcoin_rpc_getblockheader_by_height(
|
||||
client: &reqwest::Client,
|
||||
height: u64,
|
||||
) -> Result<BlockHeaderInfo> {
|
||||
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
// First get block hash for this height
|
||||
let body = serde_json::json!({
|
||||
"jsonrpc": "1.0", "id": "mesh", "method": "getblockhash", "params": [height]
|
||||
});
|
||||
let resp: BitcoinRpcResponse<String> = client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
@@ -645,7 +647,7 @@ async fn bitcoin_rpc_getblockheader_by_height(
|
||||
});
|
||||
let resp: BitcoinRpcResponse<serde_json::Value> = client
|
||||
.post("http://127.0.0.1:8332/")
|
||||
.basic_auth("archipelago", Some("archipelago123"))
|
||||
.basic_auth(&rpc_user, Some(&rpc_pass))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
|
||||
Reference in New Issue
Block a user