93be9ce4 Andy McCall 1 month ago
1
# Deploying the developer edition on Earth
2
baebdd18 Andy McCall 1 month ago ↶
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 1 month ago
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 1 month ago ↶
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 1 month ago
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 1 month ago ↶
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 21 days ago ↶
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
2cbf6f34 Andy McCall 20 days ago ↶
49
is on (2026-10-15). Strict-Transport-Security comes from ` [server] hsts_max_age` (seconds;
50
0, the default, sends none; never over plain http; oxygenhub#231).
51
Browsers remember it, so raise it in stages: 300 first, a week
52
(604800) after a few quiet days, then a year (31536000). Add
53
` includeSubDomains` only once every subdomain is HTTPS, and preload last.
54
oxygenhub.io is at 300 from 2026-10-16.
9a777fec Andy McCall 21 days ago ↶
55
d74ce22f Andy McCall 20 days ago ↶
56
## Security headers (oxygenhub#222)
57
58
Every response carries ` X-Content-Type-Options: nosniff` ,
59
` Referrer-Policy: strict-origin-when-cross-origin` and a
60
` Permissions-Policy` that turns off the camera, microphone, location and
61
the like; pages also carry ` X-Frame-Options: DENY` and a Content Security
62
Policy (` frame-ancestors 'none'` , scripts and forms from the hub only,
63
WebAssembly for the backdrop). The policy is ** report-only** for now:
64
browsers report what it would block to ` /csp-report` , and the hub logs
65
each distinct violation once an hour (` csp would block: …` ). The pages
66
still use inline scripts and handlers, which it reports; once they have
67
moved into ` hub.js` and the log is quiet, ` ENFORCE` in
68
` crates/hub/src/headers.rs` switches it on. Responses with their own
69
policy (report frames, a job's HTML artifact) keep it. HSTS is not sent:
70
it waits for the site owner's decision.
71
3dd6582d Andy McCall 20 days ago ↶
72
## security.txt (oxygenhub#224)
73
74
Administration › Security contact sets where vulnerabilities are reported
75
(an email address or an https page) and, optionally, a policy page. The
76
hub then serves ` /.well-known/security.txt` (and ` /security.txt` ) per
77
RFC 9116, with an ` Expires` date that is always just under a year ahead,
78
so it never goes stale. With no contact set, both answer 404.
2cbf6f34 Andy McCall 20 days ago ↶
79
` /security-contact` is the page to point the contact at: the reporting
80
address and Markdown text from the same panel (oxygenhub#230), with
81
sensible text until it is written.
3dd6582d Andy McCall 20 days ago ↶
82
aa0bfbd1 Andy McCall 20 days ago ↶
83
## Guessing is slowed (oxygenhub#221)
84
85
Password sign-in, the two-factor code step, device codes and password
86
reset requests count failures per account and per client address
87
(` behind_proxy = true` takes the address from the tunnel's
88
` CF-Connecting-IP` , so set it when the hub is behind one). An account
89
gets five free failures and an address twenty (people share addresses);
90
after that each failure doubles the wait before the next try, from two
91
seconds up to a fifteen-minute lockout, and the page says when to try
92
again. A reset request counts as a try whatever it matched, so the
93
answer still gives nothing away. A quiet hour forgets the count and a
94
successful sign-in clears the account's. Each wait that starts is in the
95
audit log (` login.throttled` , ` device.throttled` ,
96
` password.reset_throttled` ) with the address. The counts live in memory,
97
so a restart clears them.
98
9a777fec Andy McCall 21 days ago ↶
99
## Outbound requests (oxygenhub#219)
100
101
Webhooks and AI assistants call addresses people give, so the hub only
102
calls ** public** addresses: it resolves the host, refuses if any address
103
it resolves to is loopback, private, shared (CGNAT), link-local (the
104
cloud metadata address among them), multicast or an IPv6 local range,
105
pins the connection to the addresses it checked, and follows no
106
redirects. A refused webhook delivery records "not sent: …" with the
107
reason; an assistant says why it cannot be reached. To use a service on
108
your own network on purpose (a local Ollama), a site administrator lists
109
it in ` hub.toml` :
110
111
``` toml
112
[outbound]
113
allow = ["http://127.0.0.1:11434"] # scheme://host (any port) or scheme://host:port
114
```