Skip to content

How federation stays honest

This page sets out the rules that keep federation trustworthy: how records are signed, how a peer is bound to one identity, how mirrors stay fresh without being replayed, and how instances corroborate each other's finds. It is for sysops and integrators; joining the network is under Join the network.

Signed feeds

An instance with a signing key (FED_PRIVATE_KEY) publishes read-only, Ed25519-signed records in one envelope: caches, finds, callsign keys, bulletins, tombstones and account moves. A consumer reads a peer's descriptor (GET /.well-known/aprscaching) and its public keys, and verifies every record's signature before mirroring it into display-only tables.

What a feed carries

  • Only source='native' caches with a federating scope are published. Imported heritage data and local-only caches never leave the instance.
  • Finds federate only with their cache: a find on a local-only or imported cache stays home too.
  • A cache marked unlisted withholds its description on the wire, and a mirror keeps it off its map and offline packs as the origin does.
  • A receiver refuses a local-only or imported cache on every carrier, whoever passes it on, and no origin page serves either.

Key rotation

tools/fedkey/rotatekey.mjs publishes the new key, a rotation record signed by the old key, and the old key with an until: the rotation time plus a grace, 7 days by default (FED_ROTATION_GRACE_DAYS). A consumer:

  • pins the first key it sees for a peer and moves the pin only along verified rotation records, so a hijacked domain that publishes a new key is refused;
  • accepts an older published key only as a proven predecessor of the current one (a key it already trusted, from which a signed rotation leads to the current key), and only until its until, never later than the rotation time plus the grace;
  • treats a rotated-away key as revoked for good once that cutoff passes, on every carrier: HTTP sync, FBB batches (personal mail to ACSFED), HF beacons and packet circuits alike. A later descriptor cannot revive it.

Keys are compared before trust

