···11# Server Host
2233-> **⚠️ Note**: This is a planned configuration that has not yet been deployed to hardware. The configuration is maintained and ready for deployment when needed.
33+> **⚠️ Note**: This is a planned configuration that has not yet been deployed to hardware. The configuration is ready; follow the runbook below.
4455-Minimal NixOS server configuration with security hardening and automatic maintenance.
55+Hardened NixOS server running a Bluesky ATProto PDS, exposed via a Cloudflare tunnel (no open inbound ports except SSH).
6677-## Features
77+## Architecture
8899-**Included:**
1010-- SSH server — key-based authentication only, hardened settings
1111-- Fail2ban — automatic brute-force protection
1212-- Firewall — SSH-only by default
1313-- Auto-upgrades — daily, via `settings/config/maintenance.nix`
1414-- Monitoring tools — btop, iotop, iftop, smartmontools, network tools
1515-- SMART disk monitoring
1616-- Weekly SSD TRIM
1717-- Log rotation and garbage collection
1818-- Zsh shell via shared Home Manager config
99+```
1010+Internet → Cloudflare edge (TLS)
1111+ ↓ encrypted tunnel (outbound from server)
1212+ cloudflared daemon
1313+ ↓ HTTP
1414+ Caddy (127.0.0.1:2020)
1515+ ↓ age-assurance static responses (UK OSA)
1616+ ↓ reverse proxy
1717+ bluesky-pds (127.0.0.1:3000)
1818+```
19192020-**Not included:**
2121-- No desktop environment or GUI
2222-- No gaming or multimedia packages
2020+No ports 80/443 need to be open in the firewall. SSH is the only public port.
23212424-## Installation
2222+---
25232626-### 1. Generate hardware config
2424+## Pre-Deploy Checklist (do these NOW, before the server exists)
27252828-Boot the NixOS installer, partition disks, mount them, then:
2626+These steps interact only with Cloudflare and your local machine — the server
2727+doesn't need to exist yet.
2828+2929+### 1. Generate PDS secrets
3030+3131+If you haven't already done this (check whether `secrets/age/pds.env.age` is
3232+populated with real secrets — not a placeholder):
29333034```bash
3131-sudo nixos-generate-config --show-hardware-config > /tmp/hardware-configuration.nix
3535+# Generate each secret separately — do NOT reuse values
3636+PDS_JWT_SECRET=$(openssl rand --hex 16)
3737+PDS_ADMIN_PASSWORD=$(openssl rand --hex 16)
3838+PDS_PLC_ROTATION_KEY=$(openssl ecparam --name secp256k1 --genkey --noout \
3939+ --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32)
4040+4141+# Edit the secret file (ragenix opens $EDITOR):
4242+nix run github:yaxitech/ragenix -- \
4343+ --rules secrets/secrets.nix \
4444+ --editor "code --wait" \
4545+ -e secrets/age/pds.env.age
4646+```
4747+4848+The file should contain (one per line):
4949+5050+```
5151+PDS_JWT_SECRET=<value>
5252+PDS_ADMIN_PASSWORD=<value>
5353+PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<value>
5454+PDS_EMAIL_SMTP_URL=smtps://resend:<api-key>@smtp.resend.com:465/
5555+PDS_EMAIL_FROM_ADDRESS=pds@ewancroft.uk
3256```
33573434-Copy the output to `hosts/server/hardware-configuration.nix`.
5858+### 2. Create the Cloudflare tunnel
5959+6060+Run this on your **macmini or laptop** (not the server — it doesn't exist yet):
6161+6262+```bash
6363+# Authenticate with your Cloudflare account (opens browser)
6464+cloudflared tunnel login
35653636-### 2. Configure SSH keys
6666+# Create the tunnel — note the UUID printed in the output
6767+cloudflared tunnel create pds
6868+# → Created tunnel pds with id XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
6969+```
37703838-SSH public keys are managed in `modules/ssh-keys.nix`. Add the server's key there.
7171+### 3. Update the tunnel UUID in settings
39724040-### 3. Customise settings
7373+Edit `settings/config/pds.nix` and replace the placeholder UUID:
7474+7575+```nix
7676+cloudflare = {
7777+ tunnelId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; # ← paste real UUID here
7878+};
7979+```
41804242-All server-specific values live in `settings/config/server.nix`:
4343-- `sshd.port` — SSH port (default: 22)
4444-- `firewall.allowedTCPPorts` — open ports
4545-- `fail2ban.banTime` / `fail2ban.maxRetry` — intrusion thresholds
8181+### 4. Encrypt the tunnel credentials
46824747-### 4. Install from USB/ISO
8383+The JSON credentials file is at `~/.cloudflared/<UUID>.json` after step 2:
48844985```bash
5050-# On the installer
5151-cd /mnt/etc/nixos
5252-curl -L https://github.com/ewanc26/nix/archive/refs/heads/main.tar.gz | sudo tar -xz --strip-components=1
5353-sudo nixos-install --flake .#server
5454-reboot
8686+cp ~/.cloudflared/<UUID>.json /tmp/cf-tunnel-pds.json
8787+8888+nix run github:yaxitech/ragenix -- \
8989+ --rules secrets/secrets.nix \
9090+ --editor "code --wait" \
9191+ -e secrets/age/cf-tunnel-pds.json.age
9292+9393+# Paste the JSON file contents into the editor, save and close.
9494+# Delete the plaintext copy:
9595+rm /tmp/cf-tunnel-pds.json
9696+```
9797+9898+### 5. Add the DNS CNAME in Cloudflare
9999+100100+In the Cloudflare dashboard (or via `cloudflared tunnel route dns`):
101101+102102+```
103103+pds.ewancroft.uk CNAME <UUID>.cfargotunnel.com (proxied ✓)
104104+*.ewancroft.uk CNAME <UUID>.cfargotunnel.com (proxied ✓)
55105```
561065757-### 5. Or switch from existing NixOS
107107+The `*.ewancroft.uk` wildcard lets users choose `@user.ewancroft.uk` handles.
108108+109109+---
110110+111111+## Deploy Day
112112+113113+### 1. Boot the NixOS installer on the server
114114+115115+Partition and mount disks, then generate the hardware config:
5811659117```bash
6060-cd /tmp
6161-curl -L https://github.com/ewanc26/nix/archive/refs/heads/main.tar.gz | tar -xz
6262-mv nix-config-main nix-config && cd nix-config
6363-sudo nixos-generate-config --show-hardware-config > hosts/server/hardware-configuration.nix
6464-sudo nixos-rebuild switch --flake .#server
6565-sudo cp -r . /home/ewan/.config/nix-config
118118+sudo nixos-generate-config --show-hardware-config
66119```
671206868-## Post-Installation
121121+Copy the output into `hosts/server/minimal-hardware.nix` (replacing the
122122+placeholder content), commit, and push.
691237070-### Verify SSH
124124+### 2. Get the server age key
125125+126126+While still on the installer (or after first boot):
7112772128```bash
7373-ssh ewan@your-server-ip
129129+nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
74130```
751317676-### Check services
132132+Paste the result into `secrets/secrets.nix`:
133133+134134+```nix
135135+systems = {
136136+ # ...
137137+ server = "age1..."; # ← paste here
138138+};
139139+```
140140+141141+Also change `pdsKeys` from `[ users.ewan ]` to `[ users.ewan systems.server ]`.
142142+143143+### 3. Rekey secrets for the server
144144+145145+From your macmini or laptop (you need your private age key):
7714678147```bash
7979-systemctl status sshd
8080-systemctl status fail2ban
8181-sudo iptables -L -n
148148+cd ~/.config/nix-config
149149+nix run github:yaxitech/ragenix -- --rules secrets/secrets.nix --rekey
150150+git add secrets/age/ secrets/secrets.nix
151151+git commit -m "secrets: add server key and rekey PDS secrets"
152152+git push
82153```
831548484-### Manual update
155155+### 4. Install NixOS
8515686157```bash
8787-sudo nixos-rebuild switch --flake /home/ewan/.config/nix-config#server
158158+# On the server installer, clone the repo and install:
159159+nix-shell -p git
160160+git clone https://github.com/ewanc26/nix /mnt/etc/nixos
161161+nixos-install --flake /mnt/etc/nixos#server
162162+reboot
88163```
891649090-## Security
165165+### 5. Verify on the server
911669292-| Hardening | Status |
9393-|---|---|
9494-| Root login disabled | ✅ |
9595-| Password auth disabled | ✅ |
9696-| Fail2ban active | ✅ |
9797-| AllowUsers restricted | ✅ |
9898-| Connection timeouts | ✅ |
9999-| Firewall enabled | ✅ |
167167+```bash
168168+# Check all services are up
169169+systemctl status bluesky-pds
170170+systemctl status cloudflared
171171+systemctl status caddy
172172+173173+# Check the PDS is reachable via the tunnel
174174+curl https://pds.ewancroft.uk/xrpc/_health
100175101101-To open additional ports, edit `settings/config/server.nix` → `firewall.allowedTCPPorts`.
176176+# Check UK OSA age-assurance endpoints work
177177+curl https://pds.ewancroft.uk/xrpc/app.bsky.ageassurance.getConfig
178178+```
102179103103-## Maintenance
180180+### 6. Create your account
104181105182```bash
106106-# Disk health
107107-sudo smartctl -a /dev/sda
183183+# Install atproto-goat (already in systemPackages on the server)
184184+# Create an invite code first, then create the account
185185+atproto-goat --pds-host https://pds.ewancroft.uk account create
186186+```
108187109109-# View logs
110110-journalctl -xe
111111-journalctl -u sshd
112112-journalctl -u fail2ban
188188+---
189189+190190+## Key settings
113191114114-# Check banned IPs
115115-sudo fail2ban-client status sshd
192192+All non-secret PDS settings live in `settings/config/pds.nix`:
116193117117-# Garbage collection (auto-runs weekly)
118118-sudo nix-collect-garbage --delete-older-than 30d
119119-```
194194+| Setting | Value |
195195+|---|---|
196196+| Hostname | `pds.ewancroft.uk` |
197197+| Handle domains | `.ewancroft.uk` |
198198+| PDS port | `3000` (internal only) |
199199+| Caddy port | `2020` (internal only) |
200200+| Tunnel ID | set in `settings/config/pds.nix` |
120201121121-## Common Customisations
202202+---
122203123123-### Add a web server
124124-1. Add `80` and `443` to `settings/config/server.nix` → `firewall.allowedTCPPorts`
125125-2. Add nginx config to `hosts/server/default.nix`
126126-3. Rebuild
204204+## Security
127205128128-### Change SSH port
129129-Edit `settings/config/server.nix` → `sshd.port` (firewall updates automatically from the same value).
206206+| Hardening | Status |
207207+|---|---|
208208+| Root login disabled | ✅ |
209209+| Password auth disabled | ✅ |
210210+| Fail2ban active | ✅ |
211211+| SSH key-only | ✅ |
212212+| Firewall: SSH only | ✅ |
213213+| No public HTTP/HTTPS ports | ✅ (Cloudflare tunnel) |
214214+| Secrets age-encrypted | ✅ |
215215+| PDS secrets server-only | ✅ (after rekeying) |
130216131131-### Add a user
132132-Edit `hosts/server/default.nix` and add an entry to `users.users`.
217217+---
133218134134-## Troubleshooting
219219+## Ongoing maintenance
135220136136-**Can't SSH in:**
137221```bash
138138-sudo iptables -L
139139-systemctl status sshd
140140-sudo sshd -T
222222+# Manual rebuild
223223+sudo nixos-rebuild switch --flake /home/ewan/.config/nix-config#server
224224+225225+# PDS logs
226226+journalctl -u bluesky-pds -f
227227+228228+# Tunnel status
229229+journalctl -u cloudflared -f
230230+231231+# Check banned IPs
141232sudo fail2ban-client status sshd
142142-```
143233144144-**System not upgrading:**
145145-```bash
146146-systemctl status nixos-upgrade.timer
147147-journalctl -u nixos-upgrade.service
148148-sudo systemctl start nixos-upgrade.service
234234+# Disk health
235235+sudo smartctl -a /dev/sda
149236```
150237151238## Resources
152239153153-- [NixOS Manual](https://nixos.org/manual/nixos/stable/)
154154-- [NixOS Security Wiki](https://nixos.wiki/wiki/Security)
155155-- [SSH hardening](https://nixos.wiki/wiki/SSH_public_key_authentication)
240240+- [isabelroses PDS guide](https://isabelroses.com/blog/nix-pds-guide/) — basis for this config
241241+- [Cloudflare tunnel docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
242242+- [ATProto PDS environment variables](https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/config/env.ts)
243243+- [UK OSA age-assurance gist](https://gist.github.com/mary-ext/6e27b24a83838202908808ad528b3318)