Skip to content

Securing Deployments

There are many steps we can take to secure our applications. This list is in priority order, meaning the first steps are the most important and should be implemented on all our projects while later steps may only be necessary of the most secure applications.

Traditionally, you would have a DNS record that points directly to your application’s web server. Using Cloudflare’s proxy service, all requests get sent to Cloudflare’s network first, then Cloudflare forwards the request to your application’s web server.

Proxying requests keeps your application’s web server hidden from the public internet. This makes it harder for attackers to discover and exploit vulnerabilities and allows us to leverage Cloudflare’s advanced network security features.

  • You are using Cloudflare for your DNS
  1. Navigate to the DNS Records page in Cloudflare
  2. Add a DNS A record pointing to the web server’s IP address and enable Cloudflare proxy via the toggle.

//TODO: Add information about setting up Trusted Proxies in laravel so that the proxy doesn’t break rate limiting

While the Cloudflare Proxy secures the connection between the user and Cloudflare, we must also secure the connection between Cloudflare and our VPS. We use Cloudflare Origin Certificates to ensure valid encryption end-to-end without relying on complex Let’s Encrypt renewal chains on the origin.

Eliminates “Man-in-the-Middle” attacks between Cloudflare’s edge and our server. It also simplifies SSL management as Origin Certificates can be issued for 15 years, removing the need for frequent automated renewals on the server.

  • Cloudflare Proxy enabled.
  1. In Cloudflare Dashboard, go to SSL/TLS > Origin Server.
  2. Click Create Certificate. Keep default settings (RSA 2048, 15 years).
  3. In Laravel Forge, navigate to the Site > SSL.
  4. Select Install Existing Certificate and paste the Private Key and Certificate provided by Cloudflare.
  5. Set the Cloudflare SSL/TLS mode to Full (Strict).

This is the most critical security layer for our prod environments. We configure the Nginx web server to only accept traffic that presents a specific client certificate signed by Cloudflare.

This effectively “locks” the web server to the internet. Even if an attacker discovers the real IP address of the VPS, they cannot access the website because they cannot present the Cloudflare client certificate. The connection is dropped instantly.

  • Cloudflare Origin Certificate installed (see above).
  • Authenticated Origin Pulls enabled in Cloudflare Dashboard (SSL/TLS > Origin Server).
  1. Download the Cloudflare CA certificate to the server:
    Terminal window
    wget https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem -O /etc/nginx/certs/cloudflare-origin-pull.pem
  2. In Laravel Forge, edit the Nginx Configuration for the site.
  3. Add the following within the server { ... } block listening on port 443:
    ssl_client_certificate /etc/nginx/certs/cloudflare-origin-pull.pem;
    ssl_verify_client on;
  4. Save and restart Nginx.

Firewall IP Allow-listing & Tailscale (Dark SSH)

Section titled “Firewall IP Allow-listing & Tailscale (Dark SSH)”

We implement a “Zero Trust” network architecture. We use iptables to block all traffic at the packet level that does not come from Cloudflare (for web traffic) or our internal Tailscale network (for management).

Reduces server load by blocking bot scanners at the network level before they hit Nginx. Removes the SSH port (22) from the public internet entirely, nullifying brute-force SSH attacks/

  • Tailscale installed and authenticated on the VPS.
  • Server tagged correctly in Tailscale Admin Panel (e.g., tag:prod or tag:dev).

1. Tailscale & SSH

  • Ensure the UFW firewall allows traffic on the Tailscale interface but blocks public SSH (except for Forge):
    Terminal window
    # Allow local loopback and Tailscale network
    ufw allow in on lo
    ufw allow in on tailscale0
    # Deny public SSH (Port 22), but allow Laravel Forge IPs
    # (Forge IPs must be added manually or via script)
    ufw deny 22/tcp

2. Web Traffic (Cloudflare IP Script)

  • We run a root-level script to fetch Cloudflare IPs and update iptables to drop all other traffic on port 443.
  • This script must run via sudo crontab -e (not the Forge scheduler) to have permission to modify firewall rules.
  • Note: Ensure the script includes a failsafe to allow SSH access via the Tailscale interface.

We tweak the default PHP and Laravel configurations to reduce information leakage and prevent client-side attacks.

Prevents “reconnaissance” where attackers determine version numbers to find exploits. Prevents XSS, Clickjacking, and MIME-sniffing attacks.

1. PHP Configuration

  • In Forge, go to PHP > FPM Configuration.
  • Search for and update: expose_php = Off.
  • Restart PHP-FPM.

2. Nginx Configuration

  • In the Nginx config, ensure server_tokens off; is set.

3. Security Headers (Laravel Middleware)

  • In bootstrap/app.php, append the Security Headers middleware.
  • This middleware must inject:
    • X-Frame-Options: DENY
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: strict-origin-when-cross-origin
    • Content-Security-Policy (configured for Inertia/React).

Security is a process, not just a configuration. We assume that despite our best efforts, secrets may leak.

Limits the “blast radius” of a compromised API key. If a key was leaked 2 months ago, rotating it today ensures an attacker loses access immediately.

  • Frequency: Quarterly (Every 3 months).
  • Scope:
    • AWS IAM User Keys (S3/SES).
    • Database Passwords (if accessible externally).
    • Stripe/Payment Gateway Secret Keys.
    • Third-party Service Tokens (Slack, Twilio, etc.).
  • Protocol: Generate new key -> Update .env on all servers -> Verify functionality -> Revoke old key.