Compare commits

..
Author SHA1 Message Date
ssmithxandClaude Sonnet 5 53b158ce5a fix(wallet): translate Cashu NUT error codes into plain-language messages
Mint HTTP failures (swap/melt/mint-quote) were surfacing raw JSON bodies
like {"detail":"proofs already spent","code":11001} straight to the
user. Add a translator for the NUT-02/03/04/05 transaction-validation
error codes (10001-11017, 12001-12003; see
https://github.com/cashubtc/nuts/blob/main/error_codes.md) and layer it
onto the mint_client bail sites via anyhow context, so the top-level
message is actionable while the raw status/body stays available via
{:#} for logs. receive_token now surfaces the real reason (e.g. "This
ecash has already been redeemed") instead of a generic "Failed to
receive any proofs from token" when every mint in a token fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 09:41:44 +00:00
37 changed files with 165 additions and 382 deletions
-21
View File
@@ -1,26 +1,5 @@
# Changelog
## v1.7.123-alpha (2026-08-05)
- **Five more screens on your node were readable by anyone who could reach it, and the previous release's own check said they were fine.** The Bitcoin, Lightning, Electrum, FIPS mesh and Fedimint Guardian screens each answered on their port with no login. They were missed because they work differently from ordinary apps: they run directly on the node's network rather than behind its container plumbing, so there was no address to pin and their descriptions listed no port at all — and the node builds its list of what to protect from exactly those descriptions. It therefore neither protected them nor listed them as unprotected. A check that reports success while five screens are open is worse than no check, and this was found by scanning the node from another machine rather than asking the node about itself.
- **What was actually readable was the page, not your money.** Every request on those ports that could have returned a credential — the Lightning connection details, the wallet passthrough, container logs, and every node command — already required a login and still refused without one. The Lightning macaroon fix from v1.7.120 was verified directly rather than assumed. What leaked was the screen itself: layout and code, no wallet data, no keys.
- All five now serve only to the node itself, with the login gate in front of them, exactly like the twenty app screens closed in the previous release.
- **Every port on the node now has a stated policy — there are no undecided ones left.** Eleven ports previously had no instruction either way and stayed open by default. The BotFights arena, the router screen and the Pine voice screen now require the node password. The ones that genuinely cannot take a login page stay open with a written reason: Fedimint's guardian and gateway connections (federation members authenticate to the federation), NetBird's management and dashboard ports (your VPN devices carry their own credentials and cannot hold a browser session, and its dashboard needs its own certificate), Pine's secure listener, and the Lightning REST port, which wallets reach with a macaroon exactly as before.
- Fresh installs are covered too, not just existing nodes. The five screens are delivered as prebuilt images, so a newly flashed node would have come up open even after this fix. All five were rebuilt, published, and then pulled back and inspected to confirm the fix is really inside them.
- Two delivery faults fixed alongside, either of which would have silently undone the above: two of the five screens were reaching nodes through no update path at all, so edits to them never arrived; and a fourth copy of the Bitcoin screen's configuration was being rewritten on every health check, which would have re-opened that port after everything else was corrected.
- Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.
## v1.7.122-alpha (2026-08-04)
- **Your apps now ask for your node password before they open — over your home network, Tailscale, the mesh and Tor alike.** Until now anyone who could reach your node could open Immich, Nextcloud, Vaultwarden, Jellyfin, Grafana and the rest simply by typing the address and port, with no login at all. Twenty app screens now sit behind the same login you use for the node, showing you which app you are opening, and honouring two-factor if you have it switched on. Logging in at an app address logs you into the dashboard too, so it is one password, not one per app. This completes the groundwork disclosed in v1.7.121.
- **The things that must stay open stayed open.** Zeus and other remote wallets still reach your Lightning node directly, Electrum wallets still connect, and Bitcoin still talks to its peers — those connections carry their own proof of identity and a login page would simply break them. Every one of these seventeen exceptions now has to state in writing why it is safe to leave open, so the list is something you can read rather than something you have to discover.
- **A private address on your node was answering the mesh without a password.** One app's port was marked as being for this machine only, and the part of the node that carries mesh traffic did not know that — it forwarded requests from the whole mesh straight to it. Found while verifying the work above on a real node, not in testing. That path now refuses anything marked machine-only, and the app is reachable only from the node itself, as intended.
- **Tor addresses no longer skip the login.** An app published as a .onion address was handed straight to the app, because a Tor visitor carries no session cookie to check. The login gate now takes those addresses first, closing the last of the four routes that went around it.
- Nodes fix themselves after this update. Apps installed before this system used its current container setup kept their old wide-open address even after the signed list told them to move, and each would otherwise have needed hand-holding on every node. Your node now notices the difference and rebuilds those apps itself, keeping their data, within about half a minute of starting. Verified by putting a node back into the old state deliberately and watching it repair.
- The node had been reading two different sets of instructions about its own apps — the signed list it downloads, and older copies on disk — which is how a port meant to stay private was briefly opened on a test node. Both now come from the signed list, and a port withdrawn from the login gate is released without needing a restart.
- **The key that signs these updates has been replaced.** The previous signing key was exposed where it should not have been, so it is treated as compromised and this release installs its replacement. This update is the last one signed with the old key, by necessity — it is the one that teaches your node the new one.
- Known gaps, disclosed rather than buried: eleven app ports still have no stated policy — BotFights, the Fedimint gateway, NetBird, the voice assistant's own screens and the router screen — and remain reachable without a login until each is decided deliberately; the node reports them rather than guessing, because guessing at an unstated setting caused both incidents behind this work. Three voice-assistant ports are still open without authentication; the correct fix puts them on a private network with the assistant. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — will now meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.
## v1.7.121-alpha (2026-08-04)
- **Making another node "Trusted" now asks for your node password.** Trust was being handed out by machines rather than by you: any node able to reach yours could join and mark itself Trusted, because the check proved only that the caller owned the key it had just presented — never that you had approved it. Trust also spread on its own, since every peer a Trusted node advertised was added as Trusted too, so one grant quietly propagated across the whole federation. Uninvited joins are now capped at Observer, advertised peers arrive as Observers, and raising anyone to Trusted — whether by generating an invite or by changing the dropdown on a node — requires your password. Lowering trust deliberately does not, because the safe action must never be the inconvenient one. Existing peers are left exactly as they are rather than silently demoted, and each one now records how its trust was granted so you can review them.
+1 -12
View File
@@ -31,18 +31,7 @@ app:
# proxies to 127.0.0.1:8332 which is where the bitcoin backend binds
# its RPC. `ports:` is intentionally empty because host networking
# bypasses port mapping.
# Declared so the APP GATE can see this port. Host networking means Podman
# publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here
# is a statement of where the container's own nginx listens — 127.0.0.1 —
# not a publish instruction. Without this declaration the gate had no idea
# the port existed: it was neither protected nor listed as unprotected, and
# served the Bitcoin screen unauthenticated on every interface.
ports:
- host: 8334
container: 8334
protocol: tcp
bind: 127.0.0.1
auth: gated
ports: []
volumes:
# Bind-mount the rendered nginx.conf read-only. The prod orchestrator
-2
View File
@@ -62,8 +62,6 @@ app:
- host: 9100
container: 9100
protocol: tcp # Web UI + API
bind: 127.0.0.1
auth: gated
volumes:
# A bare relative source (was "botfights-data", no leading slash) is
+1 -12
View File
@@ -23,18 +23,7 @@ app:
network_policy: host
# Host networking: nginx listens on 50002 directly on the host IP.
# Declared so the APP GATE can see this port. Host networking means Podman
# publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here
# is a statement of where the container's own nginx listens — 127.0.0.1 —
# not a publish instruction. Without this declaration the gate had no idea
# the port existed: it was neither protected nor listed as unprotected, and
# served the Electrs screen unauthenticated on every interface.
ports:
- host: 50002
container: 50002
protocol: tcp
bind: 127.0.0.1
auth: gated
ports: []
volumes: []
-8
View File
@@ -60,17 +60,9 @@ app:
- host: 8176
container: 8176
protocol: tcp
auth: none
auth_rationale: >-
Fedimint gateway API, protected by its own bcrypt password (--bcrypt-password-hash)
and reached by federation peers and clients that cannot hold a browser session.
- host: 9737
container: 9737
protocol: tcp
auth: none
auth_rationale: >-
LDK Lightning p2p for the gateway. The BOLT-8 noise handshake authenticates and
encrypts the connection itself.
volumes:
- type: bind
-18
View File
@@ -50,29 +50,11 @@ app:
- host: 8173
container: 8173
protocol: tcp
auth: none
auth_rationale: >-
Fedimint guardian consensus. Other guardians speak the federation's own
authenticated protocol here; a login page would break consensus.
- host: 8174
container: 8174
protocol: tcp
auth: none
auth_rationale: >-
Fedimint guardian API for federation clients, which authenticate to the
federation itself and cannot hold a browser session.
# Public launch port 8175 is owned by archy-fedimint-ui, which serves a
# wait page while Bitcoin syncs and proxies here after fedimintd starts.
# Declared HERE because that companion has no manifest of its own, and the
# gate keys on the port rather than the container: without this entry it
# served the Guardian UI unauthenticated on every interface and never
# appeared in the audit. Its nginx is pinned to 127.0.0.1
# (docker/fedimint-ui/nginx.conf) so the gate can own the outside.
- host: 8175
container: 8175
protocol: tcp
bind: 127.0.0.1
auth: gated
- host: 8177
container: 8175
protocol: tcp
+1 -12
View File
@@ -27,18 +27,7 @@ app:
# Host networking: nginx listens on 8336 directly on the host IP and
# proxies to 127.0.0.1:5678 (the archipelago RPC). `ports:` is
# intentionally empty because host networking bypasses port mapping.
# Declared so the APP GATE can see this port. Host networking means Podman
# publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here
# is a statement of where the container's own nginx listens — 127.0.0.1 —
# not a publish instruction. Without this declaration the gate had no idea
# the port existed: it was neither protected nor listed as unprotected, and
# served the FIPS mesh screen unauthenticated on every interface.
ports:
- host: 8336
container: 8336
protocol: tcp
bind: 127.0.0.1
auth: gated
ports: []
volumes: []
-5
View File
@@ -41,14 +41,9 @@ app:
auth: none
auth_rationale: >-
LND gRPC, authenticated by macaroon over TLS. Remote wallets depend on reaching this directly.
# Mirrors lnd's 18080 exemption — same LND REST API, same macaroon auth.
- host: 8091
container: 8080
protocol: tcp # REST/Web UI
auth: none
auth_rationale: >-
LND REST, authenticated by macaroon over TLS. A browser login page would break
Zeus and every non-browser wallet client, exactly as for lnd's 18080.
volumes:
- type: bind
+1 -12
View File
@@ -35,18 +35,7 @@ app:
# port to a container port where nothing listens. scripts/container-specs.sh
# carried the identical mistake and was fixed alongside this; recreating from
# it on archi-dev-box left :18083 refusing connections.
# Declared so the APP GATE can see this port. Host networking means Podman
# publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here
# is a statement of where the container's own nginx listens — 127.0.0.1 —
# not a publish instruction. Without this declaration the gate had no idea
# the port existed: it was neither protected nor listed as unprotected, and
# served the LND screen unauthenticated on every interface.
ports:
- host: 18083
container: 18083
protocol: tcp
bind: 127.0.0.1
auth: gated
ports: []
volumes: []
-5
View File
@@ -48,11 +48,6 @@ app:
- host: 8086
container: 80
protocol: tcp # management API + embedded OIDC issuer (/oauth2)
auth: none
auth_rationale: >-
NetBird management API and its OIDC issuer. Enrolled devices authenticate
themselves with setup keys and JWTs, and they cannot hold a browser session —
a login page here would disconnect every VPN client on the network.
- host: 3478
container: 3478
protocol: udp # STUN — must be UDP; tcp here breaks relay discovery
-5
View File
@@ -44,11 +44,6 @@ app:
- host: 8087
container: 443
protocol: tcp
auth: none
auth_rationale: >-
NetBird dashboard over TLS, with its own login. The gate speaks plain HTTP,
so fronting this port would break the secure context the dashboard requires
(issue #15) and the certificate clients pin.
volumes:
- type: bind
-7
View File
@@ -53,16 +53,9 @@ app:
- host: 10380
container: 80
protocol: tcp
bind: 127.0.0.1
auth: gated
- host: 10381
container: 443
protocol: tcp
auth: none
auth_rationale: >-
Pine's TLS listener. The gate speaks plain HTTP, so fronting this port would
break the secure context navigator.bluetooth needs for WiFi provisioning.
The plain-HTTP entry point (10380) is gated, and it is what the UI opens.
volumes:
- type: bind
-2
View File
@@ -30,8 +30,6 @@ app:
- host: 8084
container: 8080
protocol: tcp # Web UI
bind: 127.0.0.1
auth: gated
- host: 5353
container: 5353
protocol: udp # mDNS/Bonjour
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]]
name = "archipelago"
version = "1.7.123-alpha"
version = "1.7.121-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.123-alpha"
version = "1.7.121-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]
+1 -1
View File
@@ -293,6 +293,6 @@ mod tests {
// Lock in the core shape so a bad template edit doesn't ship.
assert!(TEMPLATE.contains("proxy_pass http://127.0.0.1:8332/"));
assert!(TEMPLATE.contains("location /bitcoin-rpc/"));
assert!(TEMPLATE.contains("listen 127.0.0.1:8334"));
assert!(TEMPLATE.contains("listen 8334"));
}
}
@@ -1,12 +1,5 @@
server {
# Loopback ONLY. This container is host-networked, so this nginx binds the
# HOST's address directly — `listen 8334;` meant every interface, and the
# app gate could never stand in front of it (there is no podman publish to
# pin, and the manifest declared no port, so the gate neither protected it
# nor reported it — it served this page to anyone who asked, on LAN,
# Tailscale and the mesh alike). Binding loopback lets the daemon claim the
# external addresses and authenticate them; see appgate::listener.
listen 127.0.0.1:8334;
listen 8334;
server_name _;
root /usr/share/nginx/html;
index index.html;
+3 -18
View File
@@ -16,28 +16,13 @@ use ed25519_dalek::VerifyingKey;
/// Hex of the pinned Ed25519 release-root public key (32 bytes / 64 hex chars).
///
/// ROTATED 2026-08-04 to did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT.
///
/// The previous root (z6Mkkid…q7ur, pinned 2026-07-02) was exposed in a chat
/// transcript and is treated as compromised.
///
/// Rotation is ORDERING-CRITICAL. Nodes pin the OLD key, so the release that
/// carries this change must itself be signed with the OLD key — that is the
/// only signature a node running the previous binary will accept. Only the
/// release AFTER it may be signed with the new key. Signing the rotation
/// release with the new key makes every node reject it and ends OTA
/// fleet-wide, recoverable only by touching each node by hand.
///
/// Verified before pinning: this hex and the did:key above are the same
/// keypair (the did:key encodes exactly these 32 bytes), checked with a
/// decoder round-tripped against the previous known-good pair. An earlier
/// candidate hex was rejected because it did not match the stated DID.
/// The
/// Pinned 2026-07-02 from the release-root signing ceremony
/// (signer did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). The
/// corresponding mnemonic is held offline by the publisher — see
/// `docs/workstream-b-signing-runbook.md`. Regenerate/verify with:
/// `RELEASE_MASTER_MNEMONIC=… archipelago ceremony pubkey`.
pub const RELEASE_ROOT_PUBKEY_HEX: Option<&str> =
Some("1578adccf137024159dd936f44a56e8869ac7775785962f7e92e2faf2c034418");
Some("5d15cbee8a108f7dd288c02d29a1d9d71f198acc99186aad8008b4f28d469951");
const ENV_OVERRIDE: &str = "ARCHY_RELEASE_ROOT_PUBKEY";
+12 -2
View File
@@ -1040,6 +1040,12 @@ pub async fn receive_token(data_dir: &Path, token_str: &str) -> Result<u64> {
let mut wallet = load_wallet(data_dir).await?;
let mut received_total = 0u64;
// MintClient translates the mint's NUT error code into plain language and
// puts it at the top of the error chain (see `mint_error` in
// mint_client.rs); `{}` surfaces that, `{:#}` keeps the raw status/body
// for the log. Remember the last one so a total failure can tell the user
// *why* instead of just "nothing was received".
let mut last_reason: Option<String> = None;
// Swap proofs at each mint
for entry in &token.token {
@@ -1051,14 +1057,18 @@ pub async fn receive_token(data_dir: &Path, token_str: &str) -> Result<u64> {
received_total += amount;
}
Err(e) => {
warn!("Failed to swap proofs from mint {}: {}", entry.mint, e);
warn!("Failed to swap proofs from mint {}: {:#}", entry.mint, e);
last_reason = Some(e.to_string());
// Continue with other mints if any
}
}
}
if received_total == 0 {
anyhow::bail!("Failed to receive any proofs from token");
match last_reason {
Some(reason) => anyhow::bail!("Could not receive this ecash: {}", reason),
None => anyhow::bail!("Failed to receive any proofs from token"),
}
}
wallet.record_tx(
+71 -5
View File
@@ -59,6 +59,72 @@ pub struct MintResult {
pub proofs: Vec<Proof>,
}
/// Translate a Cashu NUT "transaction validation" error code into plain
/// language a wallet user can act on. Mints respond to a rejected request
/// with `{"code": N, "detail": "..."}`; `detail` is implementation-defined
/// free text, but `code` is the stable identifier from the spec
/// (https://github.com/cashubtc/nuts/blob/main/error_codes.md). Covers the
/// 10001-11017 "proof/transaction validation" range plus the 12001-12003
/// keyset codes shared by NUT-02/03/04/05 — the codes a swap/melt/mint call
/// can actually hit. Returns `None` for anything else (e.g. Lightning/quote
/// codes in the 20000s) so the caller falls back to the mint's own `detail`.
fn describe_mint_error_code(code: i64) -> Option<&'static str> {
Some(match code {
10001 => "The mint rejected these coins as invalid.",
11001 => "This ecash has already been redeemed — it can't be claimed twice.",
11002 => "This ecash is already being redeemed elsewhere — try again in a moment.",
11003 => "The mint already issued new coins for this exact request — there's nothing left to redeem.",
11004 => "This request is still being processed by the mint — try again in a moment.",
11005 => "The token's amounts don't add up (inputs don't match outputs) — it may be corrupt.",
11006 => "That amount is outside the range this mint allows.",
11007 => "This token contains duplicate coins — it may be corrupt or already used.",
11008 => "The mint rejected this as a duplicate request.",
11009 | 11010 => "This token mixes incompatible currency units — the mint rejected it.",
11011 => "That Lightning invoice has no amount, which isn't supported here.",
11012 => "The amount requested doesn't match the Lightning invoice.",
11013 => "The mint doesn't support this currency unit.",
11014 | 11015 => "This token has too many coins for the mint to process in one request.",
11016 => "Duplicate quote IDs were sent in this request.",
11017 => "Too many items were sent in a single request.",
12001 => "The mint no longer recognizes the keyset that signed this token.",
12002 => "The mint's signing key for this token is inactive.",
12003 => "The mint's signing key for this token has expired.",
_ => return None,
})
}
/// Parse a mint's error body (`{"code": N, "detail": "..."}`) and pick the
/// best user-facing message: the plain-language translation when we know the
/// code, otherwise the mint's own `detail` text, otherwise the raw body.
fn describe_mint_error_body(status: reqwest::StatusCode, body: &str) -> String {
let parsed: Option<serde_json::Value> = serde_json::from_str(body).ok();
let code = parsed
.as_ref()
.and_then(|v| v.get("code"))
.and_then(|c| c.as_i64());
let detail = parsed
.as_ref()
.and_then(|v| v.get("detail"))
.and_then(|d| d.as_str());
if let Some(friendly) = code.and_then(describe_mint_error_code) {
return friendly.to_string();
}
match detail {
Some(d) if !d.is_empty() => d.to_string(),
_ => format!("mint returned {} with no further detail", status),
}
}
/// Build the error for a failed mint HTTP call: `op` + status + raw body as
/// the technical cause (visible via `{:#}` in logs), with the plain-language
/// translation layered on top via `.context()` so `{}` — what reaches the
/// wallet user — shows something actionable instead of raw mint JSON.
fn mint_error(op: &str, status: reqwest::StatusCode, body: &str) -> anyhow::Error {
let friendly = describe_mint_error_body(status, body);
anyhow::anyhow!("{} failed ({}): {}", op, status, body).context(friendly)
}
/// HTTP client for a single Cashu mint.
pub struct MintClient {
url: String,
@@ -146,7 +212,7 @@ impl MintClient {
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
anyhow::bail!("Mint quote failed ({}): {}", status, body);
return Err(mint_error("Mint quote", status, &body));
}
res.json().await.context("Failed to parse mint quote")
@@ -212,7 +278,7 @@ impl MintClient {
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
anyhow::bail!("Mint tokens failed ({}): {}", status, body);
return Err(mint_error("Minting tokens", status, &body));
}
let body: serde_json::Value = res.json().await.context("Failed to parse mint response")?;
@@ -266,7 +332,7 @@ impl MintClient {
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
anyhow::bail!("Melt quote failed ({}): {}", status, body);
return Err(mint_error("Melt quote", status, &body));
}
res.json().await.context("Failed to parse melt quote")
@@ -293,7 +359,7 @@ impl MintClient {
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
anyhow::bail!("Melt failed ({}): {}", status, body);
return Err(mint_error("Melt", status, &body));
}
res.json().await.context("Failed to parse melt response")
@@ -337,7 +403,7 @@ impl MintClient {
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
anyhow::bail!("Swap failed ({}): {}", status, body);
return Err(mint_error("Swap", status, &body));
}
let body: serde_json::Value = res.json().await.context("Failed to parse swap response")?;
+1 -8
View File
@@ -1,12 +1,5 @@
server {
# Loopback ONLY. This container is host-networked, so this nginx binds the
# HOST's address directly `listen 50002;` meant every interface, and the
# app gate could never stand in front of it (there is no podman publish to
# pin, and the manifest declared no port, so the gate neither protected it
# nor reported it it served this page to anyone who asked, on LAN,
# Tailscale and the mesh alike). Binding loopback lets the daemon claim the
# external addresses and authenticate them; see appgate::listener.
listen 127.0.0.1:50002;
listen 50002;
server_name _;
root /usr/share/nginx/html;
+1 -8
View File
@@ -1,12 +1,5 @@
server {
# Loopback ONLY. This container is host-networked, so this nginx binds the
# HOST's address directly `listen 8175;` meant every interface, and the
# app gate could never stand in front of it (there is no podman publish to
# pin, and the manifest declared no port, so the gate neither protected it
# nor reported it it served this page to anyone who asked, on LAN,
# Tailscale and the mesh alike). Binding loopback lets the daemon claim the
# external addresses and authenticate them; see appgate::listener.
listen 127.0.0.1:8175;
listen 8175;
server_name _;
proxy_intercept_errors on;
+1 -8
View File
@@ -1,12 +1,5 @@
server {
# Loopback ONLY. This container is host-networked, so this nginx binds the
# HOST's address directly `listen 8336;` meant every interface, and the
# app gate could never stand in front of it (there is no podman publish to
# pin, and the manifest declared no port, so the gate neither protected it
# nor reported it it served this page to anyone who asked, on LAN,
# Tailscale and the mesh alike). Binding loopback lets the daemon claim the
# external addresses and authenticate them; see appgate::listener.
listen 127.0.0.1:8336;
listen 8336;
server_name _;
root /usr/share/nginx/html;
index index.html;
+1 -8
View File
@@ -1,14 +1,7 @@
server {
# Host-networked: listen on the app's own port directly (NOT 80, which the
# host's main nginx already owns). The app is reached at http(s)://<node>:18083.
# Loopback ONLY. This container is host-networked, so this nginx binds the
# HOST's address directly `listen 18083;` meant every interface, and the
# app gate could never stand in front of it (there is no podman publish to
# pin, and the manifest declared no port, so the gate neither protected it
# nor reported it it served this page to anyone who asked, on LAN,
# Tailscale and the mesh alike). Binding loopback lets the daemon claim the
# external addresses and authenticate them; see appgate::listener.
listen 127.0.0.1:18083;
listen 18083;
server_name _;
root /usr/share/nginx/html;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.7.123-alpha",
"version": "1.7.121-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.7.123-alpha",
"version": "1.7.121-alpha",
"dependencies": {
"@scure/bip39": "^2.2.0",
"@types/dompurify": "^3.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.7.123-alpha",
"version": "1.7.121-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",
@@ -362,38 +362,6 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.7.123-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.123-alpha</span>
<span class="text-xs text-white/40">August 5, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p><strong>Five more screens on your node were readable by anyone who could reach it and the last release's own check said they were fine.</strong> The Bitcoin, Lightning, Electrum, FIPS mesh and Fedimint Guardian screens each answered on their port with no login. They were missed because they run directly on the node's network rather than behind its container plumbing, so there was no address to pin and their descriptions listed no port at all and the node builds its list of what to protect from exactly those descriptions. It neither protected them nor listed them as unprotected. Found by scanning the node from another machine instead of asking the node about itself.</p>
<p><strong>What was readable was the page, not your money.</strong> Every request on those ports that could have returned a credential the Lightning connection details, the wallet passthrough, container logs, and every node command already required a login and still refused without one. The Lightning macaroon fix from v1.7.120 was checked directly rather than assumed. What leaked was the screen itself: layout and code, no wallet data, no keys.</p>
<p>All five now serve only to the node itself, with the login gate in front, exactly like the twenty app screens closed in the last release.</p>
<p><strong>Every port on your node now has a stated policy none are left undecided.</strong> Eleven had no instruction either way and stayed open by default. The BotFights arena, the router screen and the Pine voice screen now ask for your node password. The ones that genuinely cannot take a login page stay open with a written reason: Fedimint's guardian and gateway connections, NetBird's management and dashboard ports (your VPN devices carry their own credentials and cannot hold a browser session), Pine's secure listener, and the Lightning REST port that wallets reach with a macaroon exactly as before.</p>
<p>Freshly flashed nodes are covered too, not only existing ones those five screens ship as prebuilt images, so a new node would otherwise have come up open even after this fix. All five were rebuilt, published, then pulled back and inspected to confirm the fix is really inside them.</p>
<p>Known gaps, disclosed rather than buried: non-browser clients phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.</p>
</div>
</div>
<!-- v1.7.122-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.122-alpha</span>
<span class="text-xs text-white/40">August 4, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p><strong>Your apps now ask for your node password before they open over your home network, Tailscale, the mesh and Tor alike.</strong> Until now anyone who could reach your node could open Immich, Nextcloud, Vaultwarden, Jellyfin, Grafana and the rest simply by typing the address and port, with no login at all. Twenty app screens now sit behind the same login you use for the node, showing you which app you are opening, and honouring two-factor if you have it switched on. Logging in at an app address logs you into the dashboard too, so it is one password, not one per app.</p>
<p><strong>The things that must stay open stayed open.</strong> Zeus and other remote wallets still reach your Lightning node directly, Electrum wallets still connect, and Bitcoin still talks to its peers those connections carry their own proof of identity and a login page would simply break them. Every one of these exceptions now has to state in writing why it is safe to leave open, so the list is something you can read rather than something you have to discover.</p>
<p><strong>A private address on your node was answering the mesh without a password.</strong> One app's port was marked as being for this machine only, and the part of the node that carries mesh traffic did not know that it forwarded requests from the whole mesh straight to it. Found while checking the work above on a real node, not in testing. That path now refuses anything marked machine-only, and the app is reachable only from the node itself, as intended.</p>
<p><strong>Tor addresses no longer skip the login.</strong> An app published as a .onion address was handed straight to the app, because a Tor visitor carries no session. Those addresses now arrive at the login gate first, closing the last route that went around it.</p>
<p>Nodes fix themselves after this update. Apps installed before this system used its current container setup kept their old wide-open address even after being told to move, and each would have needed hand-holding on every node. Your node now notices the difference and rebuilds those apps itself, keeping their data, within about half a minute of starting.</p>
<p>The node had been reading two different sets of instructions about its own apps the signed list it downloads, and older copies on disk which is how a port meant to stay private was briefly opened. Both now come from the signed list.</p>
<p><strong>The key that signs these updates has been replaced.</strong> The previous signing key was exposed where it should not have been, so it is treated as compromised and this release installs its replacement. This update is the last one signed with the old key, by necessity it is the one that teaches your node the new one.</p>
<p>Known gaps, disclosed rather than buried: eleven app ports still have no stated policy BotFights, the Fedimint gateway, NetBird, the voice assistant's own screens and the router screen and remain reachable without a login until each is decided deliberately; the node reports them rather than guessing, because guessing at an unstated setting is what caused two incidents this week. Three voice-assistant ports are still open without authentication; the correct fix puts them on a private network with the assistant. Non-browser apps phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web will meet the login page and need an access token; say the word if you use one and it can be exempted. The 5x real-node lifecycle gate was not run for this release.</p>
</div>
</div>
<!-- v1.7.121-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
+23 -22
View File
@@ -1,33 +1,34 @@
{
"changelog": [
"**Five more screens on your node were readable by anyone who could reach it, and the previous release's own check said they were fine.** The Bitcoin, Lightning, Electrum, FIPS mesh and Fedimint Guardian screens each answered on their port with no login. They were missed because they work differently from ordinary apps: they run directly on the node's network rather than behind its container plumbing, so there was no address to pin and their descriptions listed no port at all — and the node builds its list of what to protect from exactly those descriptions. It therefore neither protected them nor listed them as unprotected. A check that reports success while five screens are open is worse than no check, and this was found by scanning the node from another machine rather than asking the node about itself.",
"**What was actually readable was the page, not your money.** Every request on those ports that could have returned a credential — the Lightning connection details, the wallet passthrough, container logs, and every node command — already required a login and still refused without one. The Lightning macaroon fix from v1.7.120 was verified directly rather than assumed. What leaked was the screen itself: layout and code, no wallet data, no keys.",
"All five now serve only to the node itself, with the login gate in front of them, exactly like the twenty app screens closed in the previous release.",
"**Every port on the node now has a stated policy — there are no undecided ones left.** Eleven ports previously had no instruction either way and stayed open by default. The BotFights arena, the router screen and the Pine voice screen now require the node password. The ones that genuinely cannot take a login page stay open with a written reason: Fedimint's guardian and gateway connections (federation members authenticate to the federation), NetBird's management and dashboard ports (your VPN devices carry their own credentials and cannot hold a browser session, and its dashboard needs its own certificate), Pine's secure listener, and the Lightning REST port, which wallets reach with a macaroon exactly as before.",
"Fresh installs are covered too, not just existing nodes. The five screens are delivered as prebuilt images, so a newly flashed node would have come up open even after this fix. All five were rebuilt, published, and then pulled back and inspected to confirm the fix is really inside them.",
"Two delivery faults fixed alongside, either of which would have silently undone the above: two of the five screens were reaching nodes through no update path at all, so edits to them never arrived; and a fourth copy of the Bitcoin screen's configuration was being rewritten on every health check, which would have re-opened that port after everything else was corrected.",
"Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release."
"**Making another node \"Trusted\" now asks for your node password.** Trust was being handed out by machines rather than by you: any node able to reach yours could join and mark itself Trusted, because the check proved only that the caller owned the key it had just presented — never that you had approved it. Trust also spread on its own, since every peer a Trusted node advertised was added as Trusted too, so one grant quietly propagated across the whole federation. Uninvited joins are now capped at Observer, advertised peers arrive as Observers, and raising anyone to Trusted — whether by generating an invite or by changing the dropdown on a node — requires your password. Lowering trust deliberately does not, because the safe action must never be the inconvenient one. Existing peers are left exactly as they are rather than silently demoted, and each one now records how its trust was granted so you can review them.",
"**Nodes you have peered with can be messaged straight away.** Peering was not enough: you also had to be within LoRa radio range of the other node once before chat would work. The node picked how to send a message based on which radio was plugged in, and only one of those paths knew how to reach a peer over the mesh's internet transports — so on a node with a different radio, or no radio at all, messaging a peer you had just federated with simply failed until a radio contact happened to appear. Peered nodes are reachable without radio by definition, so that choice no longer depends on the hardware. Radio is still preferred when the other node is actually in range and the message fits.",
"The dashboard no longer flickers a vertical line across its cards. A rendering seam appeared at random while moving the mouse, because the two large cards used a background-blur effect that this system already disables everywhere else on the dashboard — that browser mis-draws it inside the dashboard's animated container, and these two cards had been missed when the workaround was written. Diagnosed from a single screenshot rather than by trying to reproduce it.",
"The Lightning screen will actually update from now on. Its image was set to \"latest\", and the container system will not re-fetch a label it already holds, so nodes kept the same Lightning screen forever no matter how many updates shipped. A separate copy of the same setting used only by brand-new installs also described the screen incorrectly, so fresh installs got a screen that never answered.",
"Apps that provide their own screens stop rebuilding themselves in a loop. On this system's own node one of them rebuilt every thirty-five seconds indefinitely, burning processor time and restarting the app each round. The node decided a rebuild was needed by comparing file dates against the image's creation date, but a rebuild that changes nothing reuses the existing image and leaves that date untouched — so the condition that triggered the rebuild was still true afterwards, forever. Nodes taking this update repair themselves the first time they check.",
"Groundwork you can see but that does not change access yet: the node can now tell you which of its app ports answer without a login, and every port that is deliberately open — Bitcoin's peer connections for syncing the chain, Lightning's wallet connections, the Electrum wallet protocol — now has to state in writing why it is safe, so the list of exceptions is something you can read rather than something you have to discover. The login gate that will sit in front of the rest is built and proven working end to end on a real node, but it is not yet closing any ports; that arrives with the signed app catalog that tells each app to hand its address over.",
"Releases can no longer ship an unsigned update file. Signing was skippable, and when it was skipped the release was still committed and tagged — producing an update that every node correctly refuses to install. It had been caught by hand every cycle; now the release simply stops.",
"Known gaps, disclosed rather than buried: the 5x real-node lifecycle gate was not run for this release. App ports other than the deliberate exceptions above are still reachable without a login — the gate reports them, and closing them needs the next signed catalog. Three voice-assistant ports are open without authentication and should not be; the correct fix puts them on a private network with the assistant instead, which needs testing on a node that runs both. Two nodes on the fleet still share SSH host keys (detection shipped, rotation remains a deliberate operator decision)."
],
"components": [
{
"current_version": "1.7.123-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.123-alpha/archipelago",
"current_version": "1.7.121-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.121-alpha/archipelago",
"name": "archipelago",
"new_version": "1.7.123-alpha",
"sha256": "b7974a671f8f7987fab548d0a9cac1ad145e9a52f4099791c3d1879d065a9f47",
"size_bytes": 54957360
"new_version": "1.7.121-alpha",
"sha256": "be5ef9fb284f539b06329d4108be53e55ae8cdb06cf1cf4beb90363de364706d",
"size_bytes": 54870968
},
{
"current_version": "1.7.123-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.123-alpha/archipelago-frontend-1.7.123-alpha.tar.gz",
"name": "archipelago-frontend-1.7.123-alpha.tar.gz",
"new_version": "1.7.123-alpha",
"sha256": "075370c761f07373e553db07eb5e019d96ceb591fa9c1067e1adc6701fa30752",
"size_bytes": 210533854
"current_version": "1.7.121-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.121-alpha/archipelago-frontend-1.7.121-alpha.tar.gz",
"name": "archipelago-frontend-1.7.121-alpha.tar.gz",
"new_version": "1.7.121-alpha",
"sha256": "7898a9c11fa30cadc8f0fcf814bba1e3870d20663472f4c40e8e663b2359958f",
"size_bytes": 210526689
}
],
"release_date": "2026-08-05",
"signature": "f64eaf9731277865c0d3f757f63dc2f4921fb7d90deb1615ca2b2b0f4c173282524b39a2d0b8f027c7532e918c349a86b14ed1b7992a96b0c6a80cd46b5d500d",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.7.123-alpha"
"release_date": "2026-08-04",
"signature": "9d871c946e941b3c13f75fb799d8428841147267f4565920993ddd3aa0cd52d6d1a74481303cbbb26e68e6e5d44e2b711c9b7a22ad7dd73c94b4d3e39a0e2803",
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
"version": "1.7.121-alpha"
}
+7 -68
View File
@@ -711,15 +711,7 @@
},
"id": "bitcoin-ui",
"name": "Bitcoin UI",
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 8334,
"host": 8334,
"protocol": "tcp"
}
],
"ports": [],
"resources": {
"memory_limit": "128Mi"
},
@@ -813,8 +805,6 @@
"name": "BotFights",
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 9100,
"host": 9100,
"protocol": "tcp"
@@ -1169,15 +1159,7 @@
},
"id": "electrs-ui",
"name": "Electrs UI",
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 50002,
"host": 50002,
"protocol": "tcp"
}
],
"ports": [],
"resources": {
"memory_limit": "64Mi"
},
@@ -1373,26 +1355,15 @@
"name": "Fedimint Guardian",
"ports": [
{
"auth": "none",
"auth_rationale": "Fedimint guardian consensus. Other guardians speak the federation's own authenticated protocol here; a login page would break consensus.",
"container": 8173,
"host": 8173,
"protocol": "tcp"
},
{
"auth": "none",
"auth_rationale": "Fedimint guardian API for federation clients, which authenticate to the federation itself and cannot hold a browser session.",
"container": 8174,
"host": 8174,
"protocol": "tcp"
},
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 8175,
"host": 8175,
"protocol": "tcp"
},
{
"auth": "local",
"bind": "127.0.0.1",
@@ -1577,15 +1548,11 @@
"name": "Fedimint Gateway",
"ports": [
{
"auth": "none",
"auth_rationale": "Fedimint gateway API, protected by its own bcrypt password (--bcrypt-password-hash) and reached by federation peers and clients that cannot hold a browser session.",
"container": 8176,
"host": 8176,
"protocol": "tcp"
},
{
"auth": "none",
"auth_rationale": "LDK Lightning p2p for the gateway. The BOLT-8 noise handshake authenticates and encrypts the connection itself.",
"container": 9737,
"host": 9737,
"protocol": "tcp"
@@ -1733,15 +1700,7 @@
},
"id": "fips-ui",
"name": "FIPS Mesh",
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 8336,
"host": 8336,
"protocol": "tcp"
}
],
"ports": [],
"resources": {
"memory_limit": "128Mi"
},
@@ -2950,8 +2909,6 @@
"protocol": "tcp"
},
{
"auth": "none",
"auth_rationale": "LND REST, authenticated by macaroon over TLS. A browser login page would break Zeus and every non-browser wallet client, exactly as for lnd's 18080.",
"container": 8080,
"host": 8091,
"protocol": "tcp"
@@ -3121,15 +3078,7 @@
},
"id": "lnd-ui",
"name": "LND UI",
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 18083,
"host": 18083,
"protocol": "tcp"
}
],
"ports": [],
"resources": {
"memory_limit": "64Mi"
},
@@ -3464,8 +3413,6 @@
"name": "NetBird",
"ports": [
{
"auth": "none",
"auth_rationale": "NetBird dashboard over TLS, with its own login. The gate speaks plain HTTP, so fronting this port would break the secure context the dashboard requires (issue #15) and the certificate clients pin.",
"container": 443,
"host": 8087,
"protocol": "tcp"
@@ -3670,8 +3617,6 @@
"name": "NetBird Server",
"ports": [
{
"auth": "none",
"auth_rationale": "NetBird management API and its OIDC issuer. Enrolled devices authenticate themselves with setup keys and JWTs, and they cannot hold a browser session — a login page here would disconnect every VPN client on the network.",
"container": 80,
"host": 8086,
"protocol": "tcp"
@@ -4067,15 +4012,11 @@
"name": "Pine",
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 80,
"host": 10380,
"protocol": "tcp"
},
{
"auth": "none",
"auth_rationale": "Pine's TLS listener. The gate speaks plain HTTP, so fronting this port would break the secure context navigator.bluetooth needs for WiFi provisioning. The plain-HTTP entry point (10380) is gated, and it is what the UI opens.",
"container": 443,
"host": 10381,
"protocol": "tcp"
@@ -4517,8 +4458,6 @@
},
"ports": [
{
"auth": "gated",
"bind": "127.0.0.1",
"container": 8080,
"host": 8084,
"protocol": "tcp"
@@ -4898,7 +4837,7 @@
}
},
"schema": 1,
"signature": "cc19214673b37c2d7d41edf996d5002abe41cf933ee9f33fb7182b05c6c40aa1a555ad596228c4a83dc4a11166f3345e7478399f9693f5f2da3818020980eb03",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"updated": "2026-08-05"
"signature": "cc83d0be50ce6144e2b5693a7175d7743d4a19141f4ef9a46a3c88d2dadd848acda9c25063e7a8b5643cecb2ccde279762a00a9715a5d26c99a95b492bc82a05",
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
"updated": "2026-08-04"
}
+23 -22
View File
@@ -1,33 +1,34 @@
{
"changelog": [
"**Five more screens on your node were readable by anyone who could reach it, and the previous release's own check said they were fine.** The Bitcoin, Lightning, Electrum, FIPS mesh and Fedimint Guardian screens each answered on their port with no login. They were missed because they work differently from ordinary apps: they run directly on the node's network rather than behind its container plumbing, so there was no address to pin and their descriptions listed no port at all — and the node builds its list of what to protect from exactly those descriptions. It therefore neither protected them nor listed them as unprotected. A check that reports success while five screens are open is worse than no check, and this was found by scanning the node from another machine rather than asking the node about itself.",
"**What was actually readable was the page, not your money.** Every request on those ports that could have returned a credential — the Lightning connection details, the wallet passthrough, container logs, and every node command — already required a login and still refused without one. The Lightning macaroon fix from v1.7.120 was verified directly rather than assumed. What leaked was the screen itself: layout and code, no wallet data, no keys.",
"All five now serve only to the node itself, with the login gate in front of them, exactly like the twenty app screens closed in the previous release.",
"**Every port on the node now has a stated policy — there are no undecided ones left.** Eleven ports previously had no instruction either way and stayed open by default. The BotFights arena, the router screen and the Pine voice screen now require the node password. The ones that genuinely cannot take a login page stay open with a written reason: Fedimint's guardian and gateway connections (federation members authenticate to the federation), NetBird's management and dashboard ports (your VPN devices carry their own credentials and cannot hold a browser session, and its dashboard needs its own certificate), Pine's secure listener, and the Lightning REST port, which wallets reach with a macaroon exactly as before.",
"Fresh installs are covered too, not just existing nodes. The five screens are delivered as prebuilt images, so a newly flashed node would have come up open even after this fix. All five were rebuilt, published, and then pulled back and inspected to confirm the fix is really inside them.",
"Two delivery faults fixed alongside, either of which would have silently undone the above: two of the five screens were reaching nodes through no update path at all, so edits to them never arrived; and a fourth copy of the Bitcoin screen's configuration was being rewritten on every health check, which would have re-opened that port after everything else was corrected.",
"Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release."
"**Making another node \"Trusted\" now asks for your node password.** Trust was being handed out by machines rather than by you: any node able to reach yours could join and mark itself Trusted, because the check proved only that the caller owned the key it had just presented — never that you had approved it. Trust also spread on its own, since every peer a Trusted node advertised was added as Trusted too, so one grant quietly propagated across the whole federation. Uninvited joins are now capped at Observer, advertised peers arrive as Observers, and raising anyone to Trusted — whether by generating an invite or by changing the dropdown on a node — requires your password. Lowering trust deliberately does not, because the safe action must never be the inconvenient one. Existing peers are left exactly as they are rather than silently demoted, and each one now records how its trust was granted so you can review them.",
"**Nodes you have peered with can be messaged straight away.** Peering was not enough: you also had to be within LoRa radio range of the other node once before chat would work. The node picked how to send a message based on which radio was plugged in, and only one of those paths knew how to reach a peer over the mesh's internet transports — so on a node with a different radio, or no radio at all, messaging a peer you had just federated with simply failed until a radio contact happened to appear. Peered nodes are reachable without radio by definition, so that choice no longer depends on the hardware. Radio is still preferred when the other node is actually in range and the message fits.",
"The dashboard no longer flickers a vertical line across its cards. A rendering seam appeared at random while moving the mouse, because the two large cards used a background-blur effect that this system already disables everywhere else on the dashboard — that browser mis-draws it inside the dashboard's animated container, and these two cards had been missed when the workaround was written. Diagnosed from a single screenshot rather than by trying to reproduce it.",
"The Lightning screen will actually update from now on. Its image was set to \"latest\", and the container system will not re-fetch a label it already holds, so nodes kept the same Lightning screen forever no matter how many updates shipped. A separate copy of the same setting used only by brand-new installs also described the screen incorrectly, so fresh installs got a screen that never answered.",
"Apps that provide their own screens stop rebuilding themselves in a loop. On this system's own node one of them rebuilt every thirty-five seconds indefinitely, burning processor time and restarting the app each round. The node decided a rebuild was needed by comparing file dates against the image's creation date, but a rebuild that changes nothing reuses the existing image and leaves that date untouched — so the condition that triggered the rebuild was still true afterwards, forever. Nodes taking this update repair themselves the first time they check.",
"Groundwork you can see but that does not change access yet: the node can now tell you which of its app ports answer without a login, and every port that is deliberately open — Bitcoin's peer connections for syncing the chain, Lightning's wallet connections, the Electrum wallet protocol — now has to state in writing why it is safe, so the list of exceptions is something you can read rather than something you have to discover. The login gate that will sit in front of the rest is built and proven working end to end on a real node, but it is not yet closing any ports; that arrives with the signed app catalog that tells each app to hand its address over.",
"Releases can no longer ship an unsigned update file. Signing was skippable, and when it was skipped the release was still committed and tagged — producing an update that every node correctly refuses to install. It had been caught by hand every cycle; now the release simply stops.",
"Known gaps, disclosed rather than buried: the 5x real-node lifecycle gate was not run for this release. App ports other than the deliberate exceptions above are still reachable without a login — the gate reports them, and closing them needs the next signed catalog. Three voice-assistant ports are open without authentication and should not be; the correct fix puts them on a private network with the assistant instead, which needs testing on a node that runs both. Two nodes on the fleet still share SSH host keys (detection shipped, rotation remains a deliberate operator decision)."
],
"components": [
{
"current_version": "1.7.123-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.123-alpha/archipelago",
"current_version": "1.7.121-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.121-alpha/archipelago",
"name": "archipelago",
"new_version": "1.7.123-alpha",
"sha256": "b7974a671f8f7987fab548d0a9cac1ad145e9a52f4099791c3d1879d065a9f47",
"size_bytes": 54957360
"new_version": "1.7.121-alpha",
"sha256": "be5ef9fb284f539b06329d4108be53e55ae8cdb06cf1cf4beb90363de364706d",
"size_bytes": 54870968
},
{
"current_version": "1.7.123-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.123-alpha/archipelago-frontend-1.7.123-alpha.tar.gz",
"name": "archipelago-frontend-1.7.123-alpha.tar.gz",
"new_version": "1.7.123-alpha",
"sha256": "075370c761f07373e553db07eb5e019d96ceb591fa9c1067e1adc6701fa30752",
"size_bytes": 210533854
"current_version": "1.7.121-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.121-alpha/archipelago-frontend-1.7.121-alpha.tar.gz",
"name": "archipelago-frontend-1.7.121-alpha.tar.gz",
"new_version": "1.7.121-alpha",
"sha256": "7898a9c11fa30cadc8f0fcf814bba1e3870d20663472f4c40e8e663b2359958f",
"size_bytes": 210526689
}
],
"release_date": "2026-08-05",
"signature": "f64eaf9731277865c0d3f757f63dc2f4921fb7d90deb1615ca2b2b0f4c173282524b39a2d0b8f027c7532e918c349a86b14ed1b7992a96b0c6a80cd46b5d500d",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.7.123-alpha"
"release_date": "2026-08-04",
"signature": "9d871c946e941b3c13f75fb799d8428841147267f4565920993ddd3aa0cd52d6d1a74481303cbbb26e68e6e5d44e2b711c9b7a22ad7dd73c94b4d3e39a0e2803",
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
"version": "1.7.121-alpha"
}
+1 -5
View File
@@ -240,11 +240,7 @@ install -m 0644 "$FRONTEND_ARCHIVE" "$VERSION_DIR/archipelago-frontend-${VERSION
# warning and falls through — and the commit then happened anyway. A release
# commit carrying a manifest no node will accept has no valid use, so refuse
# to create one rather than leave a tag that has to be re-cut.
# Release root ROTATED 2026-08-05. v1.7.122-alpha was the last release signed
# with the old root (z6Mkkid…q7ur) — it is the release that installed this
# pin on every node. From v1.7.123 onward the new root signs, and nodes
# running .122+ reject anything signed with the old key.
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
EXPECTED_DID="did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur"
if ! grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
|| ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json"; then
echo "" >&2
+4 -4
View File
@@ -84,7 +84,7 @@ STRFRY_IMAGE="$ARCHY_REGISTRY/strfry:1.0.4"
NOSTR_VPN_IMAGE="$ARCHY_REGISTRY/nostr-vpn:v0.3.7"
NOSTR_VPN_UI_IMAGE="$ARCHY_REGISTRY/nostr-vpn-ui:latest"
FIPS_IMAGE="$ARCHY_REGISTRY/fips:v0.1.0"
FIPS_UI_IMAGE="$ARCHY_REGISTRY/fips-ui:1.7.123-alpha"
FIPS_UI_IMAGE="$ARCHY_REGISTRY/fips-ui:latest"
# AI / Routing
ROUTSTR_IMAGE="$ARCHY_REGISTRY/routstr:v0.4.3"
@@ -117,9 +117,9 @@ PENPOT_EXPORTER_IMAGE="$ARCHY_REGISTRY/penpot-exporter:2.4"
PENPOT_FRONTEND_IMAGE="$ARCHY_REGISTRY/penpot-frontend:2.4"
# Custom UI containers (built from docker/ dirs, pushed to registry)
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:1.7.123-alpha"
LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:1.7.123-alpha"
ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:1.7.123-alpha"
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:1.7.119-alpha"
LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:1.7.119-alpha"
ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:latest"
# Base images
NGINX_ALPINE_IMAGE="$ARCHY_REGISTRY/nginx:1.27.4-alpine"
+1 -3
View File
@@ -29,9 +29,7 @@ fail() { echo "Error: $*" >&2; exit 1; }
# with the pinned release-root anchor refuse to auto-apply unsigned manifests,
# and enforcement will tighten to hard-reject — an unsigned publish would
# strand them. Grep proves presence; ceremony verify proves the crypto.
# Release root ROTATED 2026-08-05; see create-release.sh. New root from
# v1.7.123 onward.
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
EXPECTED_DID="did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur"
grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
&& grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json" \
|| fail "releases/manifest.json is not signed by the release root — run: bash scripts/sign-manifest.sh"
+1 -6
View File
@@ -809,12 +809,7 @@ ensure_bitcoin_ui_nginx_conf() {
tmp="${CONF_PATH}.tmp.$$"
sudo tee "$tmp" >/dev/null << EOF
server {
# Loopback ONLY — this is the fourth copy of this declaration (the others
# are the Rust template in container/bitcoin_ui_nginx.conf.template, the
# image, and the manifest). Host networking means this nginx binds the
# HOST's address, so \`listen 8334;\` served the Bitcoin screen on every
# interface with no login. The app gate owns the external addresses now.
listen 127.0.0.1:8334;
listen 8334;
server_name _;
root /usr/share/nginx/html;
index index.html;
+3 -23
View File
@@ -321,17 +321,7 @@ fi
UI_DOCKER_DEST="/opt/archipelago/docker"
sudo mkdir -p "$UI_DOCKER_DEST"
UI_REBUILD_LIST=""
# fips-ui and fedimint-ui are synced but NOT added to UI_REBUILD_LIST below:
# container-specs.sh has no spec for either (and their container names break
# the archy-<ui> assumption — the FIPS one is plain `fips-ui`). Their rebuilds
# come from elsewhere — the daemon's companion installer for fedimint-ui, the
# orchestrator's build context for fips-ui — but BOTH read
# /opt/archipelago/docker/<ui>, and nothing was ever updating that directory.
# So source edits to those two trees reached nodes through no path at all:
# their nginx kept listening on 0.0.0.0 and served the Guardian and FIPS
# screens unauthenticated on every interface (found by scanning archi-dev-box
# from outside, 2026-08-05 — the in-node audit could not see them).
for ui in bitcoin-ui lnd-ui electrs-ui fips-ui fedimint-ui; do
for ui in bitcoin-ui lnd-ui electrs-ui; do
src="$REPO_DIR/docker/$ui"
dst="$UI_DOCKER_DEST/$ui"
[ -d "$src" ] || continue
@@ -345,19 +335,9 @@ for ui in bitcoin-ui lnd-ui electrs-ui fips-ui fedimint-ui; do
dst_hash=$( (cd "$dst" && find . -type f | LC_ALL=C sort | xargs sha256sum 2>/dev/null) | sha256sum | cut -d' ' -f1)
fi
if [ "$src_hash" != "$dst_hash" ]; then
log "UI source changed for $ui; syncing"
log "UI source changed for $ui; syncing and marking for rebuild"
sudo rsync -a --delete "$src/" "$dst/"
case "$ui" in
# Rebuilt below from container-specs.sh.
bitcoin-ui|lnd-ui|electrs-ui)
UI_REBUILD_LIST="$UI_REBUILD_LIST $ui" ;;
# Synced only — rebuilt by the daemon (companion installer /
# orchestrator build context), which watches this directory.
# Adding them to the rebuild list would fail: no spec exists and
# the container names are not archy-<ui>.
*)
log " $ui synced; rebuild is owned by the daemon" ;;
esac
UI_REBUILD_LIST="$UI_REBUILD_LIST $ui"
else
ok "UI source unchanged for $ui"
fi
+1 -1
View File
@@ -11,7 +11,7 @@ set -euo pipefail
REPO="/home/archipelago/Projects/archy"
CATALOG="$REPO/releases/app-catalog.json"
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
EXPECTED_DID="did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur"
# Use ONLY the prebuilt signer. If it isn't ready, stop cleanly — never compile
# here (compiling caused the earlier hangs). Claude builds it in the background.
-4
View File
@@ -11,10 +11,6 @@
# Normally create-release.sh signs the manifest inline; this script exists for
# re-signing (e.g. a manifest edited after creation) or signing on a box where
# the release run was non-interactive.
#
# The release root was rotated 2026-08-05. From v1.7.123 this signs with the
# NEW mnemonic and the signer's own anchor already pins that key, so no
# ARCHY_RELEASE_ROOT_PUBKEY override is needed (it was, for .122 only).
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"