Skip to content

Refuse a cutover while something answers on a public interface - #43

Merged
bitcoinuniverseadmin merged 4 commits into
developfrom
ops/private-listener-gate
Aug 31, 2026
Merged

Refuse a cutover while something answers on a public interface#43
bitcoinuniverseadmin merged 4 commits into
developfrom
ops/private-listener-gate

Conversation

@bitcoinuniverseadmin

Copy link
Copy Markdown

What went unnoticed

index-doge-tap shipped with HOST=0.0.0.0. It answered /ready to the
public internet for as long as it ran, on a host whose firewall is inactive:

$ curl -sk -o /dev/null -w '%{http_code}\n' https://159.195.109.76:3013/ready
200

Its only consumer is the overlay on the same machine, reaching it through an
/etc/hosts entry that already pointed at loopback. Nothing needed the public
binding and nothing caught it, because every functional check reached the
service over loopback where it behaved perfectly, and no gate ever asked which
interface it was answering on.

That service is now bound to 127.0.0.1 and the public address refuses. This
is the gate so the next one is caught before it ships rather than during an
audit.

The gate

release.sh preflight reads the listening TCP sockets, subtracts the ports
this deployment declares public, and refuses the cutover if anything is left,
printing the offending sockets.

port why it is public
22 SSH
8333 Bitcoin Core peer to peer
50001 the peer restricted Fulcrum endpoint the other host reads

Docker's bridge address counts as private: it is reachable only from
containers on this host, and the services behind it are the ones loopback
already serves.

Verified on the host, both directions

== the gate against the host as it stands ==
  ok   no unexpected public listener

== what it sees ==
  0.0.0.0:22
  0.0.0.0:8333
  159.195.109.76:50001
  [::]:22
  [::]:8333

== proving it catches an exposure ==
  FAIL undeclared public listener: 39997

RESULT: the gate passes on the current host and refuses a public listener

The decoy binds 0.0.0.0, which is the exact shape the index-doge-tap
misconfiguration had. A gate that has only been seen to pass has not been
seen to work.

Why a preflight gate rather than a firewall rule

A firewall is worth having and is a separate change with its own lockout risk
on a host reached only over SSH. This runs where the release runs, needs no
privileged network change, and fails the cutover rather than the service, so
the first person to learn about an exposure is whoever is deploying rather
than whoever is scanning.

Adding a port to PUBLIC_LISTENERS is a decision with a reason beside it.
Discovering one in production is not.

Documented in docs/operations/DEPLOYMENT.md.

🤖 Generated with Claude Code

bitcoinuniverseadmin and others added 3 commits August 30, 2026 17:22
The explorer printed "not public" in the fee column for shielded Zcash
transactions, in the lens tooltip, in the pending table, in the candidate
bucket disclosure and in the chain documentation. That was wrong, and it
taught readers that this chain hides more than it does.

Shielding hides the sender, the recipient, the amount transferred and the
memo. It does not hide the fee, which is the value left in the transparent
transaction value pool once the transparent inputs and outputs, the Sapling,
Orchard and Ironwood value balances and the Sprout JoinSplit public values
are counted. All of those are on chain in the clear.

The copy now says that. A missing fee reads as "not reported" with the read
that failed named beside it, never as a privacy claim. The transaction page
gained the provenance behind the figure: which readings answered, both
readings printed side by side when they disagreed and no fee published, and
the ZIP-317 rule in force with the upgrade that activated it, its activation
height, the marginal fee, the grace actions and the conventional fee. An
unsupported rule is shown as unsupported and never suppresses a fee that
could be read.

The Zcash documentation gained a section on why the fee is public and how it
is established, and the privacy section now names the fee among the public
facts, states the boundary in both directions, and says plainly that this
explorer never asks for a viewing key.

Six new readings tests, updated visual fixtures carrying the real shape, 413
universe tests passing, every documentation and parity gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rendering the page showed three run-on readings: the conventional fee ran
straight into the per-action explanation, and the activation height ran
straight into the branch id, producing "active from block 3,428,143branch
37a5165b". Each secondary reading is a statement about the value above it,
not a suffix to it, so each gets its own line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
index-doge-tap shipped with HOST=0.0.0.0 and answered /ready to the internet
for as long as it ran, on a host whose firewall is inactive. Nothing caught
it. Every functional check reached the service over loopback, where it behaved
perfectly, and no gate ever asked which interface it was answering on.

