Securing VergeOS API Access with Cloudflare Tunnels and Service Tokens

Hey homelab enthusiasts! Today I want to share how I set up secure remote access to my VergeOS API using Cloudflare Tunnels and Service Tokens. This setup gives me the best of both worlds: the security of Cloudflare Access protecting my infrastructure, plus the ability to automate API calls from scripts without dealing with browser-based authentication.

The Challenge

I run VergeOS as my hypervisor platform, and like any good homelab admin, I wanted to automate everything. The VergeOS API is fantastic for this - it's a comprehensive REST API with Swagger documentation and 317+ endpoints covering everything from VM management to cluster monitoring.

But here's the thing: I don't want to expose my VergeOS management interface directly to the internet. That's just asking for trouble. At the same time, I need to access it remotely for automation and monitoring. now normally i wouldn't expose this platform, but i've been using windsurf for development and i wanted to be able to access the server securely.you could just as easily run the scripts (attached at the bottom of the article) against a local server.

The Solution: Cloudflare Tunnels + Service Tokens

The setup I landed on uses a two-layer authentication approach:

  1. Cloudflare Tunnel - Securely exposes the VergeOS API without opening firewall ports
  2. Cloudflare Access - Protects the tunnel with authentication
  3. Service Tokens - Allows scripts to bypass the browser-based auth
  4. VergeOS Basic Auth - The API's native authentication layer

Let me walk you through how it all works.

Layer 1: Cloudflare Tunnel

First, I set up a Cloudflare Tunnel to expose my VergeOS instance. The tunnel runs as a deployment in my Kubernetes cluster (because why not?) and creates a secure outbound connection to Cloudflare's edge network.

The Setup:

  • Public URL: https://vergeos.happynoises.work
  • Internal Service: VergeOS management interface (HTTPS)
  • Tunnel ID: 0c70ff17-acd4-4f9f-bfc4-ac9563a09d4f

The beauty of Cloudflare Tunnels is that there are zero inbound firewall rules needed. The tunnel initiates the connection from inside my network to Cloudflare's edge, and all traffic flows through that encrypted tunnel.

Layer 2: Cloudflare Access

Next, I protected the tunnel with Cloudflare Access. This means anyone trying to access vergeos.happynoises.work has to authenticate through Cloudflare first. I could use email OTP, Google OAuth, or any number of identity providers.

But here's where it gets interesting for automation...

Layer 3: Service Tokens for Scripts

Cloudflare Access is great for humans with browsers, but what about scripts? That's where Service Tokens come in.

A Service Token is essentially a machine-to-machine authentication credential. It consists of:

  • Client ID: 1383addd5a4388f65752d129f29086e5.access
  • Client Secret: A long cryptographic secret

When my scripts make API calls, they include these as HTTP headers:

CF-Access-Client-Id: <client-id>
CF-Access-Client-Secret: <client-secret>

This tells Cloudflare Access "Hey, I'm an authorized service, let me through" without needing a browser or human interaction.

Creating the Service Token

I created the service token using the Cloudflare API:

curl -X POST \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/service_tokens" \
  -H "X-Auth-Email: ${EMAIL}" \
  -H "X-Auth-Key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  --data '{
    "name": "vergeos-api-access",
    "duration": "8760h"
  }'

The token is valid for 1 year (8760 hours), after which I'll need to rotate it.

Adding to Access Policy

Then I added the service token to my Cloudflare Access policy for vergeos.happynoises.work:

  • Policy Name: "Service Token - vergeos-api-access"
  • Decision: non_identity (no user identity required)
  • Include: The service token ID
  • Precedence: 2 (after my main user policy)

Now my scripts can bypass the browser authentication and go straight through Cloudflare Access!

Layer 4: VergeOS API Authentication

But wait, there's more! Even after getting through Cloudflare Access, the VergeOS API still requires authentication. This is actually a good thing - defense in depth!

VergeOS uses token-based authentication. Here's how it works:

Step 1: Obtain an API Token

First, you authenticate with your username and password to get a token:

curl --header "X-JSON-Non-Compact: 1" \
  --basic \
  --data-ascii '{"login": "admin", "password": "your-password"}' \
  --insecure \
  --request "POST" \
  --header 'Content-Type: application/json' \
  -H "CF-Access-Client-Id: ${CF_CLIENT_ID}" \
  -H "CF-Access-Client-Secret: ${CF_CLIENT_SECRET}" \
  'https://vergeos.happynoises.work/api/sys/tokens'

