Files
dotfiles/systems/portal-1
Richie f5a0038ab7
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 31s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-jeeves (pull_request) Successful in 2m20s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 32s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-bob (push) Successful in 37s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m1s
feat(portal): proxy Gitea SSH through HAProxy
- expose Gitea SSH through Portal on port 2223
- advertise the correct Gitea SSH user and domain
- add the Gitea host to Richie’s SSH config
- correct the local ACME challenge backend port
2026-08-26 10:43:21 -04:00
..
2026-08-26 07:33:31 -04:00
2026-08-26 07:33:31 -04:00
2026-08-26 07:33:31 -04:00

portal_1

Minimal NixOS target for a Vultr VM, installed with nixos-anywhere. The Nix flake target is portal_1; the machine hostname is portal-1 because DNS hostnames cannot contain underscores.

Before deploying

  1. Confirm the VM's system disk is /dev/vda. If it is not, update both references in disk-config.nix.
  2. Confirm the SSH public key in default.nix is the key that should have administrator access.
  3. Boot the VM into a NixOS installer or another nixos-anywhere-compatible Linux rescue environment with root SSH access. Keep this environment running while completing the SOPS bootstrap below.

Bootstrap SOPS

Use the rescue environment's SSH host key as the permanent portal identity. Replace VM_IP below:

ssh root@VM_IP 'cat /etc/ssh/ssh_host_ed25519_key.pub' | \
  nix shell nixpkgs#ssh-to-age --command ssh-to-age

This prints an age1... recipient; it does not copy the private key. Add the recipient to .sops.yaml:

- &system_portal_1 age1...

Then add *system_portal_1 to the age recipients for users/secrets.yaml. Re-encrypt the existing file for the new recipient and add the Tailscale key:

nix shell nixpkgs#sops --command sops updatekeys users/secrets.yaml
nix shell nixpkgs#sops --command sops users/secrets.yaml

Add the OAuth client secret from the Auth Keys: Write credential in the SOPS editor and save it:

tailscale_auth_key: tskey-client-...

Deploy

From the repository root, replace VM_IP with the VM's public IP:

nix run github:nix-community/nixos-anywhere -- \
  --copy-host-keys --flake .#portal_1 root@VM_IP

This repartitions /dev/vda, so anything already on that disk is erased. The layout reserves 8 GiB for swap and assigns the remaining space to the root filesystem.

--copy-host-keys preserves the same private SSH host key at /etc/ssh/ssh_host_ed25519_key on the installed system. SOPS-Nix converts that key to an age identity during activation. After the reboot, connect as richie and verify that automatic Tailscale enrollment succeeded:

ssh -p 278 richie@VM_IP
sudo tailscale status

The installed OpenSSH service listens on port 278. Port 22 is served by Endlessh and will not provide an SSH login.

HAProxy uses the same frontend, routing, and rate-limiting configuration as Jeeves. Portal manages the ACME certificates for the existing public domains; their DNS records must resolve to Portal for HTTP-01 issuance and renewal.

The application backends still use Jeeves' original 127.0.0.1 addresses. Replace them with the corresponding Tailscale addresses before directing application traffic through Portal. Ports 80 and 443 are allowed through the firewall.