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
|
|
|
|
# The instance's host at home
|
|
|
|
|
|
|
|
|
|
|
|
halkyon.li runs at home on balthasar (ADR 0039) and is published through ramiel
|
|
|
|
|
|
(`../forward/README.md`). This folder holds what the host needs beyond Docker: SSH for
|
|
|
|
|
|
melchior, the root disk unlocked over SSH at boot, Docker's data on the HDD, and home's
|
|
|
|
|
|
end of the tunnel.
|
|
|
|
|
|
|
|
|
|
|
|
| File | On the host | What for |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `sshd-halkyon.conf` | `/etc/ssh/sshd_config.d/10-halkyon.conf` | keys alone, two users (D17) |
|
|
|
|
|
|
| `dropbear.conf` | `/etc/dropbear/initramfs/dropbear.conf` | the passphrase over SSH at boot (D20) |
|
|
|
|
|
|
| `initramfs-modules` | appended to `/etc/initramfs-tools/modules` | the USB network adapter at boot (D20) |
|
|
|
|
|
|
| `fstab-docker` | appended to `/etc/fstab` | Docker's and containerd's data on the HDD (D12) |
|
|
|
|
|
|
| `docker-after-data.conf` | `docker.service.d/` and `containerd.service.d/` | both wait for it (D12) |
|
|
|
|
|
|
| `wg-halkyon.conf` | `/etc/wireguard/wg-halkyon.conf` | home's end of the tunnel (D8) |
|
|
|
|
|
|
| `wg-halkyon.nft` | `/etc/wireguard/wg-halkyon.nft` | what the tunnel may carry (D9, D17) |
|
|
|
|
|
|
| `wg-quick-before-docker.conf` | `wg-quick@wg-halkyon.service.d/` | the tunnel before Docker (D12) |
|
|
|
|
|
|
|
|
|
|
|
|
`test/deploy/halkyon_li_forward_test.dart` holds these files to ADR 0039's rules.
|
|
|
|
|
|
|
|
|
|
|
|
## Steps
|
|
|
|
|
|
|
|
|
|
|
|
As the developer at the iMac (`sudo`), from this checkout (`~/project-halkyon/halkyon`),
|
|
|
|
|
|
in this order: each step leaves the machine working, and the early ones keep a way in
|
|
|
|
|
|
for the later.
|
|
|
|
|
|
|
|
|
|
|
|
1. **sshd for melchior** (D17). On balthasar, then from melchior, then on balthasar:
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo apt install openssh-server
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
ssh-copy-id raisondetredev@192.168.178.53 # on melchior, with balthasar's password
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo install -m 644 deploy/host/sshd-halkyon.conf /etc/ssh/sshd_config.d/10-halkyon.conf
|
|
|
|
|
|
sudo sshd -t && sudo systemctl reload ssh
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Then, before closing anything, from melchior: `ssh raisondetredev@192.168.178.53`
|
|
|
|
|
|
must still get in, and `ssh -o PubkeyAuthentication=no raisondetredev@192.168.178.53`
|
|
|
|
|
|
must be refused. In the FRITZ!Box, give balthasar always the same address
|
|
|
|
|
|
(*Heimnetz › Netzwerk › balthasar › Diesem Netzwerkgerät immer die gleiche
|
|
|
|
|
|
IPv4-Adresse zuweisen*), so that 192.168.178.53 stays its address.
|
|
|
|
|
|
|
|
|
|
|
|
2. **The passphrase over SSH at boot** (D20):
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo apt install dropbear-initramfs
|
|
|
|
|
|
sudo install -m 644 deploy/host/dropbear.conf /etc/dropbear/initramfs/dropbear.conf
|
|
|
|
|
|
# melchior's public key alone (from ~/.ssh/authorized_keys, step 1), restricted:
|
|
|
|
|
|
printf 'no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command="/usr/bin/cryptroot-unlock" %s\n' \
|
|
|
|
|
|
"$(cat ~/.ssh/authorized_keys)" | sudo tee /etc/dropbear/initramfs/authorized_keys
|
|
|
|
|
|
grep -v '^#' deploy/host/initramfs-modules | sudo tee -a /etc/initramfs-tools/modules
|
|
|
|
|
|
echo 'IP=::::balthasar:enx0050b6bffcff:dhcp' | sudo tee -a /etc/initramfs-tools/initramfs.conf
|
|
|
|
|
|
sudo update-initramfs -u
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
At the next boot, at home: `ssh -p 2222 root@192.168.178.53` from melchior asks for the
|
|
|
|
|
|
root disk's passphrase, and the boot goes on. The keyboard at the iMac still works as
|
|
|
|
|
|
before, so a mistake here costs no more than the walk to it.
|
|
|
|
|
|
|
|
|
|
|
|
3. **No desktop** (D12): it stops starting at boot, and Docker Desktop, which runs a VM of
|
|
|
|
|
|
its own beside Docker, goes. Multipass only if it isn't used:
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo systemctl set-default multi-user.target # back: graphical.target
|
|
|
|
|
|
sudo apt remove docker-desktop
|
|
|
|
|
|
sudo snap disable multipass
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
4. **Docker's data on the HDD** (D12). Everything Docker holds now (the home Jellyfin's
|
2026-11-08 19:15:25 +01:00
|
|
|
|
images and containers) moves with it. ADR 0032's home Jellyfin stops here for good;
|
|
|
|
|
|
its configuration and library stay where they are and move into the instance later
|
|
|
|
|
|
(`../sidecars/README.md`, *Jellyfin on halkyon.li*):
|
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 19:15:25 +01:00
|
|
|
|
sudo docker compose -p home-jellyfin down
|
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 systemctl stop docker docker.socket containerd
|
|
|
|
|
|
sudo mkdir -p /data/docker /data/containerd
|
|
|
|
|
|
sudo rsync -aHAX --numeric-ids /var/lib/docker/ /data/docker/
|
|
|
|
|
|
sudo rsync -aHAX --numeric-ids /var/lib/containerd/ /data/containerd/
|
|
|
|
|
|
sudo mv /var/lib/docker /var/lib/docker.old
|
|
|
|
|
|
sudo mv /var/lib/containerd /var/lib/containerd.old
|
|
|
|
|
|
sudo mkdir /var/lib/docker /var/lib/containerd
|
|
|
|
|
|
grep -v '^#' deploy/host/fstab-docker | sudo tee -a /etc/fstab
|
|
|
|
|
|
for unit in docker containerd; do
|
|
|
|
|
|
sudo install -D -m 644 deploy/host/docker-after-data.conf \
|
|
|
|
|
|
/etc/systemd/system/$unit.service.d/after-data.conf
|
|
|
|
|
|
done
|
|
|
|
|
|
sudo systemctl daemon-reload
|
|
|
|
|
|
sudo mount /var/lib/docker && sudo mount /var/lib/containerd
|
|
|
|
|
|
sudo systemctl start containerd docker
|
|
|
|
|
|
sudo docker images # as before
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Once Docker runs as before: `sudo rm -rf /var/lib/docker.old /var/lib/containerd.old`.
|
|
|
|
|
|
|
|
|
|
|
|
5. **Home's end of the tunnel** (D8, D9, D12). Home's key, made here and never leaving
|
|
|
|
|
|
but into Proton Pass (vault *Infra*, an item *balthasar wg-halkyon*):
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo apt install wireguard-tools
|
|
|
|
|
|
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
|
|
|
|
|
|
sudo wg pubkey < /etc/wireguard/wg-halkyon.key # home's public key: ramiel's config
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Then the files, with ramiel's public key (`../forward/README.md`, step 3) in place of
|
|
|
|
|
|
`<ramiel's public key>`:
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
sudo install -m 600 deploy/host/wg-halkyon.conf deploy/host/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
|
|
|
|
|
|
sudo install -D -m 644 deploy/host/wg-quick-before-docker.conf \
|
|
|
|
|
|
/etc/systemd/system/wg-quick@wg-halkyon.service.d/before-docker.conf
|
|
|
|
|
|
sudo systemctl daemon-reload
|
|
|
|
|
|
sudo systemctl enable --now wg-quick@wg-halkyon
|
|
|
|
|
|
sudo wg show wg-halkyon # a handshake once ramiel is up too
|
|
|
|
|
|
ping -c 1 10.61.97.1 # refused by home's own rules: nothing new goes to ramiel
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
From melchior, away from home: `ssh -J ramiel raisondetredev@10.61.97.2`.
|
|
|
|
|
|
|
|
|
|
|
|
6. **Reboot**, at home, to see each step hold: the passphrase over SSH (step 2), no
|
|
|
|
|
|
desktop (step 3), Docker on the HDD (step 4), the tunnel up before Docker (step 5).
|
|
|
|
|
|
|
|
|
|
|
|
The instance itself follows (its users, checkout, `.env` and first deploy), once its
|
|
|
|
|
|
compose files for Meditate and Jellyfin are built.
|