TLS/SSL Best Practices: Certificates, Ciphers, Configuration
AI generated
OWASP
0x00
Security · TLS/SSL · Certificates · Magento 2
TLS/SSL Best Practices: Certificates, Ciphers, Configuration
Encryption that actually protects, not just a padlock icon

A correctly configured TLS stack determines whether customer data, payment information, and sessions in a Magento store are actually protected or only appear to be. This article explains how the TLS handshake works, why outdated protocol versions and weak cipher suites are a real risk, how certificate chains are validated correctly, and how Let's Encrypt, HSTS, and hardened server configurations fit together.

14 min. read TLS 1.3 · Forward Secrecy Let's Encrypt · HSTS

1. Understanding the TLS handshake: how a secure connection is established

Every TLS connection begins with a handshake before a single byte of payload data flows. The client sends a ClientHello with supported protocol versions, cipher suites, and extensions such as SNI (Server Name Indication), so a server hosting multiple domains can select the right certificate. The server replies with a ServerHello, picks the protocol version and cipher suite, and presents its certificate. After the key exchange, both sides confirm with a Finished message containing a MAC over the entire handshake so far, so any tampering becomes detectable. Only after that does the record layer start encrypting payload data with a symmetric session key derived from the handshake.

The difference between TLS 1.2 and TLS 1.3 shows up right here: TLS 1.2 needs two full round trips because cipher suite negotiation and key exchange are separate steps. TLS 1.3 sends the key share directly in the ClientHello and completes in a single round trip; on top of that, 0-RTT resumption enables an even faster start for returning connections. For Magento checkout flows with many individual HTTPS connections, this difference is noticeable, though 0-RTT carries a replay risk for non-idempotent requests such as a payment selection and should stay disabled for those.

2. Disabling TLS 1.0/1.1: TLS 1.2 vs. TLS 1.3

TLS 1.0 dates back to 1999 and shares structural weaknesses with the even older SSLv3: the POODLE attack exploits a downgrade to CBC padding, BEAST breaks CBC encryption through predictable initialization vectors, and the handshake itself permits weak hash functions like MD5 and SHA-1. TLS 1.1 fixes only some of these problems and has been considered equally outdated for years. The PCI Security Standards Council has required TLS 1.2 as a minimum since June 30, 2018 for any system processing credit card data, and major payment gateways such as Adyen or Braintree stopped accepting TLS 1.0/1.1 connections long ago.

TLS 1.2 still matters in practice because a small number of legacy clients, such as older Android versions or embedded IoT devices, don't support TLS 1.3. TLS 1.3 itself (RFC 8446, finalized in 2018) removes static RSA key exchange entirely from the specification, thereby enforcing forward secrecy for every connection, bans insecure legacy ciphers like RC4 and 3DES outright, and encrypts larger portions of the handshake itself, for example the certificate is only transmitted after the key agreement completes. A Magento store should offer TLS 1.2 and TLS 1.3 simultaneously and disable everything below that consistently.

3. Choosing the right cipher suites: forward secrecy with ECDHE

A cipher suite defines how key exchange, authentication, symmetric encryption, and integrity checking are combined. With static RSA key exchange (for example TLS_RSA_WITH_AES_128_CBC_SHA), the session key derives directly from the server's long-term private RSA key. If an attacker records encrypted traffic and later obtains the private key through a data breach, all previously recorded sessions can be decrypted retroactively. This exact scenario is one of the reasons forward secrecy is now considered mandatory.

ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) generates a new, temporary key for every single connection, which is discarded once the session ends. A private key compromised later therefore only protects future sessions, never past ones, which is the core property of forward secrecy. Recommended cipher suites combine ECDHE with AEAD encryption modes like AES-GCM or ChaCha20-Poly1305 instead of CBC with a separate HMAC, since CBC constructions were vulnerable to timing attacks like Lucky13. ChaCha20-Poly1305 is also noticeably faster than AES-GCM on mobile devices without AES-NI hardware acceleration.

4. Validating certificate chains: the missing intermediate problem

A TLS certificate is never valid in isolation, it's part of a chain: a root CA permanently stored in the operating system's or browser's trust store, one or more intermediate certificates, and the actual leaf certificate for the specific domain. Clients trust only the preinstalled root certificates directly. For a client to trace the chain up to that trusted root, the server must deliver every intermediate certificate along with the leaf certificate; the root itself is never sent.

The most common configuration mistake in practice: only the leaf certificate is installed on the server, the intermediates are missing entirely. Modern Chrome versions often mask this through AIA fetching, automatically retrieving missing intermediates via the Authority Information Access URL embedded in the certificate. Older browsers, mobile apps, many payment SDKs, server-to-server curl calls, and monitoring tools lack this fallback, though, and fail with an error like unable to get local issuer certificate. The result is a store that works flawlessly in your own browser but fails for a meaningful share of real users and for server-to-server integrations such as payment callbacks.


