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.
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.
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.
Access policies define who can reach a tunneled application. Policies are composed of rules that match on identity attributes:
@corp.example.com addressesCloudflare Access supports multiple identity providers and authentication flows. For internal tools, the most common configurations are:
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.
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.
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.
cloudflared on the host running your internal toolcloudflared authenticates with your Cloudflare account and establishes persistent outbound connections to Cloudflare’s edgevnc.corp.example.com) is created pointing to the tunnelvnc.corp.example.com hit Cloudflare’s edge firstlocalhost:5900)# 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
# 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"]
}
}
# /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
#!/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"
#!/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
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.
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.
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.
cloudflared requires a catch-all rule at the end of the ingress list. Without it, the tunnel daemon refuses to start. Always end with - service: http_status:404.cloudflared crashes. Set up health checks and alerting on tunnel status to catch outages before users report them.