Early Access Preview
Back to blog
engineeringsecurityinfrastructureremote-access

AitherTunnel: Remote Access to Your AI Machine Without the Middleman

March 9, 202614 min readAitherium
Share

Here's the problem with giving someone access to your machine.

You either give them too much or too little. SSH keys grant root. VPN configs expire and break. TeamViewer is a corporate surveillance tool you're voluntarily installing on your own hardware. ngrok exposes ports with the security model of "hope nobody guesses the URL." Tailscale is better but still requires the other person to install software, join your network, and deal with ACLs that feel like writing iptables rules in a web UI.

We needed something different. AitherOS runs 203 microservices on a single machine — inference engines, agent orchestrators, training pipelines, memory systems. The people who need access to this machine fall into distinct categories with distinct needs:

  • Me: Everything. Terminal, VPN, port forwarding, service management.
  • Developers: Terminal access and port forwarding to specific services. Maybe VPN for persistent connections.
  • Operators: VPN access to hit API endpoints. Maybe the dashboard to see what's running. Definitely not a shell.
  • Viewers: Read-only dashboard. See the system status. Touch nothing.

No existing tool maps to this. So we built AitherTunnel.

What It Actually Is

AitherTunnel is a single FastAPI service that serves a complete web portal at tunnel.aitherium.com. When you visit it, Cloudflare Access gates the request — you authenticate via SSO (Google, GitHub, SAML, whatever you've configured), and only then does the page load.

What loads is a single-page app with four tabs:

  • Terminal — A full xterm.js shell in your browser. It connects via WebSocket to a PTY on the host machine. It's not a fake terminal. It's bash.
  • Dashboard — System info, service health, resource usage. At-a-glance status of your AitherOS deployment.
  • VPN Peers — WireGuard peer management. Create a peer, get a .conf file, scan a QR code. The private key is shown once and never stored.
  • Services — Port forwarding rules. Expose specific internal services through the tunnel with allowlists.

The whole thing is one Python file. No separate frontend build. No React. No npm. The HTML is served directly by FastAPI with the JWT injected server-side. This is intentional — a remote access tool should have exactly one deployment artifact.

The Auth Stack: Three Layers, Zero Passwords

Authentication happens in three layers, each one a fallback for the next.

Layer 1: Cloudflare Access. Before your browser even reaches the server, Cloudflare's edge network validates your identity. You hit tunnel.aitherium.com, get redirected to your SSO provider, authenticate, and Cloudflare sets an HttpOnly cookie called CF_Authorization containing a signed JWT. This cookie is invisible to JavaScript — you can't document.cookie it out. That's by design.

Layer 2: Server-side JWT extraction. When the root page loads, the FastAPI handler reads the CF_Authorization cookie from the request, extracts the JWT, and injects it into the HTML as a JavaScript variable. This is the only way to get the token from an HttpOnly cookie into client-side code — the server has to do it.

cf_jwt = request.cookies.get("CF_Authorization", "")
return HTMLResponse(content=_build_portal_html(user_email, user_info, cf_jwt=cf_jwt))

The injected token is then used by every subsequent API call and WebSocket connection:

function apiHeaders() {
  const h = {'Content-Type': 'application/json'};
  const tok = getCfToken();
  if (tok) h['Cf-Access-Jwt-Assertion'] = tok;
  return h;
}

Layer 3: JWT payload decode. Every API endpoint calls _validate_user(), which decodes the CF JWT payload to extract the email. We don't need to verify the signature — Cloudflare's edge already did that before the request reached us. The cloudflared tunnel daemon rejects any request that hasn't passed through CF Access. This is safe because the trust boundary is at the network edge, not the application layer.

# CF Access already validated this token upstream (cloudflared enforces it)
payload_b64 = cf_jwt.split(".")[1]
claims = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))
email = claims.get("email", "")

We tried the cleaner approach first — forwarding the CF JWT to AitherIdentity's /auth/cloudflare/callback endpoint for server-side validation. It worked locally but broke in production because SecurityCore validates against its own CF Access application ID (AUD claim), which is different from the tunnel's. The direct decode approach is actually more correct here: the tunnel service trusts Cloudflare's edge validation and only needs the email from the payload.

RBAC: Five Roles, Backed by AitherIdentity

