Self-Hosted Hermes Agent on iOS: Cloudflare Tunnel + Access Service Tokens + Hermex
Get your Hermes Agent on your iPhone - without paying for a relay, without a VPN, with full Cloudflare edge protection.
The Problem
You're running Hermes Agent self-hosted on a VPS. You want to chat with it from your iPhone. There are a few paths:
- HermesPilot P2P relay - works great until it goes paid
- Tailscale VPN - works but you need the VPN connected every time
- Cloudflare Tunnel + CF Access - great for the browser, but iOS apps can't do OAuth redirects
The last option is the most interesting because it gives you Cloudflare's edge protection, your own domain, and no per-device VPN. The problem is that Cloudflare Access normally redirects to a browser login (Google, GitHub, etc.) - which doesn't work for a native app.
The fix: Cloudflare Access Service Tokens + custom headers.
The Architecture
There are two common paths - both work with Service Tokens:
Path A: Cloudflare Tunnel → Nginx Proxy Manager (NPM)
Hermex iOS App
│ Custom headers: CF-Access-Client-Id, CF-Access-Client-Secret
▼ HTTPS (orange cloud)
Cloudflare Edge - validates Service Token
▼
Cloudflare Tunnel (cloudflared) - connects to NPM
▼
Nginx Proxy Manager (origin certificate) - routes to backend
▼
Hermes API Server (:8642) or Hermes WebUI (:8787)
NPM handles SSL termination with origin certs and gives you a nice UI for managing proxy hosts.
Path B: Cloudflare Tunnel → Direct to Hermes
Hermex iOS App
│ Custom headers
▼ HTTPS
Cloudflare Edge - validates Service Token
▼
Cloudflare Tunnel (cloudflared) - pointed at localhost:8642
▼
Hermes API Server
Simpler, no reverse proxy. Cloudflare handles SSL at the edge.
Prerequisites
- A domain on Cloudflare (orange-cloud proxied)
- Hermes Agent with the API Server enabled
- An iOS device and the Hermex app from the App Store
Step 1: Enable the Hermes API Server
In your ~/.hermes/.env:
API_SERVER_ENABLED=true
API_SERVER_KEY=generate-a-strong-random-key
API_SERVER_HOST=127.0.0.1
API_SERVER_PORT=8642
Restart and verify:
hermes gateway restart
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8642/health
→ 200
Step 2: Install and Configure Cloudflare Tunnel
2a. Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
Or on Debian/Ubuntu:
sudo apt install cloudflared
2b. Authenticate and Create a Tunnel
cloudflared tunnel login
cloudflared tunnel create hermes-tunnel
2c. Route DNS
cloudflared tunnel route dns hermes-tunnel hermes-api.yourdomain.com
2d. Configure the Tunnel
Create ~/.cloudflared/config.yml:
tunnel:
credentials-file: /home/ubuntu/.cloudflared/.json
ingress:
- hostname: hermes-api.yourdomain.com service: http://localhost:8642
- hostname: hermes-webui.yourdomain.com service: http://localhost:8787
- service: http_status:404
Run it:
cloudflared tunnel run hermes-tunnel
Or install as a systemd service:
sudo cloudflared service install
2e. Tunnel → Nginx Proxy Manager
If using NPM, point the tunnel at localhost:80 instead:
ingress:
- hostname: hermes-api.yourdomain.com service: http://localhost:80
- service: http_status:404
Then in NPM, add a proxy host:
- Domain: hermes-api.yourdomain.com
- Forward to: http://127.0.0.1:8642
- Enable Websockets
- SSL tab → Cloudflare Origin Certificate
- Cloudflare SSL/TLS → Full (strict)
Step 3: Cloudflare Access - Service Token
| Mode | How it works | Use for |
|---|---|---|
| Allow | Redirects to OAuth (Google, GitHub, etc.) | Browser users |
| Service Auth | Validates static headers | Apps, APIs, scripts |
3a. Create the Service Token
Cloudflare Zero Trust Dashboard → Access → Service Auth → Create Service Token
Name it hermex-ios.
Copy the Client ID and Client Secret immediately.
3b. Create the Access Application
Access → Applications → Add an application → Self-hosted → set domain
Add a policy with:
- Action: Service Auth ← NOT "Allow"
- Select the hermex-ios service token
Save. Cloudflare now accepts requests with the correct headers.
Step 4: Configure Hermex
On your iPhone:
- Instance URL: https://hermes-api.yourdomain.com
- Custom Header 1: CF-Access-Client-Id:
- Custom Header 2: CF-Access-Client-Secret:
Press connect.
Why This Works
Service Tokens are designed for machine-to-machine auth. Cloudflare's edge reads CF-Access-Client-Id and CF-Access-Client-Secret headers on every request and validates before anything reaches your tunnel. The app never sees a login page. Same pattern as CI/CD pipelines and Terraform - just works for iOS too.
What About mTLS?
mTLS would be ideal but iOS support is painful. No native NSURLSession support without painful workarounds, certificate distribution is a UX nightmare, renewal and revocation need custom code.
Service Tokens give the same "pre-shared credential at the edge" model over HTTP headers instead of TLS handshakes.
Troubleshooting
- Cloudflare login page → Policy set to Allow instead of Service Auth
- 401 Unauthorized → Header spelling wrong (case-sensitive)
- 502 Bad Gateway → tunnel/NPM can't reach the backend
- Connection timeout → Tunnel not running or DNS not proxied
Quick health check:
systemctl status cloudflared
curl localhost:8642/health
dig hermes-api.yourdomain.com +short
Alternative: Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
hermeslink config set lanHost "100.x.x.x"
Works over WireGuard. Downside: VPN needed every time.
Conclusion
Cloudflare Access Service Tokens are the missing piece for authenticating native apps behind Cloudflare. With Hermex's custom header support, this takes about 10 minutes if you already have the tunnel running.
Top comments (0)