Skip to content
SLOT-0683 | 2U RACK

Building a Gemini MCP Gateway for Proxmox

Reading Time
14 min
~200 words/min
Word Count
2,782
6 pages
Published
Sep 19
2026
AI INFRASTRUCTURE · MCP GEMINI MCP GATEWAY FOR PROXMOX VE Streamable HTTP Transport · OAuth 2.0 Ingress · Zero Inbound Router Ports · Hypervisor DMZ Microsegmentation 1. GOOGLE GEMINI Gemini Pro / Flash • Connected Workspace App • Streamable HTTP Client • SSE Stream Listener Authentication Layer • OAuth 2.0 RFC 6749 • Bearer Token Ingress • Scopes: read:telemetry • exec:ephemeral_lxc Protocol Format • JSON-RPC 2.0 Payload POST /mcp HTTP/1.1 Authorization: Bearer *** Content-Type: app/json 2. CLOUDFLARE EDGE Edge WAF & Shield • DDoS Absorption (Anycast) • Rate Limit: 60 req/min • TLS 1.3 Termination Cloudflare Tunnel • Outbound-Only QUIC/TCP • Inbound Router Ports = 0 • Edge Domain: mcp.example • Identity Header Injection Tunnel Daemon • cloudflared (CT 100) Ingress Rule: service: http://10.0.10.25 path: /mcp (Streamable) 3. MCP GATEWAY Fastify TypeScript Service • DMZ Host: 10.0.10.25:3000 • MCP Spec: 2024-11-05 • SSE Stream + POST /mcp Security & Invariants • Strict Zod Schema Parse • Zero-Leakage Masker • Safe Resource Scrubbing • Deterministic Error Trap Tool Dispatch Catalog • proxmox_cluster_status • proxmox_list_telemetry • proxmox_exec_ephemeral • pbs_datastore_metrics 4. PROXMOX VE CLUSTER Hypervisor REST Engine • HTTPS API: :8006/api2/json • Auth: PVEAPIToken=*** • mTLS & Internal SSL Least-Privilege Security • Role: PVEAuditor (Read-Only) • Ephemeral Operator ACLs • Hypervisor Root Protection • Zero Direct Public Exposure Target Workloads • DMZ Network (vmbr1) • Ephemeral Dev LXC Workloads • QEMU Guest Agent Signals • PBS Instant Snapshot Check PRODUCTION MCP GATEWAY SPECIFICATION & TELEMETRY MATRIX Protocol: Model Context Protocol (2024-11-05) Transport: Streamable HTTP + SSE · JSON-RPC 2.0 Validation: Zod Strict Input/Output Parsing Inbound WAN Ports: Strictly 0 (Zero Open Ports) Ingress Auth: OAuth 2.0 / Bearer Handshake Sanitization: 7-Layer Zero-Leakage Privacy Guard LIVE MEASURED TELEMETRY Telemetry Query Latency: ~42ms Fastify JSON-RPC Over SSE Stream

Table of Contents

Reading Progress 0%

How to bridge Google Gemini Connected Apps to an unprivileged Proxmox VE hypervisor using Model Context Protocol (MCP) Streamable HTTP, zero-trust tunnels, and custom edge WAF rules.

  • Streamable HTTP & Universal MCP Compatibility: The official Model Context Protocol (MCP) Streamable HTTP specification allows cloud-hosted frontier LLMs to execute stateful, low-latency hypervisor operations without fragile SSH wrappers or persistent open socket connections. Crucially, because it adheres strictly to open standards, this gateway works out-of-the-box with any MCP client supporting Streamable HTTP and OAuth 2.0 / Bearer authentication.
  • Hardened Zero-Trust Authentication: While simple or public MCP servers can run unauthenticated, exposing hypervisor lifecycle primitives over the internet demands defense-in-depth. I implemented RFC 6749 OAuth 2.0 and OpenID Connect discovery with PKCE authorization code flow and signed HMAC-SHA256 JWT id_token validation.
  • The Cloudflare Free Bot Fight Mode Bottleneck: Free-tier Bot Fight Mode intercepts server-to-server OAuth POST requests from cloud datacenters with interactive JavaScript challenges. Bypassing it requires disabling Bot Fight Mode and engineering a targeted edge WAF perimeter rule for Google ASNs (AS15169 and AS396982).
  • Blast-Radius Containment Protects Ingress: Enforcing code-level safeguards (PROTECTED_VMIDS) combined with granular Proxmox RBAC (GeminiOps) guarantees autonomous AI agents cannot knock offline their own ingress tunnel or delete storage volumes.

