server { listen 80; server_name backups.example.com; # Session cookies and API keys must never cross 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; # ---------- Microsoft 365 sign-in ---------- # oauth2-proxy (deploy/oauth2-proxy.cfg) runs on 127.0.0.1:4180 and does the sign-in with # Entra ID. Every request below asks it "is this browser signed in?" via auth_request. # Its own pages: /oauth2/start (go to Microsoft), /oauth2/callback, /oauth2/sign_out location /oauth2/ { proxy_pass http://127.0.0.1:4180; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; # Where to return after signing in: the page that was asked for proxy_set_header X-Auth-Request-Redirect $request_uri; # Entra ID session cookies are large proxy_buffer_size 16k; proxy_buffers 8 16k; } # The sign-in check itself, only reachable as an auth_request subrequest location = /oauth2/auth { internal; proxy_pass http://127.0.0.1:4180; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Uri $request_uri; proxy_set_header Content-Length ""; proxy_pass_request_body off; proxy_buffer_size 16k; proxy_buffers 8 16k; } # ---------- Dashboard ---------- location / { auth_request /oauth2/auth; # Not signed in: straight to Microsoft's sign-in, then back to this page error_page 401 = /oauth2/start; # Make browsers check for a newer page/script on every load (a cheap 304 when unchanged), # so an update shows up at once. (expires, not add_header: an add_header here would drop # the security headers above.) expires -1; try_files $uri $uri/ /index.html; } location /api/ { auth_request /oauth2/auth; # No redirect here: API calls from the page get a plain 401, and the page asks the user to # reload, which goes through sign-in above. # Who's signed in, for the dashboard's "signed in as" (proxy_set_header replaces any value # the browser sent, so these can't be spoofed) auth_request_set $auth_email $upstream_http_x_auth_request_email; auth_request_set $auth_user $upstream_http_x_auth_request_user; auth_request_set $auth_name $upstream_http_x_auth_request_preferred_username; proxy_set_header X-Forwarded-Email $auth_email; proxy_set_header X-Forwarded-User $auth_user; proxy_set_header X-Forwarded-Preferred-Username $auth_name; 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, # so no Microsoft sign-in here location = /api/v1/ingest { 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; } }