release.sh preflight now asks. It reads the listening TCP sockets, subtracts
the three this deployment declares public (SSH, Bitcoin peer to peer, and the
peer restricted Fulcrum endpoint the other host reads), and refuses the
cutover if anything is left, printing the offending sockets.

Docker's bridge address counts as private: it is reachable only from
containers on this host, and the services behind it are the ones loopback
already serves.

Verified on the indexer host: the gate passes as the host now stands, and
refuses within seconds of a throwaway listener binding 0.0.0.0, which is the
exact shape the index-doge-tap misconfiguration had.

Adding a port to PUBLIC_LISTENERS is a decision with a reason beside it.
Discovering one in production is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Cancelled this PR's in-flight run to free two ultra runners, because it could not have passed.

Its Frontend build started at 19:54Z and was 73 minutes into a 100 minute cap. Even if the passes had finished, the job ends on Keep the contrast report and the route screenshots, and that step fails right now: Failed to CreateArtifact: Artifact storage quota has been hit. My run 33330398257 passed every gate in that job and then failed exactly there.

#40 fixes both halves: the deep pass is split into three shards so the job finishes in about 23 minutes instead of 95, and the screenshot uploads only run on failure so a green run writes nothing. Re-run this against develop once that lands and it should go green without any change on your side.

@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

#40 is merged, which should clear every check that was failing on this PR for reasons that had nothing to do with it:

  • Frontend build finished in 18m23s on that run instead of reaching the 100 minute cap. The deep visual pass is now three sharded jobs beside it.
  • Mobile and adaptive layout and WebKit and Firefox were failing on Failed to CreateArtifact: Artifact storage quota has been hit, at the last step, after every measurement passed. Screenshot uploads now only run on failure.
  • WebKit and Firefox also lost its GitHub-hosted runner when the organization moved to the Free plan today; develop now targets RunsOn for it.

Merge develop into this branch and the run should go green without any change on your side. Analyze (actions), Analyze (javascript-typescript) and Analyze (rust) will still fail, because CodeQL runs on GitHub-hosted runners that the current plan does not provide. Those are not required checks, so they do not block the merge.

Happy to do the develop merge for you if you have moved on.

@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Second correction on top of my earlier note, and this one was my fault.

#40 split the deep visual pass into three shards that each built their own copy, which took a run from two concurrent Angular production builds to five, on a fleet that runs more than one runner per host. Everything that started a build in the same ninety second window was killed with exit code 137, out of memory, including on pull requests that touched nothing but scripts. That is what put Frontend build, Mobile and adaptive layout and the shards red on this PR after I told you #40 would clear them.

#48 is merged and fixes it: the frontend job keeps its build under a commit-keyed cache and the shards take it, so a run is back to two concurrent builds. Verified on its own run, 33340340913: all eight jobs green, every shard's fallback build step skipped because the cache hit, whole run 48 minutes.

Merge develop into this branch now and it should go green. Still expected to fail and still not required: Analyze (actions), Analyze (javascript-typescript), Analyze (rust), which need GitHub-hosted runners the current plan does not provide.

Sorry for the churn. Say the word and I will do the develop merge for you.

@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

The CI repairs are all merged now, so this should go green on a develop merge with no change to your work.

What landed since your last run:

#42 went through all eight jobs green after its author merged develop, which is the shape to expect here.

Still failing and still not required: Analyze (actions), Analyze (javascript-typescript), Analyze (rust). CodeQL needs GitHub-hosted runners that the organization's current plan does not provide, so those three will stay red on every PR until the plan is restored. They do not block a merge.

I have left this branch alone because it is yours. Merge develop into it when you are ready, or say the word and I will do it.

@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Merged develop into this branch, since I offered twice and it had not moved in six and a half hours. A merge commit, no rebase, no force push.

Clean merge, no conflicts.

Verified on the merged tree before pushing: the roster gate passes with 38 protocols and 7 readable, its 25 cases pass, and the text gate passes.

CI is running. The three Analyze jobs will stay red, which is CodeQL needing GitHub-hosted runners the plan does not provide, and which does not block a merge.

If it comes back green I intend to merge, because the frontend deployment is blocked on the remaining PRs and the hourly production smoke has been red since 00:39Z purely because develop is ahead of what is deployed. Say so if you would rather land it yourself.

@bitcoinuniverseadmin
bitcoinuniverseadmin merged commit 00dd618 into develop Aug 31, 2026
15 of 19 checks passed
@bitcoinuniverseadmin
bitcoinuniverseadmin deleted the ops/private-listener-gate branch August 31, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant