LAN Rehearsal Node deployment (appliance LAN profile)
Status: descriptive · non-production. How to deploy the demo-profile
rehearsal appliance as a dedicated VM on an operator-controlled hypervisor,
reachable from LAN workstations through one stable browser origin. Placeholder
values (rehearsal.example.internal, IPs, VM ids) are the operator's; concrete
homelab values are deliberately not recorded in this public repository.
This deployment may claim: LAN-hosted development rehearsal · non-production Rehearsal Node · organizer-to-member workflow · current-main software build · local operator-controlled infrastructure · role-scoped sessions · digest-bound confirmation · receipts and secret-free evidence. It may not claim: production-ready, pilot-ready, organizer-approved, accessibility-complete, live federation, real institutional deployment, formal NYCN pilot, production trusted issuance.
Architecture
flowchart LR
B["Browser on a LAN workstation<br/>https://rehearsal.example.internal/"]
B -->|"443 (TLS, internal CA)"| N
subgraph VM["Dedicated appliance VM (Debian 13, onboot)"]
N["nginx — the single origin"]
N -->|"static"| S["/usr/share/icn/static/web<br/>rehearsal-landing + member-shell + fixtures"]
N -->|"/v1/*"| G["icnd gateway 127.0.0.1-visible bind :8080<br/>JWT auth, rehearsal build mode"]
N -->|"/v1/dev/demo/*"| D["icn-demo-session :8091<br/>LOOPBACK ONLY — fixed commands"]
D --> SEED["icn-demo-seed / icn-demo-status<br/>trusted-local mint, per-role scopes"]
G --> ST[("sled state<br/>/var/lib/icn<br/>durable receipts")]
end
- Every browser fetch is same-origin; the session credential is held in page memory only (never URL, cookie, or storage).
- The session endpoint keeps its loopback bind; nginx is the only LAN path to it, and its server-side origin allowlist carries exactly the one deployed origin.
- DNS: an A record for the origin host in the operator's internal zone. TLS: a leaf from the operator's internal CA (workstations must already trust its root). No public DNS, no tunnel, no port-forward.
Build the image
From a clean checkout of the intended commit, on a build host with
qemu-img, virt-customize, virt-sysprep, cargo:
ICN_APPLIANCE_BASE_IMAGE=/path/to/debian-13-genericcloud-amd64.qcow2 \
ICN_APPLIANCE_OUTPUT_DIR=/path/to/out \
ICN_APPLIANCE_DEMO_PROFILE=1 \
ICN_APPLIANCE_LAN_PROFILE=1 \
ICN_APPLIANCE_LAN_ORIGIN=https://rehearsal.example.internal \
ICN_APPLIANCE_LAN_TLS_CERT=/path/to/rehearsal.crt \
ICN_APPLIANCE_LAN_TLS_KEY=/path/to/rehearsal.key \
ICN_APPLIANCE_VERSION=0.0.3-lan-$(git rev-parse --short HEAD) \
bash deploy/appliance/build-image.sh --real
The typed manifest (*.manifest.json) records the exact git commit —
that is the deployment's provenance of record. See
deploy/appliance/lan/README.md for the profile contract — including the
build-host networking caveats (ICN_APPLIANCE_BUILD_DNS, and pre-staging
nginx/qemu-guest-agent into the base image when the build host's
libguestfs has no usable appliance network).
Deploy on Proxmox (reference procedure)
# on the hypervisor node, with the image copied to /var/lib/vz/
qm create <vmid> --name icn-rehearsal-01 --memory 4096 --cores 2 --cpu host \
--net0 virtio,bridge=<lan-bridge-or-vnet> --scsihw virtio-scsi-single \
--serial0 socket --vga serial0 --agent enabled=1 --onboot 1 --ostype l26
qm importdisk <vmid> /var/lib/vz/icn-appliance-<version>-amd64.qcow2 <storage>
qm set <vmid> --scsi0 <storage>:vm-<vmid>-disk-0 --boot order=scsi0
qm disk resize <vmid> scsi0 +17G
qm set <vmid> --ide2 <storage>:cloudinit \
--ipconfig0 ip=<static-ip>/24,gw=<gateway> \
--nameserver <internal-dns> --searchdomain <internal-domain> \
--ciuser icnops --sshkeys /root/operator-key.pub
qm start <vmid>
Cloud-init (present in the Debian genericcloud base) applies the static
network, grows the root filesystem, and creates the operator SSH user; the
appliance firstboot unit independently generates per-instance secrets and
initializes icnd. First boot takes a few minutes.
Then add the internal DNS A record for the origin host → the VM's static IP.
Services and restart behavior
| Unit | Role | Restart |
|---|---|---|
icnd |
gateway + governance (rehearsal build mode) | on-failure; enabled |
icn-member-shell |
loopback-era static server (kept for smoke parity) | always; enabled |
icn-demo-session |
loopback session/status endpoint | on-failure; enabled |
nginx |
the single LAN origin | Debian default; enabled |
icn-appliance-firstboot |
oneshot secret/identity init | first boot only |
qemu-guest-agent |
hypervisor integration | enabled |
Everything is systemctl enabled — a VM reboot recovers the full stack with
no operator action. Durable receipts (sled, /var/lib/icn) survive restarts
and reboots; the rehearsal pending-publish workspace view is rebuilt per
process (see REHEARSAL_RESET_AND_RECOVERY.md).
Operate
Use deploy/appliance/scripts/icn-rehearsal-ctl.sh from the operator machine
(REHEARSAL_SSH=icnops@<vm>): status, start|stop|restart, logs [unit],
verify, reset (destructive, confirmed), update (prints the procedure).
Logs: journalctl -u icnd|icn-member-shell|icn-demo-session|nginx on the VM.
Update / rollback
Update = rebuild the image from the new commit → copy → stop VM → re-import disk → boot → re-run the workstation walkthrough. Replacing the disk starts a fresh node (new identity, empty receipts) — the supported, honest update for a non-production rehearsal appliance. Rollback = re-import the previous qcow2 (keep at least one prior image). An in-place binary update path is deliberately not offered.
Reset / teardown / backup
- Per-run reset: the landing page's Start a new rehearsal (workspace generation reset; receipts remain).
- Full reset:
sudo icn-demo-reseton the VM (wipes/var/lib/icn, regenerates secrets). - Teardown:
qm stop <vmid> && qm destroy <vmid>, then remove the DNS record. - Backup: the appliance is disposable by design — the image + repo commit are
the recovery path. Snapshot before risky experiments if desired
(
qm snapshot), but no backup schedule is warranted for fictional data.
Known limitations
- The bearer session credential transits the LAN inside TLS when the origin is
https; with a plain-httporigin it is unencrypted on the LAN — the build warns, andhttpswith an internal CA is the intended posture. - The gateway (8080), the member-shell static server (8090), and the session
endpoint (8091) are all loopback-bound in the LAN profile; nginx is the
only LAN-reachable HTTP surface. (This narrows the demo profile, which binds
the gateway/shell to
0.0.0.0.) - nginx serves no CSP header yet (the static pages set none) — candidate follow-up, not a regression versus the witnessed loopback posture.
- The status endpoint (
GET /v1/dev/demo/status) is unauthenticated by design and therefore returns counts only — never titles, DIDs, hashes, or credentials.