Cloudflare Zero Trust: Secure Access to Internal Applications

Cloudflare Zero Trust Architecture Remote Users Browser / WARP Device Posture ✓ IdP Authentication HTTPS Cloudflare Edge Access Gateway WAF DLP Encrypted Tunnel cloudflared Private Network (no inbound ports) Web App :8080 SSH / RDP :22 / :3389 Identity Provider Azure AD / Okta SAML / OIDC Secure path Tunnel (outbound-only) Auth flow

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:8080 as 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

  1. In the Azure portal, register a new application under App registrations.
  2. Set the Redirect URI to https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback.
  3. Create a Client Secret and note the Application (client) ID and Directory (tenant) ID.
  4. In Cloudflare, select Azure AD and enter:
{
  "client_id": "<APPLICATION_CLIENT_ID>",
  "client_secret": "<CLIENT_SECRET>",
  "directory_id": "<TENANT_ID>"
}
  1. Click Test to verify the integration. A successful test shows your Azure AD user profile.

Google Workspace Configuration

  1. Go to the Google Cloud Console and create OAuth 2.0 credentials.
  2. Set the authorized redirect URI to https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback.
  3. Enter the Client ID and Client Secret in the Cloudflare dashboard.

Okta Configuration

  1. In Okta, create a new OIDC application with the redirect URI https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback.
  2. 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

  1. In the Zero Trust dashboard, go to Access > Applications and click Add an application.
  2. Select Self-hosted.
  3. Configure the application:
FieldValue
Application nameInternal Web App
Session Duration24 hours
Application domainapp.example.com
Identity providersSelect your configured IdP
  1. Click Next to configure the policy.

Create an Access Policy

A policy specifies who is allowed (or denied) access:

FieldValue
Policy nameAllow Engineering Team
ActionAllow
Include ruleEmails 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 States or Germany

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

  1. Click Add new and select WARP as the posture check type.
  2. Set the minimum client version (e.g., 2024.12.0).

Require Disk Encryption

  1. Add a new check of type Disk encryption.
  2. This verifies that the device’s boot volume is encrypted (BitLocker on Windows, FileVault on macOS, LUKS on Linux).

Require OS Version

  1. Add a check of type OS version.
  2. 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:

  1. Select Device Posture as the selector.
  2. 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:

  1. Verify you reach the internal application.
  2. Check the Access > Logs section in the Zero Trust dashboard for the authentication event.
  3. 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.