This returns a response with your API token in the $key field:

{
  "location": "/sys/tokens/95d70c3bb22a84b1df524c29d6c05ab348ee4806",
  "dbpath": "tokens/95d70c3bb22a84b1df524c29d6c05ab348ee4806",
  "$row": 6,
  "$key": "95d70c3bb22a84b1df524c29d6c05ab348ee4806"
}

The token value is in the $key field.

Step 2: Use the Token

Now you can use that token for all subsequent API calls. The token must be sent as a cookie (not a header):

curl -s \
  -H "CF-Access-Client-Id: ${CF_CLIENT_ID}" \
  -H "CF-Access-Client-Secret: ${CF_CLIENT_SECRET}" \
  -b "token=${VERGEOS_TOKEN}" \
  https://vergeos.happynoises.work/api/v4/vms

Important: Despite the Swagger spec mentioning x-yottabyte-token header, the token must actually be sent as a cookie named token.

Token Lifecycle

  • Tokens have an expiration time (check the expires field)
  • When a token expires, request a new one
  • Store tokens securely (never commit to Git!)
  • Consider implementing automatic token refresh in your scripts

The VergeOS API

Now that we're authenticated, let's talk about what we can actually do with this API!

API Endpoints

The VergeOS API v4.0 is incredibly comprehensive. Here's what you get access to:

Base URL: https://vergeos.happynoises.work/api/v4

Major Categories:

  • Virtual Machines (/vms) - Create, manage, start/stop VMs
  • Nodes (/nodes) - Physical host management
  • Cluster (/cluster_status, /cluster_stats_history_*) - Cluster health and metrics
  • Storage (/vdisks, /cloud_snapshots) - Virtual disks and snapshots
  • Networking (/vnets, /vwires) - Virtual networks and wiring
  • Monitoring (/alarms, /logs) - Alerts and system logs
  • Tenants (/tenants) - Multi-tenancy management

And that's just scratching the surface - there are 317 API endpoint categories in total!

Swagger Documentation

One of the best parts? VergeOS includes full Swagger/OpenAPI documentation:

Swagger UI: https://vergeos.happynoises.work/swagger-ui/
API Spec: https://vergeos.happynoises.work/swagger-ui/v4.json

The spec is massive - 126,990 lines of JSON describing every endpoint, parameter, and response format. It's a developer's dream!

Example: Listing VMs

Here's a simple example of listing all VMs:

curl -s \
  -H "CF-Access-Client-Id: ${CF_CLIENT_ID}" \
  -H "CF-Access-Client-Secret: ${CF_CLIENT_SECRET}" \
  --user "admin:password" \
  https://vergeos.happynoises.work/api/v4/vms | jq '.'

The response is an array of VM objects:

[
  {
    "$key": 2,
    "name": "vergeos-host1",
    "machine": 9,
    "is_snapshot": false,
    "allow_hotplug": true,
    "guest_agent": true,
    "on_power_loss": "last_state"
  },
  {
    "$key": 11,
    "name": "rke2-node1",
    "is_snapshot": false,
    "guest_agent": true
  }
]

Pro tip: VMs with "is_snapshot": true are actually templates/recipes, not running VMs. Filter them out to see your actual infrastructure!

Building a CLI Tool

To make this easier, I built a simple Bash CLI wrapper that handles token authentication:

#!/bin/bash
# vergeos-cli.sh

# Load credentials
source ~/.vergeos-credentials
source ~/.vergeos-service-token

