Files

16 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed coverage duration completed status
01-federation-mesh-hardening 11 security
secrets
bcrypt
fedimint
credentials
shell
rust
phase provides
01-federation-mesh-hardening 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
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
fedimint-gateway
container-secrets
deploy
first-boot
reconcile
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.
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
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.
FED-07
id description requirement verification human_judgment
D1 A fresh install derives a per-install gateway credential; two installs never share one FED-07
kind ref status
unit core/archipelago/src/container/secrets.rs#gateway_credential_fresh_generation_verifies_and_is_0600, #gateway_credential_is_per_install_not_per_build pass
false
id description requirement verification human_judgment
D2 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 FED-07
kind ref status
unit core/archipelago/src/container/secrets.rs#gateway_credential_missing_is_a_named_error (error names the missing file) pass
kind ref status
other grep -rl 't9YjjxkiktrlYvjajB' --include='*.rs' --include='*.sh' --include='*.yml' --include='*.json' . -> exactly 1 hit, core/archipelago/src/container/secrets.rs (the denylist) pass
false
id description requirement verification human_judgment
D3 The compromised hash exists in exactly one place, as a denylist never used to configure a container FED-07
kind ref status
unit core/archipelago/src/container/secrets.rs#gateway_credential_rejects_known_default — the accessor returns Err rather than handing the value back pass
false
id description requirement verification human_judgment
D4 One canonical secret filename across the Rust orchestrator, first-boot, reconcile and both deploy scripts FED-07
kind ref status
other GATEWAY_HASH_SECRET_NAME const in secrets.rs matches manifest generated_secrets; all four scripts read/write fedimint-gateway-hash{,.pw} pass
false
id description requirement verification human_judgment
D5 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) FED-07
kind ref status
other 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 pass
false
id description requirement verification human_judgment
D6 Generating the credential twice is idempotent — a reconcile tick never rotates a working gateway out from under itself FED-07
kind ref status
unit core/archipelago/src/container/secrets.rs#gateway_credential_is_idempotent pass
false
id description requirement verification human_judgment
D7 All five changed scripts remain syntactically valid FED-07
kind ref status
other bash -n clean on first-boot-containers.sh, reconcile-containers.sh, deploy-to-target.sh, deploy-tailscale.sh, container-specs.sh pass
false
135min 2026-07-31 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.rsget_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.rsconfigure_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.shSPEC_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_reconciler4 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