Self-signed certificates aren’t just a security shortcut; they’re the bedrock upon which secure, internal-only communication can be built without relying on external Certificate Authorities.

Let’s see one in action. Imagine a simple web server (nginx) serving content over HTTPS, but using a certificate we generated ourselves.

server {
    listen 443 ssl;
    server_name internal.example.com;

    ssl_certificate /etc/nginx/ssl/internal.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/internal.example.com.key;

    location / {
        root /var/www/html;
        index index.html;
    }
}

When a client connects to https://internal.example.com, their browser or tool will receive the internal.example.com.crt file. Since this certificate wasn’t signed by a public CA that the client inherently trusts, the client will flag it as untrusted, leading to a security warning. The magic of self-signed certificates lies in how we tell that client to trust it anyway.

The core problem self-signed certificates solve is enabling encrypted communication (TLS/SSL) in environments where obtaining certificates from a public Certificate Authority (CA) is either impractical, too expensive, or unnecessary. This is common in development, testing, or internal-only networks where the risk of impersonation from the public internet is negligible. The system works by having one entity (the "issuer") create a digital certificate for another entity (the "subject"). This certificate contains the subject’s public key, identity information, and is cryptographically signed by the issuer’s private key. A client can then verify the certificate by checking the signature against the issuer’s public key. For a self-signed certificate, the issuer and the subject are the same.

To generate a self-signed certificate, we typically use openssl. Let’s create a certificate for localhost that’s valid for 365 days:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout private.key -out certificate.crt -subj "/CN=localhost"

Here’s what’s happening:

  • openssl req: Initiates a certificate signing request (CSR) operation.
  • -x509: Specifies that we want to output a self-signed certificate instead of a CSR.
  • -nodes: "No DES" – this means we don’t encrypt the private key, which is convenient for testing but highly discouraged for production.
  • -days 365: Sets the validity period to one year.
  • -newkey rsa:2048: Generates a new 2048-bit RSA private key.
  • -keyout private.key: Saves the generated private key to private.key.
  • -out certificate.crt: Saves the generated certificate to certificate.crt.
  • -subj "/CN=localhost": Sets the Subject field of the certificate. CN stands for Common Name, which is typically the hostname.

Once generated, certificate.crt contains the public key and identity, and private.key holds the corresponding private key. The certificate itself is a statement: "I, localhost, vouch for the public key contained within this document." Because it’s self-signed, there’s no external authority to verify this claim, which is why browsers will warn you.

To use this certificate, you need to tell your client applications (browsers, curl, custom scripts) to trust this specific certificate, or rather, the "issuer" of this certificate (which is itself). This is done by adding the certificate to the client’s trust store.

For curl, you can point it to the certificate file directly:

curl --cacert certificate.crt https://localhost:443

This tells curl to treat certificate.crt as a trusted Certificate Authority. If you’re configuring a web server like nginx or apache, you’d point ssl_client_certificate (or equivalent directive) to the self-signed certificate file to enable mutual TLS authentication, or simply use the certificate for server authentication as shown in the initial nginx example.

The common name (CN) in the certificate’s subject must match the hostname you’re using to access the service. If you try to access https://my-internal-server but the certificate’s CN is localhost, you’ll get a hostname mismatch error, even if you’ve trusted the certificate itself. You can specify multiple hostnames using Subject Alternative Names (SANs) within the certificate, which is a more robust approach:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout private.key -out certificate.crt \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,DNS:my-internal-server,IP:127.0.0.1"

The subjectAltName extension is crucial for modern TLS clients. It allows you to list all valid hostnames and IP addresses for which this certificate is valid. Without it, only the CN is checked, and many clients (especially newer browsers) will prioritize SANs and ignore the CN entirely if SANs are present.

When you configure a client to trust a self-signed certificate, you’re effectively adding that certificate to the client’s list of trusted root certificates. This is precisely what you do when installing a public CA’s root certificate. The client’s TLS/SSL library then uses this list to verify the certificate chain of any server it connects to. If the server’s certificate is signed by a CA in the trusted list, or if the server’s certificate is itself in the trusted list (as with self-signed certs), the connection is considered secure.

The next hurdle you’ll face is managing certificate expiry and distribution across a growing fleet of clients and servers.

Want structured learning?

Take the full Tls-ssl course →