Project

General

Profile

Actions

Secure HTTP

Module: mod_openssl mod_mbedtls mod_wolfssl mod_gnutls mod_nss

keywords: lighttpd, SSL, TLS

Description

  • lighttpd supports TLS/SSL using mod_openssl, mod_mbedtls, mod_wolfssl, mod_gnutls, or mod_nss.

Brief history

  • openssl support was historically built into the core of lighttpd 1.4.x, but is now a standalone module (mod_openssl) since lighttpd 1.4.46.
  • mod_openssl may also be built to use the openssl-compatibility layers provided by BoringSSL or LibreSSL.
  • Support for mbedTLS (mod_mbedtls), wolfSSL (mod_wolfssl), GnuTLS (mod_gnutls) is available since lighttpd 1.4.56.
  • For TLS module alternatives to openssl, many ssl.* options are mapped from openssl to equivalents in other TLS libraries, including ssl.openssl.ssl-conf-cmd, but not completely so, and there is no plan to fully reimplement openssl SSL_CONF_cmd() (ssl.openssl.ssl-conf-cmd).
  • Experimental support for NSS (mod_nss) is available since lighttpd 1.4.56, but is not recommended due to NSS server-side limitations; NSS developers focus almost exclusively on the client-side.

Quick Start

  • While there are many configuration options, a minimal configuration requires simply:
    server.modules += ("mod_openssl")
    $SERVER["socket"] == ":443" {
        ssl.engine = "enable" 
        ssl.pemfile = "/path/to/fullchain.pem"  # public cert and intermediate cert chain, if any
        ssl.privkey = "/path/to/privkey.pem" 
    }
    
  • HowToSimpleSSL for other simple sample configs and Let's Encrypt support
  • Redirect HTTP requests to HTTPS
  • HTTP Strict Transport Security (HSTS) using mod_setenv

Stay Secure

Security software must be maintained.
  • Please regularly check that your systems are running the latest patches of TLS libraries. Checking at least once a month is recommended.
  • Do not run versions of software that have reached end-of-life (EOL) and are no longer receiving security patches!
    https://endoflife.software/applications/security-libraries/openssl
    openssl 0.9.8 EOL: 31 Dec 2015
    openssl 1.0.0 EOL: 31 Dec 2015
    openssl 1.0.1 EOL: 31 Dec 2016
    openssl 1.0.2 EOL: 31 Dec 2019
    openssl 1.1.0 EOL: 31 Aug 2018
    openssl 1.1.1 EOL: 11 Sep 2023
    As of 11 Sep 2023, only openssl 3.0 and later continue to receive security patches from openssl.org
    https://openssl-library.org/post/2023-09-11-eol-111/

SSLv2, SSLv3, TLSv1, TLSv1.1 are deprecated by international standards organizations.
In lighttpd 1.4.56 and later, these deprecated protocols are disabled by default, though can still be explicitly enabled in lighttpd.conf

Configuration

basic options

option description
ssl.engine enable/disable ssl engine
ssl.pemfile path to the PEM file certificate chain (must contain both certificate chain and private key (unless ssl.privkey is set))
ssl.privkey path to the PEM file private key (required if private key is not in ssl.pemfile) (since 1.4.53)

feature selection (optional)

