Debian backend
This guide sets up the server that sits behind Cloudflare: the Nexus gateway, the
cloudflared tunnel, the agent orchestrator, a workspace agent and the
nexus-ro read-only helper. Everything listens on loopback, and the only network
path in is the outbound tunnel.
Commands assume Debian 13, a non-root admin account with sudo, and a checkout of
NexusiOS on a trusted build machine.
Replace server with your account name.
[!IMPORTANT] Keep a second way in (console or Tailscale) while you change SSH or the firewall. Never expose ports 8787, 8896, 8897, 8080, 8081 or 8082, and never disable SSH host-key or TLS checks to make something work.
1. Base system
sudo apt update && sudo apt install -y openssh-server nftables git curl
Install Tailscale and sign in. It’s your recovery path and the private route for the apps’ direct SSH.
2. Key-only SSH and firewall
Install your Ed25519 public key first. From your computer, use
scripts/install-verified-ssh-key.sh, which checks the host fingerprint you read
on the server before running ssh-copy-id:
# on the server
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256
# on your computer
./scripts/install-verified-ssh-key.sh server.example.ts.net server ~/.ssh/nexus-ed25519.pub 'SHA256:…'
Then copy scripts/debian-harden.sh and scripts/debian/nexus-hardening.nft to
the server, edit the home-network ranges in the .nft file, and apply:
sudo SSH_OWNER=server ./debian-harden.sh apply # rolls back after 5 minutes unless confirmed
# open a NEW SSH session to check you can still get in, then:
sudo SSH_OWNER=server ./debian-harden.sh confirm
This turns off password and keyboard-interactive SSH, and loads an inbound
firewall: loopback, Tailscale and your home ranges are allowed, RCON is blocked,
and everything else is dropped. It refuses to run if your account has no Ed25519
key or Tailscale is down. rollback undoes everything.
3. Service account and folders
The system unit runs as a dedicated user:
sudo useradd --system --home /var/lib/nexus-gateway --shell /usr/sbin/nologin nexus-gateway
sudo install -d -m 0750 -o root -g nexus-gateway /etc/nexus-gateway
You can instead run the gateway as your own user with
gateway/nexus-gateway-user.service, which reads
~/.config/nexus-gateway/nexus.env. The rest of this guide uses the system unit.
4. Gateway
Build a static binary on your computer and copy it over:
cd gateway
CGO_ENABLED=0 go test ./...
CGO_ENABLED=0 go build -trimpath -o nexus-gateway .
scp nexus-gateway gateway/nexus-gateway.service server@server.example.ts.net:
sudo install -m 0755 nexus-gateway /usr/local/bin/nexus-gateway
sudo install -m 0644 nexus-gateway.service /etc/systemd/system/
Create /etc/nexus-gateway/nexus.env from
gateway/.env.example
(mode 0640, root:nexus-gateway). The gateway refuses to start until the
required values are set:
| Setting | Value |
|---|---|
NEXUS_GATEWAY_LISTEN |
127.0.0.1:8787 (non-loopback is refused) |
NEXUS_OIDC_ISSUER |
https://<team>.cloudflareaccess.com |
NEXUS_OIDC_AUDIENCE |
The Access application’s AUD tag |
NEXUS_OIDC_JWKS_URL |
https://<team>.cloudflareaccess.com/cdn-cgi/access/certs |
NEXUS_OIDC_PRINCIPAL_CLAIM |
email |
NEXUS_SUBJECT_PERMISSIONS_JSON |
{"you@example.com":["ai.models","ai.chat","ssh.connect"]} (add ai.search, ai.history, minecraft.panel as needed) |
NEXUS_AI_UPSTREAM_URL |
http://<ps5-address>:8081/v1 |
NEXUS_AI_ALLOWED_MODELS |
Model IDs from the PS5’s /v1/models |
NEXUS_MODEL_STATE_PATH |
/var/lib/nexus-gateway/model-state/pending-switch.json |
NEXUS_ORCHESTRATOR_TOKEN |
Same value as the orchestrator’s (step 6) |
Optional features stay off until configured:
| Feature | Settings |
|---|---|
| Web search | NEXUS_SEARCH_PROVIDER=brave + NEXUS_BRAVE_API_KEY, or searxng + NEXUS_SEARXNG_URL; grant ai.search |
| Conversation sync | NEXUS_CONVERSATION_ENCRYPTION_KEY (base64 of 32 random bytes; back it up), NEXUS_CONVERSATIONS_DIR; grant ai.history |
| Minecraft panel | NEXUS_MINECRAFT_PANEL_ORIGIN, NEXUS_MINECRAFT_PUBLIC_ORIGIN; grant minecraft.panel |
Generate the sync key straight into the file so it’s never printed:
echo "NEXUS_CONVERSATION_ENCRYPTION_KEY=$(head -c32 /dev/urandom | base64)" | sudo tee -a /etc/nexus-gateway/nexus.env >/dev/null
sudo systemctl daemon-reload
sudo systemctl enable --now nexus-gateway
curl -s http://127.0.0.1:8787/healthz
5. SSH relay keys
The relay lets the apps open SSH through the Worker without holding a key for the target. The gateway authenticates to each target with its own key.
sudo install -d -m 0700 -o nexus-gateway -g nexus-gateway /var/lib/nexus-gateway/ssh
sudo -u nexus-gateway ssh-keygen -t ed25519 -N '' -f /var/lib/nexus-gateway/ssh/upstream_ed25519
sudo -u nexus-gateway ssh-keygen -t ed25519 -N '' -f /var/lib/nexus-gateway/ssh/relay_host_ed25519
On each target account, authorise the upstream public key restricted to a PTY:
restrict,pty ssh-ed25519 AAAA… nexus-gateway-relay
Record each target’s host key in /var/lib/nexus-gateway/ssh/known_hosts after
checking its fingerprint on that machine, then set:
NEXUS_SSH_UPSTREAM_KEY=/var/lib/nexus-gateway/ssh/upstream_ed25519
NEXUS_SSH_RELAY_HOST_KEY=/var/lib/nexus-gateway/ssh/relay_host_ed25519
NEXUS_SSH_KNOWN_HOSTS=/var/lib/nexus-gateway/ssh/known_hosts
NEXUS_SSH_TARGETS_JSON='{"nixos":{"host":"192.0.2.10","port":22,"username":"user"},"debian":{"host":"127.0.0.1","port":22,"username":"server"}}'
The apps pin the relay host key on first use, separately from TLS.
6. Agent orchestrator
git clone https://github.com/ljcatherine/NexusiOS.git ~/nexus
mkdir -p ~/.config/nexus-orchestrator ~/.local/share/nexus-orchestrator/runs
chmod 700 ~/.config/nexus-orchestrator ~/.local/share/nexus-orchestrator
~/.config/nexus-orchestrator/env (mode 0600):
NEXUS_ORCHESTRATOR_TOKEN=<openssl rand -hex 32>
NEXUS_ORCHESTRATOR_LISTEN=127.0.0.1:8897
NEXUS_ORCHESTRATOR_RUNS_DIR=/home/server/.local/share/nexus-orchestrator/runs
NEXUS_PS5_CHAT_URL=http://<ps5-address>:8081/v1
NEXUS_PS5_MODEL_ID=auto
# optional model switching (step 9)
NEXUS_PS5_CONTROL_URL=http://<ps5-address>:8082/v1
NEXUS_PS5_CONTROL_TOKEN=<raw control token>
~/.config/systemd/user/nexus-orchestrator.service:
[Unit]
Description=Nexus agent orchestrator
[Service]
EnvironmentFile=%h/.config/nexus-orchestrator/env
ExecStart=/usr/bin/python3 %h/nexus/agent-orchestrator/server.py
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=default.target
systemctl --user enable --now nexus-orchestrator
sudo loginctl enable-linger server
Put the same NEXUS_ORCHESTRATOR_TOKEN in the gateway’s nexus.env.
7. Workspace agents
One per host (Debian and NixOS), each with its own token and roots. It needs Go 1.22+ and Linux.
cd ~/nexus/workspace-agent && go test ./... && go build -o ~/.local/bin/nexus-workspace-agent .
Environment, from
workspace-agent/.env.example (0600):
NEXUS_WORKSPACE_HOST=debian # nixos on the workstation
NEXUS_WORKSPACE_LISTEN=127.0.0.1:8896
NEXUS_WORKSPACE_TOKEN=<openssl rand -hex 32>
NEXUS_WORKSPACE_ROOTS_JSON='[{"id":"projects","name":"Projects","path":"/home/server/Projects"}]'
Run it as a user service like the orchestrator, with ProtectHome=read-only.
Roots must already exist, can’t contain symlinks, and can’t be / or a whole home
folder. Git inspection is opt-in per root ("git_read":true) and needs bubblewrap
0.12+. Give the orchestrator the URLs and tokens
(NEXUS_DEBIAN_WORKSPACE_URL/TOKEN, NEXUS_NIXOS_WORKSPACE_URL/TOKEN).
8. nexus-ro for Nexus Desktop
Nexus Desktop treats this server as read-only. nexus-ro enforces that on the
server: a forced command that only runs the Desktop’s fixed read-only catalog.
Build it from NexusNix (or NexusWindows):
cargo build --release -p nexus-ro
scp target/release/nexus-ro server@server.example.ts.net:
ssh server@server.example.ts.net 'sudo install -m 0755 nexus-ro /usr/local/bin/nexus-ro'
Add the Desktop’s public key to ~/.ssh/authorized_keys with the forced command:
restrict,command="/usr/local/bin/nexus-ro --root /home/server/Projects" ssh-ed25519 AAAA… nexus-desktop
Turn on the server catalog for that device in Nexus Desktop. The helper refuses
SFTP and anything outside the catalog with exit code 126, opens files with
O_NOFOLLOW, and writes nothing.
9. Cloudflare tunnel
Create the tunnel in Cloudflare first (Cloudflare → tunnel),
then install cloudflared 2025.7.0 or newer from Cloudflare’s Debian repository
and store the token in a root-owned file:
sudo useradd --system --shell /usr/sbin/nologin cloudflared
sudo install -d -m 0750 -o root -g cloudflared /etc/nexus-cloudflared
sudo install -m 0640 -o root -g cloudflared /dev/stdin /etc/nexus-cloudflared/tunnel-token # paste, then Ctrl-D
sudo install -m 0644 gateway/nexus-cloudflared.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now nexus-cloudflared
The unit uses --token-file so the token never appears in process arguments, and
forces QUIC (outbound UDP 7844). It requires the gateway unit to be running.
10. Check it
systemctl status nexus-gateway nexus-cloudflared
systemctl --user status nexus-orchestrator
ss -tlnp | grep -E '8787|8896|8897' # all on 127.0.0.1
sudo nft list table inet nexus_hardening
journalctl -u nexus-gateway -n 50 # audit events contain no tokens or prompts
From a phone or the Desktop, sign in through Access and load the model list.
Updating and rolling back
- Gateway: keep the previous binary (
/usr/local/bin/nexus-gateway.previous), install the new one, restart, and swap back if/healthzor sign-in fails. - Orchestrator:
git -C ~/nexus pull, then restart. Stop it before clearing an inference quarantine. - Firewall and SSH:
sudo SSH_OWNER=server ./debian-harden.sh rollback. - Tunnel:
sudo systemctl stop nexus-cloudflaredcuts all remote access at once.