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

305 lines
11 KiB
Markdown

# Make Your Node's Identity Persistent
After completing
[join-the-test-mesh](join-the-test-mesh.md), 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
```text
┌─────────────────────────────────────────┐
│ /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](join-the-test-mesh.md) 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](host-a-service.md) 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.
```sh
sudo fipsctl show status | grep '"npub"'
```
You'll see one line like:
```text
"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:
```yaml
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
```sh
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:
```sh
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:
```sh
sudo ls -l /etc/fips/fips.key /etc/fips/fips.pub
```
Expect:
```text
-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:
```sh
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:
```sh
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](#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](resolve-peers-via-nostr.md) 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](reach-mesh-services.md) 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](host-a-service.md) 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](../how-to/persistent-identity.md).
For the full identity model:
- [../design/fips-architecture.md](../design/fips-architecture.md)
— how npubs become `NodeAddr`s and IPv6 ULAs.