Managing virtualization clusters has traditionally required an administrator sitting at a terminal or web dashboard. Whether querying node memory pressure, reverting a virtual machine snapshot before an operating system upgrade, or verifying overnight backup completion, the workflow has remained largely unchanged for a decade: authenticate to the management plane, locate the guest, and issue manual commands.

With the release of Google Gemini Spark's Custom Connected Apps powered by the Model Context Protocol (MCP), a fundamentally more powerful paradigm is possible: conversational, autonomous infrastructure management. Instead of logging into a dashboard, you ask Gemini from your phone or desktop: "Check memory utilization across the cluster and trigger a snapshot of the database container before I deploy." Gemini evaluates cluster state, selects the appropriate tool, and safely reports the result in natural language.

However, connecting a cloud-hosted frontier AI directly to bare-metal or virtualized hypervisors presents serious architectural and security hurdles. How do you expose hypervisor APIs without opening firewall ports? How do you satisfy Google Gemini's strict OAuth 2.0 / OpenID Connect requirements without deploying bloated enterprise identity infrastructure? And how do you ensure an autonomous model does not accidentally shut down your ingress networking or delete production disks?

In this article, I walk through the end-to-end architecture of building the Gemini-Proxmox VE Operator: an enterprise-grade gateway combining FastMCP Streamable HTTP, an embedded OAuth 2.0 authorization server, Cloudflare Zero Trust tunnels, edge WAF perimeter filtering, and Proxmox RBAC privilege separation.

The 5-Layer Defense-in-Depth Architecture

Exposing hypervisor management to an external cloud client violates the cardinal rule of infrastructure security unless protected by multiple, independent security boundaries. In my earlier article (Zero-Trust Homelab Without Port Forwarding), I established that zero inbound router ports is non-negotiable. To connect Gemini safely, I designed a 5-layer defense-in-depth model:

Layer Component Security Mechanism Threat Mitigated
1. Edge Perimeter Cloudflare Custom WAF ASN filtering (AS15169 Google LLC, AS396982 Google Cloud) + Authorize bypass Blocks 100% of unauthorized public internet scans, brute-force bots, and DDoS probes at the edge (HTTP 403).
2. Ingress Transport Cloudflare Zero Trust Tunnel Outbound-only QUIC tunnel terminated in an isolated DMZ container Eliminates open firewall ports, port forwarding, and public IP discovery.
3. Capability Routing Cryptographic Path Prefix 128-bit unguessable UUID path: /<CAPABILITY-UUID>/mcp Prevents endpoint enumeration and reconnaissance; requests to root return 404/403.
4. Identity & Auth OAuth 2.0 & OIDC Gateway Bearer JWT signature verification (HS256) & static LAN secret Ensures only authenticated Gemini clients or authorized LAN monitoring pipelines can call tools.
5. Blast Radius & RBAC Code Guardrails & PVE RBAC PROTECTED_VMIDS guardrail + GeminiOps role omitting VM/storage deletion Prevents AI agents from shutting down ingress infrastructure or executing destructive volume removals.

Implementing MCP over Streamable HTTP

The Model Context Protocol supports multiple transport bindings, historically focusing on local stdio pipes and Server-Sent Events (SSE). However, for cloud-to-origin architectures like Google Gemini Connected Apps, Streamable HTTP represents the modern standard. Streamable HTTP handles stateful MCP tool discovery, initialization, and JSON-RPC execution over standard HTTP POST requests without holding persistent open TCP sockets across proxies.

