App Deployment·8 min read·

HTTPS, Security Headers, and Hiding PHP Version

Terminate TLS on nginx, send strict security headers, and hide PHP and nginx versions so scanners see a boring VPS instead of a versioned attack surface.

NB

Netbay Infrastructure Team

Netbay Engineering

On this page

A PHP app that still answers on port 80 with X-Powered-By: PHP/8.3.6 is advertising both the transport and the interpreter. Production on a VPS means TLS at nginx, HTTP redirected, version tokens off, and a small set of headers that browsers actually honor. This is not a compliance novel. It is the nginx and php.ini work you do once after the site loads.

The VPS in Lucknow already sits behind DDoS Protected networking with L3/L4 filtering. That does not encrypt your HTTP, and it does not hide PHP. Those are your job on the instance.

TLS at nginx, PHP never sees the cert Client SNI + TLS 1.2+ 443 nginx TLS headers, hide versions FastCGI PHP-FPM expose_php = Off Let's Encrypt cert on High-Speed SSD Redirect 80 to 443. PHP speaks FastCGI on localhost only.

Terminate TLS on nginx, not in PHP

PHP has no business loading a certificate. nginx listens on 443, serves the Let's Encrypt fullchain, and proxies PHP over the unix socket. certbot with the nginx plugin writes the ssl_certificate lines. Prefer TLS 1.2 and 1.3 only. Disable old ciphers. HTTP on 80 exists only to redirect and to answer ACME http-01.

nginx
server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.example.com;
    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    root /var/www/app/public;
    index index.php;
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    location ~ .php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_hide_header X-Powered-By;
        fastcgi_param HTTPS on;
        fastcgi_param HTTP_HOST $host;
    }
}

fastcgi_param HTTPS on so Laravel APP_URL and WordPress is_ssl() see HTTPS behind nginx. Without it, mixed-content and redirect loops appear. HSTS comes after you are sure HTTPS works; do not set max-age=31536000 on a hostname you still test over HTTP.

Cert files live on High-Speed SSD under /etc/letsencrypt. Renew with certbot.timer (Ubuntu enables it). nginx -t && systemctl reload nginx is the deploy hook after a renew. Fail to reload and you serve an expired cert with a valid file on disk.

Hide versions

Three places leak the stack:

  • nginx server_tokens on; (default) sends Server: nginx/1.24.0
  • PHP expose_php On sends X-Powered-By: PHP/8.3.x
  • phpinfo pages and default welcome sites

Turn them off.

ini
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
allow_url_fopen = On
allow_url_include = Off

In nginx.conf http block set server_tokens off. Hide X-Powered-By with fastcgi_hide_header even if expose_php is already Off; belts and braces. Delete /var/www/html/index.nginx-debian.html. Delete any phpinfo you used for debugging. Remove public/.env from the web root; it should not be there at all.

Hiding versions is not security by itself. It just stops the first pass of scanners that grep banners. Pair it with unattended-upgrades for nginx and php8.3-fpm so the versions you hide are still patched.

Headers that are worth sending

HSTS, nosniff, SAMEORIGIN (or CSP frame-ancestors), and Referrer-Policy cover most apps. CSP is useful once you can list scripts; a naive default-src 'self' will break inline admin UIs and WordPress plugin JS. Add CSP when you can test it in report-only. Permissions-Policy shuts off browser features you do not use.

Do not set X-XSS-Protection. Browsers deprecated it. Do not set a dozen duplicate add_header lines in nested locations; nginx inherits add_header poorly. Put headers in the server block that also serves PHP, or use an included snippet from every server.

Cookies: Laravel and WordPress session cookies should be Secure, HttpOnly, and SameSite=Lax or Strict. SESSION_SECURE_COOKIE=true in Laravel .env once HTTPS is on. WordPress does this when it believes the request is HTTPS, which is why the FastCGI HTTPS parameter matters.

What the VPS edge already does

L3/L4 filtering drops volumetric noise before it hits nginx. It will not stop a POST to /wp-login.php. Rate-limit that location in nginx (limit_req) if you need to. Do not confuse DDoS Protected networking with application authentication.

Open only 22, 80, and 443 in the VPS firewall. PHP-FPM listens on a unix socket, MariaDB on 127.0.0.1. There is no reason to publish 9000 or 3306.

Takeaway

HTTPS is nginx plus a certificate plus a redirect. Headers are a short allowlist. PHP version hiding is expose_php Off and fastcgi_hide_header. Do those three on every PHP VPS before you argue about WAF products.

You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds, point DNS at Lucknow, and put TLS on nginx the same day - netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance