Say how long the edge access log really lives, and fix README defaults

Read on the box 2026-10-07 (read-only): ship-logs.timer at 02:40 UTC copies
the edge log, with wp-flamenet, wp-shepherd and enigma-bbs, to b2enc:logs,
an rclone crypt remote on Backblaze B2, and deletes archive files older than
90 days. edge-accesslog-prune.timer at 03:30 UTC overwrites the live file in
place and removes rotations, and refuses to run unless a ship succeeded in
the last 26 hours. prune-edge-logs.timer hourly removes rotations past the
first. The archive held 2026-08-31 onward. The relay container's own log is
not shipped and at FN_LOG_LEVEL=notice held no client address.

/metadata (en, es) said the edge log is "erased weekly, at most seven days".
It now says about a day on the machine, longer only if the archive copy
fails, and 90 days in the encrypted archive, and that the relay's own log
holds no addresses. Same correction for the line about request sizes.
PLAINTEXT.md and IDENTIFYING-DATA.md record the reading. The intro row on
/metadata now says one pass, not three.

README's limits table said FN_BUNDLE_RATE / FN_BUNDLE_RATE_TARGET 120 / 6
and FN_SEND_RATE 600; the code defaults are 600 / 60 and 3000. relay.leaf
regenerated from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-10-07 14:37:39 -04:00
co-authored by Claude Opus 5.5
parent 78b0a576d5
commit a55af454fc
6 changed files with 45 additions and 14 deletions
+2 -2
View File
@@ -43,8 +43,8 @@ value, and a relay that accepts unsigned tokens is worse than one that is down.
| `FN_WORKDIR` | inferred | Directory containing `Resources/` and `Public/`. Set it in a unit file. |
| `FN_CLOCK_SKEW` | `60` | Seconds of tolerance on a proof timestamp. |
| `FN_FEDERATION` | `closed` | `closed`, `allowlist` or `open`. Closed is a complete relay, not a limited one, and is what a self-hoster gets without configuring anything. An unrecognised value falls back to `closed` rather than refusing to boot, a typo in an optional variable should not stop a relay serving its own users. |
| `FN_BUNDLE_RATE` / `FN_BUNDLE_RATE_TARGET` | `120` / `6` | Bundle fetches per hour, per requester and per pair. |
| `FN_SEND_RATE` | `600` | Envelopes per hour. |
| `FN_BUNDLE_RATE` / `FN_BUNDLE_RATE_TARGET` | `600` / `60` | Bundle fetches per hour, per requester and per pair. |
| `FN_SEND_RATE` | `3000` | Envelopes per hour. |
| `FN_DELIVERED_RETENTION_DAYS` | `1` | The backstop for a collected envelope the app never confirms. |
| `FN_DELETE_ON_ACK` | on | Delete an envelope when the app's next poll confirms it. Postgres only; `0` turns it off. |
| `FN_UNDELIVERED_RETENTION_DAYS` | `30` | Must stay below the client's retired-SPK retention (45 days). |
+10 -4
View File
@@ -32,7 +32,7 @@
<tr><td>Cuenta y dispositivo del remitente</td><td>Solo en envíos identificados; un envío <strong>sellado</strong> no guarda ninguno de los dos</td><td>#if(retention.deleteOnAck):en cuanto tu aplicación confirma la recepción, o #(retention.deliveredDays) día(s) después de entregarse si nunca la confirma#else:#(retention.deliveredDays) día(s) después de entregarse#endif</td></tr>
<tr><td>El texto cifrado</td><td>Es el mensaje; el servidor no tiene ninguna clave que lo abra</td><td>#(retention.undeliveredDays) días si nunca se recoge</td></tr>
<tr><td>Adjuntos (fotos, notas de voz, historias, todo cifrado)</td><td>Los dispositivos los descargan aparte del sobre que los referencia</td><td>#(retention.attachmentDays) días</td></tr>
<tr><td>Presentaciones</td><td>Cuando alguien que todavía no tiene forma de escribirte te busca por tu nombre de usuario, o abre uno de tus enlaces de contacto, el servidor le da tres pases, cada uno válido para un mensaje sellado a ti. Guarda el hash de cada pase y a qué cuenta da acceso. No anota quién preguntó, desde dónde ni cuándo. Las aplicaciones todavía no lo usan.</td><td>Cuando se usa el pase, o 10 minutos después de crearlo</td></tr>
<tr><td>Presentaciones</td><td>Cuando alguien que todavía no tiene forma de escribirte te busca por tu nombre de usuario, o abre uno de tus enlaces de contacto, el servidor le da un pase válido para un mensaje sellado a ti. Guarda el hash del pase y a qué cuenta da acceso. No anota quién preguntó, desde dónde ni cuándo. Las aplicaciones todavía no lo usan.</td><td>Cuando se usa el pase, o 10 minutos después de crearlo</td></tr>
<tr><td><strong>&ldquo;Este es urgente&rdquo;</strong>, una marca en el sobre, <strong>no se guarda</strong></td><td>Una llamada tiene que sonar mientras todavía está sonando, así que el remitente le dice al servidor que envíe la notificación con prioridad alta en vez de esperar a que la aplicación consulte. En la práctica, eso significa que el servidor distingue una llamada de un mensaje: no quién llama, ni sobre qué, pero sí que este es urgente. Es lo más informativo que un remitente decide revelar, y es una decisión: nada obliga a poner la marca.</td><td>Nunca se escribe. Se lee para decidir la notificación y se va con la petición.</td></tr>
<tr><td><strong>&ldquo;Este no necesita atención&rdquo;</strong>, la misma forma, <strong>no se guarda</strong></td><td>La otra cara de la misma moneda: el remitente dice que un sobre es mantenimiento y no algo por lo que interrumpir a una persona, así que el servidor envía un despertar en segundo plano en vez de una alerta. Eso le dice más o menos lo mismo que la marca de arriba, al revés. La alternativa, no enviar nada y dejar que la aplicación decida tras descifrar, significa depender de notificaciones silenciosas que iOS limita, así que el trato honesto es una pequeña revelación a cambio de notificaciones que sí llegan.</td><td>Nunca se escribe, igual que arriba.</td></tr>
<tr><td>Contadores del límite de registros (por IP)</td><td>Las cuentas nuevas están limitadas por dirección y por hora, y un contador necesita la dirección</td><td>una hora; el contador solo vive su ventana</td></tr>
@@ -173,8 +173,14 @@
<p>
Lo que está delante del servidor guarda su propio registro de accesos. Eso lo configura
el operador, y en un servidor que tú mismo ejecutas, el operador eres tú. En el servidor
que funciona en este dominio, el registro de accesos del proxy de entrada se borra cada
semana: las direcciones de conexión en la puerta viven como mucho siete días.
que funciona en este dominio, el proxy de entrada escribe una línea por cada petición:
la dirección de la conexión, la ruta, el tamaño y la hora. En la máquina esas líneas se
sobrescriben cada día a las 03:30 UTC, una vez que la copia de ese día ha llegado a un
archivo fuera del servidor; si la copia falla, se quedan hasta que una salga bien. La
copia del archivo está cifrada y se guarda 90 días, para que una intrusión que se
descubra semanas después todavía se pueda rastrear. Así que una dirección en la puerta
vive un día, más o menos, en la máquina y 90 días en el archivo. El registro propio del
servidor es aparte, no se archiva y no contiene direcciones.
</p>
<p>
Ahora algunas preguntas se pueden hacer sin iniciar sesión, con la cuenta por la que
@@ -190,7 +196,7 @@
claro que el registro <strong>sobrevive al sobre</strong>: el sobre se borra
#if(retention.deleteOnAck):en cuanto tu aplicación confirma la recepción#else:a los
#(retention.deliveredDays) día(s)#endif, y la línea que anota cuánto medía queda en el
registro de accesos hasta una semana. Rellenar los mensajes en el dispositivo que
registro de accesos un día, más o menos, en la máquina y 90 días en el archivo. Rellenar los mensajes en el dispositivo que
envía es lo que ocultaría ambas cosas, porque las dos miden los mismos bytes. Eso
todavía no está hecho. Un operador que no quiera ninguna de las dos puede apagar el
registro de accesos; aquí se mantiene porque sin él no hay forma de ver si el sitio es
+10 -5
View File
@@ -33,7 +33,7 @@
<tr><td>Sender account and device</td><td>Identified sends only, a <strong>sealed</strong> send stores neither</td><td>#if(retention.deleteOnAck):the moment your app confirms receipt, or #(retention.deliveredDays) day(s) after delivery if it never does#else:#(retention.deliveredDays) day(s) once delivered#endif</td></tr>
<tr><td>The ciphertext</td><td>It is the message; the server holds no key that opens it</td><td>#(retention.undeliveredDays) days if never collected</td></tr>
<tr><td>Attachment blobs (photos, voice notes, stories, all ciphertext)</td><td>Devices fetch them separately from the envelope that references them</td><td>#(retention.attachmentDays) days</td></tr>
<tr><td>Introductions</td><td>When somebody with no way to reach you yet looks you up by your username, or opens one of your contact links, the server hands them three passes, each good for one sealed message to you. It keeps the hash of each pass and which account it opens. It does not record who asked, from where, or when. The apps do not use this yet.</td><td>When the pass is used, or 10 minutes after it was made</td></tr>
<tr><td>Introductions</td><td>When somebody with no way to reach you yet looks you up by your username, or opens one of your contact links, the server hands them a pass good for one sealed message to you. It keeps the hash of the pass and which account it opens. It does not record who asked, from where, or when. The apps do not use this yet.</td><td>When the pass is used, or 10 minutes after it was made</td></tr>
<tr><td><strong>&ldquo;This one is urgent&rdquo;</strong>, a flag on the envelope, <strong>not stored</strong></td><td>A call has to ring while it is still ringing, so the sender tells the server to push at high priority instead of waiting for the app to poll. In practice that means the server can tell a call from a message, not who is calling, or about what, but that this one is time-critical. It is the single most informative thing a sender chooses to reveal, and it is a choice: nothing forces the flag on.</td><td>Not stored as a value. But it leaves a trace, and saying &ldquo;never written down&rdquo; was wrong: every row in <strong>Ring attempts</strong> below is written only for an urgent envelope, so a row existing for your device is the flag. That is 7 days, with the device and the time, and no content.</td></tr>
<tr><td><strong>&ldquo;This one needs no attention&rdquo;</strong>, the same shape, <strong>not stored</strong></td><td>The other side of the same coin: the sender says an envelope is housekeeping rather than something a person should be interrupted for, so the server sends a background wake instead of an alert. That tells it roughly as much as the flag above does, inverted. The alternative, sending nothing and letting the app decide after it decrypts, means relying on silent pushes that iOS throttles, so the honest trade is a small disclosure for notifications that actually arrive.</td><td>Never written down, and unlike the flag above this one really does leave no trace: it selects the kind of push and nothing records that it was set.</td></tr>
<tr><td>Sign-up throttle counters (per IP)</td><td>New accounts are limited per address per hour, and a counter needs the address</td><td>one hour, the counter lives only for its window</td></tr>
@@ -169,8 +169,13 @@
<p>
Whatever sits in front of the server keeps its own access log. That is the operator's
to configure, and on a server you run, the operator is you. On the server operated at
this domain, the edge proxy's access log is erased weekly, connection addresses at
the front door live for at most seven days.
this domain, the edge proxy writes a line for every request: the connection's
address, the path, the size and the time. On the machine those lines are overwritten
every day at 03:30 UTC, once that day's copy has reached an off-site archive; if the
copy fails, they stay until one succeeds. The archive copy is encrypted and kept for
90 days, so that an intrusion noticed weeks later can still be traced. So an address
at the front door lives about a day on the machine and 90 days in the archive. The
server's own log is separate, is not archived, and holds no addresses.
</p>
<p>
Some questions can now be asked without signing in, with the account being asked
@@ -185,8 +190,8 @@
disclosure as the row above arriving by a second door, and it is worth saying plainly
that the log <strong>outlives the envelope</strong>: the envelope is erased
#if(retention.deleteOnAck):the moment your app confirms receipt#else:after
#(retention.deliveredDays) day(s)#endif, and the line recording how large it was sits
in the access log for up to a week. Padding messages on the sending device is what
#(retention.deliveredDays) day(s)#endif, and the line recording how large it was stays
in the access log for about a day on the machine and 90 days in the archive. Padding messages on the sending device is what
would hide both, because both are measuring the same bytes. That has not been done
yet. An operator who wants neither can turn the access log off; this one is kept
because without it there is no way to see whether the site is reachable at all.
+3 -3
View File
@@ -3,7 +3,7 @@
<!-- GENERATED by Scripts/build-doc-pages.sh from README.md.
Edits made HERE are deleted the next time that script runs.
Change the markdown source and re-run it.
source-sha256: 232f2e1b2f514155 -->
source-sha256: d24ba7ffde0aae1c -->
<article class="band doc doc-generated">
<div class="wrap wrap-narrow">
<h1 id="flamenet-relay">flamenet-relay</h1>
@@ -91,12 +91,12 @@ variable should not stop a relay serving its own users.</td>
<tr>
<td><code>FN_BUNDLE_RATE</code> /
<code>FN_BUNDLE_RATE_TARGET</code></td>
<td><code>120</code> / <code>6</code></td>
<td><code>600</code> / <code>60</code></td>
<td>Bundle fetches per hour, per requester and per pair.</td>
</tr>
<tr>
<td><code>FN_SEND_RATE</code></td>
<td><code>600</code></td>
<td><code>3000</code></td>
<td>Envelopes per hour.</td>
</tr>
<tr>
+7
View File
@@ -37,6 +37,13 @@ still seen and usually matches the same device's authenticated poll.
The limiter keys for the new meters, and for the sealed send's recipient
buckets, hold a keyed hash of the account instead of its id.
How long that log lives, read on the box 2026-10-07 (it had drifted from what
`/metadata` said): overwritten on the machine daily at 03:30 UTC after the
02:40 UTC copy to an encrypted B2 archive succeeds, and kept in that archive
90 days. `/metadata` said "erased weekly, at most seven days"; it now says
about a day on the machine and 90 days in the archive. Details in
`PLAINTEXT.md`.
---
## Pass 1, 2026-09-07
+13
View File
@@ -122,6 +122,19 @@ Now pruned **daily** (`edge-accesslog-prune.timer`), so the window is a day
rather than a week. That is the highest-value change available here, and it is
the only one that is actually safe, see below.
That day is the window **on the machine**. Read on the box 2026-10-07:
`ship-logs.timer` (02:40 UTC) copies the edge log (with `wp-flamenet`,
`wp-shepherd` and `enigma-bbs`) to the `b2enc:logs` remote, an rclone crypt
remote on Backblaze B2, and deletes archive files older than 90 days
(`--min-age 2160h`). `edge-accesslog-prune.timer` (03:30 UTC) then overwrites
the live file in place and removes rotated ones, and refuses to run unless the
last ship succeeded within 26 hours, so a failing archive leaves the log on the
box rather than losing it. `prune-edge-logs.timer` hourly removes rotations past
the first. So a client address lives about a day on the box and 90 days in the
archive; the archive held 2026-08-31 onward that day. The relay container's own
log is not shipped, and at `FN_LOG_LEVEL=notice` held no client address (one
line with an IPv4, its own bind address).
## Why the obvious fix is wrong
Traefik can drop `ClientHost` from the access log, which would remove every IP