option description
ssl.openssl.ssl-conf-cmd specify openssl config file commands (since 1.4.48) (commit c09acbeb)
e.g. ("MinProtocol" => "TLSv1.2") to restrict protocol to only TLS 1.2 or later
e.g. ("CipherString" => "...") to configure the allowed TLS ciphers (note: prefer lighttpd defaults)
e.g. ("Options" => "-ServerPreference") to select cipher according to client preference (note: prefer lighttpd defaults)
ssl.stapling-file path to file containing binary OCSP Response (DER format) (since 1.4.56) (see OCSP Stapling)
ssl.stek-file path to file containing binary session ticket encryption key (STEK) (global setting) (since 1.4.56) (see Session Tickets)
ssl.acme-tls-1 path to directory containing TLS-ALPN-01 ("acme-tls/1") challenges (Let's Encrypt option) (since 1.4.53)
ssl.read-ahead enable/disable use of SSL read ahead (lighttpd 1.4.45+) (default: disable) (mod_openssl only)

client certificate verification (optional)

option description
ssl.verifyclient.activate enable/disable client verification
ssl.verifyclient.enforce enable/disable enforcing client verification
ssl.verifyclient.depth certificate depth for client verification
ssl.verifyclient.exportcert enable/disable client certificate export to environment as SSL_CLIENT_CERT
ssl.verifyclient.username client certificate entity to export to environment as REMOTE_USER (e.g. SSL_CLIENT_S_DN_emailAddress, SSL_CLIENT_S_DN_UID, etc.)
ssl.verifyclient.ca-file path to file for certificate authorities (CA) used for client certificate verification (since 1.4.60) (ssl.ca-file before 1.4.60)
ssl.verifyclient.ca-dn-file path to file for certificate authorities (CA) from which client should select client certs (if needed) (since 1.4.60) (ssl.ca-dn-file since 1.4.46 and before 1.4.60)
ssl.verifyclient.ca-crl-file path to file for certificate revocation list (CRL) for client certificate (since 1.4.60) (ssl.ca-crl-file since 1.4.46 and before 1.4.60)

legacy options (prefer defaults unless you know what you are doing)

option description
ssl.cipher-list configure the allowed TLS ciphers
(deprecated; prefer ssl.openssl.ssl-conf-cmd "CipherString")
ssl.honor-cipher-order enable/disable honoring server ordering preference for cipher selection (ssl.cipher-list) (default: enable)
(required for PFS unless ssl.cipher-list contains only ciphersuites which support PFS)
(allowing client preference permits clients to choose ChaCha20-Poly1305,
which might be better for mobile processors without optimized AES instruction sets)
(deprecated; prefer ssl.openssl.ssl-conf-cmd "Options" => "[+-]ServerPreference")
ssl.dh-file path to the PEM file for Diffie-Hellman key agreement protocol parameters (lighttpd >= 1.4.29 only)
(deprecated; prefer ssl.openssl.ssl-conf-cmd "DHParameters") (since 1.4.68)
ssl.ec-curve defines the set of elliptic-curve-cryptography domain parameters known as a "named curve" (lighttpd >= 1.4.29 only)
(deprecated; prefer ssl.openssl.ssl-conf-cmd "Groups")
ssl.use-sslv2 (deprecated) enable/disable use of SSL version 2 (lighttpd < 1.4.21 only, newer version don't support SSLv2)
ssl.use-sslv3 (deprecated) enable/disable use of SSL version 3 (lighttpd >= 1.4.29 only) (disabled by default since 1.4.36)
ssl.disable-client-renegotiation (deprecated) enable/disable mitigation of client triggered re-negotiation in TLSv1.2 or earlier (see CVE-2009-3555) (default: enable mitigation)
ssl.ca-file (deprecated) renamed ssl.verifyclient.ca-file (since 1.4.60)
ssl.ca-dn-file (deprecated) renamed ssl.verifyclient.ca-dn-file (since 1.4.60)
ssl.ca-crl-file (deprecated) renamed ssl.verifyclient.ca-crl-file (since 1.4.60)

Note: ssl.* configuration options are generally valid only in global scope or in the top level of a $SERVER["socket"] configuration condition, as they are needed when the socket connection is established, before the host is known. In cases where the client adds SNI (server name indication), some ssl.* options can be specified in $HTTP["host"] or $HTTP["scheme"] conditions, e.g. to select certificates for that specific connection. All other conditions occur after TLS negotiation has completed, so ssl.* directives nested in other configuration conditions may be ignored, including $SERVER["socket"] or $HTTP["host"] or $HTTP["scheme"] nested in other configuration conditions.

The following can be used in $HTTP["host"] or $HTTP["scheme"] conditions in addition to global scope or $SERVER["socket"] conditions. Use in other conditions may work in some versions of lighttpd, but is not guaranteed. Please test your configurations to validate that actual operation meets your expectations.

ssl.pemfile
ssl.privkey
ssl.verifyclient.activate
ssl.verifyclient.enforce
ssl.verifyclient.depth
ssl.verifyclient.exportcert
ssl.verifyclient.username
ssl.verifyclient.ca-file
ssl.verifyclient.ca-dn-file
ssl.acme-tls-1

In lighttpd 1.4.56 and later, ssl.verifyclient.ca-crl-file may be set alongside ssl.verifyclient.ca-file. (mod_openssl loads cert files at startup, and openssl requires both CAs and CRLs be loaded into the same (X509_STORE *))

On CPU-constrained systems (e.g. embedded systems), ssl.read-ahead = "disable" is recommended, and the default. If a system is not fast enough to keep up with crypto, a single connection with ssl.read-ahead = "enable" might monopolize the CPU and starve other connections, and might even delay server responses on its own connection. On systems where the CPU running TLS crypto is faster than the network wire speed, ssl.read-ahead = "enable" may slightly improve performance. In lighttpd 1.4.56 and later, ssl.read-ahead = "enable" may be set per $HTTP["host"]. Once enabled, read ahead may not be disabled on the connection, as the server may already have read additional blocks into memory.

Details

To enable SSL, you must provide a certificate and enable the SSL engine in each socket scope, i.e. ssl.engine = "enable" in each $SERVER["socket"] that should be SSL-enabled, and in the global scope if server.bind and server.port should be SSL-enabled. ssl.pemfile should contain both the certificate and the private key unless ssl.privkey is specified with the path to the private key. ssl.pemfile should additionally contain a properly sorted certificate chain for the certificate.

To make lighttpd SSL-only, simply put the following in your main config. By default, lighttpd listens to "0.0.0.0:80" unless server.bind and server.port are set to something else. To listen on port "0.0.0.0:443" and not on port 80:

server.port = 443
ssl.engine = "enable" 
ssl.pemfile = "/etc/lighttpd/ssl/www.example.org/fullchain.pem" 
ssl.privkey = "/etc/lighttpd/ssl/www.example.org/privkey.pem" 

To enable SSL/TLS on port 443 in addition to normal HTTP on port 80, put the ssl.engine configuration in a $SERVER["socket"] conditional block:
$SERVER["socket"] == ":443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/www.example.org/fullchain.pem" 
    ssl.privkey = "/etc/lighttpd/ssl/www.example.org/privkey.pem" 
}

If you want to serve different sites from the global scope, you can change the document root inside the $SERVER["socket"] conditional:
$SERVER["socket"] == ":443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/www.example.org/fullchain.pem" 
    ssl.privkey = "/etc/lighttpd/ssl/www.example.org/privkey.pem" 
    server.document-root = "/www/servers/www.example.org/secure/" 
}

When you are using lighttpd 1.4.19 or later, you can also use the $HTTP["scheme"] conditional to distinguish between secure and normal requests. Note that you can't use the $HTTP["scheme"] conditional around ssl.engine above, since lighttpd needs to know on what port to enable SSL.
$HTTP["scheme"] == "https" {
   server.document-root = "/www/servers/www.example.org/secure/" 
}