"Streamable HTTP is designed for internet-facing MCP servers that require stateless transport scaling, standard reverse-proxy compatibility, and robust authentication handshakes."

— Model Context Protocol Specification, Streamable HTTP Transport RFC

Using the official Python MCP SDK and FastAPI, the MCP sub-application is instantiated and mounted at the cryptographic capability path:

from fastapi import FastAPI
from fastmcp import FastMCP
from fastmcp.utilities.lifespan import combine_lifespans
from app.config import settings
from app.mcp_server import mcp, init_ops
from app.security import MCPAuthMiddleware

# Initialize FastMCP HTTP ASGI sub-app with stateless streamable HTTP handling
mcp_asgi_app = mcp.http_app(path="/mcp", stateless_http=True)

app = FastAPI(
    title="Gemini Proxmox VE Operator",
    description="Enterprise MCP and REST Gateway for Proxmox VE Management",
    version="2.0.0",
    lifespan=combine_lifespans(app_lifespan, mcp_asgi_app.lifespan),
)

# Mount MCP server at secret capability path protected by auth middleware
app.mount(f"/{settings.mcp_secret_path}", MCPAuthMiddleware(mcp_asgi_app))

Because Starlette sub-applications bypass parent FastAPI dependencies, I wrapped the mounted sub-app in MCPAuthMiddleware. This middleware extracts incoming Authorization: Bearer <token> headers and validates the signed JWT token before allowing requests into the MCP session manager.

Universal Compatibility: Beyond Gemini Spark

While Google Gemini Spark served as my primary catalyst for building this gateway, the architecture is completely client-agnostic. Any AI agent framework, developer IDE, or client application that implements Model Context Protocol (MCP) over Streamable HTTP can connect to this gateway using the exact same authentication patterns:

  • Interactive OAuth 2.0 Flow: Clients supporting RFC 8414 / OpenID Connect discovery can automatically configure authorization by querying /.well-known/oauth-authorization-server or /oauth/authorize and /oauth/token with standard PKCE negotiation.
  • Direct Bearer Authentication: For programmatic agents, background worker pipelines, or headless orchestrators that do not support interactive browser redirects, the gateway accepts direct HTTP Bearer tokens (the signed HMAC-SHA256 JWT issued during OAuth or a pre-shared service secret).
  • Edge Transport Flexibility: When accessing the gateway from non-Google cloud providers or local developer workstations, the Cloudflare WAF rule can be easily adapted to include your client's source IP or ASN—or bypassed entirely when communicating over private WireGuard or Tailscale networks.

Examples of Compatible Clients & Configurations

Client Category Client Examples Supported Connection Mode
Frontier Cloud Platforms Google Gemini (Gemini Spark / Connected Apps) OAuth 2.0 (Authorization Code + PKCE, OIDC discovery)
Developer IDEs & Coding Agents Cursor, VS Code (Cline, Roo Code, Continue), Windsurf Streamable HTTP with Bearer Auth / Pre-shared Token
Desktop & CLI Assistants Claude Desktop, Claude Code (via remote HTTP bridge) Remote Streamable HTTP endpoint with Bearer header
Open-Source AI Interfaces LibreChat, OpenWebUI, Dify Remote MCP server with Bearer Token or OAuth 2.0
Autonomous Agent Frameworks LangGraph, CrewAI, AutoGen, LlamaIndex, Python MCP SDK Native streamable_http_client with JWT / Bearer Auth

For example, connecting from an IDE or desktop tool like Cursor or Cline requires specifying the Streamable HTTP capability URL and Bearer token:

{
  "mcpServers": {
    "proxmox-operator": {
      "url": "https://pve-mcp.example.com/<CAPABILITY-UUID>/mcp",
      "headers": {
        "Authorization": "Bearer <YOUR-TOKEN>"
      }
    }
  }
}

Or connecting programmatically inside an autonomous agent with the official Python MCP SDK:

from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

async with streamable_http_client(
    "https://pve-mcp.example.com/<CAPABILITY-UUID>/mcp",
    headers={"Authorization": "Bearer <YOUR-TOKEN>"},
) as (read_stream, write_stream):
    async with ClientSession(read_stream, write_stream) as session:
        await session.initialize()
        tools = await session.list_tools()

Engineering the Embedded OAuth 2.0 / OIDC Server

While the Model Context Protocol (MCP) and Gemini Custom Connected Apps allow unauthenticated connections for simple, public, or read-only tools, exposing hypervisor infrastructure to the open internet without robust authentication is an unacceptable risk. Rather than leaving the tunnel unauthenticated or relying on a fragile static API key, I chose to harden the gateway with an enterprise-grade authentication layer: full RFC-compliant OAuth 2.0 and OpenID Connect discovery with PKCE authorization code negotiation.

Rather than deploying an external, resource-heavy identity provider (such as Keycloak or Authentik) inside an unprivileged container, I engineered a lightweight, RFC-compliant OAuth 2.0 authorization server directly inside FastAPI.

1. OpenID Connect Discovery Endpoints

When Gemini initialises a connection, it queries two standard RFC discovery metadata endpoints to discover token and authorization capabilities:

@app.get("/.well-known/openid-configuration")
@app.get("/.well-known/oauth-authorization-server")
def openid_configuration():
    base = settings.public_base_url.rstrip("/")
    return {
        "issuer": base,
        "authorization_endpoint": f"{base}/oauth/authorize",
        "token_endpoint": f"{base}/oauth/token",
        "userinfo_endpoint": f"{base}/oauth/userinfo",
        "jwks_uri": f"{base}/.well-known/jwks.json",
        "response_types_supported": ["code"],
        "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["HS256"],
        "scopes_supported": ["openid", "profile", "email"],
        "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
        "code_challenge_methods_supported": ["S256"],
    }

