diff --git a/backup-dashboard-api.service b/backup-dashboard-api.service new file mode 100644 index 0000000..6bea917 --- /dev/null +++ b/backup-dashboard-api.service @@ -0,0 +1,23 @@ +[Unit] +Description=Backup Dashboard API +After=network.target + +[Service] +User=backupdash +Group=backupdash +WorkingDirectory=/opt/backup-dashboard/backend +Environment=BACKUP_DASH_DB=/var/lib/backup-dashboard/backup-dashboard.db +Environment=BACKUP_DASH_RETENTION_DAYS=90 +# Bound to localhost: only nginx can reach it, so nginx's auth can't be bypassed from outside. +# One worker is plenty at this scale and keeps SQLite writes simple. +ExecStart=/opt/backup-dashboard/venv/bin/uvicorn --factory app.main:create_app --host 127.0.0.1 --port 8000 --proxy-headers +Restart=on-failure + +StateDirectory=backup-dashboard +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..685b247 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,52 @@ +server { + listen 80; + server_name backups.example.com; + # Basic auth sends the password with every request — never serve this over plain HTTP + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + http2 on; + server_name backups.example.com; + + # Paths as created by `certbot certonly --nginx -d backups.example.com` + ssl_certificate /etc/letsencrypt/live/backups.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/backups.example.com/privkey.pem; + + root /var/www/backup-dashboard/public; + index index.html; + + # Strict CSP works because there is no inline JS/CSS + add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000" always; + + # Dashboard users. Create with: htpasswd -B -c /etc/nginx/backup-dashboard.htpasswd + # To move to Microsoft 365 sign-in later, replace these two lines with an auth_request to + # oauth2-proxy (provider=entra-id); the app itself doesn't need to change. + auth_basic "Backup Monitor"; + auth_basic_user_file /etc/nginx/backup-dashboard.htpasswd; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Collectors authenticate with their own API key (Authorization: Bearer …), checked by the app + location = /api/v1/ingest { + auth_basic off; + client_max_body_size 256k; + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}