← Back to blog
August 6, 2026Case Studies

Tunneling mTLS Through a Cloud-to-On-Prem Connector Without Breaking Certificate Auth

#mtls#security#proxy#cloud-foundry#networking

A recurring problem in large, security-critical environments: an on-prem service only accepts connections that (a) come from an allow-listed internal IP range and (b) present a valid mutual TLS (mTLS) client certificate. That’s a solid perimeter — until a cloud-hosted application (in this case, on Cloud Foundry) needs to call it.

Why the obvious path breaks

The standard way to reach an on-prem service from a cloud platform is a cloud-to-on-prem connector — an agent that opens an outbound tunnel from inside the network and lets the cloud platform route requests through it. It solves the IP allow-list problem: traffic now originates from an internal address.

It does not solve the mTLS problem, because most connectors act as an HTTP(S) reverse proxy: they terminate the incoming TLS connection, inspect/forward the HTTP request, and open a new connection to the origin server. That second connection has no client certificate on it — the connector isn’t the app, so it can’t present the app’s identity.

✗ Connector terminates TLS — origin rejects the request
Cloud Foundry app mTLS → Connector plain HTTP → On-prem service → 401
✓ SOCKS5 tunnel — TLS passes through untouched
Cloud Foundry app SOCKS5 → Connector same mTLS → On-prem service → 200

The fix: tunnel, don’t terminate

A SOCKS5 proxy operates one layer lower than an HTTP reverse proxy — it forwards raw TCP bytes rather than parsing and re-issuing HTTP requests. So instead of asking the connector to speak HTTPS to the origin on the app’s behalf, the app opens its own TLS connection (with its own client certificate) and simply routes the encrypted bytes of that connection through the connector via SOCKS5.

The connector never sees plaintext, never touches the TLS handshake, and has no certificate to present — because it doesn’t need one. The mTLS handshake happens directly between the calling app and the on-prem origin, exactly as if the connector wasn’t in the path at all. It’s just carrying bytes.

Conceptually, a client making this call looks like a normal mTLS request, just routed through a local SOCKS5 endpoint:

import httpx

# The proxy tunnels raw bytes to the connector; TLS is still
# negotiated end-to-end between this client and the origin.
client = httpx.Client(
    proxy="socks5://127.0.0.1:1080",
    cert=("client.crt", "client.key"),
    verify="onprem-ca-bundle.pem",
)

response = client.get("https://onprem-service.internal/api/resource")

Here’s what actually happens on the wire, step by step, once that request goes through the shared proxy rather than talking to the connector directly:

STEP 1
The Cloud Foundry app calls the shared proxy over a normal internal HTTPS request — no certs or SOCKS5 details required on its side.
STEP 2
The proxy resolves which client certificate this caller is entitled to use, and pulls it from the internal credential store just for this request.
STEP 3
The proxy opens a SOCKS5 connection through the connector and negotiates a fresh mTLS handshake with the on-prem origin, using that certificate directly.
STEP 4
The connector forwards the encrypted bytes untouched — it's carrying a tunnel, not parsing HTTP or handling TLS.
STEP 5
The on-prem service sees a valid client certificate from an allow-listed IP, same as any other legitimate caller, and responds normally.

From a one-off fix to a shared platform capability

The tunnel solved the immediate problem, but it wasn’t the last app that needed on-prem access — it was one of many. Left alone, every team would rebuild the same SOCKS5 setup, and worse, every team would need direct access to sensitive client certificates.

Instead, we centralized it into a single internal proxy service:

  • Certificate custody stays in one place. The proxy service is the only thing that pulls client certificates out of the internal credential store at request time. Consuming applications never handle raw private keys.
  • One integration point for every team. Any app needing on-prem access points at the shared proxy instead of standing up its own tunnel, connector routing, and cert management.
  • Access is scoped and auditable. Because every on-prem call flows through one service, granting, rotating, or revoking a team’s access — and logging who called what — happens in one place instead of N places.
Before — every team builds its own tunnel
App A — own cert, own SOCKS5 setup → Connector
App B — own cert, own SOCKS5 setup → Connector
App C — own cert, own SOCKS5 setup → Connector
Three copies of the client certificate in the wild. Three tunnels to maintain and patch.
After — one shared proxy service
App A ↘
App B  →  Proxy service → Credential store + Connector
App C ↗
One place that ever sees a certificate. One tunnel implementation to maintain.

A simplified shape of that proxy service — the part every consuming app is spared from writing itself:

// Shared proxy service: one endpoint per on-prem service, fronted internally
app.post("/proxy/onprem/:service", async (req, res) => {
  // 1. Resolve which client certificate this caller is entitled to use
  const { cert, key } = await credentialStore.getCertificate(req.headers["x-app-id"]);

  // 2. Open a SOCKS5-tunneled, mTLS connection to the real on-prem origin
  const agent = new SocksMtlsAgent({
    socksProxy: "socks5://connector.internal:1080",
    cert,
    key,
    ca: onPremCaBundle,
  });

  // 3. Forward the original request through that tunnel and stream the response back
  const upstream = await fetch(`https://${req.params.service}.internal${req.path}`, {
    method: req.method,
    body: req.body,
    agent,
  });

  res.status(upstream.status).send(await upstream.text());
});
Without shared proxy With shared proxy
Client certificates Copied into every consuming app Held in one place only
Onboarding a new app Rebuild tunnel + cert handling Call one internal HTTP endpoint
Revoking access Chase down every app that has a copy Revoke in one service
Audit trail Scattered across N apps’ logs Centralized, one place to query

The general lesson

When a network boundary “solves” your reachability problem but silently drops something your protocol depended on (here, the client certificate), look one layer down the stack before working around it. Reverse proxies terminate; the fix was to stop asking for a reverse proxy and route raw, encrypted bytes instead. And once you’ve solved a hard cross-cutting problem like this once, the higher-leverage move is turning it into a service the rest of the org can consume — not a pattern every team has to rediscover.