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

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-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 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.

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:

  1. Checks for iso/local-artifacts/ first (offline dev builds). If present, copy from there and skip the network path.
  2. Otherwise curls 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. Any mismatch fails the build.
  4. Installs:
    • fips_*.debdpkg -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 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.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, 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