Aller au contenu

Aletheia Server Setup

Complete guide for provisioning a new server from scratch.

Prerequisites

  • Fresh Debian 12+ server with root/sudo access
  • DNS A records pointing to the server for:
  • aletheia.groupe-suffren.com (production)
  • aletheia-staging.groupe-suffren.com (staging)
  • aletheia-dev.groupe-suffren.com (dev)

Step 1: Initial Access & SSH Key

Connect to the server and install your SSH public key:

ssh root@<server-ip>
adduser debian
usermod -aG sudo debian

# Install SSH key
mkdir -p /home/debian/.ssh
echo "your-public-key" >> /home/debian/.ssh/authorized_keys
chmod 700 /home/debian/.ssh
chmod 600 /home/debian/.ssh/authorized_keys
chown -R debian:debian /home/debian/.ssh

Verify key-based login works from your machine before proceeding:

ssh debian@<server-ip>

Warning: The setup script will disable password authentication. If you skip the SSH key step, you will be locked out.

Step 2: Clone the Repository

sudo mkdir -p /opt/docker/aletheia/repo
sudo chown debian:debian /opt/docker/aletheia/repo
cd /opt/docker/aletheia/repo
git clone git@github.com:baudry-suffren/aletheia_v2.git .

Step 3: Run the Setup Script

sudo ./infra/setup.sh

The script automates:

  1. Docker CE installation
  2. Utilities: git, apache2-utils, cron, fail2ban, iptables-persistent, unattended-upgrades
  3. Swap: creates 5GB swap file, sets vm.swappiness=10
  4. Unattended upgrades: enables automatic Debian security patches
  5. SSH hardening: port 57361, key-only auth (checks for keys first)
  6. Firewall service: installs systemd service that applies iptables rules after Docker starts
  7. fail2ban: sshd jail on port 57361
  8. Directory structure: /opt/docker/{nginx,shared,aletheia,backups}
  9. Config files: nginx, postgres, redis configs copied into place
  10. Password generation: random passwords for all databases and Django SECRET_KEYs
  11. Basic auth: interactive prompt for staging/dev htpasswd credentials
  12. Docker network: creates the backend bridge network
  13. Shared services: starts PostgreSQL + Redis, waits for readiness
  14. Static volumes: creates Docker volumes for collectstatic
  15. Nginx: starts the reverse proxy (in maintenance mode)
  16. Firewall rules: applies Docker-compatible iptables rules (waits for Docker daemon)
  17. Backup cron: daily at 2 AM, 7-day retention + weekly copies

Interactive prompts during setup: - Basic auth username/password for staging/dev - SSH key confirmation (if no key detected)

Step 4: Post-Setup Checklist

After the script completes:

  • [ ] Reconnect via new SSH port: ssh -p 57361 debian@<server-ip>
  • [ ] Edit env files (email settings, Sentry DSN):
    nano /opt/docker/aletheia/envs/.env.prod
    nano /opt/docker/aletheia/envs/.env.staging
    
  • [ ] Obtain SSL certificates (requires DNS to be pointing to the server already):
    # Nginx must be running first (started by setup.sh in maintenance mode)
    docker exec certbot certbot certonly --webroot -w /var/www/certbot \
      -d aletheia.groupe-suffren.com \
      -d aletheia-staging.groupe-suffren.com \
      -d aletheia-dev.groupe-suffren.com
    
    Certbot auto-renewal runs every 12h inside the certbot container (configured in infra/nginx/docker-compose.yml). Verify renewal works:
    docker exec certbot certbot renew --dry-run
    
  • [ ] Check IPv6 firewall: The setup script only configures IPv4 iptables. IPv6 is not managed. If the server has an IPv6 address, consider blocking all IPv6 traffic or applying matching rules:
    # Check if IPv6 is active
    ip -6 addr show scope global
    # If active, block all IPv6 (simplest option):
    ip6tables -P INPUT DROP
    ip6tables -P FORWARD DROP
    ip6tables -A INPUT -i lo -j ACCEPT
    ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
    ip6tables-save > /etc/iptables/rules.v6
    
  • [ ] Deploy the application:
    cd /opt/docker/aletheia/repo
    make deploy ENV=dev
    make deploy ENV=staging
    make deploy ENV=prod REF=v1.0
    
  • [ ] Create superuser:
    docker exec -it aletheia-prod-web python manage.py createsuperuser
    
  • [ ] Verify all environments are accessible in a browser
  • [ ] Test backup cron (run manually once):
    /opt/docker/backups/scripts/backup.sh
    ls -la /opt/docker/backups/daily/
    

Day-to-Day Operations

All operations go through the Makefile:

# Deploying
make deploy ENV=staging
make deploy ENV=prod REF=v1.2

# Database
make db-backup ENV=prod
make db-sync FROM=staging ENV=dev

# Logs & shell
make logs ENV=staging
make shell ENV=staging

# Infrastructure config management
make infra-diff       # Compare git vs server configs
make infra-pull       # Server → git (after editing on server)
make infra-deploy     # Git → server (after editing in repo)

Typical workflow after editing an nginx config on the server: 1. make infra-diff — see what changed 2. make infra-pull — copy server state into git 3. git diff infra/ — review changes 4. git add infra/ && git commit — commit and push

Security Overview

Layer Configuration
SSH Port 57361, key-only, no password auth, no root login
Firewall iptables default DROP, only ports 57361/80/443 open
fail2ban sshd jail: 5 retries, 10min ban
Rate limiting SSH: max 4 connections/60s. HTTP: 10 req/s (nginx)
Basic auth Staging and dev environments (htpasswd)
Docker isolation Containers only accessible via nginx proxy
HTTPS Let's Encrypt certificates via certbot

