Barazo Docker Compose templates for self-hosting barazo.forum
at main 295 lines 8.4 kB view raw view rendered
1# Security Hardening Guide 2 3Security configuration for a production Barazo deployment on a Linux VPS. 4 5The Docker Compose templates ship with secure defaults (non-root containers, two-network segmentation, no unnecessary exposed ports). This guide covers the server-level hardening that complements those defaults. 6 7## SSH Configuration 8 9### Disable Root Login and Password Authentication 10 11Edit `/etc/ssh/sshd_config`: 12 13``` 14PermitRootLogin no 15PasswordAuthentication no 16PubkeyAuthentication yes 17MaxAuthTries 3 18ClientAliveInterval 300 19ClientAliveCountMax 2 20``` 21 22Restart SSH: 23 24```bash 25sudo systemctl restart sshd 26``` 27 28**Before disabling root login**, verify you can SSH in as your deploy user (`barazo`) with key-based auth. 29 30### Change Default SSH Port (Optional) 31 32Reduces automated scan noise. Not a security measure on its own. 33 34``` 35Port 2222 36``` 37 38If you change the SSH port, update your firewall rules accordingly. 39 40## Firewall (UFW) 41 42```bash 43# Install 44sudo apt install ufw 45 46# Default policy: deny all incoming, allow outgoing 47sudo ufw default deny incoming 48sudo ufw default allow outgoing 49 50# Allow SSH (use your port if changed) 51sudo ufw allow 22/tcp comment 'SSH' 52 53# Allow HTTP and HTTPS (required for Caddy) 54sudo ufw allow 80/tcp comment 'HTTP' 55sudo ufw allow 443/tcp comment 'HTTPS' 56sudo ufw allow 443/udp comment 'HTTP/3 QUIC' 57 58# Enable firewall 59sudo ufw enable 60 61# Verify 62sudo ufw status verbose 63``` 64 65**Expected output:** only ports 22, 80, 443 (TCP), and 443 (UDP) open. 66 67### Docker and UFW 68 69Docker manipulates iptables directly, which can bypass UFW rules. To prevent Docker from exposing ports that UFW blocks, create `/etc/docker/daemon.json`: 70 71```json 72{ 73 "iptables": false 74} 75``` 76 77Then restart Docker: 78 79```bash 80sudo systemctl restart docker 81``` 82 83**Note:** With `iptables: false`, you must ensure the host firewall allows traffic to Docker's published ports (80, 443). The UFW rules above handle this. Test after applying to confirm services remain accessible. 84 85## Automatic Security Updates 86 87```bash 88sudo apt install unattended-upgrades 89sudo dpkg-reconfigure -plow unattended-upgrades 90``` 91 92This enables automatic installation of security patches. Kernel updates may require a reboot -- consider enabling automatic reboots during a maintenance window: 93 94Edit `/etc/apt/apt.conf.d/50unattended-upgrades`: 95 96``` 97Unattended-Upgrade::Automatic-Reboot "true"; 98Unattended-Upgrade::Automatic-Reboot-Time "04:00"; 99``` 100 101## Docker Security 102 103### Container Defaults (Already Configured) 104 105The `docker-compose.yml` ships with these security measures: 106 107- **Non-root containers:** All Barazo images run as non-root users 108- **No privileged mode:** No containers use `--privileged` or `cap_add` 109- **Restart policy:** `unless-stopped` on all services (recovers from crashes, stops on manual `docker compose down`) 110- **Health checks:** All services have Docker health checks with appropriate intervals and retries 111 112### Resource Limits 113 114Uncomment the resource limits in `docker-compose.yml` to prevent any single service from consuming all server resources: 115 116```yaml 117# Recommended limits for CX32 (4 vCPU, 8 GB RAM) 118services: 119 postgres: 120 mem_limit: 2g 121 cpus: 1.5 122 valkey: 123 mem_limit: 512m 124 cpus: 0.5 125 tap: 126 mem_limit: 512m 127 cpus: 0.5 128 barazo-api: 129 mem_limit: 2g 130 cpus: 1.5 131 barazo-web: 132 mem_limit: 1g 133 cpus: 0.5 134 caddy: 135 mem_limit: 256m 136 cpus: 0.25 137``` 138 139### Read-Only Filesystems (Optional) 140 141For additional isolation, enable read-only root filesystems on containers that don't need write access beyond their volumes: 142 143```yaml 144services: 145 valkey: 146 read_only: true 147 tmpfs: 148 - /tmp 149 caddy: 150 read_only: true 151 tmpfs: 152 - /tmp 153``` 154 155### Docker Socket Protection 156 157Never mount the Docker socket (`/var/run/docker.sock`) into any container. None of the Barazo services require it. 158 159### Image Updates 160 161Keep base images updated. Dependabot is configured in the repo for automated image update PRs. On the server: 162 163```bash 164# Pull latest pinned versions 165docker compose pull 166 167# Prune old images 168docker image prune -f 169``` 170 171## Network Segmentation 172 173The Compose file uses two-network segmentation: 174 175``` 176Internet -> [80/443] -> Caddy (frontend network) 177 | 178 barazo-web (frontend network) 179 | 180 barazo-api (frontend + backend networks) 181 | 182 PostgreSQL, Valkey, Tap (backend network only) 183``` 184 185- **PostgreSQL and Valkey are not reachable from the internet** -- they are on the `backend` network only 186- **Only Caddy exposes ports** (80, 443) -- no other service is directly accessible 187- **barazo-api bridges both networks** -- it receives HTTP requests via Caddy and connects to the database 188 189Do not add `ports:` to any service other than Caddy. 190 191## Caddy Security 192 193### Headers (Already Configured) 194 195Caddy automatically enables: 196 197- **HSTS** (Strict-Transport-Security) -- enforced by default 198- **HTTP to HTTPS redirect** -- automatic 199 200### Admin API 201 202The Caddy admin API is disabled in the Caddyfile (`admin off`). This prevents runtime configuration changes via HTTP. 203 204### Internal Endpoints 205 206The `/api/health/ready` endpoint is blocked at the Caddy level (returns 403). This endpoint exposes readiness state and should only be accessed from within the Docker network for orchestration purposes. 207 208## Database Security 209 210### Role Separation 211 212Barazo uses three PostgreSQL roles with least-privilege access: 213 214| Role | Privileges | Used By | 215|------|-----------|---------| 216| `barazo_migrator` | DDL (CREATE, ALTER, DROP) | Schema changes (reserved for beta) | 217| `barazo_app` | DML (SELECT, INSERT, UPDATE, DELETE) | API server | 218| `barazo_readonly` | SELECT only | Search, public endpoints, reporting | 219 220The API server connects with the database user configured in `DATABASE_URL`. On startup, it runs pending Drizzle migrations using a dedicated single-connection client, then opens the main connection pool. In a future hardening phase, migration will use a separate `barazo_migrator` role with DDL privileges, while `barazo_app` will be restricted to DML only. 221 222### Connection Security 223 224PostgreSQL is on the backend network only. It is not exposed to the host or the internet. The `DATABASE_URL` uses Docker's internal DNS (`postgres:5432`). 225 226Do not add `ports:` to the PostgreSQL service in `docker-compose.yml`. 227 228### Password Strength 229 230Generate all database passwords with: 231 232```bash 233openssl rand -base64 24 234``` 235 236This produces a 32-character password with high entropy. 237 238## Valkey (Redis) Security 239 240### Authentication 241 242Valkey requires a password (`--requirepass`). The password is set via `VALKEY_PASSWORD` in `.env`. 243 244### Dangerous Commands Disabled 245 246The following commands are renamed to empty strings (effectively disabled): 247 248- `FLUSHALL` -- prevents accidental cache wipe 249- `FLUSHDB` -- prevents accidental database wipe 250- `CONFIG` -- prevents runtime configuration changes 251- `DEBUG` -- prevents debug information leaks 252- `KEYS` -- prevents expensive keyspace scans in production 253 254### Network Isolation 255 256Like PostgreSQL, Valkey is on the backend network only and not exposed to the host. 257 258## Secrets Management 259 260### Environment Variables 261 262- All secrets are in `.env` (never in `docker-compose.yml` or committed to git) 263- `.env` is in `.gitignore` 264- `.env.example` uses `CHANGE_ME` placeholders 265 266### File Permissions 267 268```bash 269# Restrict .env to owner only 270chmod 600 .env 271 272# Restrict backup encryption key 273chmod 600 barazo-backup-key.txt 274``` 275 276### Backup Encryption 277 278Backups must be encrypted before off-server storage. See [Backup & Restore](backups.md) for setup with `age`. 279 280## Checklist 281 282Use this as a post-deployment verification: 283 284- [ ] SSH: root login disabled, password auth disabled 285- [ ] Firewall: only 22, 80, 443 (TCP), 443 (UDP) open 286- [ ] Unattended upgrades enabled 287- [ ] Resource limits set in `docker-compose.yml` 288- [ ] No `CHANGE_ME` in `.env`: `grep CHANGE_ME .env` returns nothing 289- [ ] `.env` file permissions: `ls -la .env` shows `-rw-------` 290- [ ] PostgreSQL not exposed: `curl localhost:5432` connection refused 291- [ ] Valkey not exposed: `curl localhost:6379` connection refused 292- [ ] `/api/health/ready` returns 403 externally 293- [ ] Caddy admin API disabled: confirmed `admin off` in Caddyfile 294- [ ] Backup encryption configured and tested 295- [ ] Docker images pinned to specific versions (not `:latest`)