# Get or refresh token
get_token() {
    # Check if token exists and is valid
    if [ -f ~/.vergeos-token ]; then
        source ~/.vergeos-token
        EXPIRES=$(cat ~/.vergeos-token | grep expires | cut -d'=' -f2)
        NOW=$(date +%s)
        if [ $NOW -lt $EXPIRES ]; then
            return 0  # Token still valid
        fi
    fi
    
    # Get new token
    RESPONSE=$(curl -s --header "X-JSON-Non-Compact: 1" \
        --basic \
        --data-ascii "{\"login\": \"${VERGEOS_USER}\", \"password\": \"${VERGEOS_PASS}\"}" \
        --insecure \
        --request "POST" \
        --header 'Content-Type: application/json' \
        -H "CF-Access-Client-Id: ${CF_CLIENT_ID}" \
        -H "CF-Access-Client-Secret: ${CF_CLIENT_SECRET}" \
        'https://vergeos.happynoises.work/api/sys/tokens')
    
    VERGEOS_TOKEN=$(echo "$RESPONSE" | jq -r '.token')
    EXPIRES=$(echo "$RESPONSE" | jq -r '.expires')
    
    # Save token
    echo "export VERGEOS_TOKEN='${VERGEOS_TOKEN}'" > ~/.vergeos-token
    echo "export VERGEOS_TOKEN_EXPIRES=${EXPIRES}" >> ~/.vergeos-token
    chmod 600 ~/.vergeos-token
}

# Make API call
api_call() {
    local endpoint=$1
    get_token  # Ensure we have a valid token
    
    curl -s \
        -H "CF-Access-Client-Id: ${CF_CLIENT_ID}" \
        -H "CF-Access-Client-Secret: ${CF_CLIENT_SECRET}" \
        -b "token=${VERGEOS_TOKEN}" \
        "https://vergeos.happynoises.work/api/v4${endpoint}"
}

# List active VMs (exclude templates)
api_call "/vms" | jq '.[] | select(.is_snapshot == false) | .name'

The CLI automatically handles token refresh, so you don't have to worry about expiration!

Now I can just run:

./vergeos-cli-token.sh vm list

And get a clean list of my active VMs:

=== Active VMs ===
vergeos-host1 - Key: 2
rke2-node1 - Key: 11
rke2-node2 - Key: 33
rke2-node3 - Key: 41
veeamserver - Key: 34
nfs-for-kubernetes - Key: 35
grafana - Key: 51

Security Considerations

Let's talk about security, because this setup involves several layers of credentials:

What's Protected

  1. Cloudflare Tunnel Credentials - Stored in Kubernetes secrets
  2. Cloudflare Service Token - Stored in ~/.vergeos-service-token (chmod 600)
  3. VergeOS Credentials - Stored in ~/.vergeos-credentials (chmod 600)
  4. VergeOS API Token - Cached in ~/.vergeos-token (chmod 600, auto-refreshed)

Best Practices

DO:

  • ✅ Use service tokens with appropriate expiration (I use 1 year)
  • ✅ Store credentials with restrictive permissions (chmod 600)
  • ✅ Use separate service tokens for different applications
  • ✅ Rotate tokens regularly
  • ✅ Monitor Access logs for unusual activity

DON'T:

  • ❌ Commit credentials to Git repositories
  • ❌ Use overly permissive Cloudflare Access policies
  • ❌ Share service tokens between environments
  • ❌ Forget to set token expiration

Defense in Depth

The beauty of this setup is the layered security:

  1. Network Layer: Cloudflare Tunnel (no exposed ports)
  2. Access Layer: Cloudflare Access (authentication required)
  3. Application Layer: VergeOS Basic Auth (API credentials)

An attacker would need to compromise:

  • My Cloudflare account, AND
  • My service token, AND
  • My VergeOS credentials

That's a pretty high bar!

Real-World Use Cases

So what am I actually using this for? Here are some examples:

1. Monitoring Integration

I pull cluster stats every 5 minutes and push them to Grafana:

#!/bin/bash
# monitor-cluster.sh

STATS=$(./vergeos-cli-token.sh raw /cluster_stats_history_short)

CPU=$(echo "$STATS" | jq '.[0].cpu_usage')
RAM=$(echo "$STATS" | jq '.[0].ram_usage')

# Push to Prometheus pushgateway
echo "vergeos_cpu_usage $CPU" | curl --data-binary @- \
  http://pushgateway:9091/metrics/job/vergeos

2. Automated Backups

I trigger VM snapshots before maintenance windows:

# Create snapshot of critical VMs
for vm_key in 11 33 41; do
    ./vergeos-cli-token.sh raw "/cloud_snapshots" \
        -X POST \
        -d "{\"vm\": \"$vm_key\", \"name\": \"pre-maintenance-$(date +%Y%m%d)\"}"
done

3. Alerting

I check for alarms and send notifications:

