···1111- Add tarpit for vulnerability scanners hitting known exploit paths (#18)
12121313### Added
1414+- Migrate to 3-node HA cluster with JuiceFS and S3-backed storage (#38)
1515+- Add JuiceFS Redis and CSI manifests for S3-backed storage (#47)
1416- Add backup restoration guide for PDS and knot (#35)
1517- Create vesper and nyx accounts on PDS (#31)
1618- Add daily S3 backup cronjob for Tangled knot data (#9)
1719- Add Tangled knot with Spindle CI/CD to k3s cluster (#1)
18201921### Fixed
2222+- Remove deleted pds-test subdomain from TLS certificate (#48)
2023- Restore PDS and knot data from S3 backups (#34)
2124- Fix backup script to prevent empty source from wiping S3 data (#33)
2225- Add PDS handle resolution for vesper and nyx subdomains (#32)
···2427- Update PDS to v0.4.208 for OAuth metadata support (#13)
25282629### Changed
3030+- Remove IP allowlist restriction from kube API and SSH firewall (#49)
2731- Add health check that detects SQLite locking failures (#16)
2832- Move node SSH to port 2222 and expose knot Git SSH on port 22 (#14)
2933- Update knot hostname from git.sans-self.org to knot.sans-self.org (#12)
···11+# infrastructure
22+33+Single-tenant AT Protocol infrastructure on Hetzner Cloud. Runs a [PDS](https://github.com/bluesky-social/pds) (sans-self.org) and a [Tangled knot](https://tangled.sh) (knot.sans-self.org) on a k3s cluster.
44+55+## Stack
66+77+- **Infra**: OpenTofu (kube-hetzner module v2.18.5) + Hetzner Cloud
88+- **Cluster**: k3s with 3x CAX11 ARM nodes (HA embedded etcd)
99+- **Storage**: JuiceFS CSI backed by Hetzner Object Storage (S3), Redis metadata
1010+- **Secrets**: git-crypt
1111+- **Manifests**: Kustomize
1212+1313+## Secrets
1414+1515+All files matching `k8s/**/*.secret` are git-crypt encrypted. Unlock before building:
1616+1717+```sh
1818+git-crypt unlock
1919+```
2020+2121+`k8s/juicefs/metaurl.secret` is derived from `k8s/juicefs/redis-password.secret` — it embeds the Redis password in a connection URI. If you rotate the password, regenerate it:
2222+2323+```sh
2424+make secrets
2525+```
2626+2727+The password file must **not** have a trailing newline. The Makefile handles this correctly.
2828+2929+## Deploy
3030+3131+Manifests:
3232+3333+```sh
3434+kubectl apply -k k8s/
3535+```
3636+3737+JuiceFS CSI driver is managed by OpenTofu as a `helm_release` resource:
3838+3939+```sh
4040+tofu apply
4141+```
4242+4343+## Cluster rebuild
4444+4545+If the cluster needs to be rebuilt from scratch (new nodes, not just config changes):
4646+4747+1. The first control plane node must bootstrap with `cluster-init: true` in `/etc/rancher/k3s/config.yaml` — kube-hetzner doesn't handle this automatically when all nodes are new.
4848+2. Hetzner can't shrink disks. Switching to a smaller server type (e.g. cx33 → cax11) requires tainting the server resource: `tofu taint 'module.kube-hetzner.module.control_planes["0-0-control-plane"].hcloud_server.server'`
4949+3. After bootstrap, fetch a fresh kubeconfig from the node — the one in tofu state will have the wrong CA.
5050+4. JuiceFS CSI on SELinux (MicroOS) requires `sidecarPrivileged: true` in `juicefs-csi-values.yaml` under `node:`. Without it, the CSI socket has a label mismatch and sidecars can't connect.
5151+5252+## Backups
5353+5454+Daily S3 backups via CronJobs (02:00 PDS, 02:30 knot). See [RESTORE.md](RESTORE.md) for recovery procedures.
5555+5656+After a PDS restore, the sequencer autoincrement must be bumped past the relay's cursor — see RESTORE.md section "Fix sequencer cursor".