fix(01-11): remove every shipped Fedimint gateway credential (FED-07)

Six code paths configured the Lightning gateway with a bcrypt hash committed
to this repository — and one deploy path with a plaintext password literal —
whenever the per-install secret was missing. Anyone holding a copy of the repo
held the admin credential for every gateway that ever took a fallback.

container::secrets now owns the credential end to end: ensure_gateway_credential
(idempotent, delegates to ensure_one's bcrypt arm) and gateway_bcrypt_hash,
which returns Err when the secret is missing/empty and when the stored value is
on the KNOWN_DEFAULT_GATEWAY_HASHES denylist — so this codebase cannot hand
back the compromised value even to a node already carrying it.

get_app_config was widened to Result so a credential-less install cannot reach
podman run at all; configure_fedimint_lnd takes the resolved hash instead of
re-reading with its own fallback. The four shell paths stop generating
credentials entirely (dropping the htpasswd host dependency) and skip container
creation with a printed reason rather than substituting anything.

Naming converges on the manifest's fedimint-gateway-hash/.pw, with legacy
fedimint-gateway-password values copied forward rather than regenerated so no
node loses a working unique credential. Plan 01-16 owns rotation of installs
already carrying the default.

Verified: cargo build clean; cargo test -p archipelago 999 passed (2
boot_reconciler timing tests failed under concurrent load, green in isolation,
untouched by this diff); bash -n clean on all five scripts; the compromised
literal now appears exactly once in the tree, as the denylist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-01 05:46:02 -04:00
co-authored by Claude Opus 5
parent 5faf1a3c5f
commit 4265254700
10 changed files with 558 additions and 139 deletions
+12 -9
View File
@@ -646,18 +646,18 @@ pub(super) async fn get_app_config(
allocator: &mut PortAllocator,
rpc_user: &str,
rpc_pass: &str,
) -> (
) -> Result<(
Vec<String>,
Vec<String>,
Vec<String>,
Option<String>,
Option<Vec<String>>,
) {
)> {
if let Some(config) = dynamic_app_config(app_id).await {
return config;
return Ok(config);
}
match app_id {
Ok(match app_id {
"homeassistant" | "home-assistant" => (
vec!["8123:8123".to_string()],
vec!["/var/lib/archipelago/home-assistant:/config".to_string()],
@@ -1049,10 +1049,13 @@ pub(super) async fn get_app_config(
]),
),
"fedimint-gateway" => {
let fedi_hash = read_secret(
"fedimint-gateway-hash",
"$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC",
);
// FED-07: no fallback literal. A fresh install self-provisions its
// own credential; a node that can't obtain one fails this install
// outright (propagated via `?`) rather than start an
// unauthenticated/default-credentialed gateway.
let gateway_secrets_dir = std::path::Path::new("/var/lib/archipelago/secrets");
crate::container::secrets::ensure_gateway_credential(gateway_secrets_dir)?;
let fedi_hash = crate::container::secrets::gateway_bcrypt_hash(gateway_secrets_dir)?;
(
vec!["8176:8176".to_string(), "9737:9737".to_string()],
vec!["/var/lib/archipelago/fedimint-gateway:/data".to_string()],
@@ -1196,5 +1199,5 @@ pub(super) async fn get_app_config(
tracing::warn!("No catalog runtime config found for app: {} — using minimal defaults", app_id);
(vec![], vec![], vec![], None, None)
}
}
})
}
@@ -717,6 +717,11 @@ fn order_present_containers(package_id: &str, containers: Vec<String>) -> Vec<St
/// Configure Fedimint Gateway to use LND instead of LDK.
/// Modifies ports, volumes, and command args in place when LND credentials exist.
///
/// `fedi_hash` is the already-resolved per-install gateway credential
/// (`container::secrets::gateway_bcrypt_hash`) — this function does not read
/// the secrets file itself, so there is exactly one read site and one
/// failure point for that credential (FED-07).
pub(super) fn configure_fedimint_lnd(
host_ip: &str,
ports: &mut Vec<String>,
@@ -724,20 +729,13 @@ pub(super) fn configure_fedimint_lnd(
custom_args: &mut Option<Vec<String>>,
rpc_user: &str,
rpc_pass: &str,
fedi_hash: &str,
) {
let lnd_cert = "/var/lib/archipelago/lnd/tls.cert";
let lnd_macaroon = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
if std::path::Path::new(lnd_cert).exists() && std::path::Path::new(lnd_macaroon).exists() {
info!("LND detected with credentials — configuring gateway in lnd mode");
// Read bcrypt hash from secrets file, fall back to default
let fedi_hash =
std::fs::read_to_string("/var/lib/archipelago/secrets/fedimint-gateway-hash")
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| {
"$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC".to_string()
});
ports.retain(|p| p != "9737:9737");
volumes.push(format!("{}:/lnd/tls.cert:ro", lnd_cert));
volumes.push(format!("{}:/lnd/admin.macaroon:ro", lnd_macaroon));
@@ -748,7 +746,7 @@ pub(super) fn configure_fedimint_lnd(
"--listen".to_string(),
"0.0.0.0:8176".to_string(),
"--bcrypt-password-hash".to_string(),
fedi_hash,
fedi_hash.to_string(),
"--network".to_string(),
"bitcoin".to_string(),
"--bitcoind-url".to_string(),
@@ -589,11 +589,18 @@ impl RpcHandler {
&rpc_user,
&rpc_pass,
)
.await
.await?
};
// Fedimint Gateway: auto-detect LND and switch to lnd mode
if package_id == "fedimint-gateway" && deps.has_lnd {
// get_app_config's "fedimint-gateway" arm already called
// ensure_gateway_credential above, so the secret is guaranteed to
// exist here; re-reading it (rather than threading the value
// through) keeps one canonical read site in container::secrets.
let fedi_hash = crate::container::secrets::gateway_bcrypt_hash(
std::path::Path::new("/var/lib/archipelago/secrets"),
)?;
configure_fedimint_lnd(
&self.config.host_ip,
&mut ports,
@@ -601,6 +608,7 @@ impl RpcHandler {
&mut custom_args,
&rpc_user,
&rpc_pass,
&fedi_hash,
);
}