Cloudflare Tunnel Access Control for Internal Tools

Authentication & Authorization beginner Building with AI ~20 min read

Overview

Internal engineering tools—VNC remote desktops, n8n automation builders, admin dashboards—need to be reachable by team members but invisible to the public internet. The traditional approach involves VPNs, port forwarding, and firewall rules that are brittle to maintain and painful to debug. Cloudflare Tunnels offer a zero-trust alternative: a lightweight daemon (cloudflared) runs alongside each service, establishes an outbound-only connection to Cloudflare’s edge, and never opens an inbound port on your infrastructure.

Access control is layered on top via Cloudflare Access, which acts as an identity-aware reverse proxy. Before any request reaches your internal tool, Access enforces authentication policies: who can connect (domain-scoped email, e.g., @corp.example.com), how they prove identity (one-time PIN via email, Google SSO, or hardware keys), and what session duration is allowed. This eliminates the need for per-service authentication for internal tools while providing a centralized audit trail of every access attempt.

In a typical internal tooling ecosystem, hosted VNC workstations and n8n automation instances run behind persistent cloudflared tunnels. Cloudflare Access enforces @corp.example.com OTP logins with weekly one-time codes, and service-level password rotation adds an additional layer before exposing remote desktops or automation workflow builders to authenticated users.

Core Concepts

Zero Trust Network Access (ZTNA)

Traditional perimeter security trusts everything inside the network. Zero Trust assumes no implicit trust—every request is authenticated and authorized regardless of network location. Cloudflare Tunnels implement ZTNA by removing the network perimeter entirely: services have no public IP, no open ports, and no direct reachability. The only path to the service is through Cloudflare’s authenticated proxy.

Key Concept: Outbound-Only Tunnels

Cloudflare Tunnels use outbound-only connections from your infrastructure to Cloudflare’s edge. Your server never listens on a public port. This inverts the traditional model where servers accept inbound connections, dramatically reducing the attack surface. Even if an attacker knows the server’s IP, there are no open ports to probe.

Cloudflare Access Policies

Access policies define who can reach a tunneled application. Policies are composed of rules that match on identity attributes:

Authentication Methods

Cloudflare Access supports multiple identity providers and authentication flows. For internal tools, the most common configurations are:

Key Concept: Session Duration & Re-authentication

Access sessions have configurable lifetimes. A weekly session duration means users authenticate once and can access the tool for seven days before re-authenticating. Shorter sessions increase security but add friction. Balance session length based on tool sensitivity—admin consoles warrant shorter sessions than read-only dashboards.

Service-Level Password Rotation

Even with Cloudflare Access in front, internal tools often have their own authentication (VNC passwords, n8n login credentials). These service-level passwords should be rotated on a regular schedule, independent of the tunnel authentication. This defense-in-depth approach ensures that a compromised Access session doesn’t automatically grant full control of the underlying tool.

Key Concept: Defense in Depth

Cloudflare Access is one layer. Service-level credentials are another. Neither is sufficient alone. Access prevents unauthorized users from reaching the service. Service credentials prevent an Access bypass (misconfigured policy, stolen session token) from granting immediate control. Rotate both on independent schedules.

How It Works

Tunnel Architecture

  1. Install cloudflared on the host running your internal tool
  2. cloudflared authenticates with your Cloudflare account and establishes persistent outbound connections to Cloudflare’s edge
  3. A DNS record (e.g., vnc.corp.example.com) is created pointing to the tunnel
  4. Incoming requests to vnc.corp.example.com hit Cloudflare’s edge first
  5. Cloudflare Access evaluates the request against configured policies
  6. Authenticated requests are forwarded through the tunnel to the local service (e.g., localhost:5900)
  7. Responses travel back through the same tunnel—no inbound ports required

Tunnel Configuration

# config.yml for cloudflared
tunnel: a1b2c3d4-e5f6-7890-abcd-ef1234567890
credentials-file: /etc/cloudflared/credentials.json

ingress:
  # VNC remote desktop
  - hostname: vnc.corp.example.com
    service: http://localhost:6080
    originRequest:
      noTLSVerify: true

  # n8n automation builder
  - hostname: n8n.corp.example.com
    service: http://localhost:5678

  # Catch-all rule (required)
  - service: http_status:404

Access Policy Configuration

# Terraform configuration for Cloudflare Access
resource "cloudflare_access_application" "vnc" {
  zone_id          = var.zone_id
  name             = "VNC Remote Desktop"
  domain           = "vnc.corp.example.com"
  session_duration = "168h"  # 7-day sessions
  type             = "self_hosted"
}

