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:
Dorian
2026-03-18 00:39:52 +00:00
parent f273816405
commit 809a976960
12 changed files with 1804 additions and 251 deletions

View File

@@ -35,6 +35,56 @@ wait_for_container() {
return 1
}
# Generate per-installation credentials if not already saved
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"
chmod 600 "$SECRETS_DIR/bitcoin-rpc-password"
fi
BITCOIN_RPC_USER="archipelago"
BITCOIN_RPC_PASS=$(cat "$SECRETS_DIR/bitcoin-rpc-password")
# 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
openssl rand -base64 24 > "$SECRETS_DIR/${svc}-db-password"
chmod 600 "$SECRETS_DIR/${svc}-db-password"
fi
done
MEMPOOL_DB_PASS=$(cat "$SECRETS_DIR/mempool-db-password")
BTCPAY_DB_PASS=$(cat "$SECRETS_DIR/btcpay-db-password")
IMMICH_DB_PASS=$(cat "$SECRETS_DIR/immich-db-password")
PENPOT_DB_PASS=$(cat "$SECRETS_DIR/penpot-db-password")
MYSQL_ROOT_PASS=$(cat "$SECRETS_DIR/mysql-root-db-password")
# Generate Fedimint gateway password and bcrypt hash
if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then
FEDI_PASS=$(openssl rand -base64 16)
echo "$FEDI_PASS" > "$SECRETS_DIR/fedimint-gateway-password"
chmod 600 "$SECRETS_DIR/fedimint-gateway-password"
# Pre-compute bcrypt hash (requires htpasswd from apache2-utils)
if command -v htpasswd >/dev/null 2>&1; then
htpasswd -bnBC 10 "" "$FEDI_PASS" | tr -d ':\n' > "$SECRETS_DIR/fedimint-gateway-hash"
chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
fi
fi
FEDI_PASS=$(cat "$SECRETS_DIR/fedimint-gateway-password")
if [ -f "$SECRETS_DIR/fedimint-gateway-hash" ]; then
FEDI_HASH=$(cat "$SECRETS_DIR/fedimint-gateway-hash")
else
# Fallback: generate hash now
if command -v htpasswd >/dev/null 2>&1; then
FEDI_HASH=$(htpasswd -bnBC 10 "" "$FEDI_PASS" | tr -d ':\n')
echo "$FEDI_HASH" > "$SECRETS_DIR/fedimint-gateway-hash"
chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
else
log "WARNING: htpasswd not found, using default Fedimint gateway hash"
FEDI_HASH='$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC'
fi
fi
log "Fedimint gateway password stored in $SECRETS_DIR/fedimint-gateway-password"
log "First-boot container creation starting (host=$TARGET_IP)"
# Create swap file if not present (50% of RAM, min 2GB, max 8GB)
@@ -88,7 +138,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=archipelago -rpcpassword=archipelago123 \
-rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS \
-dbcache=$BTC_DBCACHE 2>>"$LOG"; then
log "Bitcoin Knots started"
else
@@ -99,12 +149,12 @@ else
log "Bitcoin Knots already running"
fi
# Wait for Bitcoin Knots RPC to be responsive (LND, NBXplorer, mempool depend on it)
wait_for_container "Bitcoin Knots RPC" "$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=archipelago -rpcpassword=archipelago123 getblockchaininfo" 60
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)
if ! $DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=archipelago -rpcpassword=archipelago123 listwallets 2>/dev/null | grep -q "archipelago"; then
$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=archipelago -rpcpassword=archipelago123 loadwallet "archipelago" 2>/dev/null || \
$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=archipelago -rpcpassword=archipelago123 createwallet "archipelago" 2>/dev/null
if ! $DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS listwallets 2>/dev/null | grep -q "archipelago"; then
$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS loadwallet "archipelago" 2>/dev/null || \
$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS createwallet "archipelago" 2>/dev/null
log "Bitcoin Knots wallet 'archipelago' created/loaded"
fi
@@ -114,10 +164,10 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-d
mkdir -p /var/lib/archipelago/mysql-mempool
$DOCKER run -d --name archy-mempool-db --restart unless-stopped --network archy-net \
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e MYSQL_PASSWORD=mempoolpass \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e MYSQL_PASSWORD=$MEMPOOL_DB_PASS \
-e MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS \
docker.io/mariadb:10.11 2>>"$LOG" || true
wait_for_container "Mempool MariaDB" "$DOCKER exec archy-mempool-db mariadb -uroot -prootpass -e 'SELECT 1'" 30
wait_for_container "Mempool MariaDB" "$DOCKER exec archy-mempool-db mariadb -uroot -p$MYSQL_ROOT_PASS -e 'SELECT 1'" 30
fi
MYSQL_CNT=$($DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'mysql-mempool|archy-mempool-db' | head -1)
MYSQL_CNT=${MYSQL_CNT:-archy-mempool-db}
@@ -131,7 +181,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 --network archy-net \
-p 50001:50001 -v /var/lib/archipelago/electrumx:/data \
-e DAEMON_URL=http://archipelago:archipelago123@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
@@ -145,9 +195,9 @@ 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=archipelago -e CORE_RPC_PASSWORD=archipelago123 \
-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=mempoolpass \
-e DATABASE_USERNAME=mempool -e DATABASE_PASSWORD=$MEMPOOL_DB_PASS \
docker.io/mempool/backend:v2.5.0 2>>"$LOG" || true
fi
@@ -182,14 +232,14 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db
mkdir -p /var/lib/archipelago/postgres-btcpay
$DOCKER run -d --name archy-btcpay-db --restart unless-stopped --network archy-net \
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e POSTGRES_PASSWORD=btcpaypass \
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e POSTGRES_PASSWORD=$BTCPAY_DB_PASS \
docker.io/postgres:15-alpine 2>>"$LOG" || true
wait_for_container "BTCPay PostgreSQL" "$DOCKER exec archy-btcpay-db pg_isready -U postgres" 30
fi
# Create nbxplorer DB only if postgres is running
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
$DOCKER exec archy-btcpay-db psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='nbxplorer'" 2>/dev/null | grep -q 1 || \
$DOCKER exec -e PGPASSWORD=btcpaypass archy-btcpay-db psql -U postgres -c "CREATE DATABASE nbxplorer;" 2>/dev/null || true
$DOCKER exec -e PGPASSWORD=$BTCPAY_DB_PASS archy-btcpay-db psql -U postgres -c "CREATE DATABASE nbxplorer;" 2>/dev/null || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
@@ -202,8 +252,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; the
-p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \
-e NBXPLORER_DATADIR=/data -e NBXPLORER_NETWORK=mainnet -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_BTCRPCPASSWORD=archipelago123 \
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
-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 2>>"$LOG" && sleep 5 || true
fi
fi
@@ -219,8 +269,8 @@ 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=archipelago -e BTCPAY_BTCRPCPASSWORD=archipelago123 \
-e BTCPAY_POSTGRES='User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true' \
-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
@@ -234,7 +284,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE '^lnd$'; then
mkdir -p /var/lib/archipelago/lnd
# Create lnd.conf so LND auto-connects to Bitcoin Knots via archy-net
if [ ! -f /var/lib/archipelago/lnd/lnd.conf ]; then
cat > /var/lib/archipelago/lnd/lnd.conf <<'LNDCONF'
cat > /var/lib/archipelago/lnd/lnd.conf <<LNDCONF
[Application Options]
listen=0.0.0.0:9735
rpclisten=0.0.0.0:10009
@@ -250,7 +300,7 @@ bitcoin.node=bitcoind
[Bitcoind]
bitcoind.rpchost=bitcoin-knots:8332
bitcoind.rpcuser=archipelago
bitcoind.rpcpass=archipelago123
bitcoind.rpcpass=$BITCOIN_RPC_PASS
bitcoind.rpcpolling=true
bitcoind.estimatemode=ECONOMICAL
@@ -276,7 +326,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=archipelago -e FM_BITCOIND_PASSWORD=archipelago123 \
-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 \
@@ -302,9 +352,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
-v "$LND_MACAROON":/lnd/admin.macaroon:ro \
docker.io/fedimint/gatewayd:v0.10.0 \
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
--bcrypt-password-hash '$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC' \
--bcrypt-password-hash "$FEDI_HASH" \
--network bitcoin --bitcoind-url http://"$TARGET_IP":8332 \
--bitcoind-username archipelago --bitcoind-password archipelago123 \
--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)"
@@ -315,9 +365,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
-v /var/lib/archipelago/fedimint-gateway:/data \
docker.io/fedimint/gatewayd:v0.10.0 \
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
--bcrypt-password-hash '$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC' \
--bcrypt-password-hash "$FEDI_HASH" \
--network bitcoin --bitcoind-url http://"$TARGET_IP":8332 \
--bitcoind-username archipelago --bitcoind-password archipelago123 \
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway 2>>"$LOG" || true
fi
fi
@@ -482,7 +532,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q immich_postgres; then
$DOCKER run -d --name immich_postgres --restart unless-stopped --network immich-net \
-v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=immichpass -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \
-e POSTGRES_PASSWORD=$IMMICH_DB_PASS -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>>"$LOG" || true
sleep 3
for i in 1 2 3 4 5 6 7 8 9 10; do
@@ -498,7 +548,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
$DOCKER run -d --name immich_server --restart unless-stopped --network immich-net \
-p 2283:2283 -v /var/lib/archipelago/immich:/usr/src/app/upload \
-e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e DB_PASSWORD=immichpass \
-e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e DB_PASSWORD=$IMMICH_DB_PASS \
-e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \
-e UPLOAD_LOCATION=/usr/src/app/upload \
ghcr.io/immich-app/immich-server:release 2>>"$LOG" || true
@@ -513,7 +563,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q penpot-postgres; then
$DOCKER run -d --name penpot-postgres --restart unless-stopped --network penpot-net \
-v /var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data \
-e POSTGRES_DB=penpot -e POSTGRES_USER=penpot -e POSTGRES_PASSWORD=penpot \
-e POSTGRES_DB=penpot -e POSTGRES_USER=penpot -e POSTGRES_PASSWORD=$PENPOT_DB_PASS \
docker.io/postgres:15 2>>"$LOG" || true
sleep 5
fi
@@ -529,7 +579,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the
-e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \
-e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \
-e PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot \
-e PENPOT_DATABASE_USERNAME=penpot -e PENPOT_DATABASE_PASSWORD=penpot \
-e PENPOT_DATABASE_USERNAME=penpot -e PENPOT_DATABASE_PASSWORD=$PENPOT_DB_PASS \
-e PENPOT_REDIS_URI=redis://penpot-valkey/0 \
-e PENPOT_OBJECTS_STORAGE_BACKEND=fs \
-e PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets \