OXYGENHUB
# 9aee52d392a6 oxygenhub / docs / deploy.md
8.6 KiB · Markdown · last changed deploy.md: the socket listens where server.bind says; Earth has it now 19 days ago renderedsource blame history raw
93be9ce4 Andy McCall 1
# Deploying the developer edition on Earth
2
baebdd18 Andy McCall ↶ 3
Companion to `spec.md` §17.5. Status: **installed and running** since 2026-09-27 as the systemd service `oxygenhub` under the `oxygenhub` system user, data in `/home/oxygenhub/data`, HTTP on 7300 and HTTPS on 7343 with a self-signed certificate. `oxygen`, `oxygen-vscode` and `oxygenhub` are hosted under `andymccall/` and each checkout has an `earth` remote.
93be9ce4 Andy McCall 4
5
## The host
6
7
| | |
8
|---|---|
9
| Name | `earth` (`earth.andymccall.xyz`, IPv6 ULA; `192.168.1.6` on the LAN) |
10
| OS | Ubuntu 26.04.1 LTS, x86_64, 4 cores, 31 GiB RAM |
11
| Disk | `/` 251 GB (95 GB free), `/home` 1 TB (944 GB free) |
12
| Already running | sshd :22, a Python service :6400, `wren-nest` :7842, retroplay-manager in Docker (:8080, :6670) with Postgres and a tetriblox server, two GitHub Actions runners (`doomsdayonecom`), Docker, containerd |
13
| Not installed | Rust toolchain, sqlite3 CLI, nginx or caddy, certbot |
14
| Access | `ssh -i ~/.ssh/earth_claude claude@earth`; `claude` has passwordless sudo |
15
16
Ports 7300 (HTTP) and 7343 (HTTPS) are free and are the hub's.
17
18
## Steps
19
20
1. **Toolchain** (as `claude`): `curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal`, then `sudo apt-get install -y build-essential pkg-config git`.
21
2. **Source**: from the development machine, `rsync -a --delete --exclude target --exclude node_modules ~/development/oxygen ~/development/oxygenhub claude@earth:~/src/`. Oxygen is never pushed to Git; rsync is the bootstrap, and after the hub runs, `oxy push` to the hub itself is the ongoing path.
22
3. **Build**: `cd ~/src/oxygenhub && cargo build --release`.
23
4. **System user and data**: `sudo useradd --system --home /home/oxygenhub --create-home oxygenhub`, `sudo install -o oxygenhub -g oxygenhub -m 0755 target/release/oxygenhub /usr/local/bin/`, `sudo -u oxygenhub oxygenhub init --data /home/oxygenhub/data --admin andymccall` (prints the first admin's one-time password).
24
5. **Service**: `/etc/systemd/system/oxygenhub.service` running `oxygenhub serve --data /home/oxygenhub/data` as `oxygenhub`, `Restart=on-failure`, `ProtectSystem=strict`, `ReadWritePaths=/home/oxygenhub/data`, `NoNewPrivileges=yes`; `sudo systemctl enable --now oxygenhub`.
8f3d2ccf Andy McCall ↶ 25
6. **TLS**: the developer edition generates a self-signed certificate at `init`. Download the certificate from `https://earth.andymccall.xyz:7343/.well-known/oxyhub-ca.pem` (or over plain HTTP on port 7300) and point clients at it with `oxy config remote.<name>.ca <file>` or `OXY_CA`.
93be9ce4 Andy McCall 26
7. **First repositories**: on the development machine, `oxy remote add earth https://earth.andymccall.xyz:7343/andymccall/oxygen` in each of `oxygen`, `oxygen-vscode`, `oxygenhub`, then `oxy push earth`.
baebdd18 Andy McCall ↶ 27
8. **Updates**: rsync, build, `sudo install`, `sudo systemctl restart oxygenhub`. `make deploy` in the hub wraps steps 2, 3 and 8.
28
c92a9949 Andy McCall ↶ 29
## Restarts without errors (oxygenhub#233)
30
31
A deploy restarts the hub. Since #233, on SIGTERM (what `systemctl restart`
32
sends) the hub stops taking connections, ends its live event streams
33
(browsers reconnect by themselves) and gives the requests in flight up to
34
30 seconds to finish. Requests that arrive *between* the old process and the
35
new one are refused unless systemd holds the listening socket across the
36
restart: socket activation. The hub serves a socket systemd passes
37
(`LISTEN_FDS`) and binds `server.bind` itself otherwise.
38
39
To turn it on, add `/etc/systemd/system/oxygenhub.socket`:
40
41
```ini
42
[Unit]
43
Description=Oxygenhub's listening socket, held across restarts
44
45
[Socket]
46
ListenStream=0.0.0.0:7300
47
# A restart's queue: connections wait here, not refused.
48
Backlog=512
49
NoDelay=yes
50
51
[Install]
52
WantedBy=sockets.target
53
```
54
9aee52d3 Andy McCall ↶ 55
`ListenStream` is the address `server.bind` names in `hub.toml` (Earth's is `127.0.0.1:7300`, behind cloudflared). Then give `oxygenhub.service` `Requires=oxygenhub.socket`,
c92a9949 Andy McCall ↶ 56
`After=oxygenhub.socket` and `TimeoutStopSec=40`, and run:
57
58
```sh
59
sudo systemctl daemon-reload
60
sudo systemctl stop oxygenhub
61
sudo systemctl enable --now oxygenhub.socket
62
sudo systemctl start oxygenhub
63
```
64
9aee52d3 Andy McCall ↶ 65
On Earth the three service lines are a drop-in, `/etc/systemd/system/oxygenhub.service.d/socket.conf` (installed 2026-10-17; a restart under 10 requests a second then answered all 120 with 200).
66
c92a9949 Andy McCall ↶ 67
The service's log then says "http on the socket systemd holds". From then on
68
`systemctl restart oxygenhub` keeps the socket, so nobody sees a refused
69
connection or a Cloudflare 502. HTTPS (7343, the developer edition's
70
self-signed listener) still binds itself.
71
baebdd18 Andy McCall ↶ 72
## Where things are on Earth
73
74
| | |
75
|---|---|
76
| Binaries | `/usr/local/bin/oxygenhub`, `/usr/local/bin/oxy` |
77
| Sources | `/home/claude/src/oxygen`, `/home/claude/src/oxygenhub` (rsync targets; the hub itself is the canonical remote) |
78
| Data | `/home/oxygenhub/data` (`hub.toml`, `hub.db`, `keys/`, `repos/andymccall/*`) |
79
| Push token | `/home/oxygenhub/push-token` (also in `hub.toml`); each local checkout has it as `remote.earth.token` |
80
| Certificate | `/home/oxygenhub/data/keys/tls.crt`, served at `/.well-known/oxyhub-ca.pem`; local copy `~/.config/oxy/earth-ca.pem` set as `remote.earth.ca` |
81
| Service | `sudo systemctl status oxygenhub`, `sudo journalctl -u oxygenhub` |
9a8e3b44 Andy McCall ↶ 82
83
## HTTPS only (oxygenhub#218)
84
85
When `base_url` is `https://…`, the hub takes nothing over plain http
86
through a proxy: a request that `X-Forwarded-Proto` (or Cloudflare's
87
`CF-Visitor`) reports as http is redirected to the same path on
88
`base_url` (GET and HEAD) or refused with 400 (anything else), and every
89
cookie it sets is `Secure`. Requests made to the hub directly, such as a
90
runner on the same host using `http://127.0.0.1:7300`, carry no such
91
header and are served as before. In front, Cloudflare's Always Use HTTPS
d74ce22f Andy McCall ↶ 92
is on (2026-10-15). Strict-Transport-Security is not sent yet: the security headers (#222)
93
are in, and it waits for the site owner's decision, because browsers
94
remember it for months.
9a777fec Andy McCall ↶ 95
d74ce22f Andy McCall ↶ 96
## Security headers (oxygenhub#222)
97
98
Every response carries `X-Content-Type-Options: nosniff`,
99
`Referrer-Policy: strict-origin-when-cross-origin` and a
100
`Permissions-Policy` that turns off the camera, microphone, location and
101
the like; pages also carry `X-Frame-Options: DENY` and a Content Security
102
Policy (`frame-ancestors 'none'`, scripts and forms from the hub only,
103
WebAssembly for the backdrop). The policy is **report-only** for now:
104
browsers report what it would block to `/csp-report`, and the hub logs
105
each distinct violation once an hour (`csp would block: …`). The pages
106
still use inline scripts and handlers, which it reports; once they have
107
moved into `hub.js` and the log is quiet, `ENFORCE` in
108
`crates/hub/src/headers.rs` switches it on. Responses with their own
109
policy (report frames, a job's HTML artifact) keep it. HSTS is not sent:
110
it waits for the site owner's decision.
111
3dd6582d Andy McCall ↶ 112
## security.txt (oxygenhub#224)
113
114
Administration › Security contact sets where vulnerabilities are reported
115
(an email address or an https page) and, optionally, a policy page. The
116
hub then serves `/.well-known/security.txt` (and `/security.txt`) per
117
RFC 9116, with an `Expires` date that is always just under a year ahead,
118
so it never goes stale. With no contact set, both answer 404.
119
aa0bfbd1 Andy McCall ↶ 120
## Guessing is slowed (oxygenhub#221)
121
122
Password sign-in, the two-factor code step, device codes and password
123
reset requests count failures per account and per client address
124
(`behind_proxy = true` takes the address from the tunnel's
125
`CF-Connecting-IP`, so set it when the hub is behind one). An account
126
gets five free failures and an address twenty (people share addresses);
127
after that each failure doubles the wait before the next try, from two
128
seconds up to a fifteen-minute lockout, and the page says when to try
129
again. A reset request counts as a try whatever it matched, so the
130
answer still gives nothing away. A quiet hour forgets the count and a
131
successful sign-in clears the account's. Each wait that starts is in the
132
audit log (`login.throttled`, `device.throttled`,
133
`password.reset_throttled`) with the address. The counts live in memory,
134
so a restart clears them.
135
9a777fec Andy McCall ↶ 136
## Outbound requests (oxygenhub#219)
137
138
Webhooks and AI assistants call addresses people give, so the hub only
139
calls **public** addresses: it resolves the host, refuses if any address
140
it resolves to is loopback, private, shared (CGNAT), link-local (the
141
cloud metadata address among them), multicast or an IPv6 local range,
142
pins the connection to the addresses it checked, and follows no
143
redirects. A refused webhook delivery records "not sent: …" with the
144
reason; an assistant says why it cannot be reached. To use a service on
145
your own network on purpose (a local Ollama), a site administrator lists
146
it in `hub.toml`:
147
148
```toml
149
[outbound]
150
allow = ["http://127.0.0.1:11434"]   # scheme://host (any port) or scheme://host:port
151
```