Cloudflare Zero Trust: Secure Access to Internal Applications
Traditional VPNs grant broad network-level access once a user connects — a model that introduces lateral movement risks and requires inbound firewall rules, dedicated appliances, and client software distribution. Cloudflare Zero Trust replaces this approach with identity-aware, per-application access control at the Cloudflare edge. Users authenticate against your identity provider, pass device posture checks, and receive access only to the specific applications they are authorized to use. No VPN client is needed, no inbound ports are opened, and no internal network exposure occurs.
This guide walks through configuring Cloudflare Zero Trust to protect internal web applications, from creating a Cloudflare Tunnel to setting up Access policies and device posture enforcement.
Prerequisites
Before starting, make sure the following are ready:
- A Cloudflare account with at least one domain added and active (nameservers delegated to Cloudflare).
- A Linux server (Ubuntu 22.04/24.04, Debian 12, or RHEL 9) running the internal application you want to protect.
- Root or sudo access on the server.
- An identity provider account — Azure AD (Entra ID), Okta, Google Workspace, or any SAML/OIDC provider.
- The internal application listening on a local port (this guide uses
http://localhost:8080as an example).
Verify your application is running locally:
curl -s http://localhost:8080/health
Step 1: Install cloudflared and Create a Tunnel
cloudflared is the lightweight connector daemon that establishes an encrypted, outbound-only connection from your infrastructure to the Cloudflare edge. No inbound ports need to be opened.
Install cloudflared
On Ubuntu/Debian:
# Add the Cloudflare GPG key and repository
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update
sudo apt install cloudflared -y
On RHEL/CentOS:
sudo rpm -i https://pkg.cloudflare.com/cloudflared-stable-linux-amd64.rpm
Verify the installation:
cloudflared --version
Authenticate and create the tunnel
Log in to your Cloudflare account from the terminal:
cloudflared tunnel login
This opens a browser window where you select the domain to authorize. After authorization, a certificate is saved to ~/.cloudflared/cert.pem.
Create a named tunnel:
cloudflared tunnel create internal-apps
This generates a tunnel UUID and creates a credentials file at ~/.cloudflared/<TUNNEL_UUID>.json. Record the UUID — you will need it for the configuration file.
Step 2: Configure the Tunnel
Create the cloudflared configuration file to map public hostnames to your internal services.
sudo mkdir -p /etc/cloudflared
Create /etc/cloudflared/config.yml:
tunnel: <TUNNEL_UUID>
credentials-file: /home/<your-user>/.cloudflared/<TUNNEL_UUID>.json
ingress:
- hostname: app.example.com
service: http://localhost:8080
originRequest:
connectTimeout: 10s
noTLSVerify: false
- hostname: admin.example.com
service: http://localhost:3000
- hostname: ssh.example.com
service: ssh://localhost:22
- service: http_status:404
The last - service: http_status:404 entry is a catch-all rule required by cloudflared. Any request not matching a defined hostname returns a 404.
Create DNS records
Route traffic from your public hostname through the tunnel:
cloudflared tunnel route dns internal-apps app.example.com
cloudflared tunnel route dns internal-apps admin.example.com
This creates CNAME records pointing to <TUNNEL_UUID>.cfargotunnel.com in your Cloudflare DNS.
Run the tunnel as a systemd service
Install the service and start it:
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
Verify the tunnel is connected:
sudo systemctl status cloudflared
cloudflared tunnel info internal-apps
You should see active connections to multiple Cloudflare edge data centers for redundancy.
Step 3: Integrate Your Identity Provider
Navigate to the Cloudflare Zero Trust dashboard at https://one.dash.cloudflare.com. Go to Settings > Authentication > Login methods and click Add new.
Azure AD (Entra ID) Configuration
- In the Azure portal, register a new application under App registrations.
- Set the Redirect URI to
https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback. - Create a Client Secret and note the Application (client) ID and Directory (tenant) ID.
- In Cloudflare, select Azure AD and enter:
{
"client_id": "<APPLICATION_CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"directory_id": "<TENANT_ID>"
}
- Click Test to verify the integration. A successful test shows your Azure AD user profile.
Google Workspace Configuration
- Go to the Google Cloud Console and create OAuth 2.0 credentials.
- Set the authorized redirect URI to
https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback. - Enter the Client ID and Client Secret in the Cloudflare dashboard.
Okta Configuration
- In Okta, create a new OIDC application with the redirect URI
https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback. - Copy the Client ID, Client Secret, and Okta domain into the Cloudflare Zero Trust settings.
After adding your identity provider, click Test to confirm that the login flow completes successfully.
Step 4: Create Access Applications and Policies
Access applications define what you are protecting. Access policies define who can reach them.
Create an Access Application
- In the Zero Trust dashboard, go to Access > Applications and click Add an application.
- Select Self-hosted.
- Configure the application:
| Field | Value |
|---|---|
| Application name | Internal Web App |
| Session Duration | 24 hours |
| Application domain | app.example.com |
| Identity providers | Select your configured IdP |
- Click Next to configure the policy.
Create an Access Policy
A policy specifies who is allowed (or denied) access:
| Field | Value |
|---|---|
| Policy name | Allow Engineering Team |
| Action | Allow |
| Include rule | Emails ending in @example.com |
You can combine multiple include and require rules:
- Include: Email domain
@example.com - Require: Identity provider group
Engineering - Require: Country
United StatesorGermany
For stricter control, add an Exclude rule to deny specific users even if they match the include criteria.
Additional Application Settings
Under the application’s Settings tab, configure these recommended options:
Enable Binding Cookie: Yes
HTTP Only Cookie Flag: Yes
Same Site Cookie Attribute: Lax
CORS Settings: Configure if your app makes cross-origin requests
Repeat this process for each internal application (admin.example.com, ssh.example.com, etc.), each with its own policy tailored to the appropriate user group.
Step 5: Configure Device Posture Checks
Device posture checks add a second layer of enforcement beyond identity. Navigate to Settings > WARP Client > Device posture in the Zero Trust dashboard.
Require the WARP Client
- Click Add new and select WARP as the posture check type.
- Set the minimum client version (e.g.,
2024.12.0).
Require Disk Encryption
- Add a new check of type Disk encryption.
- This verifies that the device’s boot volume is encrypted (BitLocker on Windows, FileVault on macOS, LUKS on Linux).
Require OS Version
- Add a check of type OS version.
- Specify the minimum version per platform:
macOS: >= 14.0
Windows: >= 10.0.22621
Linux: >= 6.1 (kernel)
Apply Posture Checks to Access Policies
Return to your Access policy and add a Require rule:
- Select Device Posture as the selector.
- Choose the posture checks you created (WARP client, disk encryption, OS version).
Now access requires both a valid identity and a compliant device.
Step 6: Test and Validate
Open your protected hostname in a browser:
https://app.example.com
You should see the Cloudflare Access login screen presenting your configured identity providers. After authenticating:
- Verify you reach the internal application.
- Check the Access > Logs section in the Zero Trust dashboard for the authentication event.
- Test with a user who should be denied access to confirm the policy blocks them.
Validate tunnel health
From the server, confirm the tunnel connections:
cloudflared tunnel info internal-apps
Expected output shows multiple active connections:
CONNECTOR ID CREATED CONNECTIONS
a1b2c3d4-... 2026-01-12T10:00:00Z 4xATL, 4xIAD
Test from a non-compliant device
If you configured device posture checks, attempt access from a device without the WARP client. The request should be blocked with an Access denied page explaining the unmet posture requirement.
Troubleshooting
Tunnel shows no connections
If cloudflared tunnel info shows zero connections:
# Check the service status
sudo systemctl status cloudflared
# Review cloudflared logs
sudo journalctl -u cloudflared -f --no-pager | tail -50
# Verify the credentials file exists and is readable
ls -la /home/<your-user>/.cloudflared/<TUNNEL_UUID>.json
Common causes: incorrect credentials file path in config.yml, expired certificate (cert.pem), or outbound connectivity blocked on ports 443/7844.
Access policy returns 403 Forbidden
Verify the user’s email domain, group membership, and identity provider configuration. Check Access > Logs for the specific denial reason. Ensure the application domain in the Access application exactly matches the hostname in your cloudflared configuration.
Application loads but shows connection errors
The internal service may not be running or may be listening on a different port:
# Verify the app is listening
ss -tlnp | grep 8080
# Test local connectivity
curl -v http://localhost:8080/
If your application uses HTTPS internally, update the service field in config.yml to https://localhost:8443 and set noTLSVerify: true if using self-signed certificates.
DNS resolution issues
Confirm the CNAME record exists:
dig app.example.com CNAME +short
The output should show <TUNNEL_UUID>.cfargotunnel.com. If missing, re-run cloudflared tunnel route dns.
Summary
Cloudflare Zero Trust replaces VPN-based access with identity-aware, per-application security at the edge. The architecture eliminates inbound firewall rules entirely — cloudflared establishes outbound-only encrypted tunnels, while Cloudflare Access enforces authentication and authorization on every request. By layering identity provider integration, granular access policies, and device posture checks, you achieve a defense-in-depth model where no single component failure exposes internal resources.
Key takeaways from this configuration:
- Cloudflare Tunnels create encrypted, outbound-only connections — no open inbound ports required.
- Access applications and policies provide per-application, identity-based access control.
- Identity provider integration leverages your existing SSO infrastructure (Azure AD, Okta, Google).
- Device posture checks ensure only compliant, managed devices can access protected resources.
- Centralized logging in the Zero Trust dashboard gives full visibility into every access event.
This approach scales from a single internal application to an entire organization’s internal tooling portfolio, all without deploying or maintaining VPN infrastructure.