As a special case in lighttpd 1.4.46 and later, if ssl.pemfile and other ssl.* directives are in the global scope, then $SERVER["socket"] blocks can inherit the global config by setting ssl.engine = "enable" and no other ssl.* directives in the $SERVER["socket"] block. Since lighttpd 1.4.65, ssl.engine is also inherited from the global scope. The following will set lighttpd to be HTTP on port 80 and HTTPS on port 443, on the default INADDR_ANY (0.0.0.0) and in6addr_any ([::]) wildcard addresses.
server.document-root = "/www/servers/www.example.org/" 
ssl.pemfile = "/etc/lighttpd/ssl/www.example.org/fullchain.pem" 
ssl.privkey = "/etc/lighttpd/ssl/www.example.org/privkey.pem" 
server.port = 80
$SERVER["socket"] == "[::]:80"  { }
$SERVER["socket"] ==     ":443" { ssl.engine = "enable" }
$SERVER["socket"] == "[::]:443" { ssl.engine = "enable" }

If ssl.engine = "enable" directive is also in the global scope, then for some server sockets it is possible to set plain HTTP using ssl.engine = "disable". The following example demonstrates IPv4/IPv6 dual-stack server with open ports for both http and https connections.
server.document-root = "/www/servers/www.example.org/" 
ssl.pemfile = "/etc/lighttpd/ssl/www.example.org/fullchain.pem" 
ssl.privkey = "/etc/lighttpd/ssl/www.example.org/privkey.pem" 
ssl.engine = "enable" 
server.port = 443
$SERVER["socket"] == "[::]:443" { ssl.engine = "enable" }
$SERVER["socket"] ==      ":80" { ssl.engine = "disable" }
$SERVER["socket"] ==  "[::]:80" { ssl.engine = "disable" }

If you have .crt and .key files, specify
ssl.pemfile = "/path/to/host.crt"
ssl.privkey = "/path/to/host.key"
Alternatively, cat them together into a single PEM file (the order is not strictly important): $ cat host.key host.crt > host.pem and specify ssl.pemfile = "/path/to/host.pem"

