Security Hardening Guide#
Security configuration for a production Barazo deployment on a Linux VPS.
The 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.
SSH Configuration#
Disable Root Login and Password Authentication#
Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
Restart SSH:
sudo systemctl restart sshd
Before disabling root login, verify you can SSH in as your deploy user (barazo) with key-based auth.
Change Default SSH Port (Optional)#
Reduces automated scan noise. Not a security measure on its own.
Port 2222
If you change the SSH port, update your firewall rules accordingly.
Firewall (UFW)#
# Install
sudo apt install ufw
# Default policy: deny all incoming, allow outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (use your port if changed)
sudo ufw allow 22/tcp comment 'SSH'
# Allow HTTP and HTTPS (required for Caddy)
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw allow 443/udp comment 'HTTP/3 QUIC'
# Enable firewall
sudo ufw enable
# Verify
sudo ufw status verbose
Expected output: only ports 22, 80, 443 (TCP), and 443 (UDP) open.
Docker and UFW#
Docker manipulates iptables directly, which can bypass UFW rules. To prevent Docker from exposing ports that UFW blocks, create /etc/docker/daemon.json:
{
"iptables": false
}
Then restart Docker:
sudo systemctl restart docker
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.
Automatic Security Updates#
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
This enables automatic installation of security patches. Kernel updates may require a reboot -- consider enabling automatic reboots during a maintenance window:
Edit /etc/apt/apt.conf.d/50unattended-upgrades:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Docker Security#
Container Defaults (Already Configured)#
The docker-compose.yml ships with these security measures:
- Non-root containers: All Barazo images run as non-root users
- No privileged mode: No containers use
--privilegedorcap_add - Restart policy:
unless-stoppedon all services (recovers from crashes, stops on manualdocker compose down) - Health checks: All services have Docker health checks with appropriate intervals and retries
Resource Limits#
Uncomment the resource limits in docker-compose.yml to prevent any single service from consuming all server resources:
# Recommended limits for CX32 (4 vCPU, 8 GB RAM)
services:
postgres:
mem_limit: 2g
cpus: 1.5
valkey:
mem_limit: 512m
cpus: 0.5
tap:
mem_limit: 512m
cpus: 0.5
barazo-api:
mem_limit: 2g
cpus: 1.5
barazo-web:
mem_limit: 1g
cpus: 0.5
caddy:
mem_limit: 256m
cpus: 0.25
Read-Only Filesystems (Optional)#
For additional isolation, enable read-only root filesystems on containers that don't need write access beyond their volumes:
services:
valkey:
read_only: true
tmpfs:
- /tmp
caddy:
read_only: true
tmpfs:
- /tmp
Docker Socket Protection#
Never mount the Docker socket (/var/run/docker.sock) into any container. None of the Barazo services require it.
Image Updates#
Keep base images updated. Dependabot is configured in the repo for automated image update PRs. On the server:
# Pull latest pinned versions
docker compose pull
# Prune old images
docker image prune -f
Network Segmentation#
The Compose file uses two-network segmentation:
Internet -> [80/443] -> Caddy (frontend network)
|
barazo-web (frontend network)
|
barazo-api (frontend + backend networks)
|
PostgreSQL, Valkey, Tap (backend network only)
- PostgreSQL and Valkey are not reachable from the internet -- they are on the
backendnetwork only - Only Caddy exposes ports (80, 443) -- no other service is directly accessible
- barazo-api bridges both networks -- it receives HTTP requests via Caddy and connects to the database
Do not add ports: to any service other than Caddy.
Caddy Security#
Headers (Already Configured)#
Caddy automatically enables:
- HSTS (Strict-Transport-Security) -- enforced by default
- HTTP to HTTPS redirect -- automatic
Admin API#
The Caddy admin API is disabled in the Caddyfile (admin off). This prevents runtime configuration changes via HTTP.
Internal Endpoints#
The /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.
Database Security#
Role Separation#
Barazo uses three PostgreSQL roles with least-privilege access:
| Role | Privileges | Used By |
|---|---|---|
barazo_migrator |
DDL (CREATE, ALTER, DROP) | Schema changes (reserved for beta) |
barazo_app |
DML (SELECT, INSERT, UPDATE, DELETE) | API server |
barazo_readonly |
SELECT only | Search, public endpoints, reporting |
The 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.
Connection Security#
PostgreSQL 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).
Do not add ports: to the PostgreSQL service in docker-compose.yml.
Password Strength#
Generate all database passwords with:
openssl rand -base64 24
This produces a 32-character password with high entropy.
Valkey (Redis) Security#
Authentication#
Valkey requires a password (--requirepass). The password is set via VALKEY_PASSWORD in .env.
Dangerous Commands Disabled#
The following commands are renamed to empty strings (effectively disabled):
FLUSHALL-- prevents accidental cache wipeFLUSHDB-- prevents accidental database wipeCONFIG-- prevents runtime configuration changesDEBUG-- prevents debug information leaksKEYS-- prevents expensive keyspace scans in production
Network Isolation#
Like PostgreSQL, Valkey is on the backend network only and not exposed to the host.
Secrets Management#
Environment Variables#
- All secrets are in
.env(never indocker-compose.ymlor committed to git) .envis in.gitignore.env.exampleusesCHANGE_MEplaceholders
File Permissions#
# Restrict .env to owner only
chmod 600 .env
# Restrict backup encryption key
chmod 600 barazo-backup-key.txt
Backup Encryption#
Backups must be encrypted before off-server storage. See Backup & Restore for setup with age.
Checklist#
Use this as a post-deployment verification:
- SSH: root login disabled, password auth disabled
- Firewall: only 22, 80, 443 (TCP), 443 (UDP) open
- Unattended upgrades enabled
- Resource limits set in
docker-compose.yml - No
CHANGE_MEin.env:grep CHANGE_ME .envreturns nothing -
.envfile permissions:ls -la .envshows-rw------- - PostgreSQL not exposed:
curl localhost:5432connection refused - Valkey not exposed:
curl localhost:6379connection refused -
/api/health/readyreturns 403 externally - Caddy admin API disabled: confirmed
admin offin Caddyfile - Backup encryption configured and tested
- Docker images pinned to specific versions (not
:latest)