Files
fips/docs/tutorials/persistent-identity.md
Johnathan Corgan 5abf9a9325 docs: four-section /docs/ restructure with new-user content, accuracy pass, and gateway feature-set rewrite
Restructures /docs/ by reader purpose (tutorials, how-to,
reference, design), adds the new-user-progression and
operator-recipe content the prior layout lacked, runs an
accuracy pass against current source across the pre-existing
design docs, and rewrites the gateway feature-set documentation
end-to-end around its actual operational profile (a niche
feature designed for systems already serving DHCP/DNS to a
LAN, with two independent halves — outbound LAN→mesh, inbound
mesh→LAN — sharing one nftables table, one binary, and one
control socket). Top-level README and getting-started rewritten
around two equally-weighted deployment modes (overlay on
existing IP networks; ground-up over non-IP transports).

## Additions

- 11 new tutorials in docs/tutorials/: an 8-step new-user
  progression from single-daemon test-mesh peering through
  to a ground-up two-device mesh, an IPv6-adapter side-trip
  walkthrough, an Advanced Tutorials index, and a hand-held
  OpenWrt walk-through for fips-gateway deployment that
  exercises both halves of the feature.
- 12 new how-tos in docs/how-to/: firewall activation,
  Nostr discovery (resolve / advertise / open across five
  scenarios), Tor onion (directory + control_port modes),
  UDP buffer tuning, unprivileged-user setup, persistent
  identity, host aliases, Bluetooth LE peering, MTU
  diagnostics, manual Linux-host gateway deployment (covers
  both halves), gateway troubleshooting (organised by half),
  and a section index.
- 9 new reference docs in docs/reference/: configuration,
  wire formats, control-socket protocol, four CLI references
  (fips, fipsctl, fipstop, fips-gateway), security posture
  matrix, and Nostr events catalog. Configuration and
  wire-formats are renamed-and-extended from prior design/
  versions; the other seven are net-new.
- 6 new design docs: fips-concepts, fips-architecture, and
  fips-prior-work split out of the deleted fips-intro.md;
  consolidated fips-mmp and fips-mtu aggregations; and a
  new generic port-advertisement-and-nat-traversal doc
  (Nostr-signaled port advertisement plus UDP NAT-traversal
  protocol, FIPS as an example implementation, suitable for
  eventual NIP submission).
- Top-level docs/getting-started.md walking through the
  binary-installer-only Install story.
- packaging/common/hosts pre-populated with the eight public
  test-mesh nodes so shortnames resolve out of the box on
  every fresh install.

## Changes

- 23 wire-format diagrams relocated to reference/diagrams/
  alongside the wire-formats move.
- 4 design diagrams corrected against source code
  (fips-protocol-stack, fips-identity-derivation,
  fips-coordinate-discovery, fips-routing-decision).