# Show the full certificate chain a server actually presents
openssl s_client -connect mironsoft.de:443 -servername mironsoft.de -showcerts </dev/null 2>/dev/null

# Print the leaf certificate details: issuer, validity period, SAN entries
openssl s_client -connect mironsoft.de:443 -servername mironsoft.de </dev/null 2>/dev/null \
  | openssl x509 -noout -text

# Verify the chain against the local system trust store
openssl s_client -connect mironsoft.de:443 -servername mironsoft.de -CApath /etc/ssl/certs </dev/null 2>&1 \
  | grep -E "Verify return code|depth"

# Typical output when an intermediate certificate is missing on the server:
# depth=0 CN = mironsoft.de
# verify error:num=20:unable to get local issuer certificate
# Verify return code: 21 (unable to verify the first certificate)

5. Let's Encrypt and ACME automation with certbot

Let's Encrypt issues free, automatically generated certificates via the ACME protocol, and certbot is the most widely used ACME client. The HTTP-01 challenge proves domain control through a file under /.well-known/acme-challenge/, while the DNS-01 challenge uses a TXT record in DNS and is required for wildcard certificates such as *.mironsoft.de, since HTTP-01 can only validate individual, directly reachable hostnames.

Let's Encrypt certificates are deliberately valid for only 90 days, which forces automation instead of manual renewal, something that in practice gets forgotten on a regular basis and leads to expired certificates. certbot renew automatically checks all installed certificates and only renews the ones expiring within the next 30 days. A systemd timer or cron job running twice a day is best practice here, since it leaves enough buffer for transient failures like DNS outages. A deploy-hook reloads the webserver without downtime as soon as a certificate has actually been renewed, rather than restarting unnecessarily on every cron run.


# Install certbot with the nginx plugin (Debian/Ubuntu)
apt-get install -y certbot python3-certbot-nginx

# Issue a certificate and let certbot configure the nginx vhost automatically
certbot --nginx -d mironsoft.de -d www.mironsoft.de \
  --agree-tos -m ops@mironsoft.de --redirect

# Dry run to verify the renewal flow without hitting Let's Encrypt rate limits
certbot renew --dry-run

# /etc/cron.d/certbot-renew: run twice daily, certbot silently skips
# certificates outside the 30 day renewal window on its own
0 3,15 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"

# Deploy hook: reloads nginx without downtime, only runs after
# certbot has actually renewed a certificate
#!/usr/bin/env bash
set -euo pipefail
nginx -t && systemctl reload nginx

6. Configuring HSTS: the preload list and includeSubDomains

A correct TLS setup alone doesn't protect the very first request made over plain HTTP, for instance when a user types the domain without https://. The Strict-Transport-Security header tells the browser to automatically redirect every future request to that domain to HTTPS for the configured max-age duration, without a server roundtrip. This closes SSL-stripping attacks in a man-in-the-middle scenario, where an attacker intercepts the initial HTTP request before the redirect to HTTPS can even take effect.

The includeSubDomains directive enforces HTTPS on all subdomains as well and needs careful review, since a single subdomain without a valid certificate becomes completely unreachable for users as a result. The HSTS preload list goes a step further: domains get hardcoded directly into Chrome, Firefox, and Safari, so even the very first request to that domain never travels unencrypted. Submission via hstspreload.org is effectively irreversible for many months, since browser updates take time to roll out, and should therefore only happen once HTTPS has demonstrably run stable across all subdomains.


<?php

declare(strict_types=1);

namespace Mironsoft\Security\Plugin;

use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\App\Response\Http as HttpResponse;

/**
 * Plugin that ensures every HTTPS response carries a correctly configured
 * Strict-Transport-Security header, independent of the webserver configuration.
 */
class AddStrictTransportSecurityHeaderPlugin
{
    /**
     * @param HttpRequest $request Current HTTP request, used to detect a secure connection.
     * @param int $maxAge Header max-age value in seconds, defaults to two years.
     * @param bool $includeSubDomains Whether the includeSubDomains directive is appended.
     * @param bool $preload Whether the preload directive is appended.
     */
    public function __construct(
        private readonly HttpRequest $request,
        private readonly int $maxAge = 63072000,
        private readonly bool $includeSubDomains = true,
        private readonly bool $preload = true,
    ) {
    }

