Encrypting TCP Traffic Without Changing Your App using "stunnel"
- Avinash Ghadshi
- Jan 12
- 5 min read
In modern systems, security is no longer optional. Many legacy or lightweight applications still communicate over plain TCP without encryption. This is where stunnel becomes extremely useful.
This article explains:
What stunnel is
How it works internally
Two common real-world architectures with clear examples
Common mistakes to avoid
What is stunnel?
stunnel is a lightweight proxy that adds TLS/SSL encryption to TCP-based applications.
It acts as a secure tunnel between a client and a server, encrypting data on one side and decrypting it on the other. The applications themselves do not need to understand TLS.
Key points:
stunnel works at the transport layer (TCP)
It does not understand application protocols (MySQL, Redis, ClickHouse, SMTP, etc.)
It only handles TLS + TCP
In simple terms:
stunnel allows you to run unencrypted applications securely over an encrypted channel.
Most commonly used stunnel options
client
Determines which side of the TLS handshake stunnel participates in.
client = yes → stunnel initiates a TLS connection
client = no → stunnel accepts a TLS connection
TLS is directional. One side must act as the client and one as the server. stunnel does not auto-detect this.
accept
Defines where stunnel listens locally for incoming TCP connections.
Can bind to:
A specific IP (127.0.0.1)
All interfaces (0.0.0.0)
Port must be free and accessible
This is the port your application connects to, not the backend. Binding to 127.0.0.1 is safer for local-only usage.
connect
Specifies the remote destination where stunnel forwards traffic to. stunnel is a TCP forwarder, not a load balancer. One service → one destination.
connect can point to:
Local service
Remote server
TLS or non-TLS endpoint (depends on client mode)
cert
Specifies the public certificate used during TLS handshake. Always include the full chain if clients don’t trust intermediates.
Mandatory when stunnel:
Accepts TLS (client = no)
Performs mutual TLS (mTLS)
Acceptable file format
PEM encoded
Can include certificate chain
key
Specifies the private key corresponding to the certificate. If omitted, stunnel assumes the key is inside the cert file.
Key file Restrict permissions should be: chmod 600 server.key
CAfile
Defines the trusted Certificate Authority bundle used for verification. Without this, TLS encryption works but identity verification does not. It is recommended to use cert & key OR CAfile. Not both at same time.
It is used for
Verifying remote server certificates
Verifying client certificates (mTLS)
CApath
Specifies a directory containing multiple trusted CA certificates. It is used in large environments or if there are system-wide CA trust stores presents.
Key Difference:
CAfile: single file
CApath: directory of hashed certs
verifyChain
Enforces full certificate chain validation. It prevents man-in-the-middle attacks. Never disable in production environment. It ensures certificate is signed by a trusted CA & rejects self-signed or incomplete chains
checkHost
Validates the hostname against the certificate’s CN or SAN. Even a valid certificate is useless if it belongs to a different host.
If connecting to: connect = db.example.com:443
You must set:
checkHost = db.example.com
sslVersion
Restricts which TLS protocol versions stunnel will use. It is not mandatory but recommended to use because older protocols are vulnerable.
Recommended values
TLSv1.2 (widely supported)
TLSv1.3 (modern, faster)
Avoid TLSv1.0 and TLSv1.1 entirely.
ciphers
Controls which encryption algorithms are allowed. Weak ciphers reduce encryption strength even if TLS is enabled.
Recommended cipher
ciphers = HIGH:!aNULL:!MD5:!RC4
foreground
Controls whether stunnel runs as a daemon or in foreground. It should be no in production.
Use cases
yes: Debugging, containers
no: Traditional servers
debug and output
Controls logging level and log destination.
debug = 7
output = /var/log/stunnel.log
Acceptable debug values
Level | Description |
0 | Errors only |
5 | Normal |
7 | Very verbose |
pid
Writes the process ID to a file. it is required by init systems. it helps monitoring and restarts
Example
pid = /var/run/stunnel.pid
TIMEOUTclose
Controls how long stunnel waits before closing connections. It avoids broken pipes and hanging sockets in long-lived connections. It prevents half-closed TCP connections. 0 disables timeout
This is all about commonly used options in stunnel configuration. Now, Let's understand the stunnel by taking couple of practical scenarios.
Scenario 1: Client -> stunnel -> stunnel -> Plain Server

Use case
The backend server does not support TLS
You want encrypted traffic over the network
TLS should terminate close to the server
Typical examples:
Legacy Redis deployments
Legacy MySQL deployments
Custom internal TCP services
Architecture

How it works
The client establishes a TLS connection to the first stunnel.
This stunnel decrypts the traffic.
Data is transmitted securely between the two stunnel instances.
The second stunnel decrypts the data.
The backend server receives plain TCP traffic.
The server never sees TLS at all.
Example: Securing Redis without native SSL
Client-side stunnel
client = yes
accept = 127.0.0.1:3360
connect = mysql-server.example.com:3360
cert = server.pem
key = server.keyServer-side stunnel
client = no
accept = 0.0.0.0:3360
connect = mysql-server.example.com:3306
CAfile = ca.pem
cert = client.pem
key = client.keyClient connection
mysql -h 127.0.0.1 -p 3360Key takeaway
TLS exists only between the two stunnel instances
Backend remains unchanged and simple
Scenario 2: Client -> stunnel -> Secure Server

Use case
The backend server already supports TLS
The client cannot or should not manage TLS
stunnel acts as a TLS client proxy
Common examples:
ClickHouse HTTPS port
PostgreSQL with SSL
Secure APIs
SMTP over SSL
Architecture

The client sends plain TCP traffic to stunnel.
stunnel initiates a TLS handshake with the server.
Traffic is encrypted before reaching the server.
Responses are decrypted and sent back to the client.
Only one TLS layer exists in this design.
Example: ClickHouse with SSL
ClickHouse server configuration
<!-- <https_port>9000</https_port> -->
<https_port>9440</https_port>stunnel configuration
client = yes
accept = 127.0.0.1:9000
connect = clickhouse.example.com:9440
CAfile = ca.pem
verifyChain = yes
checkHost = clickhouse.example.comClient connection
clickhouse-client --host 127.0.0.1 --port 9000(No --secure flag is needed because stunnel handles TLS.)
Key takeaway
TLS is handled by stunnel
Client remains simple and unmodified
Common mistake: Double TLS
Do not enable TLS in both stunnel and the client/server

This leads to errors such as:
packet length too long
record layer failure
Why?
TLS traffic wrapped inside another TLS layer appears as corrupted data.
Summary
Architecture | TLS Termination | Recommended |
Client -> stunnel -> stunnel -> Plain server | Between stunnels | ✅ Yes |
Client -> stunnel -> TLS server | stunnel <--> server | ✅ Yes |
Client -> TLS server directly | Client <--> server | ✅ Yes |
TLS everywhere | Multiple layers | ❌ No |
Final Takeaways,
stunnel is a powerful and simple solution for securing TCP traffic when applications cannot handle TLS themselves. The key principle to remember is: Only one component should terminate TLS.
When used correctly, stunnel provides strong encryption, flexibility, and minimal application changes.



Comments