- 10 pre-existing design docs reconciled with current
  source. Numeric corrections: stale link-MMP report bounds
  (now [1s, 5s] with 200 ms cold-start floor); UDP default
  MTU (now 1280, IPv6 minimum); node_addr formula
  (SHA-256(pubkey)[..16]); Noise patterns (IK at link, XK
  at session); peer-ACL semantics (strict allowlist requires
  ALL in peers.deny); daemon DNS upstream ([::1]:5354);
  on-the-wire bloom-filter size (1,071 bytes); obsolete
  Cargo-feature references (PR #79 dropped them) removed.
- Transport framing tightened across the docs: TCP is for
  UDP-filtered networks (not NAT traversal); Tor is a
  deployment mode (not failover); WebSocket dropped (not a
  shipped FIPS transport); WiFi promoted to Implemented via
  Ethernet in infrastructure mode; classic-Bluetooth row
  removed (BLE is the only Bluetooth-mode transport).
- docs/design/fips-gateway.md rewritten end-to-end to lead
  with the niche-feature framing and the two-halves
  structure. Title moved from "FIPS Outbound LAN Gateway"
  to "FIPS Gateway"; architecture section describes the
  common machinery (the fips-gateway service, the nftables
  table, the control socket) before splitting into separate
  "Outbound Half" and "Inbound Half" sections of equal
  weight; security considerations split per-half; no Future
  Work section (speculative directions live in the project
  tracker, not in protocol design docs). Inbound port
  forwarding is a first-class half rather than a buried
  "Implemented Extensions" subsection.
- Gateway terminology unified across all gateway docs as a
  separate Linux service running alongside the fips daemon
  (its own systemd unit / OpenWrt init script). Container-
  pattern terms (sidecar) are reserved for the
  Docker/Kubernetes sidecar deployment examples — the
  testing/sidecar/ tree, examples/k8s-sidecar/,
  examples/sidecar-nostr-relay/,
  examples/wireguard-sidecar-macos/, and the related
  CHANGELOG / top-level README entries — where the term
  carries its standard container meaning.
- Net-new design body content: rekey section in
  fips-mesh-layer (Noise IK msg1/msg2 over the established
  link, K-bit cutover, drain window, smaller-NodeAddr-wins
  tie-breaker on dual-init); Mesh Size Estimation and
  Antipoison FPR Cap sections in fips-bloom-filters;
  Mesh-Interface Query Filter subsection in
  fips-ipv6-adapter; failure-suppression knobs and clock-
  skew tolerance in fips-nostr-discovery; loop-rejection
  and mid-chain ancestor swap added to spanning-tree
  propagation / stability rules; Priority Chain in
  fips-mesh-operation renumbered to match the
  routing-decision diagram.
- Top-level README: dropped the stale nostr-discovery
  cargo-feature parenthetical. docs/README.md and the four
  section READMEs (tutorials, how-to, reference, design)
  refreshed for the new structure; index rows reflect both
  halves of the gateway feature and the new fips-gateway
  CLI reference.
- Cargo.toml [package.metadata.deb] assets path updated for
  the fips-security.md move; .gitignore /reference/ rule
  anchored to repo root so docs/reference/ is trackable.
- packaging/openwrt-ipk/files/etc/fips/fips.yaml
  configuration-doc URL updated to the new
  docs/reference/configuration.md location.

## Deletions

- docs/design/fips-intro.md (split into the three new intro
  design docs).
- docs/design/document-relationships.svg (orphan, no longer
  referenced).
- docs/proposals/ tree removed; the only proposal it
  contained (the Nostr UDP hole-punch protocol) was
  rewritten as the new generic
  design/port-advertisement-and-nat-traversal.md.
2026-05-08 03:02:12 +00:00

11 KiB

Make Your Node's Identity Persistent

After completing join-the-test-mesh, your daemon is connected to the public test mesh — but its identity is ephemeral. Every restart generates a fresh Nostr keypair, so the npub the rest of the world would use to reach you changes every time. This tutorial walks through pinning your node to a stable keypair, locating it on disk, and protecting it.

The whole exercise should take about ten minutes.

What you'll build

   ┌─────────────────────────────────────────┐
   │  /etc/fips/fips.yaml                    │
   │     node:                               │
   │       identity:                         │
   │         persistent: true   ← this flag  │
   └────────────────┬────────────────────────┘
                    │  daemon reads on start
                    ▼
   ┌─────────────────────────────────────────┐
   │  /etc/fips/fips.key      0600 root:root │
   │  /etc/fips/fips.pub      0644 root:root │
   └─────────────────────────────────────────┘
              your stable nsec / npub

After this tutorial your node will have:

  • A keypair on disk that the daemon reuses across restarts.
  • An npub you can hand to other operators so they can add you to their peers: list once and have the entry keep working.
  • A clear understanding of which file holds the secret and how to keep it that way.

Why a stable identity matters

In FIPS your Nostr keypair is your node's identity in the most literal sense. Several things derive from it:

  • Your fd97:... mesh address — derived from the public key.
  • Your <npub>.fips DNS name — the npub itself with .fips appended.
  • Every authenticated connection — Noise IK at the mesh layer, XK at the session layer, both prove you hold the matching secret key.

A keypair, briefly. Nostr identities are secp256k1 keypairs. The private half is the nsec (a bech32-encoded secret key); the public half is the npub (a bech32-encoded public key). The daemon needs the nsec to sign messages and complete handshakes; the rest of the world only sees the npub and uses it as your address.

The daemon supports two ways of holding that keypair:

Ephemeral vs. persistent.

  • Ephemeral (the default): the daemon mints a brand-new keypair every time it starts, kept only in memory. No long-term secret is ever written to disk; nothing on your machine ties one run to the next; the npub your daemon presents to the network is fresh on every restart. This is the safe-by-default posture — your node has no persistent identity unless you explicitly ask for one.
  • Persistent: the daemon reads (or, on first start, generates and writes) a keypair stored at /etc/fips/fips.key. The npub stays the same across restarts, reboots, and reinstalls as long as that file is preserved. You take on the cost of protecting an on-disk secret in exchange for being addressable by a stable name.

Persistent identity is a deliberate trade. You give up the ephemeral default's privacy posture — once your npub is stable, every connection your node makes is correlatable across time — and you take on a real secret-management responsibility. In return you get two things you can't get any other way:

  1. Other operators can reference you by npub. In join-the-test-mesh you added test-us01 to your peers: list by its npub. That entry only works because test-us01's npub doesn't change. If anyone is going to reach you the same way, your npub has to be just as stable.
  2. Services on your node get a fixed address. The host-a-service tutorial walks through running an HTTP server addressable as <your-npub>.fips. Clients reach the service by that name; if your npub changes on every restart, every client's address book breaks.

Both of these are good reasons. Neither is automatic — if your node is purely a client, reaching out to others without hosting anything itself, you may legitimately want to stay on the ephemeral default. The rest of this tutorial assumes you've decided you want a stable identity.

Step 1: Note your current ephemeral npub

Before changing anything, capture the npub the daemon is using right now so you can compare against it after the switch.

sudo fipsctl show status | grep '"npub"'

You'll see one line like:

"npub": "npub1abc...xyz"

Make a note of it. We expect this to change.

Step 2: Enable persistent identity in the config

Open /etc/fips/fips.yaml and find the node: block. The shipped default has the relevant fragment commented out; make it look like this:

node:
  identity:
    persistent: true

Save the file. That is the only configuration change.

The daemon's behavior on the next restart:

  • If /etc/fips/fips.key already exists, load it and use that identity.
  • If it does not exist, generate a fresh keypair, write it to /etc/fips/fips.{key,pub} with the correct file modes, and use that.

Step 3: Restart the daemon

sudo systemctl restart fips
sudo systemctl status fips

Status should show active (running) within a couple of seconds. Confirm the new identity is in use:

sudo fipsctl show status | grep '"npub"'

The npub should be different from the one in Step 1 — the daemon discarded the old in-memory ephemeral keypair and minted a new one which it has now persisted to disk. From here forward this is your npub.

Step 4: Locate the keypair on disk

The daemon wrote two files:

sudo ls -l /etc/fips/fips.key /etc/fips/fips.pub

Expect:

-rw------- 1 root root  ... fips.key
-rw-r--r-- 1 root root  ... fips.pub

The public file is safe to share — it is your address:

sudo cat /etc/fips/fips.pub

This must match the npub reported by sudo fipsctl show status. Hand this string to anyone you want to be reachable from; they paste it into their own peers: block as the npub: field.

The private file is the secret. Do not cat or paste its contents anywhere — there is no reason to see it, and any line of shell history or screen capture that contains it has captured the secret.

Step 5: Verify it survives a restart

Restart once more to confirm the daemon is reading fips.key rather than re-generating it:

sudo systemctl restart fips
sudo fipsctl show status | grep '"npub"'

The npub should match Step 3 exactly. If it does not, the daemon was unable to read fips.key (most likely a permission problem) — see Troubleshooting.

Step 6: Protect the nsec

fips.key is the only thing standing between you and someone else impersonating your node. The daemon ships it with the right permissions; the operator's job is to keep them that way.

What that means in practice:

  • Do not loosen the file mode. 0600 (read/write for owner only — here root) is correct; chmod 0644 to "fix" a permission error puts the secret on display to every account on the host.
  • Do not commit it to source control. If you maintain configuration in a Git repo, exclude fips.key; if you use ansible-vault or a similar mechanism, encrypt it. fips.pub may be checked in freely.
  • Do not paste it into chat or email. Operators sometimes share config snippets to demonstrate a setup; redact the contents of fips.key to a placeholder before doing so.
  • Back it up the way you back up an SSH host key. Treat the file (or its contents) the same way you would treat /etc/ssh/ssh_host_ed25519_key: encrypted, offline, available to recover the same identity if the host disappears.

There is no in-protocol "key change" message in FIPS. If fips.key is lost, the npub is lost — your node will come back up with a new identity and every downstream reference to the old one will be stale.

What you've learned

  • Identity = keypair. Every FIPS node is a Nostr keypair; the npub is its address, the nsec is its credential.
  • The flag. node.identity.persistent: true in /etc/fips/fips.yaml is the difference between ephemeral and persistent identity.
  • Where it lives. /etc/fips/fips.key and /etc/fips/fips.pub, mode 0600 and 0644, owned root:root.
  • What to share. fips.pub is public; fips.key is not.
  • What it buys you. A npub other operators can add to their peers: list once, and that addresses the services your node will eventually run.

Troubleshooting

If the post-restart npub does not match fips.pub:

  • Check file permissions. sudo ls -l /etc/fips/fips.key. If the mode is not 0600 or the owner is not root:root, the daemon may have refused to read it. Restore with sudo chmod 0600 /etc/fips/fips.key && sudo chown root:root /etc/fips/fips.key.
  • Check the journal. sudo journalctl -u fips -n 100 after the restart will show one of:
    • Loaded persistent identity from key file path=... — good.
    • Generated persistent identity, saved to key file ... — also good, but only expected on the first start after the flag flip.
    • Using ephemeral identity (new keypair each start) — the config flag was not picked up; re-check the indentation of the persistent: true line.

If you see Generated persistent identity... on every start, the file is being written but not read on subsequent starts; this is almost always the same permission/path issue.

What's next

  • Resolve peer addresses via Nostr. resolve-peers-via-nostr walks through the smallest useful step toward Nostr-mediated discovery: keep your peer entry, drop its hard-coded address, and let the daemon look up the current endpoint from public Nostr relays. The first of three tutorials covering Nostr discovery; advertising your own node and open ambient discovery come next.

  • Reach services on other mesh nodes. reach-mesh-services drives nc, traceroute6, curl, and ssh at peers by .fips name and shows that the FIPS data plane is just IPv6 from an application's point of view.

  • Host a service of your own. host-a-service walks through bringing up an HTTP server addressable as <your-npub>.fips, bound to fips0 so the exposure is mesh-only, behind the mesh firewall.

For the alternative provisioning paths — minting a keypair with fipsctl keygen before the daemon ever starts, or importing an existing Nostr nsec — and the key-rotation procedure, see the operator-style recipe at ../how-to/persistent-identity.md.

For the full identity model: