Skip to main content
Glama

On-prem MCP server over VPN Site-to-Site: let an AgentBase agent use data that never leaves your data center

A reference sample showing how an agent on GreenNode AgentBase calls an MCP server that runs in your own data center, connected to your GreenNode VPC with the managed VPN Site-to-Site (IPsec) service, through a Private MCP Gateway. The gateway keeps authentication, policy and audit; the data stays on-premises.

CI License: MIT

Architecture

The problem

Enterprise agents become useful when they can answer questions about HR, procurement, inventory or finance. That data usually lives in systems inside the company data center, and policy says it must not be copied to a cloud or exposed to the Internet. You still want the agent platform's controls: who may call which tool, with what credentials, and an audit trail.

Related MCP server: MCPipe Toolchain

The solution

Keep the system where it is and put a small MCP server next to it. Connect the data center to your GreenNode VPC with an IPsec tunnel, and let a Private MCP Gateway forward governed tool calls through that tunnel.

Agent on AgentBase Runtime
   |  tools/call  (Inbound Auth: IAM Permissions or JWT)
   v
MCP Gateway (Private)                  runs in the AgentBase VPC 172.30.0.0/16, managed by GreenNode
   |  1. Policy Group: is  erp__<tool>  allowed for this agent?  (no policy group attached => every tools/call is 403)
   |  2. Connector `erp`: MCP endpoint URL + Outbound Auth = API Key, header X-Api-Key, secret stored in Access Control
   v
private connection to your VPC         your VPC must be privately connected to AgentBase (request it from GreenNode support);
   |                                    the gateway's Route CIDRs include the on-prem CIDR
   v
Customer VPC  ->  route: on-prem CIDR via the VPN gateway
   |
   |  VPN Site-to-Site (IKEv2 / IPsec, customer side)
   v
Data center: IPsec peer -> firewall (allow source 172.30.0.0/16 and the VPC CIDR to the MCP port only)
   |
   v
MCP server  :8443 (TLS) / :8080   -> validates the API key (fail-closed) -> audit log -> SQLite "ERP" data

What this gives you: the agent never sees the API key, the server is not reachable from the Internet, every call is authorized by a Policy Group at the gateway and logged again at the server, and there is no data copy in the cloud.

What is in this repository

src/onprem_mcp/main.py        MCP server (FastMCP, streamable HTTP /mcp, GET /health), SQLite demo data, API-key auth, audit log
tests/                        Hermetic pytest suite (tools, input validation, auth middleware, audit log)
examples/mcp_client.py        Minimal MCP client: direct (API key) or through the gateway (Bearer token)
Dockerfile, requirements*.txt Container image (non-root) and dependencies
infra/greennode/README.md     Console runbook: VPC, VPN Site-to-Site, routes, ACL, private connection, gateway, connector, policy
infra/onprem/                 Data-center side: strongSwan, nftables, docker-compose (+ Caddy TLS), connectivity check, other gateways
lab/                          Test everything without a real data center (simulated "on-prem" VM)
docs/architecture.svg         Architecture diagram (referenced above)

The demo MCP server

A stand-in for an internal enterprise system, backed by SQLite and seeded with fictional data (nothing real). All tools are read-only (the database is opened in read-only mode by the tools).

Tool

Policy action

Description

find_employee(query)

erp__find_employee

Search the employee directory by name, e-mail or department

leave_balance(employee_id)

erp__leave_balance

Annual and sick leave balance of one employee

list_purchase_orders(status)

erp__list_purchase_orders

Purchase orders, filtered by pending_approval, approved, received, cancelled or all

get_purchase_order(po_id)

erp__get_purchase_order

One purchase order with line items and total

inventory_level(sku)

erp__inventory_level

Stock per warehouse with reorder status

Inputs are validated (format and length) and every query is parameterized.

Prerequisites

  • A GreenNode account with the Root or Admin role, access to vNetwork / vServer and AgentBase.

  • A data center (or the lab VM) with a public IP for the IPsec device and a host for the MCP server.

  • CIDRs that do not overlap: the AgentBase VPC 172.30.0.0/16, your VPC, and the data-center network (examples: 10.20.0.0/16 for the VPC and 192.168.0.0/16 for the data center; replace them with your ranges).

  • GreenNode support to activate the private connection between your VPC and AgentBase (only privately connected VPCs are offered when creating a Private gateway). The VPC needs DNS resolution enabled.

  • Docker (server), Python 3.12 (tests and client example), bash and curl (connectivity check).

Quick start (local)

Run the server locally, without any network setup:

export MCP_KEY=$(openssl rand -hex 32)          # keep it: the gateway connector will need the same value later
docker build -t onprem-mcp-server .
docker run --rm -p 127.0.0.1:8080:8080 -e MCP_API_KEYS="$MCP_KEY" onprem-mcp-server
curl -s http://127.0.0.1:8080/health                                       # open, no key
curl -s -X POST http://127.0.0.1:8080/mcp -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' -o /dev/null -w '%{http_code}\n'   # 401: key required

Call a tool with an MCP client (header X-Api-Key):

pip install -r requirements.txt
MCP_URL=http://127.0.0.1:8080/mcp MCP_API_KEY="$MCP_KEY" python examples/mcp_client.py \
  --call find_employee --args '{"query":"nguyen"}'

Or in your own code:

import asyncio, os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client("http://127.0.0.1:8080/mcp",
                                     headers={"X-Api-Key": os.environ["MCP_KEY"]}) as (r, w, _):
        async with ClientSession(r, w) as session:
            await session.initialize()
            print([t.name for t in (await session.list_tools()).tools])
            print((await session.call_tool("leave_balance", {"employee_id": "E1002"})).content[0].text)

asyncio.run(main())

Without Docker: pip install -r requirements.txt && MCP_API_KEYS="$MCP_KEY" python src/onprem_mcp/main.py (ALLOW_ANONYMOUS=true lets you start without a key, for local development only).

Step-by-step deployment

Step

Where

What

1

infra/greennode/README.md, (a) to (d)

CIDR plan, VPC and subnets, VPN Site-to-Site, route table

2

infra/onprem/

strongSwan (or your firewall) as IPsec peer, return routes, firewall, MCP server, check_connectivity.sh from the VPC

3

infra/greennode/README.md, (e) to (f)

Optional ACL, private connection request to GreenNode support

4

infra/greennode/README.md, (g) to (k)

Private MCP Gateway, Access Control key, connector erp, Policy Group, end-to-end verification

Lab

lab/README.md

The same flow with a simulated data center

Network summary: the gateway runs in 172.30.0.0/16; its Route CIDRs include the on-prem CIDR; the customer VPC route table sends the on-prem CIDR to the VPN gateway; the data center routes back both the customer VPC CIDR and 172.30.0.0/16 through the tunnel; its firewall allows source 172.30.0.0/16 (and the VPC CIDR) to the MCP port.

The IPsec parameters (IKEv2, AES-256-GCM, SHA-256, DH group 3072 with 2048 as fallback, pre-shared key) are taken from the GreenNode "Support IPSEC Configuration" page and the pfSense demo; see the table in the runbook.

Security checklist

  • Least-privilege policy: one Policy Group rule per agent, explicit erp__<tool> actions, no ["*"]. Without any Policy Group, all tools/call return 403 by design.

  • API key: generated with openssl rand -hex 32, stored in Access Control and in the server .env (chmod 600), never in git or in agent code. The server is fail-closed: no MCP_API_KEYS means 503 on /mcp.

  • Key rotation without downtime: list two keys (MCP_API_KEYS=old,new), switch the Access Control provider to new, then remove old.

  • Firewall source restriction: IKE, NAT-T and ESP only from the GreenNode VPN IP; the MCP port only from 172.30.0.0/16 and the VPC CIDR (verify which source the data center really sees); everything else dropped (infra/onprem/firewall/nftables.conf).

  • No exposure: the server binds to an internal address only (MCP_BIND_ADDR), never 0.0.0.0 on a host with a public interface; no public port forwarding.

  • TLS: use the Caddy profile (https://<host>:8443/mcp) with a certificate from a CA the gateway trusts; keep plain :8080 for tests only.

  • Strong IPsec: AEAD or SHA-256 or better, DH group 14 or larger, a long random pre-shared key; avoid the algorithms flagged as weak (md5, sha, modp1024 and below).

  • Audit logs: the server logs audit tool=<name> caller=<ip> for every tool call (no arguments, no secrets); ship docker logs to your SIEM and compare with the gateway audit log. Set TRUST_FORWARDED_FOR=true only behind Caddy.

  • Container hardening: non-root user, read-only root filesystem, all capabilities dropped (see infra/onprem/docker-compose.yml).

  • Network ACL / security groups tightened after the end-to-end test passes (runbook step e).

Troubleshooting

Symptom

Likely cause

What to check

VPN stays Provisioning or is Active but the data center never shows ESTABLISHED

Tunnel down: UDP 500/4500 or ESP blocked, wrong peer IP, pre-shared key or algorithm mismatch

swanctl --list-sas, journalctl -u strongswan; NO_PROPOSAL_CHOSEN = proposals differ from the VPN detail page, AUTHENTICATION_FAILED = PSK or ids; GreenNode VPN log tab

Console error 2017 / 2023 when creating the VPN

Overlapping CIDRs between VPC and data center, or between remote sites

Re-plan the CIDRs (never use 172.30.0.0/16)

Console error 2018 / 2019 / 2020 / 2021

Remote CIDR is not private, or the remote gateway IP is not a public IP

Use an RFC 1918 CIDR and a public peer IP

Tunnel ESTABLISHED but ping and check_connectivity.sh fail (step 1)

Routes missing: no route for the on-prem CIDR to the VPN gateway in the VPC route table, or no return route on the MCP host

Runbook step d; on the data center add routes for the VPC CIDR and 172.30.0.0/16 toward the IPsec peer

check_connectivity.sh passes but the gateway times out

Gateway path differs: Route CIDRs do not include the on-prem CIDR, no return route for 172.30.0.0/16, firewall blocks that source, or the source is NATed to a VPC address

Gateway Network & Compute tab; docker logs onprem-mcp | grep audit (shows whether calls arrive and from where); firewall counters

Private gateway cannot select the VPC

The VPC is not privately connected to AgentBase yet, or the list is stale

Contact GreenNode support (runbook f); use the refresh icon

Hostname does not resolve

DNS resolution (vDNS) disabled on the VPC, or no DNS path to the data-center resolver

Use the IP in the connector URL, or enable DNS and forward the zone: verify with GreenNode

TLS handshake error at the connector

The certificate is not issued by a CA the gateway trusts, or does not match the host in the URL

Use an enterprise or public CA, match CADDY_SITE to the connector host; ask GreenNode about custom CAs

401 on /mcp

Key missing or wrong: the Access Control value differs from MCP_API_KEYS, header key is not X-Api-Key, or the prefix was left as Bearer

Connector Outbound Auth settings; docker logs onprem-mcp (401 on /mcp from <ip>)

503 on /mcp

Server has no MCP_API_KEYS (fail-closed)

Set the key in .env and restart

401 from the gateway endpoint (before the server)

Inbound Auth failed: expired IAM token, wrong JWT issuer or audience

Get a fresh token; check Inbound Auth settings

403 on tools/call (tools/list works)

No Policy Group attached, or the principal or erp__<tool> action is not allowed

Gateway Policy tab; policy changes apply within about 30 seconds

404 or Session terminated

Wrong path: the gateway URL must end with the connector name (.../erp) and the connector URL must end with /mcp

Re-copy the Endpoint URL from the gateway detail page

5xx from the gateway

The server is unreachable or returned an error

Connectivity from the VPC, server logs, connector endpoint URL and scheme (HTTP vs HTTPS)

Intermittent resets on large responses

MTU / fragmentation through the tunnel

Keep the MSS clamp in nftables.conf (1360)

Verify with GreenNode

These items are not stated in the public documentation used for this sample. Confirm them with GreenNode support before production use:

  1. Gateway source address: does traffic from the MCP Gateway reach the data center as 172.30.0.0/16, or source-NATed to a VPC address? (The firewall and ACL rules open both until this is known; the MCP server audit log shows the real source.)

  2. Tunnel selectors: can the VPN tunnel carry 172.30.0.0/16, or only the VPC CIDR?

  3. Route in the VPC: is a route for 172.30.0.0/16 needed in the customer VPC route table, which route table applies to traffic that enters the VPC from AgentBase, and is the route to the VPC CIDR added on the AgentBase side automatically after the private connection is activated?

  4. Default IKE / IPsec policy: the exact values applied by the console (DH group 2048 per the keyword table versus 3072 in the pfSense demo), Phase 2 PFS, and whether the policy can be customized yet.

  5. Lifetimes: the demo page prints a Phase 1 lifetime of 144000 for "4 hours" (14400 s expected).

  6. DPD and NAT-T behavior, including data-center devices that sit behind NAT.

  7. Connector endpoint: whether http:// is accepted (the docs describe a full HTTPS URL), and how an internal or custom CA is provided for TLS.

  8. Gateway endpoint reachability: whether the Private gateway endpoint is reachable only from privately connected networks, and which network mode the calling agent runtime needs.

  9. Network mode of a gateway is fixed after creation (assumed, not documented in the pages used here).

  10. VPC DNS: where DNS resolution (vDNS) is enabled, and name resolution of data-center hostnames.

  11. Network ACL: how the VPN data path interacts with subnet ACLs, how "Port range" is matched, and the apparent difference between the default allow-any rule and "a new ACL denies all".

  12. Policy principal: the exact iam:<id> value for an agent runtime on your gateway version.

  13. VPN details: the field that shows the GreenNode VPN public IP, and the phase 1 / phase 2 status indicators (the FAQ says they are coming).

Tests

pip install -r requirements-dev.txt
python -m pytest tests/ -q          # hermetic: temporary SQLite database, no network
bash -n infra/onprem/check_connectivity.sh lab/lab_setup_onprem.sh

The suite covers each tool against a temporary database, input validation (including SQL-injection and LIKE-wildcard inputs), the fail-closed API-key middleware (503 / 401 / 200, both header styles, key rotation, /health open) and the audit log (tool name and caller recorded, no secrets).

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants and developer tools to securely access and interact with an organization's enterprise knowledge, documents, and people through natural language while respecting existing access permissions.
    166
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Provides read-only access to Confluence, GitHub, Jira, Figma, Outlook, Teams, and browser automation for AI clients, enabling safe discovery, retrieval, and summarization of company knowledge without modifying source systems.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables any LLM to query financial data through predefined, read-only tools, such as retrieving credit scores, preapproved limits, risk profiles, and consolidated customer profiles from a mapped database.
    3
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to read local business data and request database mutations, while requiring human approval before updates or deletions are executed. It provides read-only tools, approval workflows, and audit logging to prevent autonomous destructive changes.
    -