Once we know who you are, we need to decide what you can do. Rather than maintaining a separate permission store, AitherTunnel integrates directly with AitherIdentity — the central RBAC service that manages users, roles, and permissions for the entire AitherOS platform.

On startup, the tunnel service registers itself with AitherIdentity, receiving an API key it uses for all subsequent calls. Every permission check resolves through Identity's user database, with a 120-second local cache to keep things fast.

Five roles map Identity's system roles to tunnel-specific capabilities:

RoleWhat You Can DoPeer Limit
AdminEverything. Terminal, VPN, forwarding, config, user management, audit logs.20
DeveloperTerminal, VPN, port forwarding. The working set for someone building on the platform.5
Power UserSame as Developer — shell, VPN, port forwards.5
OperatorVPN and port forwarding only. Hit APIs, run tests. No shell.3
ViewerDashboard only. Read-only window into the system.0

Role resolution works by querying AitherIdentity for the user's roles, then mapping the highest-privilege matching role to a set of tunnel capabilities. The machine owner's email (via AITHER_TUNNEL_OWNER_EMAIL env var) always resolves to admin regardless of their Identity roles.

The capability mapping is a simple dict:

ROLE_TUNNEL_CAPABILITIES = {
    "admin": {"terminal", "vpn", "forward", "services", "routes", "config",
              "sessions", "users", "invite", "revoke", "audit", "sysinfo"},
    "developer": {"terminal", "vpn", "forward", "services", "sessions", "sysinfo"},
    "power_user": {"terminal", "vpn", "forward", "services", "sessions", "sysinfo"},
    "operator": {"vpn", "forward", "services", "sysinfo"},
    "viewer": {"services", "sysinfo"},
}

This means a user's tunnel permissions are derived from their platform identity, not from a separate grant file. When you promote someone to developer in AitherIdentity, they automatically get terminal and VPN access to the tunnel. When you remove that role, they lose it. One source of truth.

Inviting Users

The owner or an admin can invite users via the portal UI or the API:

curl -X POST https://tunnel.aitherium.com/tunnel/access/invite \
  -H "Cf-Access-Jwt-Assertion: $CF_JWT" \
  -H "Content-Type: application/json" \
  -d '{"email": "dev@company.com", "role": "developer", "note": "Frontend team"}'

This does three things:

  1. Creates or updates the user in AitherIdentity with the specified role.
  2. Adds the email to the Cloudflare Access application's allow-list policy via the CF API, so the user can SSO into the tunnel.
  3. Returns the grant confirmation with the user's resolved capabilities.

Revoking is the inverse — removes the user's tunnel-capable roles from Identity (preserving any non-tunnel roles like billing tiers) and removes the email from the CF Access policy. The user can still authenticate through Cloudflare, but _get_effective_tunnel_role() returns empty, the applyPermissions() JS function fires, and they see "No Access" with instructions to contact the owner.

The Terminal: xterm.js → WebSocket → PTY

The browser terminal is the feature that makes this a real remote access tool and not just a status dashboard.

It works like this:

  1. xterm.js renders a terminal in the browser. Full ANSI support, 256 colors, mouse events, clipboard, resize.
  2. On tab activation, JS opens a WebSocket to /tunnel/ssh with the CF JWT as a query parameter.
  3. The server authenticates the WebSocket (checking the JWT, resolving the role, verifying terminal permission).
  4. If authorized, the server spawns a PTY (/bin/bash) using pty.openpty() and subprocess.Popen.
  5. Two async tasks shuttle data: WebSocket → PTY stdin, PTY stdout → WebSocket.
  6. Terminal resize events (fit events from xterm.js) are forwarded as ioctl TIOCSWINSZ calls to the PTY.

The WebSocket carries raw terminal bytes. No JSON wrapping, no protocol overhead. It's as close to SSH as you can get without the SSH protocol. The latency through Cloudflare's tunnel is typically 5-15ms — imperceptible for interactive use.

Permission-Based UI

The portal doesn't just enforce permissions server-side — it adapts the UI to what you're allowed to do. On page load, applyPermissions() calls /tunnel/access/me and gets back your role and permission list:

{
  "email": "hello@aitherium.com",
  "has_access": true,
  "role": "admin",
  "permissions": ["audit", "config", "forward", "invite", "revoke", "routes",
                   "services", "sessions", "sysinfo", "terminal", "users", "vpn"],
  "limits": {"max_peers": 20, "max_forwards": 50},
  "source": "identity"
}

