fix: rpcauth credentials, reboot survival, system Tor for all containers

- Bitcoin RPC: switch to rpcauth (salted hash in bitcoin.conf, no plaintext
  in config or CLI). Password stable across reboots/restarts/deploys.
- Remove daily-reboot-test.sh cron on both servers
- Enable podman-restart.service for container auto-start after reboot
- System Tor: SocksPort 0.0.0.0:9050 with SocksPolicy for container access
- LND: tor.socks=host.containers.internal:9050 (system Tor, not container)
- Bitcoin: -proxy=host.containers.internal:9050 for Tor outbound
- bitcoin_rpc.rs: reads from secrets file, cached, stable credentials
- package.rs: dynamic rpc_user/rpc_pass, rpcauth hash generation
- network.rs: fix missing send_to_peer args (mesh encryption update)
- first-boot-containers.sh: rpcauth generation, system Tor config
- deploy-to-target.sh: rpcauth credentials, LND config migration
- Mesh: encrypted channel message support (ChaCha20-Poly1305 updates)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-03-20 11:56:20 +00:00
parent b4d204d1d6
commit b31148a8b7
8 changed files with 278 additions and 60 deletions

View File

@ -121,6 +121,8 @@ impl RpcHandler {
to_onion,
my_pubkey,
&req_msg.to_string(),
None,
None,
).await?;
// Also add them as a pending peer locally

View File