ssl.openssl.ssl-conf-cmd "CipherString" is a list of ciphers that you want (or don't want) to use when talking SSL. For a list of ciphers and how to include/exclude them, see sections "CIPHER LIST FORMAT" and "CIPHER STRINGS" on the openssl man-page for openssl-ciphers . For the latest list of recommended strong ciphers, see https://ssl-config.mozilla.org/#server=lighttpd&config=modern and periodically revisit the page. For more details about lighttpd TLS defaults (which you should generally prefer using), see Cipher Selection.

Permissions

Be careful to keep your .pem file private! lighttpd reads all pemfiles at startup, and if lighttpd is started as root, this is done before dropping privileges. If lighttpd is started as root, you can make the pem file owned by root and readable only by root:
$ chown root:root /etc/lighttpd/ssl/example.org/privkey.pem
$ chmod 400 /etc/lighttpd/ssl/example.org/privkey.pem
However, protecting keys this way means that lighttpd must be fully stopped and then started to pick up any configuration changes. Graceful restart is not available after lighttpd has dropped privileges since lighttpd needs those initial privileges to read the root permissions-protected private keys at startup.

If the lighttpd server is not run as root, the user account under which lighttpd runs must have access to read the certificates. The private keys should still be restricted from being read by other accounts.
$ chmod 400 /etc/lighttpd/ssl/example.org/privkey.pem

Chained certificates

Most certificate authorities use chained certificates. This means that your webserver certificate is not signed by the CA root certificate directly, but by an intermediate certificate (which is in turn signed by the root CA). Since browser typically only ship the root certificates and not all intermediate certificates, there is no way for a browser to verify your chained certificate if you don't supply the browser with the intermediate certificate.

For lighttpd to provide a chain of certificates, each ssl.pemfile should contain an ordered certificate chain. The order is a certificate, followed by the certificate of its issuer, and so on up to the root certificate. The final (root) certificate is optional.

Get your certificate chain right
https://medium.com/@superseb/get-your-certificate-chain-right-4b117a9c0fce

Alternative (deprecated): Historically, there is a feature/misfeature in openssl which re-uses the trusted CA certificate list (lighttpd ssl.verifyclient.ca-file) from the openssl (SSL_CTX *) to complete the server certificate chain if the chain has not been set for the openssl (SSL *). In other words, even though the CA list is intended for client certificate verification, the list is also used by openssl for the server certificate if a chain has not been set for the server certificate. As an alternative to the recommended method of having complete certificate chains in ssl.pemfile, you can set ssl.verifyclient.ca-file = "/etc/lighttpd/ssl/ca.crt" to a path with the intermediate certificates needed to complete the server certificate chain of the certificate in ssl.pemfile.

Perfect Forward Secrecy (PFS)

https://en.wikipedia.org/wiki/Forward_secrecy
"In cryptography, forward secrecy (FS), also known as perfect forward secrecy (PFS), is a feature of specific key agreement protocols that gives assurances that session keys will not be compromised even if the private key of the server is compromised.[1] Forward secrecy protects past sessions against future compromises of secret keys or passwords.[2] By generating a unique session key for every session a user initiates, the compromise of a single session key will not affect any data other than that exchanged in the specific session protected by that particular key."

lighttpd supports PFS if the lighttpd TLS module is configured to use protocols and ciphers that support PFS (enabled by default). To require PFS (and disallow downgrade) specify only ciphers which support PFS. (TLSv1.3 only uses ciphers which support PFS.)

# OLD DEFAULT: As of Sep 2020, default in lighttpd 1.4.56 and widely supported by clients, except very old clients w/o TLSv1.2
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",  # in openssl 1.0.2: "Protocol" => "-ALL, TLSv1.2" 
#                            "Options" => "ServerPreference,
#                            "CipherString" => "HIGH")
# (lighttpd 1.4.55 and earlier: For increased security, you should disable SessionTicket or you should restart lighttpd server daily)
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "ServerPreference,-SessionTicket", #(lighttpd 1.4.55 and earlier)
#                            "CipherString" => "HIGH")

# OLDER STRONGER: As of Sep 2020, a strong set of ciphers for PFS and widely supported by clients
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "-ServerPreference",
#                            "CipherString" => "EECDH+AESGCM:AES256+EECDH:CHACHA20")

# DEFAULT: As of Jan 2023 with lighttpd 1.4.68:
#
# STRONGER: As of Sep 2020, a strong set of ciphers for PFS and widely supported by modern clients, without CBC ciphers reported as weak by SSLLabs
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "-ServerPreference",
#                            "CipherString" => "EECDH+AESGCM:AES256+EECDH:CHACHA20:!SHA1:!SHA256:!SHA384")

# DEFAULT: As of Mar 2024 with lighttpd 1.4.75:
#
# STRONGER: As of Mar 2024, a strong set of ciphers for PFS and authenticated encryption (AEAD), and widely supported by modern clients
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "-ServerPreference",
#                            "CipherString" => "EECDH+AESGCM:CHACHA20:!PSK:!DHE")

# STRONGEST: As of Sep 2020, for use w/ modern clients only; not compat w/ older clients
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.3",
#                            "Options" => "-ServerPreference")
# Expanded cipher list equivalent to "CipherString" => "EECDH+AESGCM:CHACHA20:!PSK:!DHE" 
# for mod_mbedtls, mod_wolfssl, mod_gnutls, whose modules do not support the shorter syntax
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "-ServerPreference",
#                            # mbedtls (pseudo-syntax)
#                            "CipherString" => "TLS1-3-AES-256-GCM-SHA384:TLS1-3-CHACHA20-POLY1305-SHA256:TLS1-3-AES-128-GCM-SHA256:TLS1-3-AES-128-CCM-SHA256:TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256:TLS-ECDHE-RSA-WITH-CHACHA20-POLY1305-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256" 
#                            # wolfssl (pseudo-syntax)
#                            "CipherString" => "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305" 
#                            # gnutls (pseudo-syntax) (GnuTLS priority string syntax)
#                            "CipherString" => +AES-256-GCM:+AES-128-GCM:+CHACHA20-POLY1305:-RSA:-PSK:-DHE-RSA:-AES-256-CCM:-AES-128-CCM:-AES-256-CBC:-AES-128-CBC" 
#                           )

# Expanded cipher list equivalent to ssl.cipher-list = "EECDH+AESGCM:AES256+EECDH:CHACHA20" 
# for mod_wolfssl whose modules do not support the shorter CipherString syntax
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "-ServerPreference",
#                            "CipherString" => "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-CCM8:ECDHE-ECDSA-AES256-CCM:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-CHACHA20-POLY1305:RSA-PSK-CHACHA20-POLY1305:DHE-PSK-CHACHA20-POLY1305:ECDHE-PSK-CHACHA20-POLY1305:PSK-CHACHA20-POLY1305")

# Expanded cipher list equivalent to "CipherString" => "EECDH+AESGCM:AES256+EECDH:CHACHA20:!SHA1:!SHA256:!SHA384" 
# for mod_mbedtls, mod_wolfssl, mod_gnutls, whose modules do not support the shorter syntax
#ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2",
#                            "Options" => "-ServerPreference",
#                            "CipherString" => "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-CCM8:ECDHE-ECDSA-AES256-CCM:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-CHACHA20-POLY1305:RSA-PSK-CHACHA20-POLY1305:DHE-PSK-CHACHA20-POLY1305:ECDHE-PSK-CHACHA20-POLY1305:PSK-CHACHA20-POLY1305")

ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2") is strongly recommended (and "TLSv1.3" if supported by clients)
TLS protocols earlier than TLSv1.2 are deprecated, and TLSv1.3 is preferred. Major client browsers plan to drop support for TLSv1.0 and TLSv1.1 in 2020.
https://sensorstechforum.com/apple-microsoft-google-drop-support-tls1-0-tls1-1/
https://www.thesslstore.com/blog/apple-microsoft-google-disable-tls-1-0-tls-1-1/
https://scotthelme.co.uk/legacy-tls-is-on-the-way-out/

Cipher Selection

Since lighttpd 1.4.68: the default TLS ciphers chosen by lighttpd are strong (see above Perfect Forward Secrecy (PFS)).
Changing the strong default TLS ciphers is not recommended unless you have a specific requirement to do so.

Before lighttpd 1.4.68: lighttpd implicitly applies ssl.cipher-list = "HIGH" (since lighttpd 1.4.54) if ssl.cipher-list is not explicitly set in lighttpd.conf. This is a reasonable default on the modern internet. For a higher security, use "SUITEB192" or "SUITEB128". For compatibility with much older clients, try "MEDIUM". There are many other options (openssl ciphers) that can be used with mod_openssl, though only a small subset is supported by mod_mbedtls, mod_wolfssl, mod_gnutls, mod_nss parsing of ssl.openssl.ssl-conf-cmd or ssl.cipher-list.

Either ssl.openssl.ssl-conf-cmd = ("Options" => "-ServerPreference") or ssl.honor-cipher-order = "disable" (same effect) is recommended when all of the ciphers listed support Perfect Forward Secrecy, e.g. Mozilla ssl-configurator Modern config for lighttpd . Once the cipher list has been limited to those ciphers supporting Perfect Forward Secrecy, it is useful to permit cipher selection using client preferences since mobile devices often lack hardware acceleration for AES, and on those devices ChaCha20-Poly1305 might be up to 3x faster, giving mobile users a better experience and consuming less battery life. On desktops, where hardware more often has native hardware for AES acceleration, AES-based ciphers are often the fastest. (Historically, the default in lighttpd is to use the server preference for cipher selection (ssl.honor-cipher-order = "enable" to prevent cipher downgrade attacks to weaker ciphers, such as those that do not support Perfect Forward Secrecy.)

lighttpd 1.4.56 with openssl 1.1.1 or later will use openssl option SSL_OP_PRIORITIZE_CHACHA for the benefit of mobile users, even if ssl.honor-cipher-order = "enable" and AES ciphers are listed before ChaCha20-Poly1305.

References:
https://blog.cloudflare.com/do-the-chacha-better-mobile-performance-with-cryptography/
https://security.googleblog.com/2014/04/speeding-up-and-strengthening-https.html

Session Tickets

A session ticket is an optional TLS 1.2 extension that may be used to reduce latency when a client reconnects, also known as session resumption. A session ticket allows the server to delegate storage of session state to the client, but encrypts that state with a server-side session ticket encryption key (STEK). However, Perfect Forward Secrecy (PFS) is lost if the STEK has an infinite lifetime, e.g. is never rotated. On the other hand, replacing a STEK invalidates current session tickets, requiring a full TLS handshake upon a new connection, rather than quicker session resumption, so replacement should not occur too frequently.

lighttpd 1.4.55 and earlier did not explicitly configure session tickets, but the underlying openssl library enabled session tickets by default, and never rotated the STEK. Since lighttpd 1.4.48, session tickets can be disabled in lighttpd using ssl.openssl.ssl-conf-cmd = ("Options" => "-SessionTicket")

In lighttpd 1.4.56, session tickets are still enabled by default, but are automatically rotated in all lighttpd TLS modules except in mod_nss. By default, lighttpd will rotate the STEK and each key has a lifetime of 24 hours. This is a reasonable tradeoff to allow session resumption for a defined window of time while also preserving Perfect Forward Secrecy after a STEK is rotated and the previous STEK discarded. This default is sufficient for a single lighttpd process, i.e. not using lighttpd with multiple workers. For multiple workers, use ssl.stek-file (since 1.4.56).

ssl.stek-file allows the admin control over the STEK lifetime and rotation schedule, which is useful when using multiple lighttpd workers or running lighttpd across multiple servers to serve the same site(s). If ssl.stek-file is configured in the global configuration scope, the builtin STEK rotation behavior is disabled. Instead, lighttpd will read the STEK from the file and will check every 64 seconds to see if the file needs to be re-read for a new STEK. lighttpd mod_openssl stores up to three (3) STEKs, so if the encryption key lifetime is 24 hours, then it is suggested that a new STEK be generated every 8 hours. lighttpd mod_mbedtls uses mbedtls internals, which store up to (2) STEKs, so it is suggested that a new STEK be generated every 12 hours. lighttpd mod_gnutls has only one active STEK at a time, so it is suggested that a new STEK be generated every 24 hours. Using ssl.stek-file is recommended if using lighttpd with multiple workers, as this mechanism allows the same keys to be used by each lighttpd worker, as well as across multiple servers. You are responsible for generating the STEK file via an external job. The STEK file should be stored in non-persistent storage, e.g. /dev/shm/lighttpd/stek-file (in memory) with appropriate permissions set to keep stek-file from being read by other users. Where possible, systems should also be configured without swap in order to keep the key from inadventently being swapped out to persistent storage.

The format of binary ssl.stek-file is:
  • 4-byte - format version (always 0; for use if format changes)
  • 4-byte - activation timestamp
  • 4-byte - expiration timestamp
  • 16-byte - session ticket key name
  • 32-byte - session ticket HMAC encrpytion key
  • 32-byte - session ticket AES encrpytion key

The STEK file can be created with a command such as:

cd /dev/shm/lighttpd && \
  dd if=/dev/random bs=1 count=80 status=none | \
  perl -e 'print pack("iii",0,time()+300,time()+86400),<>' \
  > STEK-file.$$ && mv STEK-file.$$ STEK-file
# (alternative: 'openssl rand 80' can be substituted for 'dd if=/dev/random bs=1 count=80 status=none' above)
The above delays activation time by 5 mins (+300 sec) to allow file to be propagated to other machines. (admin must handle this independently) If STEK generation is performed immediately prior to starting lighttpd, admin should activate keys immediately (without +300). /dev/shm/lighttpd is used in this example and must be created and permissioned by the admin. lighttpd reads the stek-file but does not create the stek-file or any part of the path.

Note that if using lighttpd before lighttpd 1.4.56 with multiple lighttpd workers, no coordinated STEK rotation occurs between processes other than by (some external job) restarting lighttpd. Restarting lighttpd generates a new key that is shared by lighttpd workers for the lifetime of the new key. If the rotation period expires and lighttpd has not been restarted, lighttpd workers will generate new independent keys, making session tickets less effective for session resumption, since clients have a lower chance for future connections to reach the same lighttpd worker. However, things will still work, and a new session will be created if session resumption fails. Before lighttpd 1.4.56, admins should plan to restart lighttpd at least once every 24 hours if session tickets are enabled and multiple lighttpd workers are configured. (A graceful restart can be accomplished by sending SIGUSR1 to the parent lighttpd process.) The difficulty, complexity, and per-environment nature of key distribution is why periodically restarting lighttpd (or nginx, or Apache) is frequently the recommended method for rotating STEKs. Proper use of ssl.stek-file with lighttpd 1.4.56 makes this easier and does not need server restarts.

If there is a connection load balancer in front of multiple independent lighttpd servers (with independent STEKs), and the load balancer is not performing SSL-termination, then it may be beneficial to configure the load balancer algorithm to attempt sticky routing, if available, so that the client has a better chance to be directed to the same server that can validate the session ticket and allow session resumption. Again, this is not necessary when using lighttpd 1.4.56 with ssl.stek-file if the STEK file is propagated to each server to that the keys are shared.

Warning: Jun 2020: GnuTLS 3.6.4 to GnuTLS 3.6.13 are vulnerable to CVE-2020-13777 If using mod_gnutls, please use GnuTLS 3.6.14 or later, or disable Session Tickets in lighttpd using ssl.openssl.ssl-conf-cmd = ("Options" => "-SessionTicket")

Warning: limitation in mod_nss: NSS does not provide a public interface to rotate the STEK. lighttpd mod_nss does not support STEK rotation. If using mod_nss, disable Session Tickets in lighttpd using ssl.openssl.ssl-conf-cmd = ("Options" => "-SessionTicket") or consider restarting the lighttpd server every 24 hours.

Session Caching

Session Caching is disabled since lighttpd 1.4.56. Session Tickets (TLSv1.2) are recommended instead of Session Caching as Session Tickets are superior, using less memory and being more easily portable across multiple lighttpd workers and multiple servers when the Session Ticket Encryption Key is shared (and rotated). To do similar with Session Caching requires much more complex infrastructure (which some other web server support), but which is no longer needed when using Session Tickets with TLSv1.2 and later.

server.feature-flags += ("ssl.session-cache" => "enable") can be used to restore default behavior prior to lighttpd-1.4.56. If set, lighttpd will not explicitly disable the TLS library session cache, instead using the TLS library defaults. Note: session cache is not yet implemented in mod_mbedtls or mod_gnutls (and is low priority since session tickets should be preferred.)

OCSP Stapling

ssl.stapling-file (since lighttpd 1.4.56) may be set alongside ssl.pemfile, in the same scope. If the file is present, the contents of the file (the OCSP Response) will be stapled along with the certificate as part of the TLS handshake.

The file must be maintained and updated periodically so that the OCSP Response does not expire. A few minutes prior to expiration, lighttpd checks about once a minute for an updated ssl.stapling-file. If the expiration can not be determined, e.g. the OCSP Response does not contain a Next Update field, then lighttpd re-reads the ssl.stapling-file once an hour. The OCSP response file must be readable by the user configured in server.username.

The contents of ssl.stapling-file can be produced with appropriate variables filled in: openssl ocsp -issuer "$CHAIN_PEM" -cert "$CERT_PEM" -respout "$OCSP_RESP" -noverify -no_nonce -url "$OCSP_URI". In the lighttpd source tree, doc/scripts/cert-staple.sh provides a sample script which can be run periodically by a scheduled job.

Note: mbedTLS does not currently provide support for OCSP stapling which is ok for tiny servers with self-signed certificates, but not appropriate for use with certificates marked OCSP Must-Staple.

HTTP/2 and TLS

RFC 7540 HTTP/2 Section 9.2. Use of TLS Features specifies requirements for using TLS with HTTP/2. By default in lighttpd 1.4.56 and later, lighttpd TLS modules disable TLS compression and TLS renegotiation in TLSv1.2 as required by RFC 7540. The RFC 7540 HTTP/2 restriction on ciphers can be achieved by specifying the "STRONGER" set of ciphers "CipherString" => "EECDH+AESGCM:AES256+EECDH:CHACHA20:!SHA1:!SHA256:!SHA384" for mod_openssl (and might differ slightly for other lighttpd TLS modules; see the Perfect Forward Secrecy (PFS) section above for alternative cipher strings). The lighttpd default (since lighttpd 1.4.54) of "CipherString" => "HIGH" is for wider compatibilty, and is not strict enough to achieve the HTTP/2 recommendations. Please use the STRONGER or STRONGEST configurations recommended above to follow the stricter guidance in RFC 7540.

kTLS

kTLS - Kernel TLS - may be used to perform kernel-offload of symmetric TLS after the TLS handshake.

lighttpd support for kTLS began with lighttpd 1.4.68 and the performance improvements are most noticeable with HTTP/1.x with static files as responses or responses from backends using X-Sendfile. Note that due to HTTP/2 framing, lighttpd is unable to leverage openssl SSL_sendfile() or gnutls gnutls_record_send_file() with HTTP/2. kTLS is an optional feature for performance and if you are not sure whether or not you need this feature, then you most likely do not need it. If you choose to configure kTLS, please test to verify that the results increase performance of your system for the traffic patterns for your site(s).

kTLS may be supported in Linux kernel 4.13 and later, and FreeBSD 13 and later, though might not available by default in OS distributions. Debian 12 and later enable kTLS by default. Fedora does not enable kTLS by default (https://fedoraproject.org/wiki/Changes/KTLSSupportForGnuTLS). See the blog post Using Kernel TLS (kTLS) and TLS NIC offloading with OpenSSL for steps how to load Linux and FreeBSD kernel modules for TLS, both temporarily and persistently (to occur at system startup). See also Fedora: Working with Kernel Modules: Persistent Module Loading. If run as root, lighttpd mod_openssl and lighttpd mod_gnutls in lighttpd 1.4.74 and later will attempt to load the Linux tls kernel module if kernel tls is not already available.

In the case of lighttpd mod_gnutls, the gnutls system-wide configuration (e.g. /etc/gnutls/config or /etc/crypto-policies/back-ends/gnutls.config) must be configured to enable ktls in gnutls.
https://www.gnutls.org/manual/gnutls.html#Enabling_002fDisabling-system_002facceleration-protocols
To specify a custom gnutls library configuration file just for use by lighttpd, the GNUTLS_SYSTEM_PRIORITY_FILE environment variable can be set in the shell before starting lighttpd.
https://www.gnutls.org/manual/gnutls.html#System_002dwide-configuration-of-the-library

If using specialized hardware devices for encryption acceleration, you must make sure that kernel modules are properly loaded and configured, too, e.g. https://docs.nvidia.com/doca/sdk/tls-offload/index.html

To disable kTLS, configure
ssl.openssl.ssl-conf-cmd += ("Options" => "-KTLS")

There may be an issue on FreeBSD 14 with the patches to the FreeBSD package of openssl 3.0.12. Building and and using the official, upstream openssl source code appears to work with kTLS.
https://github.com/openssl/openssl/issues/23824#issuecomment-1992664175

SSL on multiple domains

A traditional problem with SSL in combination with name based virtual hosting has been that the SSL connection setup happens before the HTTP request. So at the moment lighttpd needs to send its certificate to the client, it does not know yet which domain the client will be requesting. This means it can only supply the default certificate (and use the corresponding key for encryption) and effectively, SSL can only be enabled for that default domain. There are a number of solutions to this problem, with varying levels of support by clients.

Server Name Indication (SNI)

Server Name Indication (SNI) is a TLS extension to the TLS handshake that allows the client to send the name of the host it wants to contact. The server can then use this information to select the correct certificate. SNI is generally well-supported by most clients, though not all mobile clients.

To use SNI with lighttpd, simply put additional ssl.pemfile configuration directives inside $HTTP["host"] conditionals. A default ssl.pemfile is still required in the $SERVER["socket"] conditional.

$SERVER["socket"] == ":443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/the-default-domain.com.pem" 
}
$HTTP["host"] == "www.example.org" {
    ssl.pemfile = "/etc/lighttpd/www.example.org.pem" 
}
$HTTP["host"] == "mail.example.org" {
    ssl.pemfile = "/etc/lighttpd/mail.example.org.pem" 
}

One IP address per domain

This is the classic solution, which requires a separate IP address for each domain. Since lighttpd knows the ip address that the client connected to before the SSL negotiation, it can simply select the right certificate to use based on the IP address. The most obvious downside to this approach is that it requires multiple addresses, which are not always available and can be expensive. This approach works with all clients but also uses many IP addresses.

To do this in lighttpd, use the $SERVER["socket"] conditional:

$SERVER["socket"] == "10.0.0.1:443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/www.example.org.pem" 
}

$SERVER["socket"] == "10.0.0.2:443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/mail.example.org.pem" 
}