A URL says where a peer answers, not who holds its key, so no peer is trusted because of how it was added.

  • Added by address in Instance admin, a peer's descriptor is fetched and its key fingerprint shown: SHA-256 of the raw Ed25519 key, its first 16 hex digits. The peer is added unvetted with that key pinned, and only when the fingerprint sent back is still the key's, so the key stored is the key compared.
  • Raising a peer to trusted needs a pinned key and the fingerprint the sysop compared, from Instance admin and over the operator secret alike. A peer with no key, such as one from the registry, syncs as unvetted first. The fingerprint must be the pinned key's. An instance only discovery listed is followed first.
  • Auto-promotion (FED_AUTO_PROMOTE) records that it raised a peer, beside how the peer arrived. Such a peer never reaches the quorum alone, and any trust decision of the sysop's replaces the automatic one.
  • A FED_PEERS entry starts unvetted, unless it pins a fingerprint (<url>#<fingerprint>). Then the first sync refuses a key that does not match it, and a key that matches raises the peer to trusted once. Later the pin follows the rotation chain like any other, and the fingerprint holds for every key that chain reaches from the matched key, also once the matched key's grace has passed.
  • Removing a peer deletes its row and pinned key. What it published stays, with no row to vouch for it: it counts as from an unknown origin, hidden by default and never a corroborating voice. Added again, the peer starts unvetted and its key is fetched and compared afresh. A transit: row is the exception: its key came from a hub, so the records only that key vouched for go with it.

One row per instance

A peer's instance id (its hostname, such as oe.example.net) is bound to the peer row that first proved it, and only one live (not blocked) row may hold that id. A block covers the instance, whatever address it answers on.

  • A second URL claiming a bound instance is refused.
  • A descriptor that renames its instance is refused.
  • Instance ids are lowercase hostnames; an id with a : or other characters outside a hostname is refused.
  • A blocked instance stays blocked on every path: a pull from another address, a 44Net add, a registry entry, a discovered address and a hub push are all refused, and its frames apply over no carrier. Blocking one row blocks every row naming the instance; lifting the block on one is refused while another still blocks it.

An impostor therefore never inherits another instance's namespace or trust. Moving a peer to a new URL means removing its old row first.

The registry binds names to keys

A signed instance registry binds instance names to keys and operators. Its authority key is always pinned in FED_REGISTRY_KEY; the document comes from FED_REGISTRY, or from the url=https://… in the DNS TXT record that FED_REGISTRY_DNS names. A DNS-located registry is cached for five minutes.

  • DNS only says where the document lives. A key= in the record is ignored, because whoever can change a DNS record must not choose the key that signs the registry.
  • A registry setting without FED_REGISTRY_KEY is a configuration error: the Node and Bun servers refuse to start.
  • An instance refuses to mirror a peer whose current key is not the key its registry entry binds.
  • A document older than the newest one already accepted is refused as a replay. When the registry cannot be fetched, the last good document keeps binding the instances it registered, so an outage never reopens them to impersonation.

Mirrors stay fresh, never replayed

  • Records only move forward. Every mirrored record carries a per-record version, and an instance applies a record only when its version is higher than the last one it applied. A replayed older record, or a different record at the same version, changes nothing. A cache's version counts its revisions, so two edits in one second are still two versions.
  • Pages are bounded. A frame signed in the future, or a timestamp version in the future, is refused. A pulled page is capped at 4 MiB and at the number of frames asked for, and must carry only its own record type.
  • Deletes come first. A pull applies tombstones before records, so a delete suppresses a re-mirror. A tombstone suppresses every version of its record, except a sysop's removal of a cache: it carries upTo, the cache's version at removal, so the cache comes back only at a higher version, through a restore.
  • A gossip ping only asks for a pull. After a federated write an instance sends its peers POST /federation/notify ("come pull from me"), which triggers an incremental sync. The endpoint is unauthenticated, so it only ever asks for a pull the instance would make anyway: a notify naming an instance it does not follow is ignored, each host and each instance is rate-limited, and the pull goes through the same coalescer as the scheduled sync.
  • Discovery only lists. An instance lists to its peers only the instances it trusts, never an unvetted or blocked one, and names each key by its fingerprint alone (FED_PEER_EXCHANGE). With FED_DISCOVER it reads only trusted peers' lists. What it learns there, or hears announced by mDNS on its LAN, is a sighting on the instance's one peer row: a new instance is listed unvetted and disabled, never pulled, and at most 200 wait at once. A sighting never sets or moves a key; a fingerprint that disagrees with another source or with the pinned key is flagged, and the pin stays. Follow fetches the key and refuses one that differs from what was listed or pinned (Discovery).
  • Private networks stay closed. Every federation fetch resolves its host first and refuses loopback, private, link-local and CGNAT addresses (IPv4 inside IPv6 included), and checks every redirect hop the same way. A URL from another party can never reach the host's LAN. The peers configured by hand (FED_PEERS, FED_HUB_URL) are exempt, and so is the address an mDNS announcement came from, once mDNS found an instance there. FED_ALLOW_PRIVATE=1 opens it for a federation that lives on a LAN.

Records passed on through hubs

An instance serves the caches, finds, tombstones and account moves it mirrored, origin by origin (GET /federation/sync/summary and GET /federation/sync/origin, Per-origin sync), as their home instances signed them, byte for byte. It re-signs nothing, so it can neither change a record nor lend it any trust. FED_RESERVE decides what it passes on: by default the records of instances it trusts. A hub, a second hub and a Pocket station that carries records are the same thing here.

  • Verified against the home's key. Each frame verifies under its origin's keys: the peer row the receiver holds for that origin, the key the signed registry binds to it, or the key a hub handed on. A hub hands on the key, accept set and rotation records it holds for each origin it passes on, in its summary.
  • Pinned on first sight. The first hub to name an origin the receiver does not know pins that origin's key in a peer row transit:<instance>: unvetted, never pulled, its fingerprint shown in Instance admin like any peer's. Later the pin moves only along verified rotation records. No hub moves the key of an origin known any other way, a blocked one included, and a registry binding wins over every hub's word.
  • The direct key wins. When the receiver follows the origin itself, by address or in FED_PEERS, the origin's own key replaces the one a hub handed on, and every record that only the hub's key vouched for is dropped. A lying hub therefore never gets a forged record shown under a trust the sysop gave the real origin.
  • The origin's trust, never the hub's. A passed-on record lands under its origin, so the receiver shows it with its own trust in that origin: hidden while the origin is unvetted, never shown when it is blocked. A trusted hub lifts nothing.
  • No new voice. The corroboration quorum asks only trusted peers this instance follows itself, directly or through a hub's relay for one nobody can dial; a passed-on record is never an answer, and an origin known only through a hub is never asked.
  • Deletes reach every hop, and come first. A tombstone passes on like any record, and a hub stops passing on what a tombstone removed. A pull applies every origin's tombstones before any cache or find, and a receiver keeps its tombstones for good, so a stale copy that reaches it later over another path is refused. A bounded tombstone (upTo) passes on too, so a restored cache follows it at its higher version.
  • Marks move on what a trusted word says. A receiver records how far it holds each origin from the origin's own pages, and from a neighbour's only when it trusts that neighbour. Pages from any other neighbour still apply but never move the mark, so a neighbour nobody vetted cannot keep a record from a path that carries it. A trusted neighbour that leaves a record out still moves the mark past it: trust decides whose word counts.
  • What did not arrive is asked for on its own. A frame that did not settle (signed ahead of the receiver's clock, one its database could not take at that moment, one that did not verify), a record kept past the hop limit, and a record a trusted neighbour says it lacks become gaps. The mark moves on, every later record passes on, and the receiver asks its neighbours for each gap by itself, backing off from 5 minutes to a day per neighbour. A copy over fewer hops closes a hop gap, so the record passes on from there. A gap no neighbour fills within 7 days and 5 asks counts as refused for good; Instance admin → Federation → Records given up and doctor list it until the sysop marks it seen. A missing tombstone is never given up: it is asked for daily until a neighbour supplies it, so a deletion always arrives. A record past the hop limit raises no alarm: at the edge of the mesh every distant record is one, and its gap goes quietly after 30 days.
  • Numbers are never reused. Each origin numbers its records, callsign keys included, at least by the time in milliseconds, and a record's global id is that number, never its row id. A database restored from an older backup still numbers its new records, deletions included, above what its peers hold, under global ids no peer holds or deleted. A peer the restored instance trusts tells it how far it holds the instance's records, which raises the numbering past that even on a box whose clock is behind. The restore itself leaves the rows as the backup holds them.
  • Bounded travel. The page carries each frame's hop count beside it; a record crosses at most four instances. A hub never sends a record back to its origin. Apply is idempotent by global id and version, so a record that comes round a ring of hubs again changes nothing and is not passed on twice; the same version over a shorter path replaces the hop count kept.
  • Regions apply. A pull narrowed by FED_SYNC_REGION narrows the passed-on caches too; deletes travel whole.
  • The policy in force decides. The summary lists what FED_RESERVE lets out at the moment it is read. An origin the hub trusts later, or a wider setting, reaches every receiver at its next pull, since a receiver asks from what it holds, not from where it stopped reading the hub.

Cross-instance corroboration

When a find cannot reach Tier A locally, the instance asks its trusted peers whether they independently heard the callsign on RF near the cache, at a receiving site the logger does not control. The logger's own calls and stations travel with the question as exclusions.

  • Quorum. Distinct identities must agree before the find is promoted to Tier A: two by default (FED_CORROBORATION_QUORUM). They are counted by registry operator where the registry names one, else by signing key, so one operator running several instances is one voice.
  • Same rule as local Tier A. A peer answers only from positions it attests itself: heard directly by one of its own receiving sites (its FIRST_PARTY_SITES) and delivered by that site's own ingest box. An APRS-IS copy naming such a site (qAR,<site>) never vouches, because anyone with a public passcode can inject one. An instance that attests no site never vouches for anyone.
  • Signed frames. The question carries a fresh nonce, and the answer is bound to that nonce and to the question's hash. No middlebox, a plain-http 44Net peer included, can forge an answer or replay it against another question. Asking requires a signing key.
  • The logger's own track. A local fix that puts the logger somewhere the answer's location cannot be reached from at a plausible speed keeps the find below Tier A. A living cache is asked about where its station last was.
  • No location oracle. The answerer snaps and bounds every question (radius 150–1000 m, a bucketed window of at most an hour, no older than seven days) and coarsens every answer. The exact IGate is revealed only if both peers opt in (FED_REVEAL_IGATE).
  • Who is answered. The shared FED_CORROBORATION_SECRET, if set, is sent only to trusted https peers, and an answerer that sets it answers only the peers holding it. FED_CORROBORATION_REQUIRE_KNOWN=1 answers only peers whose key is known, unvetted ones included.
  • Through a hub's relay. A trusted peer nobody can dial, such as a push spoke, gets the same signed question through its hub's relay, and its signed answer comes back the same way. The hub queues a question only from an instance whose key it holds and whose signature verifies, within the relay's per-requester caps, and only for one of its own push spokes; the answer is read only by that instance, signing its read, with the ticket the hub gave it. The hub sees the coarsened question and the signed answer and can delay or drop them, never change them. A relayed question stays answerable for an hour. The secret cannot ride the relay, so a spoke that sets FED_CORROBORATION_SECRET answers relayed questions only from askers it knows. The quorum counts the spoke's identity once, whichever path its answer took.
  • Reputation. Peers carry a reputation (rep_confirmed / rep_failed). A peer that denies a corroboration the quorum confirmed accrues a contradiction and is penalised. With FED_AUTO_PROMOTE, an unvetted peer becomes trusted after that many confirmed corroborations.

Asked again, within bounds

A find that misses Tier A only because trusted peers could not be reached (a timeout, a failed connection, a rate limit or a server error, never a refusal) is asked again: the identical question, to those peers only, one, six and 24 hours after the find, and never past 72 hours.

  • Evidence already in hand carries over, but counts only while its peer is still trusted.
  • The quorum, the exclusions and the logger's own-track check are the same as when the find was logged.
  • A verified "no" from a trusted peer ends it at once; no later attempt follows one.
  • An answer through a hub's relay is read as soon as it arrives (FED_RELAY_POLL_MS, 15 s), not at the next attempt. A relayed question still unanswered after an hour goes to the next attempt like a peer not reached.
  • A find lifted this way shows confirmed later and keeps the time (corroborated_later_at). Peers that mirrored the find keep the tier they first saw, since the finds feed carries each log once.

Next