@ -193,15 +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;
// Read Bitcoin RPC credentials from cookie file for container configs
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().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, &rpc_pass)
get_app_config(package_id, &self.config.host_ip, &mut allocator, &rpc_user, &rpc_pass)
};
// Fedimint Gateway: auto-detect LND and switch to lnd mode
@ -224,7 +224,7 @@ impl RpcHandler {
"$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC".to_string(),
"--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-username".to_string(), rpc_user.clone(),
"--bitcoind-password".to_string(), rpc_pass.clone(),
"lnd".to_string(),
"--lnd-rpc-host".to_string(), format!("{}:10009", self.config.host_ip),
@ -304,24 +304,34 @@ impl RpcHandler {
}
}
// Pre-install: Create bitcoin.conf for Bitcoin nodes with RPC + txindex
// Pre-install: Create bitcoin.conf with rpcauth (salted hash, no plaintext)
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
let bitcoin_dir = "/var/lib/archipelago/bitcoin";
let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
// Generate rpcauth hash: HMAC-SHA256(salt, password)
use hmac::{Hmac, Mac};
use sha2::Sha256;
let salt_bytes: [u8; 16] = rand::random();
let salt_hex = hex::encode(salt_bytes);
let mut mac = Hmac::<Sha256>::new_from_slice(salt_hex.as_bytes())
.expect("HMAC accepts any key length");
mac.update(rpc_pass.as_bytes());
let hash_hex = hex::encode(mac.finalize().into_bytes());
let rpcauth_line = format!("rpcauth={}:{}${}", rpc_user, salt_hex, hash_hex);
let bitcoin_conf = format!("\
# rpcauth: salted hash only no plaintext password in config or CLI\n\
{}\n\
server=1\n\
prune=550\n\
rpcuser=archipelago\n\
rpcpassword={}\n\
rpcbind=0.0.0.0\n\
rpcallowip=127.0.0.1/32\n\
rpcallowip=10.88.0.0/16\n\
rpcallowip=0.0.0.0/0\n\
rpcport=8332\n\
listen=1\n\
printtoconsole=1\n", rpc_pass);
printtoconsole=1\n", rpcauth_line);
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);
info!("Created bitcoin.conf with rpcauth (no plaintext credentials)");
}
// Add port mappings (skip if host network mode like Tailscale)
@ -1481,6 +1491,7 @@ fn get_app_config(
app_id: &str,
host_ip: &str,
allocator: &mut PortAllocator,
rpc_user: &str,
rpc_pass: &str,
) -> (Vec<String>, Vec<String>, Vec<String>, Option<String>, Option<Vec<String>>) {
match app_id {
@ -1514,7 +1525,7 @@ fn get_app_config(
format!("BTCPAY_HOST={}:23000", host_ip),
"BTCPAY_CHAINS=btc".to_string(),
format!("BTCPAY_BTCRPCURL=http://{}:8332", host_ip),
"BTCPAY_BTCRPCUSER=archipelago".to_string(),
format!("BTCPAY_BTCRPCUSER={}", rpc_user),
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(),
],
@ -1538,7 +1549,7 @@ fn get_app_config(
"ELECTRUM_TLS_ENABLED=false".to_string(),
format!("CORE_RPC_HOST={}", host_ip),
"CORE_RPC_PORT=8332".to_string(),
"CORE_RPC_USERNAME=archipelago".to_string(),
format!("CORE_RPC_USERNAME={}", rpc_user),
format!("CORE_RPC_PASSWORD={}", rpc_pass),
"DATABASE_ENABLED=true".to_string(),
"DATABASE_HOST=archy-mempool-db".to_string(),
@ -1556,7 +1567,7 @@ fn get_app_config(
vec!["50001:50001".to_string()],
vec!["/var/lib/archipelago/electrumx:/data".to_string()],
vec![
format!("DAEMON_URL=http://archipelago:{}@{}:8332/", rpc_pass, bitcoin_host),
format!("DAEMON_URL=http://{}:{}@{}:8332/", rpc_user, rpc_pass, bitcoin_host),
"COIN=Bitcoin".to_string(),
"DB_DIRECTORY=/data".to_string(),
"SERVICES=tcp://:50001,rpc://0.0.0.0:8000".to_string(),
@ -1720,7 +1731,7 @@ fn get_app_config(
vec!["/var/lib/archipelago/fedimint:/data".to_string()],
vec![
"FM_DATA_DIR=/data".to_string(),
"FM_BITCOIND_USERNAME=archipelago".to_string(),
format!("FM_BITCOIND_USERNAME={}", rpc_user),
format!("FM_BITCOIND_PASSWORD={}", rpc_pass),
"FM_BITCOIN_NETWORK=bitcoin".to_string(),
"FM_BIND_P2P=0.0.0.0:8173".to_string(),
@ -1746,7 +1757,7 @@ fn get_app_config(
"$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC".to_string(),
"--network".to_string(), "bitcoin".to_string(),
"--bitcoind-url".to_string(), format!("http://{}:8332", host_ip),
"--bitcoind-username".to_string(), "archipelago".to_string(),
"--bitcoind-username".to_string(), rpc_user.to_string(),
"--bitcoind-password".to_string(), rpc_pass.to_string(),
"ldk".to_string(),
"--ldk-lightning-port".to_string(), "9737".to_string(),

View File

@ -1,18 +1,21 @@
//! Shared Bitcoin RPC credential management.
//! Reads credentials from the per-installation secrets file, falling back to
//! environment variables, then a dev-only default.
//! Bitcoin RPC credential management.
//!
//! Uses `rpcauth` in bitcoin.conf (salted hash — no plaintext in config or CLI).
//! The actual password is stored in `/var/lib/archipelago/secrets/bitcoin-rpc-password`
//! and stays stable across reboots, restarts, and deploys.
use tokio::sync::OnceCell;
use tracing::debug;
const SECRETS_PATH: &str = "/var/lib/archipelago/secrets/bitcoin-rpc-password";
const DEFAULT_USER: &str = "archipelago";
const RPC_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.
/// Read the Bitcoin RPC password from the secrets file.
/// Falls back to env var (dev), then generates and persists a random password.
async fn read_password() -> String {
// 1. Try secrets file (production path)
// 1. Secrets file (production)
if let Ok(pass) = tokio::fs::read_to_string(SECRETS_PATH).await {
let pass = pass.trim().to_string();
if !pass.is_empty() {
@ -21,7 +24,7 @@ async fn read_password() -> String {
}
}
// 2. Try environment variable
// 2. Environment variable (dev)
if let Ok(pass) = std::env::var("BITCOIN_RPC_PASSWORD") {
if !pass.is_empty() {
debug!("Bitcoin RPC password loaded from env var");
@ -29,29 +32,31 @@ async fn read_password() -> String {
}
}
// 3. Generate a random password and persist it (first-boot provisioning)
// 3. Generate and persist (first boot)
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));
let _ = std::fs::set_permissions(
SECRETS_PATH,
std::fs::Permissions::from_mode(0o600),
);
}
debug!("Bitcoin RPC password: generated and saved to secrets file");
debug!("Bitcoin RPC password generated and saved");
}
Err(e) => {
tracing::warn!("Failed to save generated Bitcoin RPC password: {} — using ephemeral", e);
tracing::warn!("Failed to save Bitcoin RPC password: {}", e);
}
}
random_pass
}
/// Generate a cryptographically random password for Bitcoin RPC (32 hex chars).
/// Generate a cryptographically random password (32 hex chars).
fn generate_random_password() -> String {
let bytes: [u8; 16] = rand::random();
hex::encode(bytes)
@ -62,11 +67,16 @@ 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())
(RPC_USER.to_string(), pass.clone())
}
/// Get the Bitcoin RPC password as a plain string (for config generation).
/// Get the Bitcoin RPC password (for container config generation).
pub async fn bitcoin_rpc_password() -> String {
let (_, pass) = bitcoin_rpc_credentials().await;
pass
}
/// Get the Bitcoin RPC username.
pub async fn bitcoin_rpc_username() -> String {
RPC_USER.to_string()
}

View File

@ -66,6 +66,10 @@ pub struct MeshState {
pub stego_mode: super::steganography::SteganographyMode,
/// Chunk reassembly buffer for multi-frame messages.
chunk_buffer: RwLock<HashMap<(u32, u8), ChunkAssembly>>,
/// Double Ratchet session manager for forward-secret encryption.
pub session_manager: Arc<super::session::SessionManager>,
/// Whether to encrypt directed relay messages (config toggle for rollback).
pub encrypt_relay: bool,
}
/// In-progress chunk reassembly for a multi-frame message.
@ -81,6 +85,8 @@ impl MeshState {
block_header_cache: Arc<super::bitcoin_relay::BlockHeaderCache>,
relay_tracker: Option<Arc<super::bitcoin_relay::RelayTracker>>,
stego_mode: super::steganography::SteganographyMode,
encrypt_relay: bool,
session_manager: Arc<super::session::SessionManager>,
) -> (Arc<Self>, broadcast::Receiver<MeshEvent>, mpsc::Receiver<MeshCommand>) {
let (tx, rx) = broadcast::channel(64);
let (cmd_tx, cmd_rx) = mpsc::channel(32);
@ -108,6 +114,8 @@ impl MeshState {
relay_tracker,
stego_mode,
chunk_buffer: RwLock::new(HashMap::new()),
session_manager,
encrypt_relay,
});
(state, rx, cmd_rx)
}
@ -496,6 +504,8 @@ async fn handle_frame(
handle_typed_message(&payload, contact_id, &name, state).await;
} else if let Some(decoded) = try_base64_typed(&payload) {
handle_typed_message(&decoded, contact_id, &name, state).await;
} else if let Some(decoded) = try_decrypt_ratchet_base64(&payload, contact_id, state).await {
handle_typed_message(&decoded, contact_id, &name, state).await;
} else if let Some(decoded) = try_decrypt_base64(&payload, contact_id, state).await {
handle_typed_message(&decoded, contact_id, &name, state).await;
} else if let Some(decoded) = try_chunk_reassemble(&payload, contact_id, state).await {
@ -521,6 +531,8 @@ async fn handle_frame(
handle_typed_message(&payload, contact_id, &name, state).await;
} else if let Some(decoded) = try_base64_typed(&payload) {
handle_typed_message(&decoded, contact_id, &name, state).await;
} else if let Some(decoded) = try_decrypt_ratchet_base64(&payload, contact_id, state).await {
handle_typed_message(&decoded, contact_id, &name, state).await;
} else if let Some(decoded) = try_decrypt_base64(&payload, contact_id, state).await {
handle_typed_message(&decoded, contact_id, &name, state).await;
} else if let Some(decoded) = try_chunk_reassemble(&payload, contact_id, state).await {
@ -853,6 +865,66 @@ async fn try_decrypt_base64(
try_decrypt_typed(&decoded, sender_contact_id, &secrets)
}
/// Try to decrypt a Double Ratchet encrypted message (0xDD prefix).
/// Format: [0xDD] [RatchetHeader(40) + nonce(12) + ciphertext + tag(16)]
/// Returns the decrypted typed wire bytes ([0x02][CBOR]) if successful.
async fn try_decrypt_ratchet(
decoded: &[u8],
sender_contact_id: u32,
state: &Arc<MeshState>,
) -> Option<Vec<u8>> {
if decoded.first() != Some(&message_types::RATCHET_TYPED_MARKER) {
return None;
}
let ratchet_bytes = &decoded[1..]; // skip 0xDD marker
let ratchet_msg = match super::ratchet::RatchetMessage::from_bytes(ratchet_bytes) {
Ok(msg) => msg,
Err(e) => {
warn!(contact_id = sender_contact_id, "Failed to parse ratchet message: {}", e);
return None;
}
};
// Look up peer DID for session manager
let peer_did = state.peers.read().await
.get(&sender_contact_id)
.and_then(|p| p.did.clone())?;
match state.session_manager.decrypt_from_peer(&peer_did, &ratchet_msg).await {
Ok(plaintext) => {
debug!(contact_id = sender_contact_id, did = %peer_did, "Decrypted ratchet message (0xDD)");
// The plaintext should be the original [0x02][CBOR] typed wire
if TypedEnvelope::is_typed(&plaintext) {
Some(plaintext)
} else {
// Could be nested stego → typed
unwrap_wire_layers(&plaintext)
}
}
Err(e) => {
warn!(contact_id = sender_contact_id, "Ratchet decrypt failed: {}", e);
None
}
}
}
/// Try to base64-decode and decrypt a ratchet-encrypted message.
/// Handles the case where ratchet messages arrive as base64 text.
async fn try_decrypt_ratchet_base64(
payload: &[u8],
sender_contact_id: u32,
state: &Arc<MeshState>,
) -> Option<Vec<u8>> {
use base64::Engine;
let text = std::str::from_utf8(payload).ok()?;
let decoded = base64::engine::general_purpose::STANDARD.decode(text.trim()).ok()?;
if decoded.first() != Some(&message_types::RATCHET_TYPED_MARKER) {
return None;
}
try_decrypt_ratchet(&decoded, sender_contact_id, state).await
}
/// Unwrap wire layers: encrypted (0xEE) → stego (0xAA) → typed (0x02).
/// Returns None if decoding fails at any layer (caller should use shared_secrets variant).
fn unwrap_wire_layers(decoded: &[u8]) -> Option<Vec<u8>> {
@ -958,7 +1030,19 @@ async fn try_chunk_reassemble(
}
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(&combined) {
// Check for encrypted frame (0xEE) — decrypt then unwrap
// Check for ratchet-encrypted frame (0xDD) — decrypt then unwrap
if decoded.first() == Some(&message_types::RATCHET_TYPED_MARKER) {
// Must drop buffer lock before calling async try_decrypt_ratchet
let decoded_clone = decoded.clone();
drop(buffer);
if let Some(typed_wire) = try_decrypt_ratchet(&decoded_clone, sender_contact_id, state).await {
info!(msg_id, chunks = total, total_len = typed_wire.len(), "Reassembled ratchet-encrypted chunked message");
state.chunk_buffer.write().await.remove(&key);
return Some(typed_wire);
}
buffer = state.chunk_buffer.write().await;
}
// Check for static-encrypted frame (0xEE) — decrypt then unwrap
if decoded.first() == Some(&message_types::ENCRYPTED_TYPED_MARKER) {
let secrets = state.shared_secrets.read().await;
if let Some(typed_wire) = try_decrypt_typed(&decoded, sender_contact_id, &secrets) {
@ -1599,9 +1683,69 @@ async fn send_confirmation_update(
}
}
/// Encrypt a typed wire payload for a specific peer.
/// Attempts ratchet encryption first (forward secrecy), falls back to static
/// shared secret, falls back to plaintext if neither is available.
/// Respects the encrypt_relay config toggle for rollback.
async fn encrypt_for_peer(
state: &Arc<MeshState>,
contact_id: u32,
typed_wire: &[u8],
) -> Vec<u8> {
if !state.encrypt_relay {
return typed_wire.to_vec();
}
// Look up peer DID for ratchet session
let peer_did = state.peers.read().await
.get(&contact_id)
.and_then(|p| p.did.clone());
// Try ratchet encryption first (forward secrecy)
if let Some(ref did) = peer_did {
if state.session_manager.has_session(did).await {
match state.session_manager.encrypt_for_peer(did, typed_wire).await {
Ok(ratchet_msg) => {
let ratchet_bytes = ratchet_msg.to_bytes();
let mut buf = Vec::with_capacity(1 + ratchet_bytes.len());
buf.push(message_types::RATCHET_TYPED_MARKER);
buf.extend_from_slice(&ratchet_bytes);
debug!(contact_id, did = %did, "Encrypted with Double Ratchet (0xDD)");
return buf;
}
Err(e) => {
warn!(contact_id, did = %did, "Ratchet encrypt failed, trying static: {}", e);
}
}
}
}
// Fall back to static shared secret (0xEE)
let secrets = state.shared_secrets.read().await;
if let Some(secret) = secrets.get(&contact_id) {
match crypto::encrypt(secret, typed_wire) {
Ok(ciphertext) => {
let mut buf = Vec::with_capacity(1 + ciphertext.len());
buf.push(message_types::ENCRYPTED_TYPED_MARKER);
buf.extend_from_slice(&ciphertext);
debug!(contact_id, "Encrypted with static shared secret (0xEE)");
return buf;
}
Err(e) => {
warn!(contact_id, "Static encrypt failed, sending plaintext: {}", e);
}
}
}
// No encryption available — send plaintext
debug!(contact_id, "No encryption available, sending plaintext (0x02)");
typed_wire.to_vec()
}
/// Send raw wire bytes to a specific peer by contact_id.
/// Falls back to channel 0 broadcast if peer's pubkey is unknown.
async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, payload: Vec<u8>) {
/// Encrypts directed messages via ratchet or shared secret when available.
/// Falls back to channel 0 broadcast (plaintext) if peer's pubkey is unknown.
async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, typed_wire: Vec<u8>) {
let peers = state.peers.read().await;
if let Some(peer) = peers.get(&contact_id) {
if let Some(ref pk) = peer.pubkey_hex {
@ -1610,6 +1754,8 @@ async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, payload: Vec<u8>)
let mut prefix = [0u8; 6];
prefix.copy_from_slice(&pk_bytes[..6]);
drop(peers);
// Encrypt for this specific peer before sending
let payload = encrypt_for_peer(state, contact_id, &typed_wire).await;
let _ = state.cmd_tx.send(MeshCommand::SendRaw {
dest_pubkey_prefix: prefix,
payload,
@ -1620,9 +1766,10 @@ async fn send_to_peer(state: &Arc<MeshState>, contact_id: u32, payload: Vec<u8>)
}
}
drop(peers);
// Broadcast fallback — plaintext (no specific peer to encrypt for)
let _ = state.cmd_tx.send(MeshCommand::BroadcastChannel {
channel: 0,
payload,
payload: typed_wire,
}).await;
}

View File

@ -14,10 +14,14 @@ use serde::{Deserialize, Serialize};
/// Wire prefix for typed messages.
pub const TYPED_MESSAGE_MARKER: u8 = 0x02;
/// Wire prefix for encrypted typed messages (E2E encrypted with shared secret).
/// Wire prefix for encrypted typed messages (E2E encrypted with static shared secret).
/// Format: [0xEE] [nonce: 12 bytes] [ciphertext + auth tag]
pub const ENCRYPTED_TYPED_MARKER: u8 = 0xEE;
/// Wire prefix for Double Ratchet encrypted typed messages (forward secrecy).
/// Format: [0xDD] [RatchetHeader: 40 bytes] [nonce: 12] [ciphertext] [tag: 16]
pub const RATCHET_TYPED_MARKER: u8 = 0xDD;
/// Message type discriminator.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -73,6 +73,14 @@ pub struct MeshConfig {
/// Steganographic encoding mode for mesh messages (Normal = disabled).
#[serde(default)]
pub steganography_mode: steganography::SteganographyMode,
/// Encrypt directed relay messages (TX, Lightning, block headers) via ratchet or shared secret.
/// Set to false to disable encryption for debugging or rollback.
#[serde(default = "default_true")]
pub encrypt_relay_messages: bool,
}
fn default_true() -> bool {
true
}
impl Default for MeshConfig {
@ -86,6 +94,7 @@ impl Default for MeshConfig {
mesh_only_mode: None,
announce_block_headers: false,
steganography_mode: steganography::SteganographyMode::Normal,
encrypt_relay_messages: true,
}
}
}
@ -162,11 +171,14 @@ impl MeshService {
let block_header_cache = Arc::new(BlockHeaderCache::new());
let relay_tracker = Arc::new(RelayTracker::new());
let session_manager = Arc::new(session::SessionManager::new(data_dir));
let (state, _rx, cmd_rx) = MeshState::new(
&channel_name,
Arc::clone(&block_header_cache),
Some(Arc::clone(&relay_tracker)),
config.steganography_mode,
config.encrypt_relay_messages,
Arc::clone(&session_manager),
);
// Derive X25519 keys from Ed25519 identity

View File

@ -892,20 +892,20 @@ MANIFEST_EOF
# Bitcoin Knots: required for Mempool, ElectrumX, BTCPay, Fedimint
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"
# Read per-installation Bitcoin RPC credentials from server secrets
# Read Bitcoin RPC credentials from secrets file (rpcauth — stable across restarts)
progress "Reading Bitcoin RPC credentials"
BITCOIN_RPC_PASS=$(ssh $SSH_OPTS "$TARGET_HOST" '
SECRETS_DIR="/var/lib/archipelago/secrets"
sudo mkdir -p "$SECRETS_DIR" && sudo chmod 700 "$SECRETS_DIR"
if [ ! -f "$SECRETS_DIR/bitcoin-rpc-password" ]; then
openssl rand -base64 24 | sudo tee "$SECRETS_DIR/bitcoin-rpc-password" > /dev/null
openssl rand -hex 16 | sudo tee "$SECRETS_DIR/bitcoin-rpc-password" > /dev/null
sudo chmod 600 "$SECRETS_DIR/bitcoin-rpc-password"
fi
sudo cat "$SECRETS_DIR/bitcoin-rpc-password"
' 2>/dev/null)
BITCOIN_RPC_USER="archipelago"
if [ -z "$BITCOIN_RPC_PASS" ]; then
echo " WARNING: Could not read Bitcoin RPC password from server, aborting container fixes"
echo " WARNING: Could not read Bitcoin RPC password from server"
return 1
fi
@ -968,7 +968,6 @@ MANIFEST_EOF
docker.io/bitcoinknots/bitcoin:latest \
-server=1 \$BTC_EXTRA_ARGS \
-rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \
-rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS \
-dbcache=\$BTC_DBCACHE
echo ' Bitcoin Knots started (sync may take hours)'
else
@ -1115,7 +1114,7 @@ MANIFEST_EOF
-e NBXPLORER_CHAINS=btc \
-e NBXPLORER_BIND=0.0.0.0:32838 \
-e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \
-e NBXPLORER_BTCRPCUSER=archipelago \
-e NBXPLORER_BTCRPCUSER=$BITCOIN_RPC_USER \
-e NBXPLORER_BTCRPCPASSWORD=$BITCOIN_RPC_PASS \
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
docker.io/nicolasdorier/nbxplorer:2.6.0
@ -1427,13 +1426,20 @@ autopilot.active=false
LNDCONF
sudo cp /tmp/lnd.conf /var/lib/archipelago/lnd/lnd.conf
else
# Fix stale LND configs from older installs (localhost → container DNS, old password → current)
# Fix stale LND configs (cookie mode, localhost, wrong password)
LND_CONF=/var/lib/archipelago/lnd/lnd.conf
if grep -q "rpchost=127.0.0.1" "$LND_CONF" 2>/dev/null || grep -q "rpcpass=archipelago123" "$LND_CONF" 2>/dev/null; then
echo " Fixing stale LND config (rpchost/rpcpass)..."
NEEDS_FIX=0
grep -q "rpccookie" "$LND_CONF" 2>/dev/null && NEEDS_FIX=1
grep -q "rpchost=127.0.0.1" "$LND_CONF" 2>/dev/null && NEEDS_FIX=1
if [ "$NEEDS_FIX" = "1" ]; then
echo " Fixing stale LND config..."
cp "$LND_CONF" /tmp/lnd.conf.fix
sed -i "/bitcoind.rpccookie/d" /tmp/lnd.conf.fix
sed -i "/bitcoind.rpcuser/d" /tmp/lnd.conf.fix
sed -i "/bitcoind.rpcpass/d" /tmp/lnd.conf.fix
sed -i "s|bitcoind.rpchost=127.0.0.1:8332|bitcoind.rpchost=bitcoin-knots:8332|" /tmp/lnd.conf.fix
sed -i "s|bitcoind.rpcpass=archipelago123|bitcoind.rpcpass=$BITCOIN_RPC_PASS|" /tmp/lnd.conf.fix
sed -i "/bitcoind.rpchost=/a bitcoind.rpcuser=$BITCOIN_RPC_USER" /tmp/lnd.conf.fix
sed -i "/bitcoind.rpcuser=/a bitcoind.rpcpass=$BITCOIN_RPC_PASS" /tmp/lnd.conf.fix
sudo cp /tmp/lnd.conf.fix "$LND_CONF"
sudo chown 100000:100000 "$LND_CONF"
RESTART_LND=1

View File

@ -35,16 +35,43 @@ wait_for_container() {
return 1
}
# Generate per-installation credentials if not already saved
# rpcauth: password hash in bitcoin.conf, plaintext in secrets file only.
# Credentials are STABLE across reboots, restarts, and deploys.
SECRETS_DIR="/var/lib/archipelago/secrets"
mkdir -p "$SECRETS_DIR" && chmod 700 "$SECRETS_DIR"
if [ ! -f "$SECRETS_DIR/bitcoin-rpc-password" ]; then
openssl rand -base64 24 > "$SECRETS_DIR/bitcoin-rpc-password"
openssl rand -hex 16 > "$SECRETS_DIR/bitcoin-rpc-password"
chmod 600 "$SECRETS_DIR/bitcoin-rpc-password"
fi
BITCOIN_RPC_USER="archipelago"
BITCOIN_RPC_PASS=$(cat "$SECRETS_DIR/bitcoin-rpc-password")
# Generate rpcauth line for bitcoin.conf (salted HMAC-SHA256 hash)
generate_rpcauth() {
local user="$1" pass="$2"
local salt=$(openssl rand -hex 16)
local hash=$(echo -n "$pass" | openssl dgst -sha256 -hmac "$salt" -hex 2>/dev/null | awk '{print $NF}')
echo "${user}:${salt}\$${hash}"
}
# Write bitcoin.conf with rpcauth if not exists or needs update
BITCOIN_CONF="/var/lib/archipelago/bitcoin/bitcoin.conf"
if [ ! -f "$BITCOIN_CONF" ] || ! grep -q "^rpcauth=" "$BITCOIN_CONF" 2>/dev/null; then
mkdir -p /var/lib/archipelago/bitcoin
RPCAUTH=$(generate_rpcauth "$BITCOIN_RPC_USER" "$BITCOIN_RPC_PASS")
cat > "$BITCOIN_CONF" << BTCCONF
# rpcauth: salted hash only — no plaintext password in config or CLI
rpcauth=${RPCAUTH}
server=1
rpcbind=0.0.0.0
rpcallowip=0.0.0.0/0
rpcport=8332
listen=1
printtoconsole=1
BTCCONF
log "Generated bitcoin.conf with rpcauth (no plaintext credentials)"
fi
# Generate per-installation database passwords if not already saved
for svc in mempool btcpay immich penpot mysql-root; do
if [ ! -f "$SECRETS_DIR/${svc}-db-password" ]; then
@ -226,8 +253,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
docker.io/bitcoinknots/bitcoin:latest \
-server=1 $BTC_EXTRA_ARGS \
-rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \
-rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS \
-proxy=127.0.0.1:9050 -listen=1 -bind=0.0.0.0:8333 \
-proxy=host.containers.internal:9050 -listen=1 -bind=0.0.0.0:8333 \
-dbcache=$BTC_DBCACHE 2>>"$LOG"; then
log "Bitcoin Knots started"
else
@ -237,7 +263,7 @@ else
$DOCKER network connect archy-net bitcoin-knots 2>/dev/null || true
log "Bitcoin Knots already running"
fi
# Wait for Bitcoin Knots RPC to be responsive (LND, NBXplorer, mempool depend on it)
# Wait for Bitcoin Knots RPC to be responsive
wait_for_container "Bitcoin Knots RPC" "$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS getblockchaininfo" 60
# Ensure wallet exists (Bitcoin Knots no longer auto-creates a default wallet)
@ -270,7 +296,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then
mkdir -p /var/lib/archipelago/electrumx
$DOCKER run -d --name electrumx --restart unless-stopped --memory=$(mem_limit electrumx) --network archy-net \
-p 50001:50001 -v /var/lib/archipelago/electrumx:/data \
-e DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/ \
-e "DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/" \
-e COIN=Bitcoin -e DB_DIRECTORY=/data \
-e SERVICES=tcp://:50001,rpc://0.0.0.0:8000 \
docker.io/lukechilds/electrumx:v1.18.0 2>>"$LOG" || true
@ -284,7 +310,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then
-p 8999:8999 -v /var/lib/archipelago/mempool:/data \
-e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=electrumx -e ELECTRUM_PORT=50001 \
-e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \
-e CORE_RPC_USERNAME=$BITCOIN_RPC_USER -e CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS \
-e "CORE_RPC_USERNAME=$BITCOIN_RPC_USER" -e "CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS" \
-e DATABASE_ENABLED=true -e DATABASE_HOST="$MYSQL_CNT" -e DATABASE_DATABASE=mempool \
-e DATABASE_USERNAME=mempool -e DATABASE_PASSWORD=$MEMPOOL_DB_PASS \
docker.io/mempool/backend:v2.5.0 2>>"$LOG" || true
@ -358,7 +384,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
-e BTCPAY_HOST="$TARGET_IP:23000" -e BTCPAY_CHAINS=btc \
-e BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 \
-e BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 \
-e BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER -e BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS \
-e "BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER" -e "BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS" \
-e BTCPAY_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true' \
docker.io/btcpayserver/btcpayserver:1.13.5 2>>"$LOG" || true
fi
@ -371,7 +397,7 @@ sleep 5 # Let databases stabilize
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE '^lnd$'; then
log "Creating LND..."
mkdir -p /var/lib/archipelago/lnd
# Create lnd.conf so LND auto-connects to Bitcoin Knots via archy-net
# Create lnd.conf with rpcauth credentials (stable across restarts)
if [ ! -f /var/lib/archipelago/lnd/lnd.conf ]; then
cat > /var/lib/archipelago/lnd/lnd.conf <<LNDCONF
[Application Options]
@ -381,7 +407,7 @@ restlisten=0.0.0.0:8080
debuglevel=info
noseedbackup=true
tor.active=true
tor.socks=127.0.0.1:9050
tor.socks=host.containers.internal:9050
tor.streamisolation=true
[Bitcoin]
@ -390,7 +416,7 @@ bitcoin.node=bitcoind
[Bitcoind]
bitcoind.rpchost=bitcoin-knots:8332
bitcoind.rpcuser=archipelago
bitcoind.rpcuser=$BITCOIN_RPC_USER
bitcoind.rpcpass=$BITCOIN_RPC_PASS
bitcoind.rpcpolling=true
bitcoind.estimatemode=ECONOMICAL
@ -398,7 +424,7 @@ bitcoind.estimatemode=ECONOMICAL
[autopilot]
autopilot.active=false
LNDCONF
log "LND config created (archy-net → bitcoin-knots:8332, rpcpolling)"
log "LND config created (rpcauth credentials, Tor via system)"
fi
$DOCKER run -d --name lnd --restart unless-stopped --memory=$(mem_limit lnd) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
@ -417,7 +443,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then
--security-opt no-new-privileges:true \
-p 8173:8173 -p 8174:8174 -p 8175:8175 \
-v /var/lib/archipelago/fedimint:/data \
-e FM_DATA_DIR=/data -e FM_BITCOIND_USERNAME=$BITCOIN_RPC_USER -e FM_BITCOIND_PASSWORD=$BITCOIN_RPC_PASS \
-e FM_DATA_DIR=/data -e "FM_BITCOIND_USERNAME=$BITCOIN_RPC_USER" -e "FM_BITCOIND_PASSWORD=$BITCOIN_RPC_PASS" \
-e FM_BITCOIN_NETWORK=bitcoin -e FM_BIND_P2P=0.0.0.0:8173 \
-e FM_BIND_API=0.0.0.0:8174 -e FM_BIND_UI=0.0.0.0:8175 \
-e FM_P2P_URL=fedimint://"$TARGET_IP":8173 -e FM_API_URL=ws://"$TARGET_IP":8174 \
@ -445,7 +471,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
--bcrypt-password-hash "$FEDI_HASH" \
--network bitcoin --bitcoind-url http://"$TARGET_IP":8332 \
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
--bitcoind-username "$BITCOIN_RPC_USER" --bitcoind-password "$BITCOIN_RPC_PASS" \
lnd --lnd-rpc-host "$TARGET_IP":10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon 2>>"$LOG" || true
else
log " No LND found — using ldk (built-in Lightning)"
@ -458,7 +484,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
--bcrypt-password-hash "$FEDI_HASH" \
--network bitcoin --bitcoind-url http://"$TARGET_IP":8332 \
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
--bitcoind-username "$BITCOIN_RPC_USER" --bitcoind-password "$BITCOIN_RPC_PASS" \
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway 2>>"$LOG" || true
fi
fi