Cut the remaining was-then-now framing from scripts and docs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
564f698a69
commit
a53deddcf6
@@ -1 +1 @@
|
||||
196b42451895fa67cde415e95e3d0af9a714207c
|
||||
57732df84e8d94366374d7576a6d156cf703e432
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from docs/CONFIGURATION.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 45aadd4e888382a9 -->
|
||||
source-sha256: 20fa36ab23879f61 -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="configuring-a-relay">Configuring a relay</h1>
|
||||
@@ -547,6 +547,162 @@ looking wrong. With this set, the publishable key stays the
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="iphone-push-apns">iPhone push (APNs)</h2>
|
||||
<p>Only the iOS app needs this; Android is woken directly. All five must
|
||||
be set together, and the relay logs that push is disabled if any is
|
||||
missing.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Default</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>FN_APNS_KEY_P8</code></td>
|
||||
<td>unset</td>
|
||||
<td>Path to the <code>.p8</code> signing key from Apple. Mount it as a
|
||||
secret; it is not a file to bake into an image.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_APNS_KEY_ID</code></td>
|
||||
<td>unset</td>
|
||||
<td>The key's ten-character id.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_APNS_TEAM_ID</code></td>
|
||||
<td>unset</td>
|
||||
<td>Your Apple developer team id.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_APNS_TOPIC</code></td>
|
||||
<td><code>net.flamenet.messenger</code></td>
|
||||
<td>The app's bundle id. Change it if you ship your own build.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_APNS_ENV</code></td>
|
||||
<td><code>sandbox</code></td>
|
||||
<td><code>sandbox</code> or <code>production</code>. A token from a
|
||||
TestFlight or App Store build is meaningless to the sandbox host and the
|
||||
other way round, and APNs returns the same 400 either way. Getting this
|
||||
wrong means no iPhone is ever woken, so set it to
|
||||
<code>production</code> the moment you ship a build that is not from
|
||||
Xcode.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="behind-a-proxy">Behind a proxy</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Default</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>FN_TRUSTED_PROXIES</code></td>
|
||||
<td><code>0</code></td>
|
||||
<td>How many proxy hops to trust when reading
|
||||
<code>X-Forwarded-For</code>. Left at 0 the relay uses the socket
|
||||
address, which is your proxy, so every request looks like it comes from
|
||||
one IP and per-IP limits apply to everyone at once. Set it to the number
|
||||
of proxies in front of the relay, not higher: a client can forge the
|
||||
header for any hop you trust beyond the ones you actually run.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="pluggable-transport-obfs4">Pluggable transport (obfs4)</h2>
|
||||
<p>Reachability, not privacy: a second address that does not look like a
|
||||
relay, for networks that block the first. It is what
|
||||
<code>docker-compose.prod.yml</code> runs under the <code>obfs4</code>
|
||||
profile, and the variables are read by that compose file rather than by
|
||||
the relay.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Default</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>FN_OBFS4_PORT</code></td>
|
||||
<td><code>8443</code></td>
|
||||
<td>Published on every interface, unlike the relay. A bridge only
|
||||
loopback can reach helps nobody; what makes that safe is that the
|
||||
protocol answers nobody who cannot complete a handshake.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_OBFS4_ADVERTISE</code></td>
|
||||
<td>unset</td>
|
||||
<td>The public address to put in the bridge line handed to clients. A
|
||||
container cannot know its own, so without this the line says
|
||||
<code><this-host></code> and is not dialable.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_OBFS4_IMAGE</code></td>
|
||||
<td><code>flamenet-obfs4bridge:prod</code></td>
|
||||
<td>Set it to an image you can pull.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_OBFS4_CONTEXT</code></td>
|
||||
<td><code>../flamenet-elligator2</code></td>
|
||||
<td>Build context, a sibling checkout. That repository is private, so
|
||||
its registry image cannot be pulled anonymously.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_OBFS4_UPSTREAM</code></td>
|
||||
<td><code>relay:8080</code></td>
|
||||
<td>Where the bridge forwards to.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The bridge's state volume holds its identity key, which clients pin
|
||||
through the <code>cert</code> in the bridge line. Losing it invalidates
|
||||
every bridge line you have handed out.</p>
|
||||
<h2 id="site-copy">Site copy</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Default</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>FN_IOS_LIVE</code></td>
|
||||
<td><code>false</code></td>
|
||||
<td>Flips clauses on the public pages that describe iPhone availability.
|
||||
Turning it on states general availability, so it belongs on after an App
|
||||
Store release and not during a beta.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_DELETE_ON_ACK</code></td>
|
||||
<td>see Retention</td>
|
||||
<td>Delete a delivered envelope the moment the recipient's app confirms
|
||||
it, rather than after the delivered-retention window. The metadata page
|
||||
reads this and describes whichever is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_DISPOSABLE_EMAIL_DOMAINS</code></td>
|
||||
<td>unset</td>
|
||||
<td>Comma-separated domains refused at sign-up. Empty means none
|
||||
are.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>FN_FEDERATION_SCREEN</code></td>
|
||||
<td>unset</td>
|
||||
<td>Advisory IP screening for federated peers. It does not gate
|
||||
federation; the allowlist and the pinned key do.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="watching-the-key-log-relay32">Watching the key log
|
||||
(relay#("#")32)</h2>
|
||||
<table>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/SELF-HOSTING.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 4173d817c93eb9bf -->
|
||||
source-sha256: de7acaf9474886e5 -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="self-hosting">Self-hosting</h1>
|
||||
@@ -162,26 +162,91 @@ defaults:</p>
|
||||
<td>Used one-time prekeys</td>
|
||||
<td>7 days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unencrypted messages, once read</td>
|
||||
<td>30 days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unencrypted messages, regardless</td>
|
||||
<td>90 days</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Override any of them with <code>FN_DELIVERED_RETENTION_DAYS</code>,
|
||||
<code>FN_UNDELIVERED_RETENTION_DAYS</code>,
|
||||
<code>FN_ATTACHMENT_RETENTION_DAYS</code>,
|
||||
<code>FN_READ_MESSAGE_RETENTION_DAYS</code>,
|
||||
<code>FN_MESSAGE_RETENTION_DAYS</code>.</p>
|
||||
<code>FN_UNDELIVERED_RETENTION_DAYS</code> and
|
||||
<code>FN_ATTACHMENT_RETENTION_DAYS</code>.</p>
|
||||
<p>There is no unencrypted message store any more, so
|
||||
<code>FN_MESSAGE_RETENTION_DAYS</code> and
|
||||
<code>FN_READ_MESSAGE_RETENTION_DAYS</code> are not read. Setting them
|
||||
does nothing; see <code>docs/PLAINTEXT.md</code> in the relay
|
||||
repository.</p>
|
||||
<h2 id="the-compose-files-and-what-each-one-runs">The compose files, and
|
||||
what each one runs</h2>
|
||||
<p>Five files, layered. <code>docker-compose.yml</code> is the base and
|
||||
the others are overlays, so
|
||||
<code>-f docker-compose.yml -f <overlay></code> is the shape.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Services</th>
|
||||
<th>For</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>docker-compose.yml</code></td>
|
||||
<td><code>db</code>, <code>relay</code></td>
|
||||
<td>The base. Publishes to <code>127.0.0.1</code>; what the quick start
|
||||
uses.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>docker-compose.build.yml</code></td>
|
||||
<td><code>relay</code></td>
|
||||
<td>Builds the image from source instead of pulling it. Needed on
|
||||
ARM.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>docker-compose.prod.yml</code></td>
|
||||
<td><code>db</code>, <code>redis</code>, <code>relay</code>,
|
||||
<code>coturn</code>, <code>obfs4bridge</code></td>
|
||||
<td>A public relay: rate limiting that survives a restart, TURN for
|
||||
calls, and a bridge for networks that block the relay.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>docker-compose.federated.yml</code></td>
|
||||
<td><code>db</code>, <code>redis</code>, <code>relay</code>,
|
||||
<code>caddy</code>, <code>coturn</code></td>
|
||||
<td>The same, with Caddy terminating TLS for you.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>docker-compose.pooled.yml</code></td>
|
||||
<td><code>pgbouncer</code>, <code>relay</code></td>
|
||||
<td>Connection pooling, for when one Postgres is serving several relays.
|
||||
An overlay on <code>prod</code>, not on the base: it uses the
|
||||
<code>internal</code> network that file defines.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong><code>redis</code> is optional and the relay says so at
|
||||
boot.</strong> Without it, rate limits are counted in process: correct
|
||||
for one relay, and reset by a restart. With it they are shared and
|
||||
durable. Nothing else depends on it.</p>
|
||||
<p><strong><code>obfs4bridge</code> is for reachability, not
|
||||
privacy.</strong> It gives the relay a second address that does not look
|
||||
like a relay, for networks that block the first. It publishes its own
|
||||
port because it carries no SNI for a reverse proxy to route on. Clients
|
||||
paste the bridge line into Settings; see <code>FN_OBFS4_*</code> in
|
||||
<code>docs/CONFIGURATION.md</code> in the relay repository.</p>
|
||||
<p><strong><code>pgbouncer</code> is only worth it above one
|
||||
relay.</strong> A single relay opens few enough connections that
|
||||
Postgres handles them directly. Layer it on prod:</p>
|
||||
<pre class="bash"><code>docker compose -f docker-compose.yml -f docker-compose.prod.yml \
|
||||
-f docker-compose.pooled.yml up -d</code></pre>
|
||||
<h2 id="voice-calls">Voice calls</h2>
|
||||
<p>Optional, and only needed for calls. The compose file ships a coturn
|
||||
service behind a profile, so nothing about calls runs, or has to be
|
||||
configured, unless you ask for it:</p>
|
||||
<pre class="bash"><code>docker compose --profile calls up -d</code></pre>
|
||||
<p>Optional, and only needed for calls.</p>
|
||||
<p><code>docker-compose.yml</code>, the file the steps above use, ships
|
||||
<strong>no coturn</strong>, on purpose: it publishes to
|
||||
<code>127.0.0.1</code> and is meant for working on the relay, where two
|
||||
clients on one machine reach each other directly. <code>/e2e/turn</code>
|
||||
answering 503 there is correct rather than a missing piece.</p>
|
||||
<p>Calls across symmetric NAT and CGNAT need a relay in the middle, and
|
||||
that lives in <code>docker-compose.prod.yml</code>, behind a profile so
|
||||
nothing about calls runs, or has to be configured, unless you ask for
|
||||
it:</p>
|
||||
<pre class="bash"><code>docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile calls up -d</code></pre>
|
||||
<p>Set <code>FN_TURN_HOST</code>, <code>FN_TURN_SECRET</code> and
|
||||
<code>FN_TURN_EXTERNAL_IP</code> in <code>.env</code> before you do;
|
||||
coturn refuses to start without them. The relay mints credentials from
|
||||
|
||||
+23
-27
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/docs/SPEC.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 822aed4d9c79bbbb -->
|
||||
source-sha256: 8d1cd3d583818c4f -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="flamenet-messenger-end-to-end-encryption-spec">Flamenet
|
||||
@@ -418,11 +418,11 @@ receivers MUST ignore it unless the sender is a member of
|
||||
</ul>
|
||||
<p>The group <strong>roster</strong> lives in a server-side registry
|
||||
(§8.9) in plaintext, and that is a real leak (§22). The
|
||||
<strong>name</strong> does not, and no longer did as of 2026-08-30: it
|
||||
was stored in a column the relay never read, which made it the single
|
||||
most revealing string the server held for a field that bought it
|
||||
nothing. Members learn it from each other over the pairwise ratchets
|
||||
group traffic already uses, and hold it locally.</p>
|
||||
<strong>name</strong> does not: it is not stored in a column the relay
|
||||
never read, which made it the single most revealing string the server
|
||||
held for a field that bought it nothing. Members learn it from each
|
||||
other over the pairwise ratchets group traffic already uses, and hold it
|
||||
locally.</p>
|
||||
<p>Two deliveries carry it, and both are needed:</p>
|
||||
<ul>
|
||||
<li>an <strong>announcement</strong> at creation and on adding a member,
|
||||
@@ -1004,8 +1004,8 @@ paragraph previously said a group pin could not resolve, because a group
|
||||
transcript did not record a <code>mid</code> for each message. That
|
||||
stopped being true: <code>Session.sendGroup</code> mints a
|
||||
<code>mid</code> and records it, and <code>GroupPinResolvesTest</code>
|
||||
(Android, 2026-09-04) pins a message in a group thread and reads the
|
||||
message back out of the transcript by the id the pin names.</p>
|
||||
(Android) pins a message in a group thread and reads the message back
|
||||
out of the transcript by the id the pin names.</p>
|
||||
<p>The pin stores an id and never a copy of the text, so it follows an
|
||||
edit rather than quoting words that are no longer in the conversation,
|
||||
and it does not outlive the message being deleted. The same test covers
|
||||
@@ -1567,7 +1567,7 @@ message differently from a 1:1 message and cannot tell them apart.</p>
|
||||
the owner. Cap: <strong>32 members</strong> including the creator
|
||||
(fan-out cost is per-member; see §6.7).</p>
|
||||
<p>A <code>name</code> MAY be present and MUST be ignored: it is
|
||||
accepted only so clients written before 2026-08-30 keep working, and the
|
||||
accepted only so clients written written before it keep working, and the
|
||||
relay neither reads nor stores it. Clients from here on MUST omit the
|
||||
field and tell members the name over the ratchet (§6.7).</p>
|
||||
<p>Every member must be <strong>reachable</strong>: at least one
|
||||
@@ -1683,13 +1683,12 @@ client-side. Harness: <code>test/relay.mjs</code>, which drives the
|
||||
compiled Swift relay rather than a stand-in.</li>
|
||||
<li><strong>iOS ↔︎ live server</strong>, v1 only: a real
|
||||
<code>register → send → poll → decrypt</code> round trip against
|
||||
production (2026-08-11).</li>
|
||||
production.</li>
|
||||
<li>Each of the <strong>iOS</strong> and <strong>Web</strong> engines
|
||||
separately passes X3DH, Double Ratchet, out-of-order (skipped keys), GCM
|
||||
tamper rejection and safety numbers.</li>
|
||||
</ul>
|
||||
<h3 id="android-verified-as-of-2026-08-29">Android, verified as of
|
||||
2026-08-29</h3>
|
||||
<h3 id="android-verified">Android, verified</h3>
|
||||
<p>The Android engine exists and is exchanged with the web engine. It
|
||||
lives in <a
|
||||
href="https://gitlab.com/paulhitt/flamenet-messenger-android">flamenet-messenger-android</a>
|
||||
@@ -1714,7 +1713,7 @@ initiator. Verified by <code>test/interop-swift-kotlin.mjs</code>, the
|
||||
Swift driver compiled from the iOS app's own <code>Sources/E2E</code>,
|
||||
the Kotlin side driven through <code>session_*</code> so the negotiation
|
||||
decision belongs to <code>E2ESession</code> and not to the harness.
|
||||
<strong>77 assertions, 0 failures, re-run 2026-09-07.</strong></p>
|
||||
<strong>77 assertions, 0 failures.</strong></p>
|
||||
<p>The rule for this section: establish what has been run.
|
||||
<code>PQKEM.kt</code> being present is not evidence; the cross-engine
|
||||
run is. A spec that understates what exists sends an auditor away from a
|
||||
@@ -1722,10 +1721,9 @@ third of the client code, and one that overstates it sends them to code
|
||||
that does not do what it says.</p>
|
||||
<blockquote>
|
||||
<p>Do not infer interop from structural similarity. An earlier version
|
||||
of this section did exactly that, and it was wrong for months: the iOS
|
||||
engine was structurally correct and still could not talk to the server,
|
||||
because the two disagreed about a wire format no component test
|
||||
exercised. Byte-identical constructions are necessary and nowhere near
|
||||
of An engine can be structurally correct and still fail to talk to the
|
||||
server, when the two disagree about a wire format no component test
|
||||
exercises. Byte-identical constructions are necessary and nowhere near
|
||||
sufficient. What makes the Android claim above sayable is the fixture
|
||||
exchange, not the code reading correctly.</p>
|
||||
</blockquote>
|
||||
@@ -2131,8 +2129,7 @@ round-trips.</li>
|
||||
<p>§10's warning applies with full force: <strong>do not write these
|
||||
claims into the spec until the runs have actually happened.</strong> v1
|
||||
asserted interop for months that was impossible.</p>
|
||||
<p><strong>Status as of 2026-08-19</strong>: what has actually been run,
|
||||
and on what:</p>
|
||||
<p><strong>Status</strong>: what has actually been run, and on what:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -2580,7 +2577,7 @@ in the wrong order (§13.2) derives a different root key with no error
|
||||
anywhere. Also pinned: a distinct ML-KEM key per step, so a reused one,
|
||||
which still decrypts, and silently gives up the healing §13.1 exists
|
||||
for, fails.</p>
|
||||
<p>That run is also what found the shipped code was wrong. The engine
|
||||
<p>That run is also what caught a defect in the shipped code. The engine
|
||||
was attaching <code>pqdh</code>/<code>pqct</code> to messages for any
|
||||
peer that published a PQ prekey, which every v2 device does. A build
|
||||
with that would have broken every conversation with the released 0.6
|
||||
@@ -2588,7 +2585,7 @@ client, different root key AND different AAD, so the first message never
|
||||
opens. Sixteen Kotlin unit tests did not catch it and could not: both
|
||||
sides of a self-test share the author's reading. Recorded because it is
|
||||
the argument for this table's whole discipline.</p>
|
||||
<p>Kotlin against Swift directly has now been run, as of 2026-08-31:
|
||||
<p>Kotlin against Swift directly has been run:
|
||||
<code>test/interop-swift-kotlin.mjs</code>, 22 assertions, both
|
||||
directions, no web engine anywhere in the path. This closes an inference
|
||||
that was never valid, two engines each agreeing with a third agree with
|
||||
@@ -3434,12 +3431,11 @@ every group over time.</p>
|
||||
<p>This is the largest remaining metadata leak in the protocol. Message
|
||||
<em>contents</em> in a group are end-to-end encrypted; <em>who is in
|
||||
it</em> is not protected at all.</p>
|
||||
<p>The group's <strong>name</strong> was part of this leak and is not
|
||||
any more: as of 2026-08-30 it is not stored on the relay at all (§6.7,
|
||||
§8.9). That was the cheap half, the name was a column nothing read, so
|
||||
removing it cost a compatibility placeholder and nothing else. The
|
||||
roster is the expensive half, and it is still open; the rest of this
|
||||
section is about that.</p>
|
||||
<p>The group's <strong>name</strong> is not part of this leak: it is not
|
||||
stored on the relay at all (§6.7, §8.9). That was the cheap half, the
|
||||
name was a column nothing read, so removing it cost a compatibility
|
||||
placeholder and nothing else. The roster is the expensive half, and it
|
||||
is still open; the rest of this section is about that.</p>
|
||||
<h3 id="222-what-it-would-take">22.2 What it would take</h3>
|
||||
<p>The same primitive as §21, applied to membership rather than
|
||||
sending:</p>
|
||||
|
||||
@@ -11,15 +11,9 @@
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# The engine is a separate repository. These
|
||||
# sources used to be `$repo/engine/...`, a subtree of the iOS app this lived
|
||||
# inside, and the split left them pointing at a directory that does not exist.
|
||||
# The script then died on its first `render`, so nobody regenerated the pages:
|
||||
# /spec, /audit and /security drifted for a month while still being served as
|
||||
# current. A public audit-scope page describing a system that has changed
|
||||
# underneath it is worse than having no page.
|
||||
#
|
||||
# Found as a sibling checkout, or wherever FLAMENET_E2E_DIR points.
|
||||
# The engine is a separate repository, found as a sibling checkout or wherever
|
||||
# FLAMENET_E2E_DIR points. If it cannot be found, fail here: a public page
|
||||
# describing a system that has changed underneath it is worse than no page.
|
||||
engine="${FLAMENET_E2E_DIR:-$(cd "$here/../flamenet-e2e" 2>/dev/null && pwd || true)}"
|
||||
if [ -z "$engine" ] || [ ! -f "$engine/docs/SPEC.md" ]; then
|
||||
echo "cannot find the flamenet-e2e checkout." >&2
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
Description=Daily sweep of GitLab-runner cache volumes
|
||||
|
||||
[Timer]
|
||||
# 🔴 Daily, not weekly. Measured 2026-09-08: 3.7G of runner cache accumulated
|
||||
# in the few hours of CI after the disk was cleared that morning. A weekly
|
||||
# sweep would let ~50G build up between runs, which is the size that filled
|
||||
# the disk and took the database down in the first place.
|
||||
# Daily, not weekly. CI accumulates roughly 3.7G of runner cache in a few
|
||||
# hours, so a weekly sweep would let ~50G build up between runs: enough to
|
||||
# fill the disk and take the database down.
|
||||
OnCalendar=*-*-* 04:30:00
|
||||
# The box is not always up at 04:30, and a sweep that silently never runs is
|
||||
# the same as no sweep.
|
||||
|
||||
@@ -108,12 +108,9 @@ LP=$(curl -s -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/)
|
||||
# world-read bit, the binary was healthy, the page was not. Render every
|
||||
# public page, not just the front door.
|
||||
#
|
||||
# Read out of the source, not typed here. The list used to be five
|
||||
# hand-written names out of nineteen pages, and it drifted the moment anybody
|
||||
# added one: /metadata, /comparison, /spec and /self-hosting, the four this
|
||||
# project points people at when it makes a claim about what it stores, were
|
||||
# never rendered by the gate at all. A deploy that broke one of them would have
|
||||
# passed, exactly the way the donate page did before this loop existed.
|
||||
# Read out of the source, not typed here. A hand-written list drifts the
|
||||
# moment anybody adds a page, and a deploy that broke an unlisted page would
|
||||
# pass the gate.
|
||||
#
|
||||
# `Entrypoint.swift` has the real table, one `("slug", "title", …)` per page.
|
||||
# Deriving from it means a page added tomorrow is gated tomorrow, with nobody
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ size with different roots are evidence the operator cannot retract. Tampering
|
||||
is designed to be **detected**, not prevented at the server.
|
||||
|
||||
The exposure this closes is blast radius from a flaw in the relay itself.
|
||||
Any SQL injection or path that reaches a query with attacker-influenced input
|
||||
used to become full cluster control, and superuser can run
|
||||
Without it, any SQL injection or path that reaches a query with
|
||||
attacker-influenced input becomes full cluster control, and superuser can run
|
||||
`COPY … TO PROGRAM`, which turns a SQL flaw into remote code execution on the
|
||||
database host. That single fact is why this was worth doing regardless of how
|
||||
much confidence anyone has in the query layer.
|
||||
|
||||
+12
-14
@@ -1,17 +1,15 @@
|
||||
# Where plaintext can come to rest
|
||||
|
||||
> Update, 2026-09-04, the question this document asks has changed.
|
||||
> The relay has no route that accepts a readable message. That route, its
|
||||
> sibling on the signed surface, the `messages` table, and `FN_AT_REST_SECRET`,
|
||||
> the key the server held over those bodies, are all gone. See **"The plaintext
|
||||
> transport is gone"** at the end.
|
||||
>
|
||||
> Everything below was written while the relay still had a route that accepted a
|
||||
> readable message. That route is gone, along with its sibling on the signed
|
||||
> surface, the `messages` table, and `FN_AT_REST_SECRET`, the key the server
|
||||
> held over those bodies. See **"The plaintext transport is gone"** at the end.
|
||||
>
|
||||
> The findings below are kept as written rather than edited to match. Finding
|
||||
> 2 in particular, old row versions surviving an overwrite, is about Postgres,
|
||||
> not about messages, and applies to every table here.
|
||||
> The inventory below was taken while that route still existed, and is kept as
|
||||
> taken. Finding 2 in particular, old row versions surviving an overwrite, is
|
||||
> about Postgres, not about messages, and applies to every table here.
|
||||
|
||||
An inventory of the relay's storage, measured on production 2026-09-04. The
|
||||
An inventory of the relay's storage, measured on production. The
|
||||
question is deliberately narrow: **not what is decrypted in memory to be
|
||||
handled, that is most of the code and is fine, but what is written to disk,
|
||||
sent elsewhere, or left behind after an overwrite.**
|
||||
@@ -98,7 +96,7 @@ an expectation rather than a measurement.
|
||||
# Identifying data at the edge
|
||||
|
||||
The section above is about the database. This one is about the IP addresses that
|
||||
reach the box, measured 2026-09-04.
|
||||
reach the box.
|
||||
|
||||
## The edge access log, the big surface
|
||||
|
||||
@@ -212,7 +210,7 @@ are the same shape: a monitor reporting on itself rather than on the system.
|
||||
|
||||
---
|
||||
|
||||
## The plaintext transport is gone (relay#50, 2026-09-04)
|
||||
## The plaintext transport is gone (relay#50)
|
||||
|
||||
The decision this inventory was written to inform has been taken: remove it.
|
||||
|
||||
@@ -243,8 +241,8 @@ than fixed, which is the better outcome of the two.
|
||||
Checked rather than assumed, in four ways:
|
||||
|
||||
1. **The table was empty**: 0 live rows and 0 dead tuples, finding 4 above.
|
||||
2. **No client called it.** iOS removed `send(to:body:)` and `poll` on
|
||||
2026-08-29 with a comment saying the route was left for other callers;
|
||||
2. **No client called it.** iOS had removed `send(to:body:)` and `poll`,
|
||||
leaving a comment saying the route was there for other callers;
|
||||
Android never had them; a grep of every local repository found the routes
|
||||
named only in the conformance harness, which tested them against themselves.
|
||||
3. **No production traffic.** An hour of edge access log (3,931 requests) shows
|
||||
|
||||
Reference in New Issue
Block a user