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### 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`)