Skip to content
BILLETKnowledge Base

Put the portal behind a reverse proxy

An nginx site for the portal, with its TLS certificate, the paths that must reach the portal, and what changes for Cloudflare and Amazon DCV.

For
Administrators
Applies to
Billet 1.0
Updated

The portal listens on 127.0.0.1:5150 only. A reverse proxy in front of it holds the TLS certificate, passes on the paths the portal serves, and serves the portal's static files itself.

This page uses nginx. Any proxy works if it does the same things: see What any proxy must do.

1. Get a certificate

With the portal's DNS name pointing at this server and port 80 open, Let's Encrypt's certbot is the simplest way:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot certonly --nginx -d portal.example.com

The certificate lands in /etc/letsencrypt/live/portal.example.com/, and certbot renews it by itself.

2. Add the site

Create /etc/nginx/sites-available/billet-portal, replacing portal.example.com throughout:

upstream billet_portal {
    server 127.0.0.1:5150;
    keepalive 16;
}

server {
    listen 443 ssl;
    server_name portal.example.com;

    ssl_certificate     /etc/letsencrypt/live/portal.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/portal.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    # Nobody outside may claim to be the internal DCV zone.
    proxy_set_header X-Beam-Zone-Token "";

    # People: sign-in, their desktops, the browser session page.
    location /sso/     { proxy_pass http://billet_portal; }
    location /portal/  { proxy_pass http://billet_portal; }
    location = /portal { proxy_pass http://billet_portal; }

    # Machines: hosts and relays check in and enroll here.
    location /agent/   { proxy_pass http://billet_portal; }
    location = /enroll { proxy_pass http://billet_portal; }
    location /enroll/  { proxy_pass http://billet_portal; }
    location /i/       { proxy_pass http://billet_portal; }

    # Administrators: the console and its API.
    location /admin/    { proxy_pass http://billet_portal; }
    location /console/  { proxy_pass http://billet_portal; }
    location = /console { proxy_pass http://billet_portal; }

    # Identity providers: user provisioning.
    location /scim/ { proxy_pass http://billet_portal; }

    # The deployment's root certificate, which the Billet app fetches.
    location = /ca.pem   { proxy_pass http://billet_portal; }
    location = /_health  { proxy_pass http://billet_portal; }

    # Files nginx serves itself. The sign-in page does not work without these.
    location /static/ {
        alias /opt/billet/portal/assets/static/;
        add_header Cache-Control "no-cache";
        add_header X-Content-Type-Options "nosniff" always;
    }
    location /downloads/ {
        alias /opt/billet/portal/downloads/;
        default_type application/octet-stream;
        add_header X-Content-Type-Options "nosniff" always;
    }

    # The DCV endpoints never answer on the public listener.
    location /dcv/ { return 404; }
    location = /   { proxy_pass http://billet_portal; }
    location /     { return 404; }
}

server {
    listen 80;
    server_name portal.example.com;
    location = /_health { proxy_pass http://billet_portal; }
    location /          { return 308 https://$host$request_uri; }
}

Enable it and reload:

sudo ln -s /etc/nginx/sites-available/billet-portal /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

3. Check it from outside

From another computer:

curl -sS -o /dev/null -w '%{http_code}\n' https://portal.example.com/_health

It prints 200. Then open https://portal.example.com/ in a browser: it shows the sign-in page, styled. An unstyled page means /static/ is not being served.

Do not test with curl -k. It hides the certificate problem this step exists to catch.

What any proxy must do

  • Pass the paths above to the portal, and answer 404 for /dcv/ on the public listener.
  • Serve /static/ from /opt/billet/portal/assets/static/.
  • Keep the Host header the browser sent. The portal uses it to tell its own name from a tenant's own address.
  • Send X-Forwarded-For and X-Forwarded-Proto: https. The portal binds each session to the address that asked for it, and reads that address from X-Forwarded-For.
  • Strip any X-Beam-Zone-Token header that arrives from outside.

Cloudflare in front

The portal's name can be proxied by Cloudflare. nginx must then restore each visitor's real address from CF-Connecting-IP for Cloudflare's address ranges, using nginx's real_ip module, or every session is bound to a Cloudflare address.

Keep each relay's name DNS-only (not proxied) unless you have followed the relay's own instructions for a proxy. See Install a relay.

Amazon DCV

If you broker Amazon DCV desktops, the gateway and DCV Server reach the portal on a second listener with its own name, which passes only /dcv/t/ to the portal. Keep /dcv/ answering 404 on this site. See Networking: the DCV gateway and the Billet relay.