🚀 Executive Summary
TL;DR: Legacy TCP applications struggle to connect with modern mTLS-secured backends due to protocol mismatch. This article details how to bridge this gap using open-source proxies like Ghostunnel, Envoy, and Stunnel, enabling plain TCP traffic to be wrapped in mTLS without modifying the original application code.
🎯 Key Takeaways
- The core problem is a protocol mismatch where legacy TCP apps cannot perform the mTLS handshake or provide client certificates.
- The ‘Ambassador Pattern’ using a sidecar or proxy on localhost is the recommended solution to wrap plain TCP traffic in mTLS.
- Ghostunnel is a lightweight, single-binary Go solution ideal for modern container sidecars due to its simplicity and fast failure.
- Envoy Proxy is an enterprise-grade solution for service meshes, offering complex routing, observability, and traffic management, but with a steep learning curve.
- Stunnel is a stable, ‘old reliable’ option suitable for legacy Linux systems and bare metal environments with simpler requirements.
Stuck trying to bridge a legacy TCP application with a strict mTLS backend? I break down the top open-source tools—Ghostunnel, Envoy, and Stunnel—to handle the handshake so you don’t have to rewrite your codebase.
Bridging the Gap: Connecting Plain TCP to mTLS Services
I still wake up in a cold sweat thinking about the “Great Migration of 2019.” We had this ancient Java monolith, billing-core-v1, that was allergic to anything resembling modern security protocols. The Security team decided—rightfully so—to lock down our internal Postgres cluster (prod-db-01) with strict Mutual TLS (mTLS). Suddenly, our billing app, which only spoke plain TCP, hit a brick wall. It was 2 AM, alerts were firing, and I was staring at “Connection Reset” logs wondering how to make a dinosaur talk to a spaceship.
If you are reading this, you’re probably in the same boat. You have a service that speaks TCP, and a destination that demands a client certificate. Here is how we fix it.
The Root Problem: The Protocol Mismatch
It’s not just that your app doesn’t have the certificate; it likely doesn’t even know how to perform the handshake. Your application expects to open a socket and send bytes. The server, however, is expecting a TLS Hello and a valid client certificate before it acknowledges existence.
You need a sidecar or a proxy. You need something to sit on localhost, accept the plain traffic from your app, wrap it in the mTLS handshake, and forward it to the secure destination. This is the “Ambassador Pattern” in a nutshell.
Pro Tip: Never, and I mean never, try to hack TLS support directly into legacy code if you can avoid it. You will introduce bugs. Offload the encryption to a battle-tested proxy.
Solution 1: The Modern “Scalpel” (Ghostunnel)
If you want a single binary with zero dependencies that does one thing perfectly, Ghostunnel is my go-to. It’s written in Go, it’s lightweight, and it’s built exactly for this scenario. I use this heavily in Kubernetes Pods as a sidecar container.
The Setup: You run Ghostunnel in “client mode”. It listens on a local port (e.g., 5432) and forwards to the remote mTLS port.
ghostunnel client \
--listen localhost:5432 \
--target prod-db-01.internal:5432 \
--keystore client-combined.pem \
--cacert ca-bundle.crt \
--unsafe-target-project-codes
Why I love it: It fails fast. If the certs are wrong, it won’t start. It also exposes Prometheus metrics out of the box, which saved my skin when tracking connection leaks.
Solution 2: The Enterprise “Swiss Army Knife” (Envoy Proxy)
If you are running a service mesh or need complex routing (like retry logic or circuit breaking) alongside the mTLS bridging, Envoy is the industry standard. It is overkill for a simple script, but for a production microservice architecture, it is the correct answer.
Here is a stripped-down envoy.yaml configuration to bridge TCP to mTLS. We use the tcp_proxy filter.
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 127.0.0.1, port_value: 9000 }
filter_chains:
- filters:
- name: envoy.filters.network.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
stat_prefix: destination_tcp
cluster: service_mtls
clusters:
- name: service_mtls
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service_mtls
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: prod-backend-secure, port_value: 443 }
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain: { filename: "/etc/certs/client.crt" }
private_key: { filename: "/etc/certs/client.key" }
The Reality Check: Envoy has a steep learning curve. I’ve spent entire weekends debugging YAML indentation here. Only use this if you need the observability or traffic management features.
Solution 3: The “Old Reliable” (Stunnel)
Sometimes, you are working on a legacy Linux box where you can’t just drop a Go binary or install Envoy. You are stuck with what’s in the repo. That is where Stunnel shines. It has been around since the 90s, and it just works.
It’s not sexy, but it’s stable. Here is a config I used recently for an old payment gateway integration:
; /etc/stunnel/stunnel.conf
[mtls-bridge]
client = yes
accept = 127.0.0.1:8000
connect = secure-api.partner.com:443
cert = /etc/stunnel/client.pem
key = /etc/stunnel/client.key
CAfile = /etc/stunnel/ca-chain.pem
verify = 2
Warning: Stunnel process management can be tricky. Ensure you have a proper systemd unit file or supervisor process to restart it if it crashes, otherwise your app fails silently.
Comparison at a Glance
| Tool | Complexity | Best Use Case |
|---|---|---|
| Ghostunnel | Low | Modern containers, sidecars, “set and forget”. |
| Envoy | High | Service meshes, high scale, need metrics. |
| Stunnel | Medium | Legacy OS, bare metal, simple requirements. |
For that 2 AM incident with billing-core-v1? I went with Ghostunnel. It took 5 minutes to deploy, and we were back online. Choose the tool that fits your infrastructure, not just the one that looks coolest on a resume.
🤖 Frequently Asked Questions
âť“ What is the primary challenge when connecting a plain TCP application to an mTLS service?
The main challenge is that the plain TCP application doesn’t know how to perform the mTLS handshake or provide the required client certificate, while the mTLS server expects a TLS Hello and a valid certificate before establishing a connection.
âť“ How do Ghostunnel, Envoy, and Stunnel compare for bridging TCP to mTLS?
Ghostunnel is low complexity, best for modern containers and sidecars. Envoy is high complexity, suited for service meshes and advanced traffic management. Stunnel is medium complexity, ideal for legacy OS and bare metal with simple requirements.
âť“ What is a common implementation pitfall when using Stunnel for mTLS bridging?
A common pitfall with Stunnel is process management. It’s crucial to ensure a proper systemd unit file or supervisor process is in place to automatically restart Stunnel if it crashes, preventing silent application failures.
Leave a Reply