Securing a Backstage deployment in production: setting up an nginx or Caddy reverse proxy with HTTPS and trusted proxies, restricting admin access, applying a Content Security Policy, securing cookies and OAuth2 sessions, and holding the principle of least privilege for every integration.

In episode 14, you connected Backstage to external services with tightly guarded credentials. Episode 15 closes the remaining gap: the deployment itself. An application running directly on port 7007 with no proxy, no HTTPS, and insecure cookies is an easy target. This episode covers deployment security & hardening — from the network gateway in front all the way to the cookie details in the back.
Backstage shouldn't stand alone on the internet. A reverse proxy — nginx or Caddy — sits in front, and it's the only point publicly exposed. Its two main jobs: forwarding requests to the backend, and terminating TLS so all traffic runs over HTTPS.
server {
listen 443 ssl;
server_name portal.example.com;
ssl_certificate /etc/letsencrypt/live/portal/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/portal/privkey.pem;
location / {
proxy_pass http://backstage-backend:7007;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Caddy is a simpler alternative because certificates are managed automatically via Let's Encrypt. Whatever you choose, the principle is the same: TLS terminates at the proxy, and the backend stays on an internal network not directly exposed.
With a proxy in front, Backstage needs to know that incoming requests really came through a trusted proxy — not fake requests injecting X-Forwarded-For headers at will. Make sure the X-Forwarded-Proto and X-Forwarded-Host headers are forwarded correctly, and that the configuration in app-config.yaml points to the public HTTPS URL:
app:
baseUrl: https://portal.example.com
backend:
listeningHost: 0.0.0.0With the correct baseUrl, the links Backstage generates (for example OAuth callbacks) always use the public URL, and sessions aren't broken just because requests detour through the proxy.
The fewer people with full access, the smaller the risk. Combining RBAC from episode 13 with network-level restrictions creates layers of defense: only the admin group has permission to change configuration, and admin pages can only be reached from certain networks if necessary.
The recommended pattern: the admin role is only for a small group like platform-eng, human accounts for day-to-day activities, and emergency access through a documented mechanism. Admin access spread across many people is one of the leading causes of production damage.
The Content Security Policy (CSP) tells the browser which resources a page is allowed to load — scripts, styles, frames, and more. Without CSP, a single XSS vulnerability can be exploited immediately. Backstage loads many assets from itself, and TechDocs needs iframes, so the CSP needs careful crafting:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://techdocs.example.com; img-src 'self' data:" always;The rule of thumb: start from the strictest policy, then add sources only when a feature truly needs them — don't blindly copy an example header. Every added source is a new attack surface.
Caution
TechDocs renders documentation inside an iframe. If your CSP is too strict
and blocks frame-src, documentation pages will appear empty with no clear
message. Test the CSP in a staging environment before applying it to production.
User sessions in Backstage are stored in cookies, and an unsecured cookie can be stolen over the network or through JavaScript. Make sure cookies use the secure flag so they're only sent over HTTPS, and sameSite to narrow cross-site requests:
auth:
session:
secret: ${SESSION_SECRET}
providers:
github:
production:
clientId: ${GITHUB_CLIENT_ID}
clientSecret: ${GITHUB_CLIENT_SECRET}
callbackUrl: https://portal.example.com/api/auth/github/handler/frameOAuth2 sessions also need proper configuration: the session secret is guarded, the callback URL is consistent with baseUrl, and sessions are refreshed or terminated when no longer needed. Remember the lesson from episode 14: short-lived tokens are always better.
| Control | Function | Layer |
|---|---|---|
| TLS and reverse proxy | Encrypt traffic, hide the backend | Network |
| Trusted proxies | Prevent header spoofing | Network |
| CSP | Limit resource sources in the browser | Application |
| Secure cookies | Protect sessions from theft | Application |
| RBAC | Control who can do what | Application |
| Least privilege | Reduce the access surface | Data and integrations |
Finally, the least privilege principle applies to all integrations: every token, service account, and API key you connect only gets the minimum scope needed. A CI token that can delete repos isn't for a portal that only reads status. Combine this with the rotation from episode 14 — minimal rights and short lifetimes are two habits that reinforce each other.
Episode 15 secured your Backstage deployment from the outside in: reverse proxy with TLS, trusted proxies, restricted admin access, Content Security Policy, secure cookies and OAuth2 sessions, and the least privilege principle for all integrations. Your portal is now not only functional, but worthy of production use.
The key takeaways:
In the next episode, episode 16, you put all that secured data and access to use: search & discovery — how Backstage indexes entities, TechDocs, and other sources, then searches them through a single query API.