Inherit ssl.* config from global config scope

To enable TLS/SSL in multiple $SERVER["socket"] contexts, but sharing ssl.* config, either duplicate the ssl.* config within each $SERVER["socket"], or put the ssl.* directives in the global context and have $SERVER["socket"] config block contain ssl.engine = "enable" without any other ssl.* directives in that $SERVER["socket"] config block. (Since lighttpd 1.4.65, if ssl.engine = "enable" in the global scope, then it is inherited by and need not be repeated in each $SERVER["socket"] config block.)

ssl.pemfile = "/etc/lighttpd/ssl/www.example.org.pem" 
$SERVER["socket"] == "10.0.0.1:443" {
    ssl.engine  = "enable" 
}
$SERVER["socket"] == "10.0.0.2:443" {
    ssl.engine  = "enable" 
}

UCC / SAN Certificates

It is possible to put multiple names in a single certificate. These certificates are usually referred to as UCC (Unified Communications Certificates) certificates, or SAN (Subject Alternative Name) certificates. These certificates use the SAN property to store multiple domain names in a single certificate. This allows lighttpd to always use the same certificate, which is valid for all the domains it serves.

The main disadvantage of this approach is that you will have a single certificate with a lot of domain names in it and the certificate needs to change whenever you add a new domain name. When serving sites for different parties, this can be a problem.