    /**
     * Adds the Strict-Transport-Security header before the response is sent.
     *
     * @param HttpResponse $subject The response object about to be dispatched.
     * @return void
     */
    public function beforeSendResponse(HttpResponse $subject): void
    {
        if ($this->request->isSecure()) {
            $subject->setHeader('Strict-Transport-Security', $this->buildHeaderValue(), true);
        }
    }

    /**
     * Builds the header value from the configured directives.
     *
     * @return string The assembled Strict-Transport-Security header value.
     */
    private function buildHeaderValue(): string
    {
        $directives = ['max-age=' . $this->maxAge];

        if ($this->includeSubDomains) {
            $directives[] = 'includeSubDomains';
        }

        if ($this->preload) {
            $directives[] = 'preload';
        }

        return implode('; ', $directives);
    }
}

7. Hardening TLS in practice: nginx and Apache

A hardened nginx configuration restricts ssl_protocols to TLSv1.2 and TLSv1.3, defines an explicit ssl_ciphers list with ECDHE and AEAD cipher suites in a sensible order, and enables OCSP stapling. With stapling, the server fetches its own certificate's revocation status from the CA server ahead of time and includes it directly in the handshake, instead of letting every single client query the CA server on its own. This reduces handshake latency and additionally prevents the CA from tracking individual users' browsing behavior.

Session resumption via TLS session tickets significantly speeds up returning connections, but the underlying ticket encryption keys should be rotated regularly, so a compromised key doesn't permanently make old sessions decryptable. For containerized setups, Traefik or nginx-proxy handle TLS termination and ACME automation together, saving a separate certbot installation. The Mozilla SSL Configuration Generator provides current, tested configuration templates for nginx, Apache, and other servers by compatibility level, instead of copying cipher lists from outdated blog posts that frequently contain ciphers considered insecure by now.


version: "3.9"
services:
  reverse-proxy:
    image: traefik:v3.1
    command:
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      # Redirect all plain HTTP traffic to HTTPS
      - --entrypoints.web.http.redirections.entryPoint.to=websecure
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      # Apply the minimum TLS version defined in dynamic_conf.yml
      - --entrypoints.websecure.http.tls.options=default
      - --providers.file.filename=/etc/traefik/dynamic_conf.yml
      - --certificatesresolvers.letsencrypt.acme.email=ops@mironsoft.de
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "letsencrypt:/letsencrypt"
      - "./dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  magento:
    image: mironsoft/magento-web:2.4.8-p4
    labels:
      - traefik.enable=true
      - traefik.http.routers.magento.rule=Host(`mironsoft.de`)
      - traefik.http.routers.magento.entrypoints=websecure
      - traefik.http.routers.magento.tls.certresolver=letsencrypt

volumes:
  letsencrypt:

# dynamic_conf.yml: forces TLS 1.2 as the minimum negotiated version
# tls:
#   options:
#     default:
#       minVersion: VersionTLS12
#       cipherSuites:
#         - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
#         - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name mironsoft.de www.mironsoft.de;

    ssl_certificate     /etc/letsencrypt/live/mironsoft.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mironsoft.de/privkey.pem;

    # Only TLS 1.2 and TLS 1.3, no legacy protocol versions
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # OCSP stapling: the server fetches revocation status once and serves it to every client
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/mironsoft.de/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    # Session resumption without weakening forward secrecy guarantees
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://magento_upstream;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name mironsoft.de www.mironsoft.de;
    return 301 https://$host$request_uri;
}

8. Testing and monitoring: SSL Labs and testssl.sh

The Qualys SSL Labs Test (ssllabs.com/ssltest) checks protocol versions, cipher suites, the certificate chain, forward secrecy, and known vulnerabilities like Heartbleed or POODLE against a publicly reachable domain and assigns a grade from A+ to F. It's the industry standard for a quick, independent external assessment and works well for verifying TLS configuration changes before going live.

testssl.sh is the right complement for CI/CD pipelines and internal systems without public reachability, since it runs locally or directly in the pipeline and performs the same checks as a command-line tool with machine-readable output. Continuous monitoring stays important because best practices shift over time: browser vendors coordinated the removal of TLS 1.0/1.1 support in 2020 and 2021, and new vulnerabilities in cipher implementations surface regularly. Certificate expiry should additionally be monitored through independent tooling, such as expiry checks via a cron job or an uptime tool, separate from the certbot automation itself, so a silent failure in the renewal pipeline doesn't only surface once the certificate has already expired.

9. Insecure vs. secure compared side by side

The biggest TLS risks in practice rarely come from exotic attacks, but from a handful of recurring configuration mistakes. The table below contrasts the insecure variant with the recommended, secure configuration.

