Barazo Docker Compose templates for self-hosting
barazo.forum
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**Exception: cAdvisor** -- The monitoring stack includes cAdvisor, which requires read-only access to `/var/run` (containing the Docker socket) and `/sys` to collect per-container metrics. This is a deliberate, documented exception:
160- cAdvisor is a widely-used, Google-maintained monitoring tool
161- The mount is read-only (`:ro`) -- cAdvisor does not write to the socket
162- cAdvisor runs with a read-only root filesystem
163- No other monitoring container has socket access
164
165### Image Updates
166
167Keep base images updated. Dependabot is configured in the repo for automated image update PRs. On the server:
168
169```bash
170# Pull latest pinned versions
171docker compose pull
172
173# Prune old images
174docker image prune -f
175```
176
177## Network Segmentation
178
179The Compose file uses two-network segmentation:
180
181```
182Internet -> [80/443] -> Caddy (frontend network)
183 |
184 barazo-web (frontend network)
185 |
186 barazo-api (frontend + backend networks)
187 |
188 PostgreSQL, Valkey, Tap (backend network only)
189```
190
191- **PostgreSQL and Valkey are not reachable from the internet** -- they are on the `backend` network only
192- **Only Caddy exposes ports** (80, 443) -- no other service is directly accessible
193- **barazo-api bridges both networks** -- it receives HTTP requests via Caddy and connects to the database
194
195Do not add `ports:` to any service other than Caddy.
196
197## Caddy Security
198
199### Headers (Already Configured)
200
201Caddy automatically enables:
202
203- **HSTS** (Strict-Transport-Security) -- enforced by default
204- **HTTP to HTTPS redirect** -- automatic
205
206### Admin API
207
208The Caddy admin API is disabled in the Caddyfile (`admin off`). This prevents runtime configuration changes via HTTP.
209
210### Internal Endpoints
211
212The `/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.
213
214## Database Security
215
216### Role Separation
217
218Barazo uses three PostgreSQL roles with least-privilege access:
219
220| Role | Privileges | Used By |
221|------|-----------|---------|
222| `barazo_migrator` | DDL (CREATE, ALTER, DROP) | Schema changes (reserved for beta) |
223| `barazo_app` | DML (SELECT, INSERT, UPDATE, DELETE) | API server |
224| `barazo_readonly` | SELECT only | Search, public endpoints, reporting |
225
226The 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.
227
228### Connection Security
229
230PostgreSQL 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`).
231
232Do not add `ports:` to the PostgreSQL service in `docker-compose.yml`.
233
234### Password Strength
235
236Generate all database passwords with:
237
238```bash
239openssl rand -base64 24
240```
241
242This produces a 32-character password with high entropy.
243
244## Valkey (Redis) Security
245
246### Authentication
247
248Valkey requires a password (`--requirepass`). The password is set via `VALKEY_PASSWORD` in `.env`.
249
250### Dangerous Commands Disabled
251
252The following commands are renamed to empty strings (effectively disabled):
253
254- `FLUSHALL` -- prevents accidental cache wipe
255- `FLUSHDB` -- prevents accidental database wipe
256- `CONFIG` -- prevents runtime configuration changes
257- `DEBUG` -- prevents debug information leaks
258- `KEYS` -- prevents expensive keyspace scans in production
259
260### Network Isolation
261
262Like PostgreSQL, Valkey is on the backend network only and not exposed to the host.
263
264## Secrets Management
265
266### Environment Variables
267
268- All secrets are in `.env` (never in `docker-compose.yml` or committed to git)
269- `.env` is in `.gitignore`
270- `.env.example` uses `CHANGE_ME` placeholders
271
272### File Permissions
273
274```bash
275# Restrict .env to owner only
276chmod 600 .env
277
278# Restrict backup encryption key
279chmod 600 barazo-backup-key.txt
280```
281
282### Backup Encryption
283
284Backups must be encrypted before off-server storage. See [Backup & Restore](backups.md) for setup with `age`.
285
286## Checklist
287
288Use this as a post-deployment verification:
289
290- [ ] SSH: root login disabled, password auth disabled
291- [ ] Firewall: only 22, 80, 443 (TCP), 443 (UDP), and 51820 (UDP, WireGuard) open
292- [ ] Unattended upgrades enabled
293- [ ] Resource limits set in `docker-compose.yml`
294- [ ] No `CHANGE_ME` in `.env`: `grep CHANGE_ME .env` returns nothing
295- [ ] `.env` file permissions: `ls -la .env` shows `-rw-------`
296- [ ] PostgreSQL not exposed: `curl localhost:5432` connection refused
297- [ ] Valkey not exposed: `curl localhost:6379` connection refused
298- [ ] `/api/health/ready` returns 403 externally
299- [ ] Caddy admin API disabled: confirmed `admin off` in Caddyfile
300- [ ] Backup encryption configured and tested
301- [ ] Docker images pinned to specific versions (not `:latest`)