14 KiB
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— Rust mesh-routing daemon, npub-addressed IPv6 overlay (optional start)c-relay— C Nostr relay with event-based config and embedded admin (optional start)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, nginx.sh,
strfry.sh, deploy.sh,
test.sh, docker.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
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-relaygenerates 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.ginxsomneeds--server-privkeyfrom somewhere. Todayginxsom.servicehardcodes one. We replace that with anEnvironmentFilepopulated by the first-boot unit.fipscan run ephemeral by default; persistent mode drops afips.keynext tofips.yaml.
4. Boot sequence
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
that:
- Checks for
iso/local-artifacts/first (offline dev builds). If present, copy from there and skip the network path. - Otherwise
curls each artifact from Gitea release URLs. Auth viaGITEA_TOKENenvironment variable passed through to the hook via live-build's--bootstrap-optionsor a.env-style sourced file inauto/config. - Verifies each download against
iso/SHA256SUMS. Any mismatch fails the build. - 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 userc-relayginxsom-fcgi_static_x86_64→/usr/local/bin/ginxsom/ginxsom-fcgi
- 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
fipsfrom source viacargo-debinside 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 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):
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:
# 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:
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
# /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:
#!/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.servicewe ship is a modified version that replaces the hardcoded--server-privkeywith--server-privkey ${GINXSOM_SERVER_PRIVKEY}and addsEnvironmentFile=-/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
- 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. ginxsomexpects spawn-fcgi per the unit file. Make surespawn-fcgiis inbase.list.chroot.- libwebsockets ABI — the c-relay binary is built static-musl per
STATIC_MUSL_GUIDE.md, so we don't need to match the libwebsockets soname in Debian. Good. - ginxsom static binary is x86_64 only today; arm64 comes in Phase 6.
fipsDebian package declares its own systemd units; don't duplicate them viaincludes.chroot— install viadpkg -iand let the postinst do the work.- Live-build caches aggressively; remember
lb clean --purgebetween 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 identityprints an npubsystemctl status c-relay→ active,journalctl -u c-relayshows the admin nsec printed oncesystemctl status ginxsom nginx→ both activecurl -k https://localhost/returns the landing pagewebsocat wss://localhost/relayaccepts a NIP-01REQand returns anEOSEcurl -k -X PUT https://localhost/upload --data-binary @foo.txtwith a signed authorization header stores a blob, thencurl -k https://localhost/<sha256>retrieves the same bytes