If you don't have terminal permission, the Terminal tab disappears. No vpn? VPN Peers tab is gone. No users? The Users management tab never renders. The server still enforces everything — hiding UI elements is UX, not security — but it means a partner sees a clean interface with only the tabs they can actually use.

If you have no access at all, the entire main content area is replaced with a "No Access" message and instructions to contact the owner. You can see you're authenticated (your email shows in the header), but you can't do anything until you're invited.

WireGuard VPN: Kernel-Level, Not Userspace

The VPN integration uses kernel WireGuard, not userspace implementations like wireguard-go. The container runs with NET_ADMIN and SYS_MODULE capabilities, which lets it create and configure WireGuard interfaces directly.

When you create a peer:

  1. A new keypair is generated (wg genkey | wg pubkey).
  2. An IP is allocated from the 10.66.0.0/24 subnet (server is .1, peers start at .2).
  3. The peer config is added to the kernel WireGuard interface via wg set.
  4. A .conf file is generated with the private key, server endpoint, and allowed IPs.
  5. The config is returned to the browser — shown once, with a copy button and download option.

The private key exists only in that HTTP response. The server stores the public key and IP allocation, never the private key. If you lose the config, you revoke the peer and create a new one. This is the WireGuard way.

What We Learned Building This

HttpOnly cookies break JavaScript-based auth patterns. Cloudflare Access sets CF_Authorization as HttpOnly, which means document.cookie can't read it. Every tutorial that says "just read the CF cookie in JS" is wrong. The server has to extract it and inject it into the page. This took three iterations to get right.

AUD claim mismatches are silent failures. When we forwarded the CF JWT to AitherIdentity for validation, it rejected it because the JWT's audience claim (aud) didn't match SecurityCore's CF Access application ID. The error message was "Invalid JWT" with no mention of AUD. The fix was to stop trying to validate the signature at all — Cloudflare already did that — and just decode the payload for the email.

Docker volumes created by root are hostile to non-root containers. Our container runs as UID 1000. The data volume's directory was created by Docker as root. The startup handler tried to write a WireGuard key file and got PermissionError. The fix was making the startup resilient (catch the error, generate an ephemeral keypair instead) and fixing the volume permissions separately. Infrastructure should never crash on a permissions issue it can gracefully degrade around.

WebSocket auth is different from HTTP auth. WebSocket handshakes include cookies, but the browser doesn't let you set custom headers on a WebSocket upgrade request. So you can't send a Cf-Access-Jwt-Assertion header. The solution is to pass the token as a query parameter (/tunnel/ssh?token=...), with a fallback to reading the CF_Authorization cookie from the WebSocket handshake's cookie jar.

A single Python file can be a complete product. AitherTunnel is one file. It serves HTML, handles WebSocket connections, manages WireGuard peers, enforces RBAC through AitherIdentity, and proxies terminal I/O. It's around 3,100 lines. We could have split it into a FastAPI app, a React frontend, a database service, and a WireGuard daemon. We would have gained "clean architecture" and lost the ability to understand the entire system by reading one file. For infrastructure tools, legibility beats elegance every time.

Try It

If you're running AitherOS, the tunnel service is already there:

# Start with the tunnel profile
docker compose --profile tunnel up -d

# Or include it with core services
docker compose --profile core --profile tunnel up -d

Configure your Cloudflare Tunnel to route tunnel.yourdomain.com to the tunnel service container, set up a CF Access application, and you're live. The portal works on any device with a browser — including phones, which is surprisingly useful when you need to check on a training run from the grocery store.

Invite your team:

# From the portal's Users tab, or via API:
POST /tunnel/access/invite
{"email": "alice@company.com", "role": "developer"}

Alice gets SSO access. She sees Terminal, Dashboard, VPN Peers. She can't manage users or change config. She can create up to 5 VPN peers. She can forward ports to the services she needs. And when the project ends, you revoke her access with one click — her tunnel roles are removed from AitherIdentity, but her platform account stays intact.

That's the whole thing. Remote access to your AI machine, protected by real SSO, with permissions that make sense, in a single file that you can read, audit, and trust.

No middleman required.

Enjoyed this post?
Share