Area Insecure Recommended
Protocol version TLS 1.0 / TLS 1.1 enabled TLS 1.2 and TLS 1.3 only
Key exchange Static RSA, no forward secrecy ECDHE with forward secrecy
Cipher mode RC4, 3DES, CBC with a separate HMAC AES-GCM or ChaCha20-Poly1305
Certificate chain Leaf certificate only, intermediates missing Full chain including intermediates
Renewal Manual, regularly forgotten Automated via ACME/certbot
HSTS No header, SSL stripping possible max-age, includeSubDomains, preload

These six points are closely connected in practice: a store with correctly configured protocol versions and cipher suites but missing intermediate certificates still counts as broken by automated scans. Securing all six areas together and verifying regularly via SSL Labs or testssl.sh results in a TLS configuration that works reliably not just in your own browser, but for every real client.

Mironsoft

TLS hardening, certificate management, and security audits for Magento stores

Ready to lock down your TLS configuration?

We review your Magento store's TLS configuration, remove outdated protocols and weak cipher suites, and automate certificate management and HSTS cleanly across your entire infrastructure.

TLS security audit

SSL Labs and testssl.sh analysis, prioritized by risk

Certificate automation

Let's Encrypt/ACME setup with a reliable renewal pipeline

Hardening & monitoring

nginx/Apache hardening, HSTS preloading, and continuous monitoring

10. Summary

TLS/SSL best practices address one core problem: an encrypted connection alone doesn't guarantee security when protocol version, cipher suite, certificate chain, and companion headers like HSTS don't work together cleanly. TLS 1.0 and TLS 1.1 need to be disabled consistently, with TLS 1.2 as the minimum and TLS 1.3 as the preferred version forming the baseline. ECDHE cipher suites with AEAD encryption deliver forward secrecy, a complete certificate chain including all intermediates prevents failures for clients without AIA fetching, and automated renewal via certbot or an ACME-capable reverse proxy like Traefik eliminates the risk of expired certificates.

The decisive difference between a TLS configuration that actually works in practice and one that only looks good in your own browser lies in consistently implementing all six areas together and continuously verifying them via SSL Labs and testssl.sh. HSTS with includeSubDomains and, once operations are stable, submission to the preload list closes the last gap: the very first, potentially unencrypted request, before the TLS configuration can even take effect.

TLS/SSL Best Practices, the Essentials at a Glance

TLS 1.2/1.3 only

Disable TLS 1.0 and TLS 1.1 consistently, PCI DSS has required TLS 1.2 as a minimum since 2018.

ECDHE instead of static RSA

Combine forward secrecy with AEAD ciphers like AES-GCM or ChaCha20-Poly1305.

Complete certificate chain

Serve all intermediate certificates, verify with openssl s_client -showcerts.

Automation & HSTS

certbot/ACME for renewal, HSTS with includeSubDomains and preload for the first request.

11. FAQ: TLS/SSL Best Practices

1What is the difference between TLS and SSL?
SSL is the historical, insecure predecessor. TLS is the current successor; colloquially "SSL" today practically always means TLS 1.2 or TLS 1.3.
2Why must TLS 1.0 and TLS 1.1 be disabled?
Known attacks like POODLE and BEAST, weak hash functions in the handshake, no longer PCI DSS compliant since 2018. Browser vendors removed support in 2020/2021.
3What is forward secrecy and why does ECDHE matter?
A key compromised later cannot decrypt past sessions. ECDHE generates a temporary key per connection and delivers exactly this property, static RSA does not.
4What is a certificate chain and why are intermediate certificates often missing?
Root CA, intermediates, and leaf certificate form the chain. Servers often serve only the leaf because Chrome fetches missing intermediates via AIA fetching automatically, other clients then fail.
5How does automatic certificate renewal work with Let's Encrypt?
certbot renew periodically checks all certificates and renews only those expiring within 30 days. A cron job plus a deploy-hook that reloads the webserver makes it maintenance-free.
6What exactly does the HSTS header do?
Tells the browser to automatically redirect future requests to HTTPS, without sending the insecure HTTP request. Prevents SSL-stripping attacks.
7What is the HSTS preload list and when should I use it?
A domain list hardcoded into browsers, protects even the first request. Only use once HTTPS is stable across all subdomains, removal takes months.
8How do I reliably test my TLS configuration?
Qualys SSL Labs Test for publicly reachable domains (grade A+ to F), testssl.sh as a command-line tool for CI/CD pipelines and internal systems.
9What is OCSP stapling and why does it improve performance and privacy?
The server fetches the revocation status ahead of time and includes it directly in the handshake, saving every client a separate CA query and preventing tracking by the CA.
10Is a valid certificate alone enough for a secure connection?
No. Outdated protocols, weak ciphers without forward secrecy, incomplete certificate chains, or missing HSTS can still leave a valid connection exploitable.