This guide describes how to harden Transport Layer Security (TLS) connections configured with the ssl application. It covers cryptographic algorithm selection, protocol version configuration, certificate verification, and revocation checking.
Note
There can exist trade-offs between interoperability and security, and between resource consumption and security. If you make such trade-offs it is important that you make an informed decision and understand the consequences. Also this guide will always reflect the status of defaults for the release where it is published, unless otherwise specifically stated. Hence using older releases may need more configuration to achieve the same result.
Erlang's TLS implementation provided by the ssl application strives to
have secure defaults. That said, we will avoid changing defaults in
non-major releases.
For embedded systems with constrained bandwidth, cipher suites using Advanced Encryption Standard (AES) with counter cipher block chaining message authentication code, that is AES-CCM-8 (8-byte authentication tag) can be configured and provides 64-bit integrity instead of 128-bit.
Warning
Be aware that enabling anything documented as legacy will make your application less secure.
Warning
Network Security Services Key Log Format (NSS Key Logging) is a debugging functionality that should not be enabled in production systems as it defeats the purpose of the TLS protocol.
Cryptographic Algorithms
The available cryptographic algorithms depend on the version of OpenSSL cryptolib that Erlang/OTP is built and linked with.
When you configure algorithm options such as
supported_groups, or
signature_algs, the ssl application
automatically removes any algorithms not supported by the underlying
OpenSSL cryptolib. The exception is the
ciphers option: user-supplied cipher
suites are not automatically filtered against crypto support, this is
due to the more complex composition of algorithms in cipher suites
pre TLS-1.3. In this case use ssl:filter_cipher_suites/2
to remove suites that are not supported by linked OpenSSL cryptolib.
Note
The TLS protocol is implemented in Erlang, and therefore only the cryptographic
functions come from the OpenSSL cryptolib. This reduces the error
surface by excluding buffer overflows and other pointer related errors
that does not exist in Erlang programs.
Algorithms selection
The TLS protocol typically specifies that the client's algorithm
preference determines the algorithm selection. The server option
honor_cipher_order overrides this for
cipher suite selection in all TLS versions. For TLS-1.2 and legacy
versions, honor_ecc_order
does the same for ECC curve selection.
Signature Algorithms
The signature_algs option configures
which signature algorithms are acceptable for TLS protocol messages.
The signature_algs_cert, option
separately configures acceptable algorithms for certificate chain
signatures.
If signature_algs is specified but not
signature_algs_cert the value of
signature_algs will implicitly be used
for signature_algs_cert; however if none
of them are explicitly specified certificate signatures are verified
against default signature_algs (with
additional legacy SHA-1 algorithms — see note below).
Note
Currently using the default
signature_algs will add
signature_algs_cert that allows
signature algorithms using SHA-1 as pseudo random function for
certificate signatures. This is not allowed by default for protocol
signatures. It is expected to be removed after 2030, when such
certificates should have been phased out.
Post Quantum Algorithms
Post-Quantum Cryptography (PQC) algorithms are designed to resist attacks from future cryptographically relevant quantum computers (CRQCs), which are expected to break algorithms such as RSA and ECC. PQC is only available in TLS-1.3.
Note
Even though CRQCs do not yet exist, enabling PQC protects against "harvest now, decrypt later" attacks where an adversary records encrypted traffic today for future decryption.
PQC algorithms generally require more computation and network bandwidth than classical algorithms. Key sizes, signatures, and ciphertexts are larger.
For key exchange, several ML-KEM hybrid groups are supported. By default, only x25519mlkem768 is offered — a hybrid that combines classical X25519 with ML-KEM-768 to balance current security with future quantum resistance, as the newer algorithms are less battle-tested.
For authentication, the signature algorithms ML-DSA and SLH-DSA are supported. ML-DSA is preferred due to faster signing and smaller signatures. Using PQC signature algorithms requires appropriate PQC certificates and keys.
Privacy and Integrity
The protocol version TLS-1.3 is a major upgrade of the protocol over previous versions, and supporting both TLS-1.3 and TLS-1.2 will require a configuration including options from both.
TLS 1.3 significantly improves session handling over TLS 1.2 by reducing the handshake from two round-trips (2-RTT) to one (1-RTT). It mandates forward secrecy, encrypts more of the handshake, and introduces 0-RTT resumption for previously visited sites.
The protocol itself will prevent version downgrade when using TLS-1.3 and TLS-1.2, which are the default supported versions. All other TLS versions still configurable are legacy versions. Datagram transport layer security, DTLS-1.2 over User Datagram Protocol (UDP) can also be configured and has functionality similar to TLS-1.2 but will always be less reliable and may lose application data just like plain UDP communication. DTLS over other transports can possibly be achieved but not out of the box. DTLS-1.3 is currently not supported.
Cipher Suites
TLS-1.3 does not share any cipher suites with previous protocol versions.
You will need to include at least one cipher suite for both
TLS-1.3 and TLS-1.2 to be able to support both versions in the
ciphers option.
TLS 1.3 uses only modern Authenticated Encryption with Associated Data (AEAD) cipher suites, so does the ssl application's TLS-1.2 default.
Session Key Renewal
Renegotiation options only apply to versions prior to TLS-1.3. TLS-1.3 replaces renegotiation with the key update mechanism, which only handles session key rotation and does not allow renegotiating cipher suites or certificates mid-connection.
A server can disable client-initiated renegotiation with the
client_renegotiation option to
avoid possible DoS-attacks.
By default, the server mitigates renegotiation abuse by enforcing a 12-second delay between client initiated renegotiations.
Key Exchange Groups
TLS-1.3 decouples key exchange algorithms from cipher suites. The key
exchange algorithms are configured using the
supported_groups option. In TLS-1.2
in addition to the cipher suites the options
eccs,
dh,
dhfile, are available for
configuring the key exchange.
TLS 1.3 removes static RSA, supporting only ephemeral Diffie-Hellman, which provides mandatory Perfect Forward Secrecy (PFS), so does the ssl application's TLS-1.2 default.
Pre-Shared Keys
Warning
Do not use the same Pre-Shared Key (PSK) across both TLS 1.2 and TLS 1.3, as it is considered unsafe.
In TLS-1.2, PSK is supported by PSK cipher suites, which generally lack forward secrecy unless combined with DHE or ECDHE key exchange. In TLS-1.3, PSK are most commonly used together with a key exchange algorithm, providing forward secrecy, although a PSK only option still exists.
Early Data
Early data (0-RTT) in TLS-1.3 is not forward secret and is
vulnerable to replay attacks. The server option
early_data is disabled by default.
Warning
Only enable early data if your application can safely handle replayed requests (that is for instance, idempotent HTTP methods).
See Early Data in TLS-1.3 for mitigation strategies and examples.
Authenticity
Certificate verification is essential for establishing trust in TLS
connections. The ssl application provides several mechanisms for
customizing and strengthening certificate validation.
Certificate and Keys
Use the certs_keys option to be able
to configure more than one possible certificate for the client or
server. The best one compatible with the peer will be chosen.
Verify Function
The verify_fun option lets you
customize certificate path validation for both TLS clients and
servers. Use it to add application-specific checks beyond the
standard chain verification.
Warning
However, the verify_fun can also be
used to accept certain errors. Doing so is at your own risk and will
weaken the security properties of your application. Legacy client
verify option set to the value
verify_none skips all certificate verification errors using
a special verify_fun
and should only be used for testing and debugging purposes.
Note
On the server side, verify_none is default as client certification
is an optional feature of the protocol and it needs configuring. When
client certification is configured, setting
verify option to verify_peer, the
server legacy option
fail_if_no_peer_cert defaults to
true.
To summarize:
- Always use
verify_peeron the client side (default since OTP 26) - Avoid accepting
{bad_cert, _}errors inverify_fununless there is some edge-case workaround that you have confidence in excluding. - When enabling client certification on the server, ensure
fail_if_no_peer_certis true (default since OTP 26).
Certificate Revocation Verification
Certificate revocation verification is important for security and
needs to be performed. However it requires some application specific
knowledge to set it up correctly, hence the option
crl_check default to
false. Use true to verify the entire certificate chain, or peer to
verify only the peer certificate.
Warning
The best_effort value is legacy and insecure — it silently
accepts certificates when revocation status cannot be determined.
The built-in Certificate Revocation List (CRL) cache can
be pre-populated via ssl_crl_cache:insert/1 or configured to
fetch CRLs from HTTP distribution points in certificates.
For custom CRL sources, implement the
ssl_crl_cache_api behaviour.
The client can request Online Certificate Status Protocol (OCSP)
stapling from the server with the
stapling option (disabled by
default). In TLS-1.2, only end-entity certificate stapling is
supported as opposed to TLS-1.3. If the server does not provide a
staple, the connection by default fails with {bad_cert, missing_ocsp_staple}.
A custom verify_fun can intercept
this to implement fallback revocation checking (that is, direct CRL
fetch).
Warning
Accepting {bad_cert, missing_ocsp_staple} without performing
alternative revocation checking is insecure, as it allows a MITM
attacker to suppress revocation information by omitting the staple.
Note
The ssl application does not perform direct OCSP queries (client
contacting the OCSP responder via the certificate's Authority
Information Access (AIA) extension), only OCSP stapling
(server-provided responses in the TLS handshake) is supported to be
verified by the client. The Erlang server has no stapling support.
Also, OCSP support seems to be on the decline, for instance
Let's Encrypt has dropped it.