feat(deploy): halkyon.li's forward through ramiel and its host at home (ADR 0039)
ramiel's HAProxy passes halkyon.li's TLS connections, unopened, through a
WireGuard tunnel to the instance at home, with the visitor's address in a
PROXY header, and everything else to ramiel's own Caddy, moved to the
loopback. Each end's nftables table lets the tunnel carry HAProxy's 443 and
the SSH jump's 22 alone, and nothing from home to ramiel.
The host at home: sshd with keys alone, the root disk unlocked over SSH at
boot (dropbear-initramfs, D20), Docker's and containerd's data bind-mounted
from the HDD, and home's end of the tunnel before Docker. An instance behind
the forward (compose.forwarded.yaml) publishes 443 on the tunnel's address
alone, trusts the PROXY header from ramiel alone, and gets its certificates
through TLS-ALPN, without HTTP/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:05:47 +01:00
|
|
|
# ramiel's forward for halkyon.li
|
|
|
|
|
|
|
|
|
|
ramiel, the halkyon.app server, publishes halkyon.li, which runs at home (ADR 0039).
|
|
|
|
|
HAProxy takes ramiel's 443, reads the name each TLS connection asks for, and passes
|
|
|
|
|
halkyon.li's, unopened, through a WireGuard tunnel to the instance at home. Every other
|
|
|
|
|
name goes to ramiel's own Caddy, which serves `code.halkyon.app`, `halkyon.app` and
|
|
|
|
|
`learn.halkyon.app` as before, now on `127.0.0.1:8443`. Port 80 stays Caddy's.
|
|
|
|
|
|
|
|
|
|
| File | On ramiel | What for |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `haproxy.cfg` | `/etc/haproxy/haproxy.cfg` | the front on 443 (D3, D10) |
|
|
|
|
|
| `Caddyfile.halkyon-li` | merged into `/etc/caddy/Caddyfile` | Caddy on 8443 with the PROXY header, halkyon.li's HTTP to HTTPS (D11) |
|
|
|
|
|
| `wg-halkyon.conf` | `/etc/wireguard/wg-halkyon.conf` | ramiel's end of the tunnel (D8) |
|
|
|
|
|
| `wg-halkyon.nft` | `/etc/wireguard/wg-halkyon.nft` | what the tunnel may carry (D9, D17) |
|
|
|
|
|
|
|
|
|
|
`test/deploy/halkyon_li_forward_test.dart` holds these files to ADR 0039's rules.
|
|
|
|
|
|
|
|
|
|
## Steps
|
|
|
|
|
|
|
|
|
|
All as root on ramiel (`sudo`), but step 1 on melchior. The home end is set up
|
|
|
|
|
alongside (`../host/README.md`): each side needs the other's public key.
|
|
|
|
|
|
|
|
|
|
1. **The files**, from a checkout on melchior, into the admin's home on ramiel:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
scp deploy/forward/haproxy.cfg deploy/forward/Caddyfile.halkyon-li \
|
|
|
|
|
deploy/forward/wg-halkyon.conf deploy/forward/wg-halkyon.nft ramiel:
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. **The packages**:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo apt install wireguard-tools haproxy nftables
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Ubuntu's HAProxy starts with a configuration that listens nowhere, so nothing changes
|
|
|
|
|
yet.
|
|
|
|
|
|
|
|
|
|
3. **ramiel's key** for the tunnel, made on ramiel and never leaving it but into Proton
|
|
|
|
|
Pass (vault *Infra*, an item *ramiel wg-halkyon*: a new server needs it again):
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo install -d -m 700 /etc/wireguard
|
|
|
|
|
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/wg-halkyon.key'
|
|
|
|
|
sudo cat /etc/wireguard/wg-halkyon.key # into Proton Pass
|
2026-11-08 20:30:10 +01:00
|
|
|
sudo sh -c 'wg pubkey < /etc/wireguard/wg-halkyon.key' # ramiel's public key: home's config
|
feat(deploy): halkyon.li's forward through ramiel and its host at home (ADR 0039)
ramiel's HAProxy passes halkyon.li's TLS connections, unopened, through a
WireGuard tunnel to the instance at home, with the visitor's address in a
PROXY header, and everything else to ramiel's own Caddy, moved to the
loopback. Each end's nftables table lets the tunnel carry HAProxy's 443 and
the SSH jump's 22 alone, and nothing from home to ramiel.
The host at home: sshd with keys alone, the root disk unlocked over SSH at
boot (dropbear-initramfs, D20), Docker's and containerd's data bind-mounted
from the HDD, and home's end of the tunnel before Docker. An instance behind
the forward (compose.forwarded.yaml) publishes 443 on the tunnel's address
alone, trusts the PROXY header from ramiel alone, and gets its certificates
through TLS-ALPN, without HTTP/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:05:47 +01:00
|
|
|
```
|
|
|
|
|
|
2026-11-08 20:22:13 +01:00
|
|
|
4. **The tunnel's files**, with home's public key (from `../host/README.md`, step 5) in
|
feat(deploy): halkyon.li's forward through ramiel and its host at home (ADR 0039)
ramiel's HAProxy passes halkyon.li's TLS connections, unopened, through a
WireGuard tunnel to the instance at home, with the visitor's address in a
PROXY header, and everything else to ramiel's own Caddy, moved to the
loopback. Each end's nftables table lets the tunnel carry HAProxy's 443 and
the SSH jump's 22 alone, and nothing from home to ramiel.
The host at home: sshd with keys alone, the root disk unlocked over SSH at
boot (dropbear-initramfs, D20), Docker's and containerd's data bind-mounted
from the HDD, and home's end of the tunnel before Docker. An instance behind
the forward (compose.forwarded.yaml) publishes 443 on the tunnel's address
alone, trusts the PROXY header from ramiel alone, and gets its certificates
through TLS-ALPN, without HTTP/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:05:47 +01:00
|
|
|
place of `<home's public key>`:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo install -m 600 -o root -g root ~/wg-halkyon.conf ~/wg-halkyon.nft /etc/wireguard/
|
|
|
|
|
sudoedit /etc/wireguard/wg-halkyon.conf
|
|
|
|
|
sudo nft -c -f /etc/wireguard/wg-halkyon.nft # checks the rules, loads nothing
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
5. **WireGuard's port**, opened in ufw:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo ufw allow 51820/udp comment 'WireGuard: halkyon.li at home'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
6. **The tunnel**, up now and at every boot:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo systemctl enable --now wg-quick@wg-halkyon
|
|
|
|
|
sudo wg show wg-halkyon # a handshake once home is up too
|
|
|
|
|
sudo nft list table inet halkyon_tunnel
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
7. **Caddy moved behind HAProxy.** Keep the Caddyfile as it is, then put
|
|
|
|
|
`Caddyfile.halkyon-li`'s global options at the very top of `/etc/caddy/Caddyfile`
|
2026-11-08 20:22:13 +01:00
|
|
|
(before `code.halkyon.app {`) and its sites at the end, and check it; 8443 must stay
|
|
|
|
|
closed from outside (ufw's default for incoming: deny):
|
feat(deploy): halkyon.li's forward through ramiel and its host at home (ADR 0039)
ramiel's HAProxy passes halkyon.li's TLS connections, unopened, through a
WireGuard tunnel to the instance at home, with the visitor's address in a
PROXY header, and everything else to ramiel's own Caddy, moved to the
loopback. Each end's nftables table lets the tunnel carry HAProxy's 443 and
the SSH jump's 22 alone, and nothing from home to ramiel.
The host at home: sshd with keys alone, the root disk unlocked over SSH at
boot (dropbear-initramfs, D20), Docker's and containerd's data bind-mounted
from the HDD, and home's end of the tunnel before Docker. An instance behind
the forward (compose.forwarded.yaml) publishes 443 on the tunnel's address
alone, trusts the PROXY header from ramiel alone, and gets its certificates
through TLS-ALPN, without HTTP/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:05:47 +01:00
|
|
|
|
|
|
|
|
```sh
|
2026-11-08 20:22:13 +01:00
|
|
|
sudo ufw status verbose | grep -E 'Default|8443' # deny (incoming), no 8443 rule
|
feat(deploy): halkyon.li's forward through ramiel and its host at home (ADR 0039)
ramiel's HAProxy passes halkyon.li's TLS connections, unopened, through a
WireGuard tunnel to the instance at home, with the visitor's address in a
PROXY header, and everything else to ramiel's own Caddy, moved to the
loopback. Each end's nftables table lets the tunnel carry HAProxy's 443 and
the SSH jump's 22 alone, and nothing from home to ramiel.
The host at home: sshd with keys alone, the root disk unlocked over SSH at
boot (dropbear-initramfs, D20), Docker's and containerd's data bind-mounted
from the HDD, and home's end of the tunnel before Docker. An instance behind
the forward (compose.forwarded.yaml) publishes 443 on the tunnel's address
alone, trusts the PROXY header from ramiel alone, and gets its certificates
through TLS-ALPN, without HTTP/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:05:47 +01:00
|
|
|
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-haproxy
|
|
|
|
|
sudoedit /etc/caddy/Caddyfile
|
|
|
|
|
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
|
|
|
sudo install -m 644 ~/haproxy.cfg /etc/haproxy/haproxy.cfg
|
|
|
|
|
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
8. **The switch**, the forge away for the seconds between the two commands: Caddy lets go
|
|
|
|
|
of 443, HAProxy takes it.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo systemctl reload caddy && sudo systemctl restart haproxy
|
|
|
|
|
sudo ss -tlnp | grep -E ':(443|8443) ' # haproxy on 443, caddy on 8443
|
|
|
|
|
curl -sI https://code.halkyon.app | head -1 # from melchior: HTTP/2 200
|
2026-11-08 20:22:13 +01:00
|
|
|
curl -sI http://code.halkyon.app | grep -i '^location' # https://code.halkyon.app/, no :8443
|
feat(deploy): halkyon.li's forward through ramiel and its host at home (ADR 0039)
ramiel's HAProxy passes halkyon.li's TLS connections, unopened, through a
WireGuard tunnel to the instance at home, with the visitor's address in a
PROXY header, and everything else to ramiel's own Caddy, moved to the
loopback. Each end's nftables table lets the tunnel carry HAProxy's 443 and
the SSH jump's 22 alone, and nothing from home to ramiel.
The host at home: sshd with keys alone, the root disk unlocked over SSH at
boot (dropbear-initramfs, D20), Docker's and containerd's data bind-mounted
from the HDD, and home's end of the tunnel before Docker. An instance behind
the forward (compose.forwarded.yaml) publishes 443 on the tunnel's address
alone, trusts the PROXY header from ramiel alone, and gets its certificates
through TLS-ALPN, without HTTP/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:05:47 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If anything fails, step 8 is undone by `sudo systemctl stop haproxy`, the old
|
|
|
|
|
Caddyfile copied back and `sudo systemctl reload caddy`.
|
|
|
|
|
|
|
|
|
|
9. **Checks** once home's Caddy runs (`../host/README.md`):
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -sI https://learn.halkyon.li | head -1 # from anywhere: an answer from home
|
|
|
|
|
curl -sI http://learn.halkyon.li | head -3 # 308 to https://, from ramiel
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Taking it away
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo systemctl disable --now wg-quick@wg-halkyon haproxy
|
|
|
|
|
sudo ufw delete allow 51820/udp
|
|
|
|
|
sudo cp /etc/caddy/Caddyfile.before-haproxy /etc/caddy/Caddyfile
|
|
|
|
|
sudo systemctl reload caddy
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Caddy then holds 443 and serves HTTP/3 again, as before.
|
|
|
|
|
|
|
|
|
|
## What changes for the forge
|
|
|
|
|
|
|
|
|
|
- Caddy serves no HTTP/3 any more (`protocols h1 h2`). Browsers and git use HTTP/2,
|
|
|
|
|
which they fall back to on their own.
|
|
|
|
|
- Its log keeps visitors' addresses, from HAProxy's PROXY header, which Caddy trusts from
|
|
|
|
|
the loopback alone.
|
|
|
|
|
- HAProxy logs nothing.
|