Files
n_os_tr/plans/iso_architecture.md
2026-05-02 09:21:19 -04:00

402 lines
14 KiB
Markdown

# n-OS-tr ISO Architecture — Design Notes
This document captures the key design decisions for the n-OS-tr Debian Live
ISO build. It is the companion to the phased todo list and is intended to
survive individual work sessions.
## 1. Big-picture goal
A single bootable/installable Debian Live ISO that, the moment it finishes
booting, has all n-OS-tr services installed and configured, then presents a
boot-time identity + service-selection TUI before login:
- [`fips`](../includes/fips) — Rust mesh-routing daemon, npub-addressed IPv6 overlay (optional start)
- [`c-relay`](../includes/c-relay) — C Nostr relay with event-based config and embedded admin (optional start)
- [`ginxsom`](../includes/ginxsom) — C Blossom (blob) server as FastCGI behind nginx (optional start)
- `nginx` — TLS front door, static blob serving, reverse proxy (optional start)
No Docker. No `curl | bash` provisioning. All three apps run as native
systemd services directly on the host.
## 2. Repo layout (target)
```
/ project root
├── live-build/ upstream live-build vendored
├── lb-wrapper.sh thin wrapper for ./live-build/frontend/lb
├── iso/ the live-build config (promoted from tutorial2/)
│ ├── auto/
│ │ ├── config lb config invocation with our flags
│ │ ├── build optional
│ │ └── clean optional
│ ├── config/
│ │ ├── package-lists/
│ │ │ ├── base.list.chroot
│ │ │ ├── fips-deps.list.chroot
│ │ │ ├── tor.list.chroot (optional)
│ │ │ ├── gui.list.chroot (optional)
│ │ │ └── installer.list.chroot (optional)
│ │ ├── hooks/
│ │ │ └── live/
│ │ │ ├── 0010-fetch-artifacts.hook.chroot
│ │ │ └── 0020-enable-services.hook.chroot
│ │ ├── includes.chroot/
│ │ │ ├── etc/systemd/system/*.service
│ │ │ ├── etc/nginx/sites-available/n-os-tr.conf
│ │ │ ├── etc/nginx/fastcgi_params
│ │ │ ├── etc/nginx/mime.types
│ │ │ ├── usr/local/sbin/n-os-tr-firstboot
│ │ │ ├── var/www/html/index.html
│ │ │ └── etc/issue
│ │ └── SHA256SUMS pinned artifact digests
│ └── local-artifacts/ optional offline override (gitignored)
├── includes/ the three component projects
│ ├── c-relay/
│ ├── ginxsom/
│ └── fips/
├── plans/ this doc and future design notes
└── README.md
```
Legacy scripts ([`root.sh`](../root.sh), [`nginx.sh`](../nginx.sh),
[`strfry.sh`](../strfry.sh), [`deploy.sh`](../deploy.sh),
[`test.sh`](../test.sh), [`docker.sh`](../docker.sh),
[`n-os-tr.sh`](../n-os-tr.sh)) are obsolete for the ISO build. They can
move to `scratch/` for historical reference or be deleted.
## 3. Runtime architecture on the booted ISO
```mermaid
flowchart TB
subgraph Ext[External]
U1([Internet peer])
U2([LAN host])
U3([Tor client])
end
subgraph Host[n-OS-tr booted ISO]
direction TB
Ng[nginx<br/>:80 :443]
C[c-relay<br/>127.0.0.1:8888]
G[ginxsom FastCGI<br/>/tmp/ginxsom-fcgi.sock]
F[fips daemon<br/>fips0 TUN]
Fd[fips-dns<br/>:5354]
Blobs[/var/www/blobs/]
RelayDb[/var/lib/c-relay/]
GinxDb[/var/lib/ginxsom/]
FbUnit[n-os-tr-firstboot<br/>oneshot]
FbUnit -.generates keys.-> KeyStore[/var/lib/n-os-tr/]
KeyStore -.EnvironmentFile.-> G
KeyStore -.identity.-> F
Ng -->|wss /relay| C
Ng -->|GET /sha256| Blobs
Ng -->|FastCGI PUT DELETE LIST HEAD| G
G --> Blobs
G --> GinxDb
C --> RelayDb
end
U1 --> Ng
U2 --> Ng
U3 -.onion.-> Ng
U1 -.npub IPv6.-> F
```
Notes:
- `c-relay` generates its own admin keypair on first start and prints the
nsec to the journal once. This is c-relay's native behavior and we should
not override it.
- `ginxsom` needs `--server-privkey` from **somewhere**. Today
[`ginxsom.service`](../includes/ginxsom/ginxsom.service:16) hardcodes one.
We replace that with an `EnvironmentFile` populated by the first-boot unit.
- `fips` can run ephemeral by default; persistent mode drops a `fips.key`
next to [`fips.yaml`](../includes/fips/packaging/common/fips.yaml:1).
## 4. Boot sequence
```mermaid
sequenceDiagram
participant LB as live-boot / initramfs
participant Sd as systemd PID 1
participant Fb as n-os-tr-firstboot.service
participant IA as nostr-id.service
participant TUI as nostr-id-tui.service
participant Fp as fips.service
participant Cr as c-relay.service
participant Gx as ginxsom.service
participant Ng as nginx.service
participant GT as getty@tty1.service
LB->>Sd: hand off
Sd->>Fb: start oneshot (Before= identity + app units)
alt First boot
Fb->>Fb: mkdir /var/lib/n-os-tr<br/>generate ginxsom key<br/>generate fips key<br/>write EnvironmentFile<br/>touch .provisioned
else Already provisioned
Fb-->>Sd: exit 0 (ConditionPathExists trip)
end
Fb-->>Sd: done
Sd->>IA: start identity-agent
IA-->>Sd: active (control socket ready)
Sd->>TUI: start boot-time identity + services flow on tty1
TUI->>IA: load_mnemonic / load_bunker / load_amnesia
IA-->>TUI: identity loaded
TUI->>Sd: start/stop selected services via systemctl
opt User enabled nginx
Sd->>Ng: start
end
opt User enabled c-relay
Sd->>Cr: start
end
opt User enabled ginxsom
Sd->>Gx: start
end
opt User enabled fips
Sd->>Fp: start
end
TUI->>Sd: exit 0
Sd->>GT: start (after TUI exits)
```
## 5. Phase 0.A — Gitea artifact fetch
### Problem
Three binaries/packages are built in Gitea and must land inside the chroot
during `lb build`. We will not commit binaries to this repo.
### Strategy
A chroot hook [`0010-fetch-artifacts.hook.chroot`](../iso/config/hooks/live/0010-fetch-artifacts.hook.chroot)
that:
1. Checks for [`iso/local-artifacts/`](../iso/local-artifacts) first (offline
dev builds). If present, copy from there and skip the network path.
2. Otherwise `curl`s each artifact from Gitea release URLs. Auth via
`GITEA_TOKEN` environment variable passed through to the hook via
live-build's `--bootstrap-options` or a `.env`-style sourced file in
`auto/config`.
3. Verifies each download against [`iso/SHA256SUMS`](../iso/SHA256SUMS).
Any mismatch fails the build.
4. Installs:
- `fips_*.deb``dpkg -i` (pulls in systemd units, `fipsctl`, `fipstop`)
- `c_relay_x86``/opt/c-relay/c_relay_x86`, chmod 0755, owned by
system user `c-relay`
- `ginxsom-fcgi_static_x86_64``/usr/local/bin/ginxsom/ginxsom-fcgi`
5. Removes the download staging directory and runs `apt-get clean`.
### Open questions (in todos)
- Are the Gitea repos/releases public or do they need a token?
- How do we pass the token through to `lb build` (it runs as root)?
- Do we build `fips` from source via `cargo-deb` inside the chroot as
a fallback, or only consume a prebuilt `.deb`?
## 6. Phase 2.B — nginx + TLS template
### Listening surface
| Path | Method | Handler | Rationale |
|---|---|---|---|
| `/` | GET | static index.html from `/var/www/html/` | Landing page |
| `/relay` | GET upgrade | `proxy_pass http://127.0.0.1:8888` with WebSocket upgrade headers | c-relay Nostr wss |
| `/admin/` | GET | `proxy_pass http://127.0.0.1:8888/api/` | c-relay embedded admin |
| `^/[a-f0-9]{64}$` | GET HEAD | `try_files /var/www/blobs/$uri =404` | Direct disk serve — [`ginxsom's`](../includes/ginxsom/README.md:33) core design |
| `/upload` | PUT HEAD | `fastcgi_pass unix:/tmp/ginxsom-fcgi.sock` | Authenticated upload |
| `^/[a-f0-9]{64}$` | DELETE | `fastcgi_pass unix:/tmp/ginxsom-fcgi.sock` | Authenticated delete |
| `/list/` | GET | `fastcgi_pass unix:/tmp/ginxsom-fcgi.sock` | Per-pubkey listing |
| `/mirror` | PUT | `fastcgi_pass unix:/tmp/ginxsom-fcgi.sock` | BUD-04 |
| `/report` | PUT | `fastcgi_pass unix:/tmp/ginxsom-fcgi.sock` | BUD-09 |
### TLS posture — chosen default
**Self-signed cert generated on first boot**, valid for the box's hostname
and any DNS names in `/etc/n-os-tr/tls-names`. Reason: the ISO has no idea
what domain name it will live under, and most first users will run on a
LAN or behind another reverse proxy. A self-signed cert means wss:// works
out of the box for clients that trust it.
We ship an enable-certbot helper script for users who have a real domain:
```
n-os-tr-certbot <domain>
```
This stops nginx, runs `certbot certonly --standalone`, rewrites the cert
paths in the nginx config, and restarts nginx.
### Key nginx template snippets
WebSocket proxy to c-relay (the Upgrade dance is mandatory):
```nginx
location /relay {
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}
```
Direct blob serve with FastCGI fallback for non-GET:
```nginx
# GET of a 64-hex path: serve straight from disk
location ~ "^/(?<hash>[a-f0-9]{64})$" {
root /var/www/blobs;
try_files /$hash @ginxsom;
}
location @ginxsom {
fastcgi_pass unix:/tmp/ginxsom-fcgi.sock;
include fastcgi_params;
}
```
Uploads:
```nginx
location /upload {
client_max_body_size 100m;
fastcgi_pass unix:/tmp/ginxsom-fcgi.sock;
include fastcgi_params;
}
```
## 7. Phase 3.C — First-boot provisioning
### Unit
```ini
# /etc/systemd/system/n-os-tr-firstboot.service
[Unit]
Description=n-OS-tr first-boot key provisioning
ConditionPathExists=!/var/lib/n-os-tr/.provisioned
Before=c-relay.service ginxsom.service fips.service nginx.service
RequiredBy=c-relay.service ginxsom.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/n-os-tr-firstboot
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
```
### The script
Outline of `/usr/local/sbin/n-os-tr-firstboot`:
```bash
#!/bin/bash
set -euo pipefail
STATE=/var/lib/n-os-tr
KEYS=$STATE/keys
ENV=$STATE/env
mkdir -p "$STATE" "$KEYS" /var/www/blobs /var/lib/c-relay /var/lib/ginxsom
chmod 0700 "$KEYS"
# 1. ginxsom server keypair
if [[ ! -f $KEYS/ginxsom.nsec ]]; then
nak key generate > "$KEYS/ginxsom.nsec"
chmod 0400 "$KEYS/ginxsom.nsec"
fi
GINX_SEC=$(nak decode "$(cat "$KEYS/ginxsom.nsec")" | jq -r .private_key)
# 2. fips identity (optional persistent mode)
# If operator wants persistent identity, uncomment:
# if [[ ! -f /etc/fips/fips.key ]]; then
# fips keygen > /etc/fips/fips.key
# chmod 0600 /etc/fips/fips.key
# fi
# 3. self-signed TLS cert if none
if [[ ! -f /etc/ssl/n-os-tr/fullchain.pem ]]; then
mkdir -p /etc/ssl/n-os-tr
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-subj "/CN=$(hostname)" \
-keyout /etc/ssl/n-os-tr/privkey.pem \
-out /etc/ssl/n-os-tr/fullchain.pem
fi
# 4. Write the environment file consumed by ginxsom.service
cat > "$ENV" <<EOF
GINXSOM_SERVER_PRIVKEY=$GINX_SEC
EOF
chmod 0400 "$ENV"
# 5. Ownership
chown -R www-data:www-data /var/www/blobs
chown -R c-relay:c-relay /var/lib/c-relay 2>/dev/null || true
touch "$STATE/.provisioned"
```
Notes:
- c-relay is intentionally **not** provisioned here. On first start it
generates its own admin keypair and prints the nsec to the journal. The
operator is expected to grab it from `journalctl -u c-relay`.
- The `ginxsom.service` we ship is a modified version that replaces the
hardcoded `--server-privkey` with `--server-privkey ${GINXSOM_SERVER_PRIVKEY}`
and adds `EnvironmentFile=-/var/lib/n-os-tr/env`.
### MOTD hint
`/etc/motd` will include:
```
Welcome to n-OS-tr.
Your Nostr relay admin key was generated on first boot. Retrieve it with:
journalctl -u c-relay | grep -i 'admin'
Your Blossom server pubkey:
cat /var/lib/n-os-tr/keys/ginxsom.nsec
Your FIPS identity:
fipsctl identity
```
## 8. Identity / persistence posture
Three supported modes, one default:
| Mode | Who it's for | What happens on reboot |
|---|---|---|
| **Ephemeral (default)** | Demos, privacy-maximal users, disposable relays | All keys regenerate, all data is lost |
| **Persistence partition** | Users running from USB who want continuity | Second partition labeled `persistence` with `persistence.conf` listing `/var/lib/n-os-tr`, `/var/lib/c-relay`, `/var/lib/ginxsom`, `/var/www/blobs` as `union` mounts. Keys and data survive reboots of that stick. |
| **Installed to disk** | Long-running nodes | Normal Debian behavior; user ran the debian-installer from the live session |
All three work from the same ISO image. The user chooses posture by how
they write the ISO and what partitions they create.
## 9. Known traps / risks
1. **c-relay has its own port** (8888) and its own `/api/` admin path,
while nginx also wants to serve `/admin/`. We either reverse-proxy
`/admin/``localhost:8888/api/` or expose c-relay's admin on a
separate port bound to localhost and forward it over SSH only.
2. **`ginxsom` expects spawn-fcgi** per the unit file. Make sure
`spawn-fcgi` is in `base.list.chroot`.
3. **libwebsockets ABI** — the c-relay binary is built static-musl per
[`STATIC_MUSL_GUIDE.md`](../includes/c-relay/STATIC_MUSL_GUIDE.md), so
we don't need to match the libwebsockets soname in Debian. Good.
4. **ginxsom static binary** is x86_64 only today; arm64 comes in Phase 6.
5. **`fips` Debian package** declares its own systemd units; don't
duplicate them via `includes.chroot` — install via `dpkg -i` and let
the postinst do the work.
6. **Live-build caches aggressively**; remember `lb clean --purge` between
structural changes.
## 10. Success criteria for the first real ISO
A single end-to-end demo, run from a booted QEMU VM:
- [ ] `systemctl status fips` → active, `fipsctl identity` prints an npub
- [ ] `systemctl status c-relay` → active, `journalctl -u c-relay` shows the admin nsec printed once
- [ ] `systemctl status ginxsom nginx` → both active
- [ ] `curl -k https://localhost/` returns the landing page
- [ ] `websocat wss://localhost/relay` accepts a NIP-01 `REQ` and returns an `EOSE`
- [ ] `curl -k -X PUT https://localhost/upload --data-binary @foo.txt` with a signed authorization header stores a blob, then `curl -k https://localhost/<sha256>` retrieves the same bytes