OXYGENHUB
# a7aa76ac52d7 oxygenhub / docs / deploy.md
6.9 KiB · Markdown · last changed security.txt from a contact set under Administration 20 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
29
## Where things are on Earth
30
31
| | |
32
|---|---|
33
| Binaries | `/usr/local/bin/oxygenhub`, `/usr/local/bin/oxy` |
34
| Sources | `/home/claude/src/oxygen`, `/home/claude/src/oxygenhub` (rsync targets; the hub itself is the canonical remote) |
35
| Data | `/home/oxygenhub/data` (`hub.toml`, `hub.db`, `keys/`, `repos/andymccall/*`) |
36
| Push token | `/home/oxygenhub/push-token` (also in `hub.toml`); each local checkout has it as `remote.earth.token` |
37
| 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` |
38
| Service | `sudo systemctl status oxygenhub`, `sudo journalctl -u oxygenhub` |
9a8e3b44 Andy McCall ↶ 39
40
## HTTPS only (oxygenhub#218)
41
42
When `base_url` is `https://…`, the hub takes nothing over plain http
43
through a proxy: a request that `X-Forwarded-Proto` (or Cloudflare's
44
`CF-Visitor`) reports as http is redirected to the same path on
45
`base_url` (GET and HEAD) or refused with 400 (anything else), and every
46
cookie it sets is `Secure`. Requests made to the hub directly, such as a
47
runner on the same host using `http://127.0.0.1:7300`, carry no such
48
header and are served as before. In front, Cloudflare's Always Use HTTPS
d74ce22f Andy McCall ↶ 49
is on (2026-10-15). Strict-Transport-Security is not sent yet: the security headers (#222)
50
are in, and it waits for the site owner's decision, because browsers
51
remember it for months.
9a777fec Andy McCall ↶ 52
d74ce22f Andy McCall ↶ 53
## Security headers (oxygenhub#222)
54
55
Every response carries `X-Content-Type-Options: nosniff`,
56
`Referrer-Policy: strict-origin-when-cross-origin` and a
57
`Permissions-Policy` that turns off the camera, microphone, location and
58
the like; pages also carry `X-Frame-Options: DENY` and a Content Security
59
Policy (`frame-ancestors 'none'`, scripts and forms from the hub only,
60
WebAssembly for the backdrop). The policy is **report-only** for now:
61
browsers report what it would block to `/csp-report`, and the hub logs
62
each distinct violation once an hour (`csp would block: …`). The pages
63
still use inline scripts and handlers, which it reports; once they have
64
moved into `hub.js` and the log is quiet, `ENFORCE` in
65
`crates/hub/src/headers.rs` switches it on. Responses with their own
66
policy (report frames, a job's HTML artifact) keep it. HSTS is not sent:
67
it waits for the site owner's decision.
68
3dd6582d Andy McCall ↶ 69
## security.txt (oxygenhub#224)
70
71
Administration › Security contact sets where vulnerabilities are reported
72
(an email address or an https page) and, optionally, a policy page. The
73
hub then serves `/.well-known/security.txt` (and `/security.txt`) per
74
RFC 9116, with an `Expires` date that is always just under a year ahead,
75
so it never goes stale. With no contact set, both answer 404.
76
aa0bfbd1 Andy McCall ↶ 77
## Guessing is slowed (oxygenhub#221)
78
79
Password sign-in, the two-factor code step, device codes and password
80
reset requests count failures per account and per client address
81
(`behind_proxy = true` takes the address from the tunnel's
82
`CF-Connecting-IP`, so set it when the hub is behind one). An account
83
gets five free failures and an address twenty (people share addresses);
84
after that each failure doubles the wait before the next try, from two
85
seconds up to a fifteen-minute lockout, and the page says when to try
86
again. A reset request counts as a try whatever it matched, so the
87
answer still gives nothing away. A quiet hour forgets the count and a
88
successful sign-in clears the account's. Each wait that starts is in the
89
audit log (`login.throttled`, `device.throttled`,
90
`password.reset_throttled`) with the address. The counts live in memory,
91
so a restart clears them.
92
9a777fec Andy McCall ↶ 93
## Outbound requests (oxygenhub#219)
94
95
Webhooks and AI assistants call addresses people give, so the hub only
96
calls **public** addresses: it resolves the host, refuses if any address
97
it resolves to is loopback, private, shared (CGNAT), link-local (the
98
cloud metadata address among them), multicast or an IPv6 local range,
99
pins the connection to the addresses it checked, and follows no
100
redirects. A refused webhook delivery records "not sent: …" with the
101
reason; an assistant says why it cannot be reached. To use a service on
102
your own network on purpose (a local Ollama), a site administrator lists
103
it in `hub.toml`:
104
105
```toml
106
[outbound]
107
allow = ["http://127.0.0.1:11434"]   # scheme://host (any port) or scheme://host:port
108
```