resource "cloudflare_access_policy" "vnc_allow" {
  application_id = cloudflare_access_application.vnc.id
  zone_id        = var.zone_id
  name           = "Allow corp.example.com team"
  precedence     = 1
  decision       = "allow"

  include {
    email_domain = ["corp.example.com"]
  }
}

resource "cloudflare_access_policy" "vnc_otp" {
  application_id = cloudflare_access_application.vnc.id
  zone_id        = var.zone_id
  name           = "Require OTP"
  precedence     = 2
  decision       = "allow"

  include {
    login_method = ["otp"]
  }
}

Implementation Patterns

Systemd Service for Persistent Tunnels

# /etc/systemd/system/cloudflared.service
[Unit]
Description=Cloudflare Tunnel for Internal Tools
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/local/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run
Restart=on-failure
RestartSec=5
User=cloudflared
Group=cloudflared

# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/cloudflared

[Install]
WantedBy=multi-user.target

Automated Password Rotation Script

#!/usr/bin/env bash
# rotate-service-passwords.sh
# Rotates VNC and n8n passwords on a weekly schedule via cron

set -euo pipefail

LOG="/var/log/password-rotation.log"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

rotate_vnc_password() {
    local new_pass
    new_pass=$(openssl rand -base64 18 | tr -dc 'A-Za-z0-9' | head -c 16)

    # Update VNC password file
    echo "$new_pass" | vncpasswd -f > /home/workspace/.vnc/passwd
    chmod 600 /home/workspace/.vnc/passwd

    # Store in AWS Secrets Manager for team access
    aws secretsmanager update-secret \
        --secret-id "internal/vnc-password" \
        --secret-string "$new_pass"

    echo "$TIMESTAMP [VNC] Password rotated successfully" >> "$LOG"
}

rotate_n8n_password() {
    local new_pass
    new_pass=$(openssl rand -base64 18 | tr -dc 'A-Za-z0-9' | head -c 20)

    # Update n8n basic auth via environment
    sed -i "s/^N8N_BASIC_AUTH_PASSWORD=.*/N8N_BASIC_AUTH_PASSWORD=$new_pass/" \
        /opt/n8n/.env

    # Restart n8n to pick up new password
    systemctl restart n8n

    aws secretsmanager update-secret \
        --secret-id "internal/n8n-password" \
        --secret-string "$new_pass"

    echo "$TIMESTAMP [n8n] Password rotated successfully" >> "$LOG"
}

rotate_vnc_password
rotate_n8n_password
echo "$TIMESTAMP [ALL] Rotation complete" >> "$LOG"

Health Check Script for Tunnel Monitoring

#!/usr/bin/env bash
# check-tunnel-health.sh
# Verify tunnels are alive and Access policies are enforced

TUNNELS=("vnc.corp.example.com" "n8n.corp.example.com")

for host in "${TUNNELS[@]}"; do
    # Should get 302 redirect to Cloudflare Access login
    status=$(curl -s -o /dev/null -w "%{http_code}" "https://$host")

    if [[ "$status" == "302" || "$status" == "403" ]]; then
        echo "[OK] $host - Access gate active (HTTP $status)"
    elif [[ "$status" == "000" ]]; then
        echo "[FAIL] $host - Tunnel appears down (no response)"
    else
        echo "[WARN] $host - Unexpected status HTTP $status"
    fi
done

Practical Applications

Remote Development Workstations

GPU-equipped workstations for ML training or data processing can be accessed via browser-based VNC through a Cloudflare Tunnel. The noVNC web client runs on localhost:6080, and the tunnel exposes it at vnc.corp.example.com. Team members authenticate through Access, get a full desktop environment, and never need VPN software installed on their local machines.

Automation Workflow Builders

n8n is a workflow automation platform that connects APIs, databases, and services. Running n8n behind a tunnel means the automation builder UI is only accessible to authenticated team members, while n8n’s webhook endpoints can be selectively exposed through separate tunnel ingress rules with different (or no) Access policies, allowing external services to trigger workflows.

Internal Admin Dashboards

Database admin tools (pgAdmin, Redis Commander), monitoring dashboards, and deployment control panels are common candidates for tunnel-based access. Rather than managing VPN access lists, each tool gets its own hostname and Access policy, enabling fine-grained control over who can reach what.

Common Pitfalls

Further Reading