# 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
