January 2025 · Security · 2 min read

Secure Python Server-Client Communication with OpenSSL

If you want to actually understand how TLS works, the best way is to build it yourself. Not just read about it. Create your own Certificate Authority, sign your own certs, and write a server and client that talk over an encrypted channel.

This is useful for a few reasons. If you're learning about TLS for pentesting or security work, you need to understand the certificate chain from the ground up. If you're building tools that intercept or proxy HTTPS traffic, you need to know how CAs and cert signing actually work. And if you're ever troubleshooting certificate errors in production, this hands-on knowledge pays off fast.

Let's build the whole thing from scratch.

Install OpenSSL

You probably already have it. If not:

Create Your Certificate Authority

First, generate a private key for your CA. This is the root of trust for everything else.

openssl genpkey -algorithm RSA -out ca-key.pem -aes256

It'll ask for a passphrase. Pick something you'll remember for this lab.

Now create the CA certificate itself:

openssl req -x509 -new -nodes -key ca-key.pem -sha256 -days 365 -out ca-cert.pem

Fill in whatever you want for the prompts. This is your local CA, so the details don't matter.

Generate the Server Certificate

Create a private key for the server:

openssl genpkey -algorithm RSA -out server-key.pem

Generate a Certificate Signing Request. When it asks for Common Name, enter localhost since that's where our server will run:

openssl req -new -key server-key.pem -out server.csr

Now sign it with your CA:

openssl x509 -req -in server.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365 -sha256

You now have a full certificate chain: CA key, CA cert, server key, server cert. The same structure every HTTPS site on the internet uses.

The Python Server

Save this as server.py. It creates a TLS-wrapped socket that presents our signed certificate to any connecting client:

import ssl
import socket

def create_server():
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.load_cert_chain(certfile="server-cert.pem", keyfile="server-key.pem")

    bindsocket = socket.socket()
    bindsocket.bind(('localhost', 8443))
    bindsocket.listen(5)

    while True:
        print("Server is waiting for a connection...")
        newsocket, fromaddr = bindsocket.accept()
        conn = context.wrap_socket(newsocket, server_side=True)
        try:
            print("Connection established:", fromaddr)
            data = conn.recv(1024)
            print("Received data:", data.decode())
            conn.sendall(b"Hello, client!")
        finally:
            conn.shutdown(socket.SHUT_RDWR)
            conn.close()

if __name__ == "__main__":
    create_server()

The Python Client

Save this as client.py. The client loads our CA cert so it can verify the server's certificate is legit. We disable hostname checking here since this is a local lab:

import ssl
import socket

def create_client():
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    context.load_verify_locations(cafile="ca-cert.pem")
    context.check_hostname = False

    conn = context.wrap_socket(socket.socket(socket.AF_INET), server_hostname='localhost')
    conn.connect(('localhost', 8443))

    conn.sendall(b"Hello, server!")
    data = conn.recv(1024)
    print("Received data:", data.decode())

    conn.close()

if __name__ == "__main__":
    create_client()

Run It

Open two terminals. Start the server first:

python3 server.py

Then connect with the client:

python3 client.py

You should see the handshake happen and messages pass back and forth over an encrypted channel. No certificate errors, no warnings.

What's Actually Happening

The client connects and the server presents its certificate. The client checks that certificate against the CA cert it loaded. Since our CA signed the server cert, the chain of trust checks out and the TLS handshake completes. From there, all traffic is encrypted.

This is the exact same flow your browser uses when you visit any HTTPS site. The only difference is that browsers ship with a pre-installed list of trusted CAs, while our client loads ours manually.

Try breaking it on purpose. Remove the CA cert from the client and watch it fail. Change the Common Name and see what happens. That's where the real learning is.

← Back to all posts