According to digicert SAN compatibility, SAN certificates are supported by nearly all browsers (except for some mobile browsers) and should be pretty safe to use.

Wildcard certificates

Certificates support the use of wildcards, which can be useful to support multiple subdomains. For example, one can get a certificate for the *.example.org domain to support all of the subdomains of the example.org domain.

This approach of course does not scale well to multiple different domains. Again, according to digicert, wildcard compatibility, wildcard certificates should be supported by virtually all clients.

Other issues

Let's Encrypt bootstrap

lighttpd supports Let's Encrypt bootstrap using TLS-ALPN-01 verification challenge (since lighttpd 1.4.53)

See steps in HowToSimpleSSL

Self-Signed Certificates

A self-signed SSL certificate can be generated like this: ::

$ openssl req -new -x509 \
-keyout server.pem -out server.pem \
-days 365 -nodes

Serve ONLY the domain you want under an SSL IP

The simple config suggested above, is perhaps too "vague" when serving multiple hosts, one per IP (the traditional and still most common way, until Windows XP dies and nearly all browsers support SNI). Consider this config:

$SERVER["socket"] == "10.0.0.1:443" {
ssl.engine = "enable"
ssl.pemfile = "/etc/lighttpd/ssl/domain1.com.pem"
server.document-root = "/www/servers/domain1.com"
}
$SERVER["socket"] == "10.0.0.2:443" {
ssl.engine = "enable"
ssl.pemfile = "/etc/lighttpd/ssl/domain2.com.pem"
server.document-root = "/www/servers/domain1.com"
}

