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:
co-authored by
Claude Opus 5
parent
5faf1a3c5f
commit
4265254700
@@ -0,0 +1,196 @@
|
||||
---
|
||||
phase: 01-federation-mesh-hardening
|
||||
plan: 11
|
||||
subsystem: security
|
||||
tags: [secrets, bcrypt, fedimint, credentials, shell, rust]
|
||||
|
||||
requires:
|
||||
- phase: 01-federation-mesh-hardening
|
||||
provides: "apps/fedimint-gateway/manifest.yml's existing `generated_secrets: fedimint-gateway-hash (kind: bcrypt)` block and container::secrets::ensure_one's Bcrypt arm, which already materialised a per-install credential at 0600 — this plan makes the five paths that bypassed it agree with the manifest"
|
||||
provides:
|
||||
- "One canonical per-install gateway credential accessor (container::secrets::gateway_bcrypt_hash) plus an idempotent generator (ensure_gateway_credential), used by the daemon and relied on by all four shell paths"
|
||||
- "A detection-only denylist (KNOWN_DEFAULT_GATEWAY_HASHES) that plan 01-16's migration consumes to find and rotate installs still carrying the shipped default"
|
||||
- "Fail-loud semantics on every configure path: no credential means no gateway container, never a shipped default"
|
||||
affects: [fedimint-gateway, container-secrets, deploy, first-boot, reconcile]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Credential-shaped secrets get one canonical accessor in container::secrets that returns Result, plus a denylist check inside that accessor — so refusing a known-compromised value is structurally impossible to bypass, rather than a rule each caller has to remember."
|
||||
- "Shell install paths stop generating credentials entirely and defer to the daemon's generator; where a script cannot obtain one it skips container creation with a printed reason instead of substituting anything."
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- core/archipelago/src/container/secrets.rs
|
||||
- core/archipelago/src/api/rpc/package/config.rs
|
||||
- core/archipelago/src/api/rpc/package/dependencies.rs
|
||||
- core/archipelago/src/api/rpc/package/install.rs
|
||||
- scripts/first-boot-containers.sh
|
||||
- scripts/reconcile-containers.sh
|
||||
- scripts/deploy-to-target.sh
|
||||
- scripts/deploy-tailscale.sh
|
||||
- scripts/container-specs.sh
|
||||
|
||||
key-decisions:
|
||||
- "Credential-less install failure mechanism: `get_app_config` was widened from a bare tuple to `Result<tuple>` and the fedimint-gateway arm propagates with `?`. Chosen over the plan's alternative (log + an argv the install path rejects) because it makes the failure unrepresentable rather than conventional — an install with no credential cannot reach `podman run` at all, and every other arm was mechanically wrapped in `Ok(...)` with no behaviour change. The one call site in install.rs became `.await?`."
|
||||
- "`configure_fedimint_lnd` now takes the resolved hash as a parameter instead of re-reading the secrets file with its own fallback, so there is exactly one read site and one failure point."
|
||||
- "`read_secret(name, default)` in config.rs was left intact — 6 other call sites still use its default parameter, so per the plan's explicit branch the gateway was simply routed off it rather than removing the helper."
|
||||
- "Shell paths generate nothing: first-boot, reconcile and both deploys defer bcrypt generation to the daemon. This removes the htpasswd host dependency entirely (the plan's preferred branch) and keeps bcrypt generation in exactly one implementation."
|
||||
- "deploy-tailscale.sh's gateway argv was switched from the plaintext `--password` flag to `--bcrypt-password-hash`, matching every other path; it reads the hash on the target rather than shipping the deploy host's copy of it."
|
||||
- "Legacy `fedimint-gateway-password` files are copied forward to the canonical `fedimint-gateway-hash.pw` name (0600) and never deleted or regenerated, so a node with a working unique credential keeps it. Plan 01-16 owns retirement of the legacy name."
|
||||
|
||||
requirements-completed: [FED-07]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "A fresh install derives a per-install gateway credential; two installs never share one"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "core/archipelago/src/container/secrets.rs#gateway_credential_fresh_generation_verifies_and_is_0600, #gateway_credential_is_per_install_not_per_build"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "No code path configures a gateway with a credential literal carried in this repository; a missing credential fails loudly instead of starting a defaulted gateway"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "core/archipelago/src/container/secrets.rs#gateway_credential_missing_is_a_named_error (error names the missing file)"
|
||||
status: pass
|
||||
- kind: other
|
||||
ref: "grep -rl 't9YjjxkiktrlYvjajB' --include='*.rs' --include='*.sh' --include='*.yml' --include='*.json' . -> exactly 1 hit, core/archipelago/src/container/secrets.rs (the denylist)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "The compromised hash exists in exactly one place, as a denylist never used to configure a container"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "core/archipelago/src/container/secrets.rs#gateway_credential_rejects_known_default — the accessor returns Err rather than handing the value back"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "One canonical secret filename across the Rust orchestrator, first-boot, reconcile and both deploy scripts"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "GATEWAY_HASH_SECRET_NAME const in secrets.rs matches manifest generated_secrets; all four scripts read/write fedimint-gateway-hash{,.pw}"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "A first boot on a host without htpasswd still yields a unique credential rather than a shipped one (the ISO path that put the default on real nodes)"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "scripts/first-boot-containers.sh — htpasswd removed entirely (grep -v '^\\s*#' | grep -c htpasswd == 0); gateway creation is skipped with a logged reason until the daemon generates the credential"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D6
|
||||
description: "Generating the credential twice is idempotent — a reconcile tick never rotates a working gateway out from under itself"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "core/archipelago/src/container/secrets.rs#gateway_credential_is_idempotent"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D7
|
||||
description: "All five changed scripts remain syntactically valid"
|
||||
requirement: "FED-07"
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "bash -n clean on first-boot-containers.sh, reconcile-containers.sh, deploy-to-target.sh, deploy-tailscale.sh, container-specs.sh"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: 135min
|
||||
completed: 2026-07-31
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 1 Plan 11: Remove Every Shipped Fedimint Gateway Credential (FED-07) Summary
|
||||
|
||||
**Deleted the six sites that configured a Lightning gateway with a bcrypt hash (and, on one path, a plaintext password) committed to this repository, replaced them with one canonical per-install accessor that refuses to return the known-compromised value, and made every install path fail loudly — or skip the container with a printed reason — rather than fall back to anything shipped.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~135 min across two sessions (see Deviations — the first session's executor was killed mid-Task-2 by an SSH disconnect)
|
||||
- **Completed:** 2026-07-31
|
||||
- **Tasks:** 2/2
|
||||
- **Files modified:** 9 (4 Rust, 5 shell)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `container::secrets` gained the canonical trio: `GATEWAY_HASH_SECRET_NAME` (matching the manifest), `ensure_gateway_credential` (idempotent, delegates to `ensure_one`'s existing bcrypt arm so there is one generation implementation), and `gateway_bcrypt_hash` (returns `Err` naming the file when missing/empty/unreadable, and `Err` when the stored value is denylisted).
|
||||
- `KNOWN_DEFAULT_GATEWAY_HASHES` holds the compromised hash as detection-only data. It is now the single occurrence of that value in the entire tree, and the only function that reads it uses it to *refuse*.
|
||||
- Five fallback sites deleted: `config.rs`'s `read_secret(..., "$2y$10$t9Yjj…")`, `dependencies.rs`'s `unwrap_or_else` onto the same literal, and the generate-or-default blocks in `first-boot-containers.sh`, `reconcile-containers.sh` and `deploy-to-target.sh`. A sixth — `deploy-tailscale.sh`'s plaintext `|| echo 'archipelago'` — is gone too, along with the `--password` argv it fed.
|
||||
- The gateway is now unconfigurable without a per-install credential: in Rust the error propagates out of `get_app_config` via `?`; in shell each path skips container creation and prints why.
|
||||
- Naming unified on the manifest's `fedimint-gateway-hash` / `.pw`, with legacy `fedimint-gateway-password` values copied forward rather than regenerated, so no node with a working unique credential loses it.
|
||||
- `container-specs.sh` gained a `SPEC_SKIP_REASON` mechanism so a missing credential produces a skipped spec with a message instead of an empty `--bcrypt-password-hash` argument.
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: End-to-end — a gateway spec that cannot be built without a per-install credential** — Rust accessor, denylist, 5 tests, and the three call-site changes.
|
||||
2. **Task 2: The shell install paths generate their own credential instead of shipping one** — all five scripts.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `core/archipelago/src/container/secrets.rs` — canonical name const, detection-only denylist, `ensure_gateway_credential`, `gateway_bcrypt_hash`, and 5 new tests (fresh generation + 0600 + plaintext verifies against hash, idempotence, named missing-secret error, denylist rejection, per-install uniqueness).
|
||||
- `core/archipelago/src/api/rpc/package/config.rs` — `get_app_config` now returns `Result<...>`; the `fedimint-gateway` arm calls `ensure_gateway_credential` then `gateway_bcrypt_hash` and propagates. No ports/volumes/network/health/other-argv changes.
|
||||
- `core/archipelago/src/api/rpc/package/dependencies.rs` — `configure_fedimint_lnd` takes `fedi_hash: &str`; its own read-with-fallback deleted.
|
||||
- `core/archipelago/src/api/rpc/package/install.rs` — `.await?` on `get_app_config`; resolves the hash once and passes it into `configure_fedimint_lnd`.
|
||||
- `scripts/first-boot-containers.sh` — htpasswd dependency and generation removed; legacy migration copy; gateway creation skipped with a logged reason when no credential exists.
|
||||
- `scripts/reconcile-containers.sh` — same pattern.
|
||||
- `scripts/deploy-to-target.sh` — remote generation block replaced with legacy-migration-only copy; the empty-`FEDI_HASH` literal substitution replaced with a printed NOTE; gateway creation wrapped in an `if [ -n '$FEDI_HASH' ]` guard with an else-branch explaining the skip. The dead `GW_COMMON` variable (its only definition, referenced nowhere) was removed with the literal it carried.
|
||||
- `scripts/deploy-tailscale.sh` — same generation/fallback removal; container-creation block now reads the target's `fedimint-gateway-hash`, skips with a reason when empty, and uses `--bcrypt-password-hash` in both the lnd and ldk branches.
|
||||
- `scripts/container-specs.sh` — `SPEC_SKIP_REASON` empty-value guard; secret name and `$`-escaping left untouched as instructed.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
See `key-decisions` above. The load-bearing one is the `Result` widening of `get_app_config`: the plan left the mechanism open and required the choice be recorded. `Result` was chosen because the alternative (an argv the install path rejects) leaves a defaulted gateway one refactor away from being reachable again, whereas a `?` makes it a compile-time impossibility.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Process deviation: executor killed mid-task by an SSH disconnect; plan completed in a second session
|
||||
|
||||
**Found during:** Task 2, while editing `scripts/deploy-to-target.sh`
|
||||
**Issue:** The orchestrating session and all its background agents died when the operator's SSH connection dropped (broken pipe). The 01-11 executor's transcript ends on an unanswered `tool_use`. Task 1 was complete and correct; Task 2 was three scripts done, one left **syntactically broken**, and one never started.
|
||||
**Resolution:** A follow-on session verified Task 1 against the plan's acceptance criteria (all pass), repaired `deploy-to-target.sh`, and implemented `deploy-tailscale.sh` from scratch following the pattern the dead executor had established in its three finished scripts.
|
||||
**Files modified:** `scripts/deploy-to-target.sh`, `scripts/deploy-tailscale.sh`
|
||||
|
||||
### Auto-fixed Issue: apostrophe inside a single-quoted ssh heredoc broke deploy-to-target.sh
|
||||
|
||||
**Found during:** Task 2 verification (`bash -n` failed at line 1953, ~700 lines below the actual edit)
|
||||
**Issue:** The dead executor's new comment read `…is generated by the daemon's` — inside `ssh "$TARGET_HOST" '…'`, that apostrophe **terminates the single-quoted remote command string**, so the remainder of the block was reparsed as local shell and the error surfaced at an unrelated `fi` far below. This is a live hazard for anyone editing these deploy scripts: prose comments inside single-quoted ssh blocks must not contain apostrophes.
|
||||
**Resolution:** Reworded to `…is generated by the daemon / via container::secrets::ensure_gateway_credential`. `bash -n` clean. The same rule was applied to all new comments added to `deploy-tailscale.sh`.
|
||||
**Files modified:** `scripts/deploy-to-target.sh`
|
||||
|
||||
## Planner Assumptions — resolved
|
||||
|
||||
- **Whether `get_app_config` could return `Result` without a wide refactor:** yes. Every arm wrapped in a single `Ok(match …)`, one call site updated. No behaviour change to any other app.
|
||||
- **Whether the compromised hash's plaintext is publicly recoverable: UNRESOLVED — and it should not block 01-16.** No bcrypt implementation is available on this box outside the Rust crate (no `python3-bcrypt`, no `passlib`, no node `bcryptjs`, no `htpasswd`), so no candidate list was tested. **Strong lead for 01-16:** `deploy-tailscale.sh`'s plaintext fallback for the very same credential was the literal string `archipelago`, so that is the first candidate to check. Until someone verifies it, 01-16's operator message should assume the plaintext IS recoverable — the value shipped in a public repo either way, so the rotation is mandatory regardless.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
- **T-01-50 / T-01-51 (critical, elevation + spoofing) — mitigated.** No configure path can produce the shipped credential; the accessor refuses it even on a node that already carries it.
|
||||
- **T-01-52 (information disclosure) — mitigated.** Generation reuses `write_secret` (atomic, 0600); no script logs the value, only that generation was deferred or a container skipped.
|
||||
- **T-01-53 (DoS by removing the fallback) — mitigated.** No path hard-fails a whole install: the gateway container is skipped with a printed reason and created on a later reconcile once the daemon has generated the credential.
|
||||
- **Residual, owned by 01-16:** nodes already running a gateway configured with the shipped default keep running it. This plan makes them detectable and unre-configurable with that value; it does not rotate them.
|
||||
- **T-01-SC:** no packages added.
|
||||
|
||||
## Self-Check
|
||||
|
||||
- FOUND: `KNOWN_DEFAULT_GATEWAY_HASHES` in `core/archipelago/src/container/secrets.rs` (3 references: doc-linked definition + accessor use + test)
|
||||
- FOUND: `gateway_bcrypt_hash` used in `config.rs` and `install.rs`
|
||||
- CONFIRMED: `grep -rl 't9YjjxkiktrlYvjajB'` across `*.rs *.sh *.yml *.json` returns exactly one file — the denylist
|
||||
- CONFIRMED: `grep -c -- '--password ' scripts/deploy-tailscale.sh` == 0; `grep -c "|| echo 'archipelago'"` == 0
|
||||
- CONFIRMED: `bash -n` clean on all five scripts
|
||||
- CONFIRMED: `cargo build -p archipelago` exits 0 (3 pre-existing warnings, none from this plan)
|
||||
- CONFIRMED: `cargo test -p archipelago` — 999 passed, 2 failed. Both failures are `container::boot_reconciler::tests::{second_pass_fires_after_interval, shutdown_terminates_loop}`, wall-clock-timed loop tests (50ms tick, 5s timeout) that ran while the box was executing two cargo builds and the full vitest suite concurrently. Re-run in isolation: `cargo test -p archipelago boot_reconciler` → **4 passed, 0 failed in 0.29s**. `boot_reconciler.rs` contains no `gateway`/`secrets` references and is untouched by this plan's diff — load flakiness, not a regression.
|
||||
- CONFIRMED: `npx vitest run` (full frontend suite) exits 0
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,81 @@ fn random_base64(bytes: usize) -> String {
|
||||
base64::engine::general_purpose::STANDARD.encode(buf)
|
||||
}
|
||||
|
||||
/// Canonical secret name for the Fedimint gateway's admin bcrypt hash — must
|
||||
/// match `generated_secrets: fedimint-gateway-hash` in
|
||||
/// `apps/fedimint-gateway/manifest.yml` so the Rust orchestrator, first-boot
|
||||
/// script, reconcile script and both deploy scripts all agree on one file
|
||||
/// (FED-07: before this, scripts wrote `fedimint-gateway-password` while the
|
||||
/// daemon read `fedimint-gateway-hash`).
|
||||
pub const GATEWAY_HASH_SECRET_NAME: &str = "fedimint-gateway-hash";
|
||||
|
||||
/// Detection-only denylist of bcrypt hashes that shipped as hardcoded
|
||||
/// fallback credentials in this repository before FED-07. `t9YjjxkiktrlYvjajB
|
||||
/// /zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC` was substituted for the Fedimint
|
||||
/// gateway's admin password whenever the real per-install secret was
|
||||
/// missing — in `config.rs`, `dependencies.rs`, and every shell install path
|
||||
/// — meaning anyone holding a copy of this repo held the admin credential for
|
||||
/// every gateway that ever took that fallback.
|
||||
///
|
||||
/// This value exists **only** so an install still carrying it can be
|
||||
/// detected and rotated (plan 01-16 owns the migration). It must NEVER be
|
||||
/// passed to a container, written to a fresh install, or handed back to a
|
||||
/// caller by [`gateway_bcrypt_hash`] — that function returns `Err` instead.
|
||||
/// This is the one and only place this value may appear in the tree.
|
||||
const KNOWN_DEFAULT_GATEWAY_HASHES: &[&str] =
|
||||
&["$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC"];
|
||||
|
||||
/// Idempotently ensure the Fedimint gateway's admin credential exists under
|
||||
/// `secrets_dir`: a fresh per-install bcrypt hash plus its `.pw` plaintext
|
||||
/// sibling, both 0600. Delegates to [`ensure_one`] for the actual bcrypt
|
||||
/// generation so there is exactly one implementation of that logic — this
|
||||
/// also means a second call is a no-op (idempotent fast path) and a
|
||||
/// present-but-unreadable file self-heals, so a reconcile tick never rotates
|
||||
/// a working gateway credential out from under it.
|
||||
pub fn ensure_gateway_credential(secrets_dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(secrets_dir)
|
||||
.with_context(|| format!("creating secrets dir {}", secrets_dir.display()))?;
|
||||
let gs = GeneratedSecret {
|
||||
name: GATEWAY_HASH_SECRET_NAME.to_string(),
|
||||
kind: SecretGenKind::Bcrypt,
|
||||
};
|
||||
ensure_one(secrets_dir, &gs)
|
||||
}
|
||||
|
||||
/// Read the Fedimint gateway's canonical per-install bcrypt hash.
|
||||
///
|
||||
/// Returns `Err` naming the missing file when it is absent, empty, or
|
||||
/// unreadable — callers must propagate that error rather than substitute a
|
||||
/// literal, so an install with no credential fails loudly instead of quietly
|
||||
/// starting an unauthenticated/default-credentialed gateway. Also returns
|
||||
/// `Err` when the stored value matches [`KNOWN_DEFAULT_GATEWAY_HASHES`]: a
|
||||
/// node carrying the shipped default must not be handed that value back by
|
||||
/// this codebase, even to reconfigure itself with the same value it already
|
||||
/// (insecurely) has.
|
||||
pub fn gateway_bcrypt_hash(secrets_dir: &Path) -> Result<String> {
|
||||
let path = secrets_dir.join(GATEWAY_HASH_SECRET_NAME);
|
||||
let hash = fs::read_to_string(&path).with_context(|| {
|
||||
format!(
|
||||
"gateway credential missing at {} — call ensure_gateway_credential (or wait for the \
|
||||
next reconcile tick) to generate a per-install credential before starting the gateway",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let hash = hash.trim();
|
||||
if hash.is_empty() {
|
||||
anyhow::bail!("gateway credential {} is empty", path.display());
|
||||
}
|
||||
if KNOWN_DEFAULT_GATEWAY_HASHES.contains(&hash) {
|
||||
anyhow::bail!(
|
||||
"gateway credential {} is a publicly known default that shipped hardcoded in this \
|
||||
repository before FED-07 — this install must rotate it (see plan 01-16) before the \
|
||||
gateway can be (re)configured",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
|
||||
/// Write an externally computed secret value (0600, atomic). For derived
|
||||
/// secrets that aren't random generators — e.g. the btcpay internal-LND
|
||||
/// connection string assembled in `container::lnd`.
|
||||
@@ -209,6 +284,79 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_credential_fresh_generation_verifies_and_is_0600() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
ensure_gateway_credential(dir.path()).unwrap();
|
||||
|
||||
let hash = std::fs::read_to_string(dir.path().join(GATEWAY_HASH_SECRET_NAME)).unwrap();
|
||||
let pw =
|
||||
std::fs::read_to_string(dir.path().join(format!("{GATEWAY_HASH_SECRET_NAME}.pw")))
|
||||
.unwrap();
|
||||
assert!(bcrypt::verify(pw.trim(), hash.trim()).unwrap());
|
||||
|
||||
for f in [
|
||||
GATEWAY_HASH_SECRET_NAME.to_string(),
|
||||
format!("{GATEWAY_HASH_SECRET_NAME}.pw"),
|
||||
] {
|
||||
let mode = std::fs::metadata(dir.path().join(&f))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777;
|
||||
assert_eq!(mode, 0o600, "{f} must be 0600");
|
||||
}
|
||||
|
||||
let read_back = gateway_bcrypt_hash(dir.path()).unwrap();
|
||||
assert_eq!(read_back, hash.trim());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_credential_is_idempotent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
ensure_gateway_credential(dir.path()).unwrap();
|
||||
let first = gateway_bcrypt_hash(dir.path()).unwrap();
|
||||
ensure_gateway_credential(dir.path()).unwrap();
|
||||
let second = gateway_bcrypt_hash(dir.path()).unwrap();
|
||||
assert_eq!(first, second, "second call must not rotate the credential");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_credential_missing_is_a_named_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let err = gateway_bcrypt_hash(dir.path()).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains(GATEWAY_HASH_SECRET_NAME),
|
||||
"error must name the missing secret file: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_credential_rejects_known_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join(GATEWAY_HASH_SECRET_NAME),
|
||||
KNOWN_DEFAULT_GATEWAY_HASHES[0],
|
||||
)
|
||||
.unwrap();
|
||||
let err = gateway_bcrypt_hash(dir.path()).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().to_lowercase().contains("default"),
|
||||
"error must explain the denylisted value: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_credential_is_per_install_not_per_build() {
|
||||
let dir_a = tempfile::tempdir().unwrap();
|
||||
let dir_b = tempfile::tempdir().unwrap();
|
||||
ensure_gateway_credential(dir_a.path()).unwrap();
|
||||
ensure_gateway_credential(dir_b.path()).unwrap();
|
||||
let hash_a = gateway_bcrypt_hash(dir_a.path()).unwrap();
|
||||
let hash_b = gateway_bcrypt_hash(dir_b.path()).unwrap();
|
||||
assert_ne!(hash_a, hash_b, "two fresh installs must not share a hash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_heals_unreadable_secret() {
|
||||
// Simulate the root-owned case: a present-but-unreadable file. We can't
|
||||
|
||||
@@ -101,6 +101,10 @@ reset_spec() {
|
||||
SPEC_SECURITY="no-new-privileges:true" SPEC_RESTART="unless-stopped"
|
||||
SPEC_HEALTH_CMD="" SPEC_ENV="" SPEC_CUSTOM_ARGS="" SPEC_READONLY="false"
|
||||
SPEC_TMPFS="" SPEC_TIER="3" SPEC_DATA_DIR="" SPEC_DATA_UID="100000:100000"
|
||||
# Set by a loader (e.g. fedimint-gateway) to signal "spec is valid but this
|
||||
# container must not be created/recreated right now" — e.g. a required
|
||||
# per-install secret hasn't been generated yet. Empty means no skip.
|
||||
SPEC_SKIP_REASON=""
|
||||
# SPEC_OPTIONAL defaults true: reconcile-containers.sh only REPAIRS existing
|
||||
# containers — it never creates missing ones. Baseline (filebrowser) is
|
||||
# bootstrapped by first-boot-containers.sh; all other apps come from the
|
||||
@@ -323,6 +327,16 @@ load_spec_fedimint-gateway() {
|
||||
SPEC_DATA_DIR="/var/lib/archipelago/fedimint-gateway"
|
||||
SPEC_DEPENDS="bitcoin-knots fedimint"
|
||||
SPEC_OPTIONAL="true"
|
||||
# FED-07: no shipped fallback credential. If the per-install bcrypt hash
|
||||
# (fedimint-gateway-hash, generated by container::secrets::ensure_gateway_credential
|
||||
# via the daemon, first-boot, or reconcile secret-generation step) doesn't
|
||||
# exist yet, do not build an entrypoint with an empty --bcrypt-password-hash
|
||||
# — skip creating/recreating this container and let the caller retry once
|
||||
# the credential exists.
|
||||
if [ -z "$FEDI_HASH" ]; then
|
||||
SPEC_SKIP_REASON="fedimint-gateway credential not generated yet (no shipped default; will retry once a per-install credential exists)"
|
||||
return
|
||||
fi
|
||||
# Custom entrypoint depends on whether LND is available
|
||||
local LND_CERT=/var/lib/archipelago/lnd/tls.cert
|
||||
local LND_MAC=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
|
||||
+55
-40
@@ -493,15 +493,17 @@ deploy_node() {
|
||||
sudo chmod 600 "$SECRETS_DIR/${svc}-db-password"
|
||||
fi
|
||||
done
|
||||
# Fedimint gateway
|
||||
if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then
|
||||
FEDI_PASS=$(openssl rand -base64 16)
|
||||
echo "$FEDI_PASS" | sudo tee "$SECRETS_DIR/fedimint-gateway-password" > /dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-password"
|
||||
if command -v htpasswd >/dev/null 2>&1; then
|
||||
htpasswd -bnBC 10 "" "$FEDI_PASS" | tr -d ":\n" | sudo tee "$SECRETS_DIR/fedimint-gateway-hash" > /dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
|
||||
fi
|
||||
# FED-07: no shipped fallback, ever. The canonical per-install gateway
|
||||
# credential (fedimint-gateway-hash / .pw) is generated by the daemon
|
||||
# via container::secrets::ensure_gateway_credential — this deploy script
|
||||
# no longer generates it (removes the htpasswd host dependency too).
|
||||
# Legacy migration only: carry an existing fedimint-gateway-password
|
||||
# value forward to the canonical fedimint-gateway-hash.pw name if that
|
||||
# name does not exist yet; never regenerate a working credential, and
|
||||
# never delete the legacy file (plan 01-16 owns retirement).
|
||||
if [ -f "$SECRETS_DIR/fedimint-gateway-password" ] && [ ! -f "$SECRETS_DIR/fedimint-gateway-hash.pw" ]; then
|
||||
sudo cp "$SECRETS_DIR/fedimint-gateway-password" "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
fi
|
||||
' 2>/dev/null
|
||||
# Read each password individually (avoids eval on SSH output)
|
||||
@@ -509,7 +511,12 @@ deploy_node() {
|
||||
BTCPAY_DB_PASS=$(ssh $SSH_OPTS "$TARGET" 'sudo cat /var/lib/archipelago/secrets/btcpay-db-password 2>/dev/null' 2>/dev/null)
|
||||
MYSQL_ROOT_PASS=$(ssh $SSH_OPTS "$TARGET" 'sudo cat /var/lib/archipelago/secrets/mysql-root-db-password 2>/dev/null' 2>/dev/null)
|
||||
FEDI_HASH=$(ssh $SSH_OPTS "$TARGET" 'sudo cat /var/lib/archipelago/secrets/fedimint-gateway-hash 2>/dev/null' 2>/dev/null)
|
||||
[ -z "${FEDI_HASH:-}" ] && FEDI_HASH='$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC'
|
||||
# FED-07: no fallback literal. If the target has not generated its
|
||||
# per-install gateway credential yet, FEDI_HASH stays empty and the
|
||||
# gateway container creation below is skipped, never substituted.
|
||||
if [ -z "${FEDI_HASH:-}" ]; then
|
||||
echo " NOTE: no fedimint-gateway credential on target yet — gateway container creation will be skipped (no shipped default; the daemon generates one on next install/reconcile)"
|
||||
fi
|
||||
|
||||
if [ -z "$BITCOIN_RPC_PASS" ]; then
|
||||
echo " WARNING: Could not read Bitcoin RPC password — skipping container setup"
|
||||
@@ -772,37 +779,45 @@ LNDCONF
|
||||
fi
|
||||
fi
|
||||
if ! \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; then
|
||||
echo ' Creating fedimint-gateway...'
|
||||
sudo mkdir -p /var/lib/archipelago/fedimint-gateway
|
||||
FEDI_PASS=\$(sudo cat /var/lib/archipelago/secrets/fedimint-gateway-password 2>/dev/null || echo 'archipelago')
|
||||
LND_CERT=/var/lib/archipelago/lnd/tls.cert
|
||||
LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
if \$DOCKER ps --format '{{.Names}}' | grep -q '^lnd\$' && sudo test -f \$LND_CERT && sudo test -f \$LND_MACAROON; then
|
||||
\$DOCKER run -d --name fedimint-gateway --restart unless-stopped \$NET_OPT \
|
||||
--health-cmd 'curl -sf http://localhost:8176/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 -v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
-v /var/lib/archipelago/lnd/tls.cert:/lnd/tls.cert:ro \
|
||||
-v /var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon:/lnd/admin.macaroon:ro \
|
||||
$FEDIMINT_GATEWAY_IMAGE \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--password \"\$FEDI_PASS\" \
|
||||
--network bitcoin --bitcoind-url http://\$TARGET_IP:8332 \
|
||||
--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
|
||||
# FED-07: the gateway is configured from the per-install bcrypt
|
||||
# hash only — no shipped default, and no plaintext --password
|
||||
# flag. If the target has no credential yet, skip creation and
|
||||
# say why rather than starting a gateway with a known password.
|
||||
GW_HASH=\$(sudo cat /var/lib/archipelago/secrets/fedimint-gateway-hash 2>/dev/null)
|
||||
if [ -z \"\$GW_HASH\" ]; then
|
||||
echo ' Skipping fedimint-gateway — no per-install credential on target yet (no shipped default; the daemon generates one on next install/reconcile)'
|
||||
else
|
||||
\$DOCKER run -d --name fedimint-gateway --restart unless-stopped \$NET_OPT \
|
||||
--health-cmd 'curl -sf http://localhost:8176/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 -p 9737:9737 -v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
$FEDIMINT_GATEWAY_IMAGE \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--password \"\$FEDI_PASS\" \
|
||||
--network bitcoin --bitcoind-url http://\$TARGET_IP:8332 \
|
||||
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
|
||||
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway
|
||||
echo ' Creating fedimint-gateway...'
|
||||
sudo mkdir -p /var/lib/archipelago/fedimint-gateway
|
||||
LND_CERT=/var/lib/archipelago/lnd/tls.cert
|
||||
LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
if \$DOCKER ps --format '{{.Names}}' | grep -q '^lnd\$' && sudo test -f \$LND_CERT && sudo test -f \$LND_MACAROON; then
|
||||
\$DOCKER run -d --name fedimint-gateway --restart unless-stopped \$NET_OPT \
|
||||
--health-cmd 'curl -sf http://localhost:8176/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 -v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
-v /var/lib/archipelago/lnd/tls.cert:/lnd/tls.cert:ro \
|
||||
-v /var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon:/lnd/admin.macaroon:ro \
|
||||
$FEDIMINT_GATEWAY_IMAGE \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash \"\$GW_HASH\" \
|
||||
--network bitcoin --bitcoind-url http://\$TARGET_IP:8332 \
|
||||
--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
|
||||
else
|
||||
\$DOCKER run -d --name fedimint-gateway --restart unless-stopped \$NET_OPT \
|
||||
--health-cmd 'curl -sf http://localhost:8176/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 -p 9737:9737 -v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
$FEDIMINT_GATEWAY_IMAGE \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash \"\$GW_HASH\" \
|
||||
--network bitcoin --bitcoind-url http://\$TARGET_IP:8332 \
|
||||
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
|
||||
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
+22
-12
@@ -1259,15 +1259,17 @@ BUILDINFO_EOF
|
||||
echo "IMMICH_DB_PASS=$(sudo cat "$SECRETS_DIR/immich-db-password")"
|
||||
echo "PENPOT_DB_PASS=$(sudo cat "$SECRETS_DIR/penpot-db-password")"
|
||||
echo "MYSQL_ROOT_PASS=$(sudo cat "$SECRETS_DIR/mysql-root-db-password")"
|
||||
# Fedimint gateway password and hash
|
||||
if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then
|
||||
FEDI_PASS=$(openssl rand -base64 16)
|
||||
echo "$FEDI_PASS" | sudo tee "$SECRETS_DIR/fedimint-gateway-password" > /dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-password"
|
||||
if command -v htpasswd >/dev/null 2>&1; then
|
||||
htpasswd -bnBC 10 "" "$FEDI_PASS" | tr -d ":\n" | sudo tee "$SECRETS_DIR/fedimint-gateway-hash" > /dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
|
||||
fi
|
||||
# FED-07: no shipped fallback, ever. The canonical per-install gateway
|
||||
# credential (fedimint-gateway-hash / .pw) is generated by the daemon
|
||||
# via container::secrets::ensure_gateway_credential — this deploy script no
|
||||
# longer generates it (removes the htpasswd host dependency too).
|
||||
# Legacy migration only: carry an existing fedimint-gateway-password
|
||||
# value forward to the canonical fedimint-gateway-hash.pw name if that
|
||||
# name does not exist yet; never regenerate a working credential, and
|
||||
# never delete the legacy file (plan 01-16 owns retirement).
|
||||
if [ -f "$SECRETS_DIR/fedimint-gateway-password" ] && [ ! -f "$SECRETS_DIR/fedimint-gateway-hash.pw" ]; then
|
||||
sudo cp "$SECRETS_DIR/fedimint-gateway-password" "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
fi
|
||||
if [ -f "$SECRETS_DIR/fedimint-gateway-hash" ]; then
|
||||
echo "FEDI_HASH=$(sudo cat "$SECRETS_DIR/fedimint-gateway-hash")"
|
||||
@@ -1288,9 +1290,11 @@ BUILDINFO_EOF
|
||||
*) echo " WARNING: Ignoring unexpected variable from server: $key" ;;
|
||||
esac
|
||||
done <<< "$DB_PASSWORDS"
|
||||
# Fallback if hash not available
|
||||
# FED-07: no fallback literal. If the target hasn't generated its
|
||||
# per-install gateway credential yet, FEDI_HASH stays empty and the
|
||||
# Fedimint Gateway creation step below is skipped (not substituted).
|
||||
if [ -z "${FEDI_HASH:-}" ]; then
|
||||
FEDI_HASH='$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC'
|
||||
echo " NOTE: no fedimint-gateway credential on target yet — gateway container creation will be skipped (no shipped default; the daemon generates one on next install/reconcile)"
|
||||
fi
|
||||
|
||||
progress "Ensuring Bitcoin Knots"
|
||||
@@ -1712,12 +1716,15 @@ print("torrc generated with %d services" % (enabled or 7))
|
||||
|
||||
# Ensure Fedimint Gateway companion container
|
||||
# Auto-detect LND: if running with credentials, use lnd mode; otherwise use ldk (built-in)
|
||||
# FED-07: no shipped fallback — if no per-install credential was read
|
||||
# back from this target above, do not create/recreate the gateway
|
||||
# container at all (an empty --bcrypt-password-hash is never passed).
|
||||
if [ -n '$FEDI_HASH' ]; then
|
||||
\$DOCKER rm -f fedimint-gateway 2>/dev/null || true
|
||||
echo ' Creating fedimint-gateway...'
|
||||
sudo mkdir -p /var/lib/archipelago/fedimint-gateway
|
||||
LND_CERT=/var/lib/archipelago/lnd/tls.cert
|
||||
LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
GW_COMMON=\"-p 8176:8176 -v /var/lib/archipelago/fedimint-gateway:/data "$FEDIMINT_GATEWAY_IMAGE" 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\"
|
||||
if \$DOCKER ps --format '{{.Names}}' | grep -q '^lnd\$' && sudo test -f \$LND_CERT && sudo test -f \$LND_MACAROON; then
|
||||
echo ' LND detected — using lnd mode'
|
||||
\$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
|
||||
@@ -1747,6 +1754,9 @@ print("torrc generated with %d services" % (enabled or 7))
|
||||
--bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS \
|
||||
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway
|
||||
fi
|
||||
else
|
||||
echo ' Skipping fedimint-gateway — no per-install credential on target yet (no shipped default; will create it on a future deploy/reconcile once one is generated)'
|
||||
fi
|
||||
" 2>&1 | sed 's/^/ /') || echo " (Fedimint fix timed out or skipped - run manually if needed)"
|
||||
section_end
|
||||
|
||||
|
||||
@@ -398,32 +398,33 @@ MEMPOOL_DB_PASS=$(cat "$SECRETS_DIR/mempool-db-password")
|
||||
BTCPAY_DB_PASS=$(cat "$SECRETS_DIR/btcpay-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
|
||||
# Fedimint gateway credential: FED-07 — no shipped fallback, ever. The
|
||||
# canonical per-install credential is the manifest's `fedimint-gateway-hash`
|
||||
# (+ `.pw` plaintext sibling), generated by the daemon's
|
||||
# container::secrets::ensure_gateway_credential (bcrypt, 0600, idempotent,
|
||||
# self-healing). This ISO first-boot path — the site that put the shipped
|
||||
# default on real nodes before this fix — no longer generates the hash
|
||||
# locally, which also removes the htpasswd host dependency entirely and
|
||||
# keeps bcrypt generation in exactly one place.
|
||||
#
|
||||
# Legacy migration only: earlier builds wrote the plaintext under
|
||||
# fedimint-gateway-password instead of the manifest's canonical
|
||||
# fedimint-gateway-hash.pw name. Carry a pre-existing value forward under the
|
||||
# canonical name rather than regenerating (migrations never destroy data or
|
||||
# rotate a working credential); the legacy file is left in place — plan
|
||||
# 01-16 owns its retirement.
|
||||
if [ -f "$SECRETS_DIR/fedimint-gateway-password" ] && [ ! -f "$SECRETS_DIR/fedimint-gateway-hash.pw" ]; then
|
||||
cp "$SECRETS_DIR/fedimint-gateway-password" "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
chmod 600 "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
log "Migrated legacy fedimint-gateway-password -> fedimint-gateway-hash.pw"
|
||||
fi
|
||||
FEDI_PASS=$(cat "$SECRETS_DIR/fedimint-gateway-password")
|
||||
FEDI_HASH=""
|
||||
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"
|
||||
if [ -z "$FEDI_HASH" ]; then
|
||||
log "Fedimint gateway credential not yet generated — the daemon/reconcile will generate a per-install bcrypt hash (no shipped default); the gateway container creation below is skipped until then"
|
||||
fi
|
||||
|
||||
BITCOIN_READY=false
|
||||
TOTAL=0
|
||||
@@ -1018,42 +1019,50 @@ track_container "fedimint"
|
||||
# 5b. Fedimint Gateway (companion to fedimint)
|
||||
# Auto-detect LND: if running with credentials, use lnd mode; otherwise use ldk (built-in)
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; then
|
||||
log "Creating Fedimint Gateway..."
|
||||
mkdir -p /var/lib/archipelago/fedimint-gateway
|
||||
LND_CERT=/var/lib/archipelago/lnd/tls.cert
|
||||
LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^lnd$' && [ -f "$LND_CERT" ] && [ -f "$LND_MACAROON" ]; then
|
||||
log " LND detected — using lnd mode"
|
||||
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:8176/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 \
|
||||
-v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
-v "$LND_CERT":/lnd/tls.cert:ro \
|
||||
-v "$LND_MACAROON":/lnd/admin.macaroon:ro \
|
||||
"$FEDIMINT_GATEWAY_IMAGE" \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash "$FEDI_HASH" \
|
||||
--network bitcoin --bitcoind-url "http://$BTC_HOST:$BTC_PORT" \
|
||||
--bitcoind-username "$BTC_RPC_USER" --bitcoind-password "$BTC_RPC_PASS" \
|
||||
lnd --lnd-rpc-host lnd:10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon 2>>"$LOG" || true
|
||||
if [ -z "$FEDI_HASH" ]; then
|
||||
# FED-07: no shipped fallback. Without a per-install credential yet, do
|
||||
# not start gatewayd with an empty --bcrypt-password-hash — skip creation
|
||||
# here; reconcile-containers.sh creates it once the daemon has generated
|
||||
# the credential.
|
||||
log " Skipping Fedimint Gateway creation — no per-install credential yet (no shipped default); reconcile will create it once one is generated"
|
||||
else
|
||||
log " No LND found — using ldk (built-in Lightning)"
|
||||
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:8176/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 -p 9737:9737 \
|
||||
-v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
"$FEDIMINT_GATEWAY_IMAGE" \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash "$FEDI_HASH" \
|
||||
--network bitcoin --bitcoind-url "http://$BTC_HOST:$BTC_PORT" \
|
||||
--bitcoind-username "$BTC_RPC_USER" --bitcoind-password "$BTC_RPC_PASS" \
|
||||
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway 2>>"$LOG" || true
|
||||
log "Creating Fedimint Gateway..."
|
||||
mkdir -p /var/lib/archipelago/fedimint-gateway
|
||||
LND_CERT=/var/lib/archipelago/lnd/tls.cert
|
||||
LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^lnd$' && [ -f "$LND_CERT" ] && [ -f "$LND_MACAROON" ]; then
|
||||
log " LND detected — using lnd mode"
|
||||
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:8176/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 \
|
||||
-v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
-v "$LND_CERT":/lnd/tls.cert:ro \
|
||||
-v "$LND_MACAROON":/lnd/admin.macaroon:ro \
|
||||
"$FEDIMINT_GATEWAY_IMAGE" \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash "$FEDI_HASH" \
|
||||
--network bitcoin --bitcoind-url "http://$BTC_HOST:$BTC_PORT" \
|
||||
--bitcoind-username "$BTC_RPC_USER" --bitcoind-password "$BTC_RPC_PASS" \
|
||||
lnd --lnd-rpc-host lnd: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)"
|
||||
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:8176/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 8176:8176 -p 9737:9737 \
|
||||
-v /var/lib/archipelago/fedimint-gateway:/data \
|
||||
"$FEDIMINT_GATEWAY_IMAGE" \
|
||||
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
|
||||
--bcrypt-password-hash "$FEDI_HASH" \
|
||||
--network bitcoin --bitcoind-url "http://$BTC_HOST:$BTC_PORT" \
|
||||
--bitcoind-username "$BTC_RPC_USER" --bitcoind-password "$BTC_RPC_PASS" \
|
||||
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway 2>>"$LOG" || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
track_container "fedimint-gateway"
|
||||
|
||||
@@ -409,6 +409,12 @@ reconcile() {
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -n "$SPEC_SKIP_REASON" ]; then
|
||||
skip "$name — $SPEC_SKIP_REASON"
|
||||
COUNT_SKIPPED=$((COUNT_SKIPPED + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
[ "$name" = "portainer" ] && ensure_portainer_host_paths
|
||||
|
||||
# Filter by tier
|
||||
@@ -693,19 +699,31 @@ ensure_secrets() {
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
local fpass
|
||||
fpass=$(openssl rand -base64 16)
|
||||
echo "$fpass" | sudo tee "$SECRETS_DIR/fedimint-gateway-password" >/dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-password"
|
||||
if command -v htpasswd >/dev/null 2>&1; then
|
||||
htpasswd -bnBC 10 "" "$fpass" | tr -d ':\n' | sudo tee "$SECRETS_DIR/fedimint-gateway-hash" >/dev/null
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
|
||||
fi
|
||||
info "Generated fedimint gateway secret"
|
||||
# FED-07: the gateway's bcrypt credential is generated by the daemon's
|
||||
# container::secrets::ensure_gateway_credential (bcrypt, 0600, idempotent,
|
||||
# self-healing) — this script no longer generates it locally (that removed
|
||||
# the htpasswd host dependency AND the shipped-default fallback that used
|
||||
# to fire when htpasswd was absent). It never shipped, and never ships,
|
||||
# a fallback value.
|
||||
#
|
||||
# Legacy migration only: earlier builds wrote the plaintext under
|
||||
# fedimint-gateway-password instead of the manifest's canonical
|
||||
# fedimint-gateway-hash.pw name. Carry a pre-existing value forward under
|
||||
# the canonical name rather than regenerating (migrations never destroy
|
||||
# data or rotate a working credential); the legacy file is left in place —
|
||||
# plan 01-16 owns its retirement.
|
||||
if [ -f "$SECRETS_DIR/fedimint-gateway-password" ] && [ ! -f "$SECRETS_DIR/fedimint-gateway-hash.pw" ]; then
|
||||
if $CHECK_ONLY; then
|
||||
info "Would migrate legacy fedimint-gateway-password -> fedimint-gateway-hash.pw"
|
||||
else
|
||||
sudo cp "$SECRETS_DIR/fedimint-gateway-password" "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash.pw"
|
||||
info "Migrated legacy fedimint-gateway-password -> fedimint-gateway-hash.pw"
|
||||
fi
|
||||
fi
|
||||
if [ ! -f "$SECRETS_DIR/fedimint-gateway-hash" ]; then
|
||||
info "fedimint-gateway credential not yet generated — the daemon will generate a per-install bcrypt hash (no shipped default); the gateway container is skipped until then"
|
||||
fi
|
||||
|
||||
# Reload after generation
|
||||
detect_environment
|
||||
|
||||
Reference in New Issue
Block a user