During the account linking step, Gemini redirects the user's browser to /oauth/authorize with an authorization code challenge (PKCE). My gateway issues a cryptographically secure, single-use authorization code and returns an immediate HTTP 302 Redirect back to Google's redirect URI (https://oauth-redirect.googleusercontent.com/r/...):

@app.get("/oauth/authorize")
async def oauth_authorize(
    redirect_uri: str = Query(...),
    state: str = Query(None),
    code_challenge: str = Query(None),
):
    code = secrets.token_urlsafe(32)
    auth_codes[code] = {
        "code_challenge": code_challenge,
        "expires_at": datetime.now(timezone.utc) + timedelta(minutes=15),
    }
    params = {"code": code}
    if state:
        params["state"] = state
    redirect_url = f"{redirect_uri}?{urlencode(params)}"
    return RedirectResponse(url=redirect_url, status_code=302)

3. Solving the Starlette Body Consumption Trap in /oauth/token

The most subtle bug in implementing OAuth for frontier AI clients involves payload serialization. While RFC 6749 specifies that token requests should use application/x-www-form-urlencoded, several modern AI orchestrators submit token exchange requests formatted as application/json.

In Starlette and FastAPI, invoking await request.form() on a JSON payload or inspecting the body multiple times causes an unhandled ClientDisconnect or body stream exhaustion error. To support all callers deterministically, my token endpoint reads raw bytes once and routes parsing conditionally:

@app.post("/oauth/token")
async def oauth_token(request: Request):
    content_type = request.headers.get("content-type", "")
    body_bytes = await request.body()
    params = {}

    if "application/json" in content_type:
        params = json.loads(body_bytes.decode("utf-8"))
    else:
        qs = parse_qs(body_bytes.decode("utf-8"))
        params = {k: v[0] for k, v in qs.items()}

    # Validate authorization code, PKCE verifier, and client credentials
    ...
    # Issue access token and signed OIDC id_token JWT
    id_payload = {
        "sub": settings.oauth_client_id,
        "iss": settings.public_base_url,
        "aud": settings.oauth_client_id,
        "exp": int((now + timedelta(hours=24)).timestamp()),
    }
    id_token = jwt.encode(id_payload, settings.oauth_client_secret, algorithm="HS256")

    return {
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 86400,
        "refresh_token": refresh_token,
        "id_token": id_token,
    }

The Cloudflare Edge Bottleneck: Bot Fight Mode vs. WAF Rules

Once the application and OAuth layers were deployed, initial connection attempts from Google Gemini failed with a red error banner: "Account linking is required to use this custom app. Try again."

Examining edge HTTP logs revealed the root cause: Cloudflare's **Bot Fight Mode**. When a user completes the browser consent step, Google's backend worker makes an automated server-to-server HTTP POST https://pve-mcp.example.com/oauth/token from Google datacenters. Cloudflare Bot Fight Mode classifies automated datacenter POSTs as bot traffic and serves an interactive JavaScript challenge ("Just a moment..."). Because an automated OAuth worker expects a JSON payload and cannot execute browser JavaScript, the token exchange fails silently.

Many administrators attempt to solve this by creating a WAF Custom Rule with action Skip. However, on Cloudflare's Free tier, this fails due to architectural pipeline separation: Bot Fight Mode evaluates before the WAF Ruleset Engine. Custom Skip rules only apply to Super Bot Fight Mode on paid Pro plans ($20+/month).

The Architectural Fix: Targeted WAF Perimeter Filtering

Instead of relying on the blunt, zone-wide Bot Fight Mode switch, I disabled Bot Fight Mode entirely and deployed a surgical WAF Custom Perimeter Rule under Security > Security rules:

(http.host eq "pve-mcp.example.com" and not ip.src.asnum in {15169 396982} and not http.request.uri.path contains "/oauth/authorize")

Action: Block (HTTP 403)

This rule achieves an optimal security posture:

  • Google Datacenters (AS15169 Google LLC & AS396982 Google Cloud): Permitted direct, unhindered access to /oauth/token and the secret MCP capability endpoint.
  • Browser Consent (/oauth/authorize): Allowed through so users can authorize the app from residential ISPs without getting blocked.
  • All Other Internet Traffic: Random port scanners, vulnerability scanners, and automated bots attempting to reach the subdomain are dropped cold at Cloudflare's edge before reaching the tunnel or origin.

The 12 Operational Tools & Blast-Radius Containment

The gateway registers 12 granular operations covering cluster telemetry, guest lifecycle, storage verification, and live snapshotting:

Tool Identifier Category Operation Scope
pve_cluster_status Read Cluster quorum, node health, CPU load, and aggregate RAM utilization.
pve_nodes_list Read Enumerates all hypervisor nodes with online states and uptimes.
pve_guests_list Read Full inventory of virtual machines and LXC containers with running states.
pve_guest_status Read In-depth inspection of specific guest resource consumption and status.
pve_storage_list Read Storage pool utilization, free space, and active datastore health.
pve_recent_tasks Read Audit log of recent vzdump backup jobs, snapshots, and exit codes.
pve_guest_start Write Powers on stopped virtual machines or containers.
pve_guest_stop Write Shuts down a guest (strictly blocked on protected VMIDs).
pve_guest_reboot Write Gracefully reboots a guest (strictly blocked on protected VMIDs).
pve_guest_snapshot Write Takes live disk snapshots without vmstate freeze to avoid I/O lockups.
pve_guest_rollback Write Reverts a guest to a specified prior snapshot.
pve_guest_backup Write Triggers an immediate vzdump backup of a guest to PBS or local storage.

Blast-Radius Safeguards: Protecting Ingress

When empowering an autonomous agent to execute lifecycle verbs, you must safeguard the connection path itself. If Gemini attempts to shut down the container hosting the Cloudflare Tunnel daemon, it would sever external connectivity, causing an unrecoverable self-inflicted outage.

In app/proxmox_ops.py, I enforced a programmatic guardrail:

def stop_guest(self, vmid: int) -> dict:
    if vmid in settings.protected_vmids:
        return {"error": f"Rejected: Guest {vmid} is in protected_vmids (stopping it would disrupt critical infrastructure)."}
    ...

Furthermore, on Proxmox VE, the service user gemini-operator@pve is bound to a custom GeminiOps role granting VM.Audit, VM.PowerMgmt, VM.Snapshot, VM.Backup, and Datastore.AllocateSpace. Rights to delete virtual machines (VM.Allocate) or reformat storage pools are strictly omitted, establishing hard hypervisor-enforced boundaries.


Live Verification & Production Results

Testing the complete end-to-end pipeline verified every layer of the architecture:

1. Edge WAF Block Verification: Running an unauthenticated HTTP probe against root from a non-Google IP address confirms immediate rejection at Cloudflare's edge:

$ curl -I https://pve-mcp.example.com/
HTTP/2 403 Forbidden
server: cloudflare
cf-ray: ...

2. Browser Authorize Passthrough: Hitting the authorization endpoint passes through the WAF and reaches the gateway service:

$ curl -I https://pve-mcp.example.com/oauth/authorize
HTTP/2 400 Bad Request
server: cloudflare

3. Gemini Spark Connected App Handshake: In the container systemd journal, Google Gemini's automated backend completes token exchange and MCP tool registration in sub-second time:

INFO:gemini-proxmox:HTTP POST /oauth/token -> 200 [Client: 108.177.71.32] [UA: OpenAuth]
INFO:gemini-proxmox:HTTP POST /<CAPABILITY-UUID>/mcp -> 200 [Client: 108.177.72.98] [UA: Google]
INFO:gemini-proxmox:HTTP POST /<CAPABILITY-UUID>/mcp -> 200 [Client: 108.177.72.98] [UA: Google]

Empirical Gateway Telemetry & Benchmarks

To quantify production performance and resource consumption, I captured real-world operating telemetry across end-to-end client sessions:

Operational Metric Observed Value Architectural Impact
OAuth Token Exchange Latency 42 ms Cold PKCE token negotiation round-trip over tunnel.
MCP Tool Execution Latency 38 ms Streamable HTTP JSON-RPC dispatch to Proxmox API.
Gateway Memory Footprint 118 MB Resident memory (RAM) in unprivileged container.
Hard Memory Floor 256 MB Invariant threshold enforced by tune_guest_resources.
Edge Tunnel Frame Loss 0% Zero dropped packets across Cloudflare QUIC tunnel.

Open Source Repository & Takeaways

The entire project is open-sourced and available on GitHub:

https://github.com/eddygk/gemini-proxmox

Deploying conversational AI operators against production hypervisors is no longer speculative—it is an immensely practical operational tool when anchored in solid systems engineering:

  • Embrace Standards for Universal Compatibility: Relying on the official Model Context Protocol (MCP) Streamable HTTP transport and standard RFC OAuth 2.0 / OIDC ensures you are never locked into a single AI ecosystem. Any compliant MCP client, agent framework, or developer tool can seamlessly control your hypervisor infrastructure.
  • Do Not Trust Edge Defaults: High-level bot protection features like Bot Fight Mode are designed for consumer web pages, not automated B2B/API workflows. Engineer explicit ASN perimeter rules instead.
  • Enforce Invariant Guardrails: Autonomous systems require deterministic boundaries. Combine code-level blast-radius checks with hypervisor privilege separation so that no prompt or hallucination can breach operational safety.
Eddy Kawira

About Eddy Kawira

Infrastructure Engineer & AI Systems Architect with 20+ years across IT systems, data center operations, and cloud architecture. Experience ranges from high-scale AWS data center deployments (Trainium2) and enterprise Azure Virtual Desktop migrations to building open-source AI agent tooling (ClawHub) used by 10,000+ developers. Runs a battle-tested hybrid Proxmox VE and Hyper-V homelab focused on reliability, deep observability, and safe automation.

View all posts by Eddy Kawira →
user@eddykawira:~/comments$ ./post_comment.sh

# Leave a Reply

# Note: Your email address will not be published. Required fields are marked *

LIVE
CPU:
MEM: