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:
- Cloudflare Tunnel - Securely exposes the VergeOS API without opening firewall ports
- Cloudflare Access - Protects the tunnel with authentication
- Service Tokens - Allows scripts to bypass the browser-based auth
- 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
expiresfield) - 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
- Cloudflare Tunnel Credentials - Stored in Kubernetes secrets
- Cloudflare Service Token - Stored in
~/.vergeos-service-token(chmod 600) - VergeOS Credentials - Stored in
~/.vergeos-credentials(chmod 600) - 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:
- Network Layer: Cloudflare Tunnel (no exposed ports)
- Access Layer: Cloudflare Access (authentication required)
- 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
--basicflag with the POST request - Include
X-JSON-Non-Compact: 1header for readable JSON - Token is returned in the
$keyfield (nottokenfield)
Token Usage:
- Despite Swagger spec mentioning
x-yottabyte-tokenheader, 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:
- Token Rotation Automation - Auto-renew service tokens before expiration
- Prometheus Exporter - Native VergeOS metrics exporter
- Terraform Provider - Manage VergeOS resources as code
- Webhook Integration - Trigger actions based on VergeOS events
- 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! 🚀