Config files in infra/security/: - sshd-00-hardening.conf — SSH drop-in config (/etc/ssh/sshd_config.d/00-hardening.conf) - iptables-setup-docker.sh — Firewall rules script (Docker-compatible, flushes INPUT only) - iptables-firewall.service — systemd service that runs the script after Docker starts - fail2ban-aletheia.conf — fail2ban jail config (/etc/fail2ban/jail.d/aletheia.conf)

Note on cloud-init: Some cloud providers (OVH, AWS, etc.) install /etc/ssh/sshd_config.d/50-cloud-init.conf which may set PasswordAuthentication yes. Since sshd uses first-match-wins, our config is numbered 00- to take priority. After deployment, check if 50-cloud-init.conf exists and consider disabling it if it conflicts with security hardening.

Secret Management (SOPS + age)

All secrets are encrypted with SOPS + age and committed to the repo as .enc files. Only the age private key needs to be stored externally (password manager).

Encrypted files in repo: - infra/shared/.env.enc — Postgres admin credentials - infra/envs/.env.{dev,staging,prod}.enc — Django secrets per environment - infra/monitoring/.env.enc — Grafana, SMTP, Teams webhook - infra/nginx/.htpasswd.enc — Basic auth credentials - infra/shared/init-scripts/01-create-databases.sql.enc — DB init script

Key location on server: /opt/docker/.age-key.txt (readable by root and debian)

Makefile targets:

make infra-encrypt    # Server secrets → encrypted files in repo
make infra-decrypt    # Encrypted repo files → server locations

Rotating secrets: 1. Edit the plaintext file on the server 2. Run make infra-encrypt to re-encrypt 3. Commit and push the updated .enc files

New server / lost key: - The age private key must be retrieved from the password manager - Place it at /opt/docker/.age-key.txt with permissions 640 root:debian - Run make infra-decrypt to restore all secrets

Disaster Recovery

To rebuild the server from scratch with full data restore:

Prerequisites

  • Fresh Debian 12+ server with SSH access
  • Age private key from password manager
  • Latest database backup (from off-server backup or previous /opt/docker/backups/daily/)
  • Latest media backup (staging + prod tar.gz)

Procedure

# 1. Initial access & SSH key (see Step 1 above)
ssh root@<server-ip>

# 2. Clone repo
sudo mkdir -p /opt/docker/aletheia/repo
sudo chown debian:debian /opt/docker/aletheia/repo
cd /opt/docker/aletheia/repo
git clone git@github.com:baudry-suffren/aletheia_v2.git .

# 3. Run setup script — choose option 2 (Server migration / recovery)
#    This handles: Docker install, security hardening, directory setup,
#    secret decryption (from age key), network creation, and starts
#    shared services (postgres/redis), nginx, and monitoring stack.
sudo ./infra/setup.sh

# 4. Restore databases (from backup files)
docker cp backup_aletheia_prod_YYYYMMDD.dump shared_postgres:/tmp/
docker exec shared_postgres pg_restore -U aletheia_prod -d aletheia_prod -Fc --no-owner /tmp/backup_aletheia_prod_YYYYMMDD.dump
docker cp backup_aletheia_staging_YYYYMMDD.dump shared_postgres:/tmp/
docker exec shared_postgres pg_restore -U aletheia_staging -d aletheia_staging -Fc --no-owner /tmp/backup_aletheia_staging_YYYYMMDD.dump

# 5. Restore media files
tar xzf media_aletheia_prod_YYYYMMDD.tar.gz -C /opt/docker/aletheia/media/prod/
tar xzf media_aletheia_staging_YYYYMMDD.tar.gz -C /opt/docker/aletheia/media/staging/

# 6. Follow the post-setup instructions printed by setup.sh:
#    - Obtain SSL certificates (certbot)
#    - Activate HTTPS nginx configs (.conf.full)
#    - Deploy application (make deploy)

# 7. Verify
curl -k https://aletheia.groupe-suffren.com/health/
curl -k https://aletheia-staging.groupe-suffren.com/health/
make infra-diff  # Should show no drift

Directory Layout

/opt/docker/
├── nginx/                      Reverse proxy + SSL
│   ├── docker-compose.yml
│   ├── nginx.conf
│   ├── .htpasswd               Basic auth credentials
│   └── conf.d/
│       ├── aletheia-*.conf      Active vhost configs
│       ├── aletheia-*.conf.full Full configs (restored after deploy)
│       └── aletheia-*.conf.temp Maintenance page configs
├── shared/                     PostgreSQL + Redis
│   ├── docker-compose.yml
│   ├── .env                    Postgres admin credentials
│   └── init-scripts/           DB initialization SQL
├── aletheia/
│   ├── repo/                   Git repository (this repo)
│   ├── envs/                   Environment files (.env.dev/staging/prod)
│   └── media/                  User uploads (dev/staging/prod)
├── monitoring/                 Prometheus + Grafana + Loki + exporters
│   ├── docker-compose.yml
│   ├── .env                    Grafana admin, SMTP, Teams webhook
│   ├── prometheus/             Scrape configs + alert rules
│   ├── grafana/provisioning/   Dashboards, alerting, datasources
│   ├── loki/                   Log storage config
│   ├── alloy/                  Log collection config
│   └── blackbox/               HTTPS probe config
└── backups/
    ├── scripts/backup.sh       Cron backup script
    ├── daily/                  7-day retention
    └── weekly/                 28-day retention