Files
Backup-Dashboard/README.md
T
kyleandClaude Opus 5.5 2fa9eea274 Replace basic auth with Microsoft 365 sign-in
nginx now checks every dashboard and API request with oauth2-proxy
(auth_request), which signs users in with Microsoft Entra ID. Who may
sign in is controlled in Entra: a single-tenant app registration with
"Assignment required", so only assigned users/groups get through.

- deploy/oauth2-proxy.cfg (entra-id provider, auth-only mode, secure
  cookie, trusted proxy = localhost) and deploy/oauth2-proxy.service
- nginx: pages redirect to Microsoft sign-in and back to the same URL;
  API calls get a plain 401 (the page already asks to reload);
  /api/v1/ingest stays key-only for collectors; /oauth2/auth is
  internal; the signed-in user is passed to the API in headers nginx
  sets (so browsers can't spoof them); dashboard files are served with
  Cache-Control: no-cache so updates show up at once
- GET /api/v1/me; the top bar shows the signed-in user and a Sign out
  that also signs out of Microsoft (otherwise SSO signs straight back in)
- README: Entra and server setup steps

Tested locally with oauth2-proxy 7.15.5 and nginx 1.31 running the
deploy configs (TLS, self-signed): redirects to the tenant's sign-in
with the return URL kept, 401s, ingest bypass, sign-out allow-list,
cookie flags, security headers. The signed-in path was tested with a
stand-in for oauth2-proxy's auth answer; a real Entra sign-in wasn't.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 11:31:35 -06:00

8.4 KiB
Raw Blame History

Backup-Dashboard

Proof of concept backup dashboard, intended to display backup jobs from multiple sources.

collectors ──POST /api/v1/ingest (API key)──▶ ┌──────────────┐
                                              │ FastAPI +    │
browser ──nginx (TLS)──▶ /api/* ─────────────▶│ SQLite       │
         │  └─ static files: public/          └──────────────┘
         └─ auth_request ─▶ oauth2-proxy ─▶ Microsoft Entra ID (Microsoft 365 sign-in)
  • public/: static frontend (no build step). Add ?mock=1 to the URL to use built-in sample data.
  • backend/: FastAPI API backed by a single SQLite file.
  • deploy/: nginx site and systemd unit.
  • collectors/: PowerShell scripts that report results from backup products (Veeam B&R, Azure Backup / MARS, Windows Server Backup, Veeam Backup for M365). Code they share (config, logging, posting, state, installing) is in collectors/common/BackupDashboard.Collector.psm1.

Data model

  • Tenant (company) → Job → Run.
  • A job is keyed by (tenant, source_system, job.external_id), so renaming a job at the source updates it instead of creating a duplicate.
  • A run has status (running | success | warning | failed), started_at and finished_at. The dashboard's Last Run is the most recent finished_at. While a job is running it shows Running, and Last Run still shows when the previous run finished.
  • Runs older than BACKUP_DASH_RETENTION_DAYS (default 90, minimum 30) are purged every 6 hours.

Reporting results (collectors)

Each collector (a script on a Veeam server, a webhook relay, etc.) gets its own API key:

cd backend && python manage.py create-key veeam-acme

Then it POSTs one report per run. Send running when a run starts (optional) and the final status when it ends. Reports with the same started_at (or run.external_id) update the same run, and retries are safe.

curl -X POST https://backups.example.com/api/v1/ingest \
  -H "Authorization: Bearer bdk_..." -H "Content-Type: application/json" \
  -d '{
    "tenant": "Acme Corp",
    "source_system": "Veeam B&R",
    "job": { "external_id": "4f1c…", "name": "File Server – Nightly", "next_run_at": "2026-10-05T22:00:00-04:00" },
    "run": {
      "status": "success",
      "started_at": "2026-10-04T22:00:05-04:00",
      "finished_at": "2026-10-04T22:47:12-04:00",
      "message": "Job completed successfully.",
      "size_bytes": 128849018880,
      "details": { "repository": "REPO-01" }
    }
  }'
  • All timestamps must include a timezone offset (Get-Date -Format o in PowerShell does this).
  • job.next_run_at controls the Overdue flag. If you omit it, the stored value is kept. If you send null, it's cleared.
  • run can be omitted to only register or update a job.
  • Unknown fields are rejected (HTTP 422), so typos in collector scripts fail loudly.

API (read)

Endpoint Returns
GET /api/v1/jobs All jobs with current status, running_since, last_run_at, next_run_at
GET /api/v1/jobs/{id} One job, plus the last finished run's duration, size, message and details
GET /api/v1/jobs/{id}/runs?days=30 Run history (no messages), newest first
GET /api/v1/runs/{id} One run including message and details
GET /api/v1/health {"status": "ok"}

The OpenAPI schema is at /api/v1/openapi.json.

Authentication

  • Dashboard users: Microsoft 365 sign-in. nginx checks every request with oauth2-proxy, which signs users in with Microsoft Entra ID. Who may sign in is controlled in Entra (users or a group assigned to the app). Every signed-in user can see every tenant. Sessions last 12 hours; signing in again is usually silent. The dashboard shows who's signed in; Sign out also signs out of Microsoft in that browser, otherwise the next visit would sign straight back in.
  • Collectors: per-collector bearer keys, checked by the app. Only a SHA-256 hash is stored. To revoke one, run python manage.py revoke-key NAME. /api/v1/ingest is the only path that doesn't need Microsoft sign-in.

Microsoft 365 sign-in setup

In the Microsoft Entra admin center (entra.microsoft.com), as an admin of your tenant:

  1. App registrations → New registration. Name: Backup Dashboard. Supported account types: Accounts in this organizational directory only (single tenant). Redirect URI: platform Web, https://<your dashboard host>/oauth2/callback. Register.
  2. On its Overview, note the Application (client) ID and Directory (tenant) ID.
  3. Certificates & secrets → New client secret. Copy the secret's Value now (it's only shown once). Note its expiry date: sign-in stops working when it expires, so set a reminder to create a new one before then.
  4. Enterprise applications → Backup Dashboard → Properties: set Assignment required to Yes. Then Users and groups → Add user/group and add the people (or a group) who should see the dashboard. Everyone else in the tenant is refused by Microsoft before reaching it.

No API permissions beyond the default User.Read are needed.

On the dashboard server:

  1. Install oauth2-proxy 7.8 or newer from its releases as /usr/local/bin/oauth2-proxy, and create its user: sudo useradd --system --no-create-home oauth2-proxy.
  2. Install the config and the client secret, readable only by root and oauth2-proxy:
    sudo install -d -m 750 -g oauth2-proxy /etc/oauth2-proxy
    sudo install -m 640 -g oauth2-proxy deploy/oauth2-proxy.cfg /etc/oauth2-proxy/backup-dashboard.cfg
    sudo sh -c 'umask 027; printf %s "<client secret value>" > /etc/oauth2-proxy/client-secret' && sudo chgrp oauth2-proxy /etc/oauth2-proxy/client-secret
    
    In /etc/oauth2-proxy/backup-dashboard.cfg, fill in <TENANT_ID>, <APPLICATION_CLIENT_ID>, <DASHBOARD_HOST>, and <COOKIE_SECRET>, generated with: openssl rand -base64 32 | tr -- '+/' '-_'
  3. Install deploy/oauth2-proxy.service into /etc/systemd/system/, then sudo systemctl daemon-reload && sudo systemctl enable --now oauth2-proxy. Check it with journalctl -u oauth2-proxy -n 20: it should say "OAuthProxy configured for Microsoft Entra ID".
  4. Install the new deploy/nginx.conf (keeping your server name and certificate paths), then sudo nginx -t && sudo systemctl reload nginx. Basic auth and its htpasswd file are no longer used.

Troubleshooting: sign-in problems show in journalctl -u oauth2-proxy. Microsoft's own errors (wrong redirect URI, user not assigned, expired secret) appear on its sign-in page with an AADSTS… code.

Development

cd backend
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
pytest
python manage.py --create seed-demo            # 30 days of fake history in ./backup-dashboard.db
BACKUP_DASH_SERVE_STATIC=1 uvicorn --factory app.main:create_app --reload
# → http://localhost:8000

Deployment (Linux, nginx)

  1. Copy the repo to /opt/backup-dashboard, create a venv at /opt/backup-dashboard/venv, and run pip install -r backend/requirements.txt.
  2. Copy deploy/backup-dashboard.env to /etc/backup-dashboard/backup-dashboard.env. Both the service and manage.py read their settings from it.
  3. Create a backupdash system user, then install deploy/backup-dashboard-api.service into /etc/systemd/system/ and enable it. The database is created on first start.
  4. Copy public/ to /var/www/backup-dashboard/public, install deploy/nginx.conf, get a certificate with certbot, and set up Microsoft 365 sign-in (see Microsoft 365 sign-in setup).
  5. Run manage.py as the service user. It finds the database through the env file:
    cd /opt/backup-dashboard/backend
    sudo -u backupdash /opt/backup-dashboard/venv/bin/python manage.py create-key veeam-acme
    
    It prints the database path it's using and won't create a new database unless you pass --create.

Back up the SQLite file with sqlite3 backup-dashboard.db ".backup copy.db". Don't use a plain cp while the service is running.