Your DNS config might be:

domain1.com A 10.0.0.1
domain2.com A 10.0.0.2

Like this all is fine normally. However if your client has poisoned or stale DNS, or a /etc/hosts entry and makes the following request:

https://domain1.com

connecting to IP 10.0.0.2 because it has somehow resolved domain1.com to 10.0.0.2 (see above). Then lighty WILL serve the content of domain2 despite recevieving "Host:domain1.com" in the request header! You are kind of protected because your client should give an SSL domain mismatch warning here. However, as was the case in our config, domain1 and domain2 were actually subdomains and covered by the same wildcard cert => no ssl warning!

So we were getting CONTENT FROM "WRONG" DOMAIN/DOCROOT just because of stale DNS.

For a "tighter" config we would suggest:

server.document-root = "/www/servers/other-catchall  # e.g. HTTP/1.0 clients sending HTTP request headers _without_ Host:
$SERVER["socket"] == "10.0.0.1:443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/domain1.com.pem" 
    $HTTP["host"] == "domain1.com" {
        server.document-root = "/www/servers/domain1.com" 
    }
}

$SERVER["socket"] == "10.0.0.2:443" {
    ssl.engine  = "enable" 
    ssl.pemfile = "/etc/lighttpd/ssl/domain2.com.pem" 
    $HTTP["host"] == "domain2.com" {
        server.document-root = "/www/servers/domain2.com" 
    }
}
This ensures that 10.0.0.2:443 will ONLY EVER serve content with a "Host: domain2.com" request header. If domain1.com is requested from 10.0.0.2:443 lighty will respond with 404.

