# SSL/TLS Setup

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

Source: https://scansuite.eu/documentation/installation/ssl-tls-setup
Last reviewed: 2026-10-19

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:

| Route | When |
|---|---|
| Your certificate on ScanSuite's own nginx | The usual case: ScanSuite has ports 80 and 443 to itself. |
| Behind a web server the host already runs | The 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](https://scansuite.eu/documentation/installation/set-up-defectdojo).

The values this page uses:

| Placeholder | Where 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

> **A person does this step:** Create a DNS A record for <YOUR_DOMAIN> pointing at <SERVER_IP>, in your DNS provider or with your DNS administrator.

```bash
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

> **A person does this step:** 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.

```bash
sudo snap install certbot --classic
```

> **A person does this step:** 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:

```bash
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;
}
```

> **Warning:** 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 console | TRUSTED_PROXY_HOPS |
|---|---|
| ScanSuite's own nginx, nothing in front | 1 (the default) |
| The host's web server in place of ScanSuite's nginx, as above | 1 (the default) |
| A load balancer or another proxy in front of either | 2 |

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 setting | What it does | Default | When to change |
|---|---|---|---|
| TRUSTED_PROXY_HOPS | How many proxies in front of the console are trusted to report the visitor's address. | 1 | A load balancer in front: 2. |
| MAX_UPLOAD_MB | The largest upload the console accepts, in MB. | 2000 | Larger 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). |