ALARMS=$(./vergeos-cli-token.sh alarms list)

if [ -n "$ALARMS" ]; then
    # Send to notification service (Slack, Discord, etc.)
    curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
        -H 'Content-Type: application/json' \
        -d "{\"text\": \"VergeOS Alarms: $ALARMS\"}"
fi

4. Infrastructure as Code

I can even provision VMs programmatically:

# Create new VM
curl -X 'POST' \
  'https://vergeos.happynoises.work/api/v4/vms' \
  -H 'accept: application/json' \
  -H "CF-Access-Client-Id: ${CF_CLIENT_ID}" \
  -H "CF-Access-Client-Secret: ${CF_CLIENT_SECRET}" \
  -b "token=${VERGEOS_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "new-web-server",
    "description": "Web server instance",
    "machine_type": "pc-q35-9.0",
    "allow_hotplug": true,
    "cpu_cores": 2,
    "cpu_type": "Broadwell",
    "ram": 4096,
    "os_family": "linux",
    "boot_order": "cd",
    "uefi": false,
    "note": "Created via API"
  }'

Lessons Learned

Here are some things I learned while setting this up:

1. Service Tokens vs API Tokens

Cloudflare has both Service Tokens (for Access) and API Tokens (for the Cloudflare API). Don't confuse them!

  • Service Tokens = Bypass Cloudflare Access authentication
  • API Tokens = Manage Cloudflare resources via API

You need both if you want to automate service token creation!

2. The $key Field

VergeOS uses $key as the unique identifier for resources. In jq, you need to access it with bracket notation:

# Wrong
jq '.[] | .$key'

# Right
jq '.[] | .["$key"]'

3. Templates vs VMs

The API returns both templates and actual VMs in the same endpoint. Always filter by is_snapshot to separate them:

# Active VMs only
jq '.[] | select(.is_snapshot == false)'

# Templates only
jq '.[] | select(.is_snapshot == true)'

4. Token Authentication Details

The VergeOS API uses token-based authentication via the /api/sys/tokens endpoint. Here are the key details I learned:

Token Generation:

  • Token endpoint: /api/sys/tokens (not /api/v4/...)
  • Use --basic flag with the POST request
  • Include X-JSON-Non-Compact: 1 header for readable JSON
  • Token is returned in the $key field (not token field)

Token Usage:

  • Despite Swagger spec mentioning x-yottabyte-token header, tokens must be sent as cookies
  • Use -b "token=<token_value>" with curl
  • Tokens appear to be session-based and may expire (default 24 hours)
  • Implement automatic token refresh in your scripts

Performance and Reliability

How does this setup perform in the real world?

Latency: ~50-100ms for API calls (includes Cloudflare edge routing)
Uptime: 99.9%+ (thanks to Cloudflare's global network)
Rate Limits: None that I've hit (VergeOS side)
Cloudflare Limits: Service tokens have no rate limits

The Cloudflare Tunnel adds minimal latency since I'm routing through their edge network anyway. For automation tasks, the extra 50ms is negligible.

Future Improvements

Some things I'm planning to add:

  1. Token Rotation Automation - Auto-renew service tokens before expiration
  2. Prometheus Exporter - Native VergeOS metrics exporter
  3. Terraform Provider - Manage VergeOS resources as code
  4. Webhook Integration - Trigger actions based on VergeOS events
  5. Multi-Region Failover - Use Cloudflare Load Balancing for HA

Conclusion

This setup gives me the best of all worlds:

  • Secure - No exposed ports, multi-layer authentication
  • Accessible - Available from anywhere via Cloudflare's global network
  • Automatable - Service tokens enable script-based access
  • Reliable - Cloudflare's infrastructure handles the heavy lifting

If you're running VergeOS (or any other management platform) in your homelab, I highly recommend this approach. The combination of Cloudflare Tunnels and Service Tokens is incredibly powerful for secure remote access.

Resources

Cloudflare Documentation:

VergeOS:

My Setup:

  • Cloudflare Tunnel: vergeos.happynoises.work
  • API Endpoint: https://vergeos.happynoises.work/api/v4
  • Swagger UI: https://vergeos.happynoises.work/swagger-ui/

Questions?

Have questions about this setup? Want to share your own homelab API automation stories? Drop a comment below!

Happy homelabbing! 🚀

Read more