Deployment

SSL/TLS Setup

Serve the ScanSuite console over a custom certificate, or behind a web server the host already runs.

View as Markdown

A new installation serves the console with a self-signed certificate, so browsers warn. Replace it with a certificate for your domain in one of two ways:

RouteWhen
Your certificate on ScanSuite's own nginxThe usual case: ScanSuite has ports 80 and 443 to itself.
Behind a web server the host already runsThe host's nginx, Apache or a load balancer already serves ports 80 and 443, and keeps the certificate.

DefectDojo is a separate installation with its own domain and certificate — see Set up DefectDojo.

The values this page uses:

PlaceholderWhere it comes from
<YOUR_DOMAIN>The name the console is served on, such as scansuite.example.com. Your DNS administrator creates it.
<SERVER_IP>The server's address that <YOUR_DOMAIN> points at.
<ACCOUNT>The account that installed ScanSuite and runs ./scansuite.
<YOUR_EMAIL>For Let's Encrypt only: where expiry notices go.

Your certificate on ScanSuite's nginx

1. Point a DNS name at the server

Create a DNS A record for <YOUR_DOMAIN> pointing at <SERVER_IP>, in your DNS provider or with your DNS administrator.

Shell
dig +short <YOUR_DOMAIN>

Expected: <SERVER_IP>. A new record can take a while to appear.

2. Get a certificate

You need the certificate with its full chain and its private key, both in PEM format. Take one of these:

From your own certificate authority

Request a certificate for <YOUR_DOMAIN> from your organisation's certificate authority and copy the certificate chain and the private key to the server, for example to ~/certs/fullchain.pem and ~/certs/privkey.pem. Then go to step 3.

From Let's Encrypt

For a server the internet reaches on port 80. Certbot borrows port 80 for a minute, so the command below stops ScanSuite's nginx while it runs, copies the certificate into place and starts nginx again — on the first run and at every automatic renewal. Scans keep running; the console is unreachable for that minute.

Shell
sudo snap install certbot --classic

The --agree-tos option accepts the Let's Encrypt subscriber agreement on your behalf. A person reads it and decides to run the command.

Issue the certificate and set up renewal
sudo certbot certonly --standalone -d <YOUR_DOMAIN> -m <YOUR_EMAIL> --agree-tos \
  --pre-hook 'cd /opt/scansuite && docker compose stop nginx' \
  --deploy-hook 'install -o <ACCOUNT> -m 644 "$RENEWED_LINEAGE/fullchain.pem" /opt/scansuite/services/nginx/certs/cert.pem && install -o <ACCOUNT> -m 600 "$RENEWED_LINEAGE/privkey.pem" /opt/scansuite/services/nginx/certs/key.pem' \
  --post-hook 'cd /opt/scansuite && docker compose start nginx'

Expected: Successfully received certificate. The certificate is already in place; go to step 4. Check that renewal will work — this stops and starts nginx too:

Shell
sudo certbot renew --dry-run

Expected: Congratulations, all simulated renewals succeeded.

3. Put the certificate in place

For a certificate from your own CA. nginx expects the certificate with its full chain as cert.pem and the private key as key.pem. Both must belong to your account, or the next ./scansuite update stops with cannot read …:

bash
cd /opt/scansuite
cp ~/certs/fullchain.pem services/nginx/certs/cert.pem
cp ~/certs/privkey.pem services/nginx/certs/key.pem
chmod 600 services/nginx/certs/key.pem
docker compose restart nginx

docker compose restart nginx reloads only nginx, so scans keep running (prefix sudo if your account is not in the docker group). If you copied the files with sudo, make them yours first: sudo chown "$USER": services/nginx/certs/cert.pem services/nginx/certs/key.pem.

4. Check the certificate

bash
cd /opt/scansuite && ./scansuite doctor
openssl s_client -connect <YOUR_DOMAIN>:443 -servername <YOUR_DOMAIN> </dev/null 2>/dev/null | openssl x509 -noout -subject -enddate

Expected: doctor reports TLS certificate valid for N more days and ends with No problems found; openssl prints your domain as the subject and the expiry date; the browser opens https://<YOUR_DOMAIN> without a warning.

If doctor reports services/nginx/certs/key.pem is not the private key of cert.pem, the two files do not belong together and nginx will not start with them: copy the matching key. Updates keep your certificate and key as a pair. If they do not match at an update, it puts the release's self-signed pair in place, so the console still answers, and keeps yours in a .replaced-<date> directory in /opt/scansuite.

Behind a web server the host already runs

When the host already serves ports 80 and 443 — its own nginx or Apache, or a proxy in front of several sites — keep the certificate there and let that server forward to ScanSuite.

1. Hand the ports to the host

Publish the console (the web service, port 5000) on the loopback address and switch off ScanSuite's own nginx in /opt/scansuite/docker-compose.local.yml:

docker-compose.local.yml
services:
  web:
    ports:
      - "127.0.0.1:5000:5000"
  nginx:
    profiles: ["bundled-nginx"]

The file belongs to the host: updates never change it, and install, update and start always include it. Apply it and check:

bash
cd /opt/scansuite && ./scansuite restart
./scansuite doctor
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5000/log_in

Expected: doctor reports ports 80 and 443 are left to the host: this installation runs no nginx, and curl prints 200.

2. Point the host's web server at it

For nginx:

nginx
server {
    listen 443 ssl;
    server_name <YOUR_DOMAIN>;
    ssl_certificate     /etc/letsencrypt/live/<YOUR_DOMAIN>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<YOUR_DOMAIN>/privkey.pem;
    client_max_body_size 2000M;
    proxy_read_timeout 600;
    proxy_connect_timeout 600;
    proxy_send_timeout 600;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name <YOUR_DOMAIN>;
    return 301 https://$host$request_uri;
}

Keep the body size and the timeouts, which are the ones ScanSuite's own nginx uses: code archives and reports are uploaded through the console, and without them the proxy cuts large uploads and long requests short with 413 and 504 errors.

Check: curl -sI https://<YOUR_DOMAIN>/log_in | head -1 prints a 200 status line.

3. Tell ScanSuite how many proxies stand in front of it

ScanSuite reads the visitor's address — for rate limits and the sign-in log — from what the last proxy forwards, and trusts only as many proxies as TRUSTED_PROXY_HOPS in /opt/scansuite/.env says:

In front of the consoleTRUSTED_PROXY_HOPS
ScanSuite's own nginx, nothing in front1 (the default)
The host's web server in place of ScanSuite's nginx, as above1 (the default)
A load balancer or another proxy in front of either2

Too low, and every visitor looks like the proxy and shares one rate limit; too high, and a visitor can forge their address. After changing it, run ./scansuite restart.

.env settingWhat it doesDefaultWhen to change
TRUSTED_PROXY_HOPSHow many proxies in front of the console are trusted to report the visitor's address.1A load balancer in front: 2.
MAX_UPLOAD_MBThe largest upload the console accepts, in MB.2000Larger code archives: raise it, and client_max_body_size in the proxy with it (and in services/nginx/default.conf.template for ScanSuite's own nginx).

Last reviewed 2026-10-19