---
title: Web Server, SSL and HTTPS
description: "RackHelm is served by nginx by default, with a Let's Encrypt certificate issued during installation. This article covers the server block the installer writes, how certificates are obtained and renewed, what to do when TLS is terminated by Cloudflare or…"
url: "https://rackhelm.com/documentation/web-server-ssl-and-https/"
updated: "2026-09-24"
category: "Installation & Server Setup"
---

# Web Server, SSL and HTTPS

RackHelm is served by nginx by default, with a Let's Encrypt certificate issued during installation. This article covers the server block the installer writes, how certificates are obtained and renewed, what to do when TLS is terminated by Cloudflare or another proxy, and why a portal without a certificate cannot be logged into at all.

## HTTPS is not optional

RackHelm's session cookie is marked **Secure**, which means the browser will only store and return it over HTTPS. Over plain HTTP the cookie is discarded, so the CSRF token in the login form never matches the session and every sign-in attempt fails with `419 Page Expired`. Session data is encrypted as well, and generated links are forced to HTTPS whenever `APP_URL` begins with `https://`.

> **Warning:** If certificate issuance fails during installation, the installer prints an explicit warning that the site is serving plain HTTP and that logins will fail with 419 until a certificate is in place. Treat that warning as a blocking error, not a note.

## The nginx site the installer writes

The configuration is written to `/etc/nginx/sites-available/rackhelm.conf` on Debian and Ubuntu (symlinked into `sites-enabled`) or `/etc/nginx/conf.d/rackhelm.conf` on RHEL. In outline:

```nginx
server {
    listen 80;
    server_name portal.example.com;
    root /var/www/rackhelm/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()";

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ .php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
    }

    location ~ /.(?!well-known).* {
        deny all;
    }
}
```

On RHEL the FastCGI socket is `unix:/run/php-fpm/www.sock` and the Debian-specific snippet is replaced by `try_files $fastcgi_script_name =404;` followed by `include fastcgi_params;` and an explicit `SCRIPT_FILENAME`. The installer also enables PHP-FPM as the `nginx` user rather than `apache`.

The generated file also carries a `Content-Security-Policy-Report-Only` header, which reports but never blocks. That is deliberate: the enforced policy is built per request in PHP (see the warning below), and a second *enforcing* header here would silently narrow it.

Two further pieces of hardening are applied outside the block above:

- **A catch-all default server (Debian and Ubuntu).** After removing nginx's stock default site, the installer adds `/etc/nginx/sites-available/rackhelm-catchall.conf`, which listens on port 80 as the default server for any other `server_name` and returns `444` — closing the connection with no response. A request with a spoofed `Host` therefore never reaches PHP.
- **`cgi.fix_pathinfo=0`.** Written to `99-rackhelm-security.ini` in the PHP configuration directory, so a request such as `/storage/…/evil.jpg/x.php` cannot walk back to an uploaded image and execute it.

![Terminal showing the generated rackhelm.conf nginx server block](/wp-content/uploads/rackhelm-docs/installation-nginx-site-config.png)
Check the generated site file if you need to add a custom directive; keep the try_files guard in the PHP location.

## Let's Encrypt

When you answer **Y** to the certificate question, the installer runs:

```bash
certbot --nginx -d portal.example.com --non-interactive --agree-tos
  -m admin@portal.example.com --redirect
```

Certbot rewrites the server block to listen on 443, installs the certificate, and adds an HTTP-to-HTTPS redirect. Once a `listen 443` directive is present, the installer inserts an HSTS header:

```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
```

HSTS is added only after a certificate exists, because emitting it over plain HTTP is meaningless and would risk locking the site out if issuance later failed.

Certbot's own systemd timer handles renewal. Verify it with:

```bash
certbot renew --dry-run
```

### Issuing a certificate later

If issuance failed, or you installed without one, fix DNS and firewalling first, then run the command the installer printed:

```bash
certbot --nginx -d portal.example.com --redirect
```

Then update `.env` so the application knows it is on HTTPS:

```ini
APP_URL=https://portal.example.com
ASSET_URL=https://portal.example.com
```

## Behind Cloudflare or a reverse proxy

When TLS is terminated in front of the server, answer **n** to the certificate question and **y** to **Are you running behind a reverse proxy or Cloudflare using HTTPS?**. The installer then writes `APP_URL` and `ASSET_URL` with an `https://` scheme even though nginx itself only listens on port 80.

That is not the whole job. You must also tell RackHelm which proxies it may trust, or it will read the proxy's IP address as the visitor's — breaking login throttling, audit trails and fraud checks:

1. Open **Platform Settings → General & System**.
2. Fill in **Trusted Proxies** with a comma-separated list of IP addresses or CIDR ranges, for example `10.20.151.165, 192.168.1.0/24`.
3. Save.

Once a proxy is trusted, RackHelm honours `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto` and `X-Forwarded-Prefix` from it. The field is validated: only literal IP addresses and CIDR ranges with a prefix of `/8` or narrower are accepted, and the keywords `REMOTE_ADDR`, `PRIVATE_SUBNETS` and `private_ranges` are rejected, because trusting everything would let any caller forge those headers. Leaving the field blank trusts no proxy at all.

> **Note:** **Trusted Proxies** requires the `edit_company_settings` permission.

## Running on Apache

The installer only configures nginx, but RackHelm ships a complete `public/.htaccess`, so Apache works if you point a virtual host at `/var/www/rackhelm/public`, enable `mod_rewrite` and `mod_headers`, and allow overrides on that directory. The shipped file sets HSTS, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy` and `Permissions-Policy`, disables directory indexes, forwards the `Authorization` and `X-XSRF-Token` headers, and routes everything that is not a real file to `index.php`.

> **Warning:** Do not add a `Content-Security-Policy` header in your web-server configuration. RackHelm builds the policy per request in PHP, and some pages widen it at runtime — the Pterodactyl console has to open a websocket to whichever node a customer's server lives on. Browsers intersect two CSP headers on one response, so a static copy silently cancels every runtime addition and breaks those pages. Use the `CSP_*` variables in `.env` instead.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| Login always returns `419 Page Expired` | No TLS on the portal. The session cookie is Secure-only, so the browser never stores it. Install a certificate, or terminate HTTPS at a proxy and set `APP_URL` to the `https://` address. |
| Certbot failed during installation | The hostname does not resolve to this server, or port 80 is not reachable from the internet. Fix both, then re-run the `certbot --nginx` command. |
| `400 Bad Request` with nothing in `laravel.log` | The request's `Host` is not the host in `APP_URL`. This is rejected before the application logs anything. Check `APP_URL` and the **Company URL** setting. |
| Connections to the server's IP or another hostname hang or return nothing | Expected on Debian and Ubuntu — the catch-all server returns `444`. Browse by the portal's own hostname. |
| `nginx configuration test or restart failed` during install | Another site already claims port 80 or the same `server_name`. Run `nginx -t` for the specific error and check `/var/log/rackhelm_install_servers.log`. |
| Visitor IP addresses all show as the proxy's | **Trusted Proxies** is empty or the entry was rejected by validation. Use a literal IP or a CIDR of `/8` or narrower. |
| Assets load but the game-server console or a third-party widget is blocked | A CSP directive is too narrow, or a second CSP header is being set by the web server. Remove any static CSP header and widen the policy with `CSP_CONNECT_SRC` and friends. |
| PHP files download instead of executing | The `location ~ .php$` block or its `fastcgi_pass` socket path is wrong. Check the PHP-FPM socket matches the PHP version actually installed. |
