Archive relocate-brouter-to-dedicated-host

Final step (task 9.4) of the BRouter relocation. The change is moved to
openspec/changes/archive/2026-04-24-relocate-brouter-to-dedicated-host/
and the four delta specs (brouter-integration, infrastructure,
observability, security-hardening) are merged into the main specs:

  + 6 added requirements
  ~ 4 modified requirements

The two intentionally-unchecked tasks are 2.2 (obsoleted, see crossed-out
note) and 9.4 itself (this archive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-24 18:35:37 +02:00
parent 2e5606458d
commit 66fb5d3044
12 changed files with 121 additions and 27 deletions

View file

@ -1,9 +1,7 @@
## Purpose
Route computation between waypoints via the BRouter HTTP API, including routing host election, result broadcasting via Yjs, profile selection, and rate-limited proxying.
## Requirements
### Requirement: Route computation from waypoints
The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API with tiledesc enabled and returning the result as an EnrichedRoute, preserving per-point elevation, surface data, and segment boundary indices.
@ -53,11 +51,15 @@ The Planner SHALL support selecting a routing profile that determines how BRoute
- **THEN** the profile change syncs via Yjs and the routing host recomputes the route
### Requirement: BRouter API proxy
The Planner backend SHALL proxy all BRouter API calls. Clients SHALL NOT communicate with BRouter directly.
The Planner backend SHALL proxy all BRouter API calls. Clients SHALL NOT communicate with BRouter directly. The proxy SHALL attach a `X-BRouter-Auth: <token>` header to every upstream request using the `BROUTER_AUTH_TOKEN` environment variable.
#### Scenario: Proxied route request
- **WHEN** the routing host client requests a route computation
- **THEN** the Planner backend forwards the request to BRouter, applies rate limiting, and returns the response
- **THEN** the Planner backend forwards the request to BRouter over the configured upstream URL with the `X-BRouter-Auth` header set, applies rate limiting, and returns the response
#### Scenario: Missing auth token at startup
- **WHEN** the Planner starts in production without `BROUTER_AUTH_TOKEN` set
- **THEN** the Planner logs a fatal error and refuses to start
### Requirement: Rate limiting
The Planner backend SHALL rate limit BRouter API calls to prevent abuse.
@ -67,11 +69,19 @@ The Planner backend SHALL rate limit BRouter API calls to prevent abuse.
- **THEN** subsequent requests receive a 429 response with a Retry-After header
### Requirement: BRouter Docker deployment
BRouter SHALL run as a separate Docker container with Germany RD5 segments mounted as a volume.
BRouter SHALL run as a Docker container on a dedicated Hetzner host reached over a private Hetzner vSwitch, with planet-wide RD5 segments mounted as a volume. The BRouter container SHALL NOT be exposed on any public network interface.
#### Scenario: BRouter container starts
- **WHEN** the Docker Compose stack starts
- **THEN** the BRouter container is reachable at its internal HTTP port and can compute routes using the mounted RD5 segments
- **WHEN** the `docker compose up -d` command runs in `~trails/brouter/` on the dedicated host
- **THEN** the BRouter container is reachable only on the vSwitch-bound IP and can compute routes using the mounted planet-wide RD5 segments
#### Scenario: Public network isolation
- **WHEN** a request is sent to the dedicated host's public IP on the BRouter port
- **THEN** the request is refused at the host firewall or times out; BRouter does not respond
#### Scenario: JVM memory sizing
- **WHEN** the BRouter container starts
- **THEN** the JVM is launched with `-Xmx8g` (or equivalent) so that the heap does not exceed 8 GB on the 32 GB host
### Requirement: BRouter routing with constraints
Route computation SHALL include no-go area polygons as avoidance constraints.
@ -79,3 +89,19 @@ Route computation SHALL include no-go area polygons as avoidance constraints.
#### Scenario: Route with no-go areas
- **WHEN** the routing host computes a route and no-go areas exist
- **THEN** the BRouter request includes nogo parameters for each polygon
### Requirement: Shared-secret auth on BRouter
The BRouter deployment SHALL require a shared-secret header on every request. Requests without a valid `X-BRouter-Auth` header SHALL be rejected before reaching the BRouter process.
#### Scenario: Valid token
- **WHEN** a request arrives at the BRouter host with the correct `X-BRouter-Auth` header
- **THEN** the Caddy sidecar forwards the request to BRouter and returns its response
#### Scenario: Missing or wrong token
- **WHEN** a request arrives without an `X-BRouter-Auth` header or with an incorrect value
- **THEN** the Caddy sidecar responds with HTTP 403 and does not forward the request to BRouter
#### Scenario: Token not logged
- **WHEN** Caddy emits an access log line for a BRouter request
- **THEN** the `X-BRouter-Auth` header value is redacted or omitted from the log line

View file

@ -1,9 +1,7 @@
## Purpose
Server provisioning on Hetzner, Docker Compose deployment, CI/CD pipelines, database and BRouter management, TLS, Sentry, Grafana, and monitoring stack for the flagship instance.
## Requirements
### Requirement: Terraform Hetzner provisioning
Infrastructure SHALL be provisioned on Hetzner Cloud using Terraform with the Hetzner provider.
@ -12,11 +10,11 @@ Infrastructure SHALL be provisioned on Hetzner Cloud using Terraform with the He
- **THEN** a Hetzner cx23 server (2 vCPU, 4 GB RAM, 40 GB SSD) is created with Docker installed
### Requirement: Docker Compose deployment
All services SHALL be deployed via Docker Compose on the Hetzner server.
All services SHALL be deployed via Docker Compose, including Grafana, Prometheus, and Loki for the flagship instance.
#### Scenario: Start all services
- **WHEN** `docker compose up -d` is run on the server
- **THEN** the Journal, Planner, BRouter, and PostgreSQL containers start and are reachable
#### Scenario: Monitoring stack starts
- **WHEN** `docker compose up -d` is run
- **THEN** Grafana, Prometheus, Loki, Promtail, postgres-exporter, node-exporter, and cAdvisor containers start alongside the application containers
### Requirement: Service configuration
Each service SHALL be configured via environment variables defined in Docker Compose, with security best practices including non-root execution and security headers.
@ -45,29 +43,33 @@ The database SHALL be PostgreSQL with the PostGIS extension for spatial queries.
- **THEN** the PostGIS extension is available and can be enabled with `CREATE EXTENSION postgis`
### Requirement: BRouter segment management
The infrastructure SHALL support downloading and updating Germany RD5 segments from brouter.de.
The infrastructure SHALL support downloading and updating planet-wide RD5 segments from brouter.de to the dedicated BRouter host. Segment files SHALL live under `~trails/brouter/segments/` on the dedicated host and SHALL be owned by the `trails` user.
#### Scenario: Download segments
- **WHEN** the segment download script is run
- **THEN** Germany RD5 files (E5_N45, E5_N50, E10_N45, E10_N50) are downloaded to the segments volume
- **WHEN** the segment download script runs on the dedicated host as the `trails` user
- **THEN** all planet-wide RD5 files referenced by the tile list are downloaded to `~trails/brouter/segments/`, skipping files that already exist
#### Scenario: Weekly segment update
- **WHEN** the weekly cron job runs
- **THEN** RD5 segments are updated from brouter.de and the BRouter container is restarted
#### Scenario: Segment update
- **WHEN** an operator re-runs the segment download script
- **THEN** outdated or missing RD5 files are re-fetched from brouter.de and the BRouter container is restarted
### Requirement: CI/CD pipeline
GitHub Actions SHALL use separate workflows for app deployment and infrastructure deployment, with secrets decrypted from a SOPS-encrypted file.
GitHub Actions SHALL use separate workflows for app deployment, infrastructure deployment, and BRouter deployment, with secrets decrypted from a SOPS-encrypted file.
#### Scenario: App deployment
- **WHEN** code changes are pushed to main in apps/ or packages/
- **THEN** the cd-apps workflow builds Docker images, pushes to ghcr.io, and deploys app containers
- **THEN** the cd-apps workflow builds Docker images, pushes to ghcr.io, and deploys app containers to the flagship host
#### Scenario: Infrastructure deployment
- **WHEN** changes are pushed to main in infrastructure/
- **THEN** the cd-infra workflow copies configs and restarts infrastructure services without rebuilding app images
- **THEN** the cd-infra workflow copies configs and restarts infrastructure services on the flagship host without rebuilding app images and without touching the BRouter host
#### Scenario: BRouter deployment
- **WHEN** changes are pushed to main in docker/brouter/ or the BRouter host compose config
- **THEN** the cd-brouter workflow SSHes as the `trails` user into the dedicated BRouter host using `BROUTER_DEPLOY_HOST` / `BROUTER_DEPLOY_SSH_KEY` and runs `docker compose up -d` in `~trails/brouter/`
#### Scenario: Secret decryption at deploy time
- **WHEN** either CD workflow runs
- **WHEN** any CD workflow runs
- **THEN** the SOPS-encrypted secrets file is decrypted and provided to docker-compose as an env file
#### Scenario: Gitleaks scan
@ -157,3 +159,26 @@ Caddy SHALL emit structured JSON access logs for all requests.
#### Scenario: Access log emitted
- **WHEN** any HTTP request passes through Caddy
- **THEN** a JSON log line with remote IP, method, path, status, and duration is written to stdout
### Requirement: Private network between flagship and BRouter hosts
The flagship host and the dedicated BRouter host SHALL be joined on a Hetzner vSwitch in the same datacenter. All traffic between Planner and BRouter SHALL traverse this private network.
#### Scenario: vSwitch reachability
- **WHEN** the flagship host issues a request to the BRouter host's vSwitch IP on the BRouter service port with a valid `X-BRouter-Auth` header
- **THEN** the request succeeds over the private network without traversing the public internet
#### Scenario: No public BRouter exposure
- **WHEN** Hetzner Cloud firewall rules or equivalent host firewall rules are inspected
- **THEN** no rule allows inbound traffic to the BRouter service port from any public IP
### Requirement: Non-root deploy user on the BRouter host
The BRouter host SHALL be administered by the trails.cool project through a non-root `trails` user that is a member of the `docker` group. The CD workflow SHALL NOT require sudo or root SSH access on this host.
#### Scenario: Deploy with trails user
- **WHEN** the cd-brouter workflow connects to the BRouter host
- **THEN** it authenticates as `trails` and successfully runs `docker compose` commands without invoking sudo
#### Scenario: Scoped ownership
- **WHEN** files are created by the deploy or segment-download scripts
- **THEN** they live under `~trails/brouter/` and are owned by `trails:trails`

View file

@ -1,9 +1,7 @@
## Purpose
Health endpoints, Prometheus metrics, structured logging, Grafana dashboards, and alerting for both apps and the flagship instance.
## Requirements
### Requirement: Health endpoints
Both apps SHALL expose a `/health` endpoint returning service and database status.
@ -73,3 +71,30 @@ Loki SHALL collect and index logs from all Docker containers.
#### Scenario: Search logs
- **WHEN** an operator searches logs in Grafana
- **THEN** they can filter by container name, log level, and time range
### Requirement: Remote BRouter metrics
Prometheus on the flagship host SHALL scrape BRouter-specific metrics from the dedicated BRouter host over the vSwitch. Scraping SHALL be scoped to BRouter containers only and SHALL NOT collect host-level metrics from the dedicated host's other (non-trails.cool) workloads.
#### Scenario: BRouter container metrics collected
- **WHEN** Prometheus scrapes the dedicated BRouter host
- **THEN** metrics from the BRouter container (and, if enabled, the Caddy sidecar) are collected via cAdvisor filtered by container label, or via a BRouter/JMX metrics endpoint exposed on the vSwitch
#### Scenario: No shared-host noise
- **WHEN** Prometheus is configured with the remote BRouter scrape target
- **THEN** no configuration scrapes the dedicated host's `node_exporter` or metrics from containers other than BRouter and its sidecar
#### Scenario: BRouter scrape failure alert
- **WHEN** the BRouter scrape target returns `up == 0` for more than 2 minutes
- **THEN** Grafana fires an alert distinct from flagship-side alerts
### Requirement: Remote BRouter log shipping
Loki on the flagship host SHALL receive BRouter container logs from the dedicated BRouter host via a Promtail or Alloy agent running on that host as the `trails` user. Log shipping SHALL be scoped to BRouter-related containers only.
#### Scenario: BRouter logs visible in Grafana
- **WHEN** the BRouter container writes to stdout or stderr
- **THEN** the log line is available in Grafana Explore via Loki with container and host labels identifying it as coming from the dedicated BRouter host
#### Scenario: Non-BRouter logs not shipped
- **WHEN** a non-BRouter container on the dedicated host writes logs
- **THEN** those logs are not ingested by the flagship Loki instance

View file

@ -1,9 +1,7 @@
## Purpose
Security headers, scanner path blocking, secret scanning, dependency auditing, non-root containers, and vulnerability disclosure policy.
## Requirements
### Requirement: Security response headers
All HTTP responses SHALL include security headers to protect against common web attacks.
@ -61,3 +59,23 @@ The repository SHALL include a SECURITY.md with responsible disclosure instructi
#### Scenario: Security contact
- **WHEN** a security researcher finds a vulnerability
- **THEN** SECURITY.md provides clear instructions for reporting it
### Requirement: BRouter access control
The BRouter service SHALL be reachable only from the flagship host over a private Hetzner vSwitch, and every request SHALL carry a valid `X-BRouter-Auth` shared-secret header. The token SHALL be stored only in SOPS-encrypted secrets and GitHub Actions secrets, never committed in cleartext.
#### Scenario: Request from public internet
- **WHEN** an attacker sends a request to the BRouter host's public IP on the BRouter service port
- **THEN** the host firewall refuses the connection
#### Scenario: Request from vSwitch without token
- **WHEN** a request arrives on the BRouter host over the vSwitch without a valid `X-BRouter-Auth` header
- **THEN** the Caddy sidecar responds with HTTP 403 and BRouter never sees the request
#### Scenario: Token storage
- **WHEN** `BROUTER_AUTH_TOKEN` is added or rotated
- **THEN** the token is written only to `infrastructure/secrets.infra.env` (SOPS-encrypted) and to the GitHub Actions secret store, and is never committed in cleartext to the repository
#### Scenario: Token rotation
- **WHEN** the token is rotated
- **THEN** operators update SOPS, redeploy the Planner (new token in outbound header), and redeploy the Caddy sidecar (new token in the matcher), in that order, with zero downtime