Troubleshooting

  • Please note that certificate files are opened before the server performs chroot (if configured to chroot). So these paths are system wide.
  • On server side, ssldump is your friend: ssldump -i your_network_interface_goes_here port 443
  • On client side, openssl will help you: openssl s_client -connect your_server_name_goes_here:443
  • SSL options not working or ignored? Put the options in the same block where you enabled SSL (ssl.engine = "enable").

SSL passwords

  • If you set a password on your SSL certificate, then each time lighttpd starts you will be requested to enter it manually. ie: "Enter PEM pass phrase:". To prevent this, remove the password from your private key file. At present there is no configuration option to enable storage of SSL certificate passwords in the lighttpd config file (but it wouldn't make much sense, since having the password stored on disk is not more secure than having no password at all).

Diffie-Hellman and Elliptic-Curve Diffie-Hellman parameters

Diffie-Hellman and Elliptic-Curve Diffie-Hellman key agreement protocols will be supported in lighttpd 1.4.29. By default, Diffie-Hellman and Elliptic-Curve Diffie-Hellman key agreement protocols use, respectively, the 1024-bit MODP Group with 160-bit prime order subgroup from RFC 5114 and "prime256v1" (also known as "secp256r1") elliptic curve from RFC 4492. The Elliptic-Curve Diffie-Hellman key agreement protocol is supported in OpenSSL from 0.9.8f version onwards. For maximum interoperability, OpenSSL only supports the "named curves" from RFC 4492.

Using the ssl.dh-file and ssl.ec-curve configuration variables, you can define your own set of Diffie-Hellman domain parameters. For example:

ssl.dh-file = "/etc/lighttpd/ssl/dh2048.pem" 
ssl.ec-curve = "secp384r1"

A set of Diffie-Hellman domain parameters can be generated like this (see OpenSSL dhparam doc for help):

$ openssl dhparam -out dh2048.pem -outform PEM -2 2048

The set of supported elliptic-curves can be obtained like this (see OpenSSL ecparam doc for help):

$ openssl ecparam -list_curves

Limitations

  • mbedTLS does not currently provide support for OCSP stapling. That is ok for tiny servers with self-signed certificates, but is not appropriate for use with certificates marked OCSP Must-Staple.
  • mod_nss does not support EC certificates or ssl.verifyclient.ca-file. NSS libraries prefers to store certificates in a certificate database and does not provide the same support for individual files, which is the typical usage with other TLS libraries. NSS is not wrong, but it is different. Patches welcome.

Links

Further TLS Security Enhancements

Reference:
- https://depthsecurity.com/blog/pins-and-staples-enhanced-ssl-security
- https://web.archive.org/web/20190604160808/https://depthsecurity.com/blog/pins-and-staples-enhanced-ssl-security

Some keywords and phrases to enter into a search engine for your own further research.

  • OCSP Stapling
    - RFC 6961 The Transport Layer Security (TLS) Multiple Certificate Status Request Extension
    - servers send (Online Certificate Status Protocol (OCSP) response to client along with the server's own certificate
    - ssl.stapling-file (since 1.4.56)
  • OCSP Must-Staple
    RFC 7633 X.509v3 Transport Layer Security (TLS) Feature Extension
    - Instructs client to reject certificate if TLS handshake does not also provide OCSP stapling status information
    - OCSP Must-Staple X.509v3 extension must be requested in the certificate signing request (CSR) sent to the certificate authority (CA)
    - HTTP response header Expect-Staple might also be leveraged to validate the results of site OCSP stapling operations and procedures before new certificates with Must-Staple are deployed.

Updated by gstrauss 21 days ago · 168 revisions