Securing API Systems on VPS: A Comprehensive Guide to Cloudflare Access and Zero-Trust JWT Validation with Caddy
Introduction to Modern API Security on Virtual Private Servers
In the contemporary landscape of cloud computing, Virtual Private Servers (VPS) remain a cost-effective and powerful choice for hosting backend APIs. However, exposing a raw VPS directly to the public internet introduces significant security vulnerabilities. Traditional perimeter-based security measures, such as relying solely on basic firewalls or API keys embedded in code, are no longer sufficient against sophisticated automated scanning and zero-day exploits.
To safeguard sensitive data and business logic, modern architecture demands a Zero-Trust security model. Zero-Trust operates on a simple yet strict premise: never trust, always verify. This blog post provides an end-to-end guide on implementing a robust, production-ready API security system on your VPS by combining the edge network protection of Cloudflare Access with the high-performance, automated validation of JSON Web Tokens (JWT) at the Caddy web server layer.
---The Anatomy of the Defense Stack: Why Cloudflare Access and Caddy?
Before diving into the configuration, it is critical to understand how these two technologies complement each other to create a defense-in-depth architecture.
- Cloudflare Access (Cloudflare Zero Trust): Acts as an intelligent reverse proxy and identity firewall at the global edge. It intercepts incoming traffic before it ever reaches your VPS. By forcing authentication against your chosen identity providers (such as Google Workspace, GitHub, Okta, or OIDC), Cloudflare ensures that only explicitly authorized users or service accounts can pass. When a request is authenticated, Cloudflare signs a unique JWT and attaches it to the request header.
- Caddy Server with JWT Validation: Caddy is a modern, memory-safe web server written in Go, famous for its automatic SSL/TLS management. By equipping Caddy with a JWT validation module, the web server acts as the final gatekeeper on your VPS. It cryptographically verifies the signature of the incoming Cloudflare JWT against Cloudflare's public keys. If the token is missing, expired, or tampered with, Caddy rejects the request immediately at the web server layer, preventing malicious traffic from ever hitting your upstream application code (e.g., Node.js, Python, or Go runtimes).
Key Benefit: This dual-layer approach means your backend applications do not need to waste CPU cycles parsing or validating authentication tokens, nor do they need to handle complex OAuth flows. Security is decoupled from the business logic.---
Step 1: Setting Up Cloudflare Access for Your API Subdomain
The first line of defense begins at the Cloudflare Edge. You must configure Cloudflare Access to protect the specific subdomain pointing to your VPS API endpoint.
1.1 Create an Access Application
Navigate to your Cloudflare Zero Trust Dashboard and follow these steps:
- Go to Access > Applications and click Add an Application.
- Select Self-Hosted as the application type.
- Configure the Application Information by entering an Application Name (e.g.,
Production API Gateway) and setting the Session Duration. - Specify the Application Domain. For example, if your API is hosted at
[https://api.yourdomain.com/v1](https://api.yourdomain.com/v1), enterapi.yourdomain.comas the subdomain/domain.
1.2 Configure Access Policies
Policies define who or what is allowed to communicate with your API. For programmatic API access (such as a mobile application or a frontend single-page application communicating with the backend), you should configure a Service Token or explicit identity rules.
For standard user-facing or developer APIs, create a policy with an Allow action and assign rules based on email domains, specific IP ranges, or GitHub organization membership. Once saved, Cloudflare will block any unauthenticated curl requests or unauthorized browser sessions with a 403 Forbidden or redirect them to an identity provider login page.
---Step 2: Installing Caddy with the JWT Verification Plugin
Standard distributions of Caddy do not include JWT validation capabilities out of the box. Because Caddy is highly modular, we need to compile or download a custom Caddy binary that includes a trusted JWT plugin, such as [github.com/techknowlogick/caddy-jwt](https://github.com/techknowlogick/caddy-jwt) or utilizing the robust caddy-security suite by Greenpau.
2.1 Downloading via xcaddy
The cleanest way to build Caddy with custom modules is using xcaddy, the official build tool. Run the following commands on your VPS development environment or CI/CD pipeline:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf '[https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key](https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key)' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
curl -1sLf '[https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt](https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt)' | sudo tee /etc/apt/sources.list.d/caddy-xcaddy.list
sudo apt update
sudo apt install xcaddy
# Build Caddy with the security module
xcaddy build --with [github.com/greenpau/caddy-security](https://github.com/greenpau/caddy-security)Move the resulting binary to /usr/bin/caddy and ensure it has the correct execution permissions. Verify the installation by running caddy list-modules | grep security.
Step 3: Configuring the Caddyfile for Cryptographic JWT Validation
Now, we must configure the Caddyfile on your VPS to automatically pull Cloudflare’s public JSON Web Key Sets (JWKS) and use them to validate the Cf-Access-Jwt-Assertion header sent by Cloudflare Access.
Cloudflare publishes its public keys per account. You can find your specific keys at your team endpoint: https://.
3.1 Writing the Caddyfile Configuration
Open your /etc/caddy/Caddyfile and structure it as follows:
api.yourdomain.com {
# Enable robust logging for security auditing
log {
output file /var/log/caddy/api_access.log
}
# Route all incoming traffic through the JWT verification gate
route {
jwt {
primary
trusted_issuer [https://your-team-name.cloudflareaccess.com](https://your-team-name.cloudflareaccess.com)
jwks_url [https://your-team-name.cloudflareaccess.com/cdn-cgi/access/certs](https://your-team-name.cloudflareaccess.com/cdn-cgi/access/certs)
source header Cf-Access-Jwt-Assertion
# Validate audience tag matching your Cloudflare Access App AUD
claims aud = "your-cloudflare-application-audience-tag-here"
}
# If JWT is valid, reverse proxy the clean request to your internal application backend
reverse_proxy localhost:8080
}
}In this setup, Caddy handles the SSL certificate generation seamlessly via Let's Encrypt or ZeroSSL, opens the route, extracts the token from the Cf-Access-Jwt-Assertion header, checks the cryptographic signature against the live jwks_url, ensures it has not expired, and verifies that the audience (aud) claim matches your specific application. If any check fails, Caddy aborts the request before it reaches localhost:8080.
Step 4: Tightening the VPS Firewall (Network Hardening)
A common mistake developers make is setting up Cloudflare Access but leaving the raw IP address of the VPS exposed on ports 80 and 443. Malicious actors scanning IP ranges can bypass Cloudflare entirely and attack your Caddy server or internal APIs directly.
To prevent this, you must configure your VPS system firewall (such as UFW on Ubuntu) to only accept incoming traffic originating from Cloudflare's official IP ranges.
4.1 Automating UFW Configuration with Cloudflare IPs
Execute the following script on your VPS to fetch the current Cloudflare IP list and allow them explicitly while denying everything else on web ports:
# Reset UFW rules
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH to avoid getting locked out
sudo ufw allow 22/tcp
# Allow HTTP/HTTPS only from Cloudflare IPs
for ip in $(curl -s [https://www.cloudflare.com/ips-v4](https://www.cloudflare.com/ips-v4)); do sudo ufw allow from $ip to any port 443 proto tcp; done
for ip in $(curl -s [https://www.cloudflare.com/ips-v6](https://www.cloudflare.com/ips-v6)); do sudo ufw allow from $ip to any port 443 proto tcp; done
# Enable Firewall
sudo ufw --force enableBy enforcing this network-level constraint, your VPS becomes completely invisible to unauthorized direct scanner traffic, establishing a highly secure, private tunnel-like behavior over public infrastructure.
---Conclusion and Operational Best Practices
By combining Cloudflare Access and Caddy JWT Validation, you have built a world-class, production-grade security perimeter for your VPS APIs. The edge network handles DDoS mitigation, geolocation rules, identity enforcement, and initial access control, while your origin Caddy server guarantees cryptographically that no bypassed or unauthenticated packet passes into your core application layer.
As you manage this setup long-term, remember these production best practices:
- Monitor Certificate Expiry and Rotations: Caddy handles JWKS caching natively, but ensure your server has stable internet access to fetch updated keys when Cloudflare rotates them.
- Audit Logs Frequently: Regularly ship your Caddy access logs (
/var/log/caddy/api_access.log) to a centralized logging system to monitor for anomalies, unexpected 401/403 spikes, or continuous credential probing. - Keep Software Updated: Regularly rebuild Caddy and update your OS dependencies to protect against evolving vulnerabilities at the transport or software levels.
