namalzi

← All posts

Digging into Apache httpd's Source 15: mod_ssl — Where TLS Plugs In

Apache httpd · Published

The httpd core knows nothing about TLS, yet one SSLEngine on line gives you HTTPS. We follow, in the source and with examples, how mod_ssl hooks into startup and every connection, slips OpenSSL into the filter chain with connection filters and home-made BIOs, swaps the virtual host and certificate based on SNI, and hands SSL_* variables to other modules.

What this post covers

This is the fifteenth post in the "Digging into Apache httpd's Source" series. In part 14 we looked at mod_proxy, which passes requests on to other servers. This time we look behind the curtain of the one line SSLEngine on in httpd.conf that turns on HTTPS: mod_ssl.

There are four questions to answer.

Item Details
Subject Apache httpd
Version 2.5.1-dev (trunk)
Commit e4af30fe6d (2026-10-24)
Main files modules/ssl/mod_ssl.c, ssl_engine_io.c, ssl_engine_init.c, ssl_engine_kernel.c, ssl_engine_vars.c, server/ssl.c, include/http_ssl.h

All line numbers and details in this post refer to this commit. How to get the same source and build the Docker image used for the examples is in part 1.

mod_ssl is a big module: the C files under modules/ssl/ alone add up to more than 23,000 lines (counting add-ons such as mod_ssl_ct.c). So that someone opening httpd's source for the first time can keep up, this post follows only the main path: the places where TLS plugs into the server. Client certificates, the session cache, OCSP and the like are listed by name and location in "What this post leaves out" at the end.

Terms to know first

TLS (Transport Layer Security) is the protocol that puts encryption on top of a TCP connection. Its old name was SSL, which is why httpd's directives and source still say SSL (SSLEngine, mod_ssl). In this post the two words mean the same thing. HTTPS is "HTTP spoken over a connection wrapped in TLS".

The handshake is the greeting the client and server exchange at the very start of a TLS connection. The client speaks first with a ClientHello (the TLS versions and ciphers it supports, and so on), and the server answers by sending its certificate. A certificate is a document proving "this server really is a.example", and the server must hold the matching private key. HTTP requests only flow once the handshake is over.

SNI (Server Name Indication) is a field inside the ClientHello that says "I want to reach b.example". HTTP's Host header can only be read after decryption, so at the moment the certificate is chosen SNI is the only clue available.

OpenSSL is the C library that does the actual TLS work. mod_ssl doesn't implement any cryptography itself; it calls OpenSSL. Three OpenSSL names show up in this post.

Connection filters are the layer of filters from part 11 that attach to a single connection and deal in bytes. Filters line up in the chain by the number of their type (AP_FTYPE_*), and at the very bottom sit the core filters CORE_IN and CORE, which actually read from and write to the socket. Data travels in buckets, passed along as brigades (lists of buckets).

Virtual hosts, as we saw in part 9, are chosen twice: once by IP and port when the connection arrives (c->base_server), and once by the Host header after the request is read (r->server). TLS needs one more choice in between.

1. The core doesn't know TLS — the hooks mod_ssl registers

If you read the core code in server/, you won't find OpenSSL calls like SSL_read() or SSL_accept(). The core doesn't know about TLS; it just leaves places (hooks and filters) open for modules to step in. What mod_ssl puts in those places is gathered in a single registration function.

static void ssl_register_hooks(apr_pool_t *p)
{
    /* ... */
    ssl_io_filter_register(p);

    ap_hook_pre_connection(ssl_hook_pre_connection,NULL,NULL, APR_HOOK_MIDDLE);
    ap_hook_process_connection(ssl_hook_process_connection, 
                                                   NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_test_config   (ssl_hook_ConfigTest,    NULL,NULL, APR_HOOK_MIDDLE);
    ap_hook_post_config   (ssl_init_Module,        NULL,b_pc, APR_HOOK_MIDDLE);
    ap_hook_http_scheme   (ssl_hook_http_scheme,   NULL,NULL, APR_HOOK_MIDDLE);
    ap_hook_default_port  (ssl_hook_default_port,  NULL,NULL, APR_HOOK_MIDDLE);
    ap_hook_pre_config    (ssl_hook_pre_config,    NULL,NULL, APR_HOOK_MIDDLE);
    ap_hook_child_init    (ssl_init_Child,         NULL,NULL, APR_HOOK_MIDDLE);
    ap_hook_post_read_request(ssl_hook_ReadReq, pre_prr,NULL, APR_HOOK_MIDDLE);
    /* ... check_access, check_authn, check_authz ... */
    ap_hook_fixups        (ssl_hook_Fixup,         NULL,NULL, APR_HOOK_MIDDLE);
    /* ... */
    ssl_var_register(p);
    ap_hook_ssl_bind_outgoing  (ssl_hook_ssl_bind_outgoing, NULL, NULL, APR_HOOK_MIDDLE);
    /* ... */
}

modules/ssl/mod_ssl.c lines 761–809

ap_hook_xxx(function, modules that must come before, modules that must come after, position) is exactly the hook registration we saw in part 6. Passing a function's name as an argument is a C function pointer: it means "call this function later" (part 2). Put the list in time order and you have the map for this post.

When Hook / registration What mod_ssl does Section
Startup, before reading config pre_config → ssl_hook_pre_config() Initialise OpenSSL 2
Startup, after reading config post_config → ssl_init_Module() Build an SSL_CTX per virtual host 2
At registration ssl_io_filter_register() Register three connection filters 3
Each connection, first thing pre_connection → ssl_hook_pre_connection() Create the SSL object, insert filters 3, 4
Each connection, before reading a request process_connection → ssl_hook_process_connection() Trigger the handshake 5
Right after a request is read post_read_request → ssl_hook_ReadReq() Check SNI against Host 6
Just before the handler fixups → ssl_hook_Fixup() HTTPS, SSL_* environment variables 7
Any time ssl_conn_is_ssl hook, ssl_var_lookup optional function Answer "is this connection TLS?" and "what is SSL_PROTOCOL?" 7

The http_scheme and default_port hooks are worth a look too. When the core builds something like a redirect URL, it doesn't decide by itself whether the request's scheme is http or https; it asks a hook (#define ap_http_scheme(r) ap_run_http_scheme(r) in include/httpd.h, line 302). mod_ssl returns "https" for a TLS request and NULL otherwise (mod_ssl.c lines 676–679). This hook is RUN_FIRST, stopping at the first function that gives a non-NULL answer (server/protocol.c line 2558), so if mod_ssl stays quiet, the http module's http_scheme(), registered at the very end (APR_HOOK_REALLY_LAST), answers "http" (modules/http/http_core.c lines 117–128, line 362).

How the core asks for the scheme. ap_http_scheme(r), which the core calls when building something like a redirect URL, is really the macro ap_run_http_scheme(r). The http_scheme hook is RUN_FIRST, so it stops at the first non-NULL answer. mod_ssl's ssl_hook_http_scheme, hooked at MIDDLE, checks whether the request is over TLS; if so it returns "https" and the hook stops there. If not it returns NULL, and the http module's http_scheme(), hooked at REALLY_LAST, answers "http" The core never writes the word https itself; it only asks the hook. Without mod_ssl loaded, the "http" at the end is always the answer.

The directives you know from config files, such as SSLEngine and SSLCertificateFile, live in the ssl_config_cmds table, the fifth slot of the module struct after STANDARD20_MODULE_STUFF (mod_ssl.c lines 811–822). How a directive becomes a struct field is the same as in part 7.

2. Startup — post_config builds an SSL_CTX for each virtual host

TLS setup is expensive. You have to read and check the certificate and private key files, and decide which protocols and ciphers to allow. Doing that per connection would be slow, so mod_ssl builds an SSL_CTX once per virtual host at startup. The function that does it is ssl_init_Module(), registered on the post_config hook (ssl_engine_init.c lines 451–714).

mod_ssl's startup sequence. The pre_config hook initialises OpenSSL; reading the config puts the SSL directive values into the server config struct; then ssl_init_Module on the post_config hook walks every server_rec building vhost_id, prepares the session cache and mutexes, and for each server with SSLEngine on, ssl_init_server_ctx calls the ssl_add_cert_files hook, then creates the SSL_CTX, loads the certificate and key, and registers the SNI callbacks. Finally init_bio_methods builds the BIO function tables. Blue is a hook, yellow is something built and kept. All of this is done before the first connection arrives.

  1. pre_config — wake OpenSSL up. With OpenSSL 1.1 or later this is a single line, OPENSSL_init_ssl() (mod_ssl.c lines 428–434). The #else part below it is for old OpenSSL versions. #ifndef/#else is a preprocessor branch that keeps only one of the two at compile time.
  2. Settle defaults for each server. It walks the server_rec list, marks servers without an explicit SSLEngine as off (the only exception: a server whose protocol is https, as in Listen 443 https, and that has certificates configured is switched on), and builds vhost_id, the name used in logs (ssl_engine_init.c lines 497–543).
  3. An SSL_CTX for each enabled server. A second loop calls ssl_init_ConfigureServer() → ssl_init_server_ctx() (lines 625–640, 3268–3308). This is where SSL_CTX_new() creates the settings bundle (line 972) and the SNI callbacks we'll meet in section 6 are registered, and then the certificate and key are loaded (ssl_init_server_certs(), from line 2200).
  4. BIO function tables. Near the end, init_bio_methods() builds the BIO function tables of section 4 (lines 686–689).

Right before loading certificates, other modules get a chance too.

    /* Allow others to provide certificate files */
    pks = sc->server->pks;
    n = pks->cert_files->nelts;
    ap_ssl_add_cert_files(s, p, pks->cert_files, pks->key_files);
    ssl_run_add_cert_files(s, p, pks->cert_files, pks->key_files);

modules/ssl/ssl_engine_init.c lines 3145–3149

ap_ssl_add_cert_files() is a hook in the core (server/ssl.c). mod_md, which fetches certificates automatically from places like Let's Encrypt, hangs a function on it (modules/md/mod_md.c line 1592) and adds the file names of the certificates it has obtained to the list. mod_md never calls mod_ssl, and mod_ssl never calls mod_md; both only know the core's hook. We come back to these core hooks in section 7.

Easy to get wrong

3. Per connection — pre_connection creates the SSL object and inserts filters

When a connection arrives, the MPM follows the sequence from part 9: pick the default virtual host by IP and port (ap_update_vhost_given_ip()), run the pre_connection hook, then run the process_connection hook (server/connection.c lines 244–253; the event MPM follows the same order itself inside process_socket(), server/mpm/event/event.c lines 1099–1153).

At pre_connection not a single byte has been read yet. mod_ssl checks whether this connection should use TLS and, if so, only prepares.

static int ssl_hook_pre_connection(conn_rec *c, void *csd)
{
    SSLSrvConfigRec *sc;
    SSLConnRec *sslconn = myConnConfig(c);

    /*
     * Immediately stop processing if SSL is disabled for this connection
     */
    if (ssl_engine_status(c, sslconn) != OK) {
        return DECLINED;
    }
    /* ... */
    ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, c, APLOGNO(01964)
                  "Connection to child %ld established "
                  "(server %s)", c->id, sc->vhost_id);

    return ssl_init_ssl_connection(c, NULL);
}

modules/ssl/mod_ssl.c lines 686–715

ssl_engine_status() checks whether this connection's c->base_server (the virtual host chosen by IP and port) has SSLEngine on (mod_ssl.c lines 542–571). If not, it returns DECLINED and steps aside. That is why, on the same server, mod_ssl does nothing at all for connections on port 80. myConnConfig(c) is a macro that fetches mod_ssl's share of the connection's per-module config slots (ssl_private.h lines 383–384). mod_ssl keeps one SSLConnRec struct there per connection, holding things like the connection's SSL object and the currently chosen virtual host (server) (ssl_private.h lines 643–677).

The skeleton of ssl_init_ssl_connection() is three lines.

    if (!(sslconn->ssl = ssl = SSL_new(mctx->ssl_ctx))) {
        /* ... log the error ... */
    }
    /* ... */
    SSL_set_app_data(ssl, c);
    modssl_set_app_data2(ssl, NULL); /* will be request_rec */

    SSL_set_verify_result(ssl, X509_V_OK);

    return ssl_io_filter_init(c, r, ssl);
}

modules/ssl/mod_ssl.c lines 641–674 (middle shortened)

  1. SSL_new() — creates this connection's SSL from the SSL_CTX built at startup. The SSL_CTX used here belongs to c->base_server, that is, the default virtual host chosen by IP and port. SNI hasn't been read yet, so there's no other option. Section 6 changes this.
  2. SSL_set_app_data(ssl, c) — hangs httpd's conn_rec on OpenSSL's SSL object. Later, when OpenSSL calls one of mod_ssl's callbacks, all it passes is the SSL *; the callback uses this to get the conn_rec back (SSL_get_app_data()).
  3. ssl_io_filter_init() — inserts the filters.

Three SSL filters on the connection

These are the filters mod_ssl registers.

void ssl_io_filter_register(apr_pool_t *p)
{
    ap_register_input_filter  (ssl_io_filter, ssl_io_filter_input,  NULL, AP_FTYPE_CONNECTION + 5);
    ap_register_output_filter (ssl_io_coalesce, ssl_io_filter_coalesce, NULL, AP_FTYPE_CONNECTION + 4);
    ap_register_output_filter (ssl_io_filter, ssl_io_filter_output, NULL, AP_FTYPE_CONNECTION + 5);

    ap_register_input_filter  (ssl_io_buffer, ssl_io_filter_buffer, NULL, AP_FTYPE_PROTOCOL);

    return;
}

modules/ssl/ssl_engine_io.c lines 2305–2314

The filter name ssl_io_filter is the string "SSL/TLS Filter", and ssl_io_coalesce is "SSL/TLS Coalescing Filter" (lines 1019–1021). As for the type numbers, AP_FTYPE_CONNECTION is 50 and AP_FTYPE_NETWORK, where the core handles the socket, is 60 (include/util_filter.h lines 180–183). So the SSL filter (55) sits just above the core filters (60). The last one, ssl_io_buffer, is a special filter that holds a request body for a while because of client certificates, so we leave it out here.

The connection filter chain. On the incoming side, below "read the HTTP request" sits the SSL/TLS Filter (55), and below that CORE_IN (60) reads from the socket. On the outgoing side, below the HTTP filters come the SSL/TLS Coalescing Filter (54), the SSL/TLS Filter (55) and CORE (60), which writes to the socket. The blue boxes are mod_ssl's filters. Everything above them sees only plaintext; ciphertext only flows between the blue and grey boxes.

ssl_io_filter_init() actually attaches these filters to the connection.

apr_status_t ssl_io_filter_init(conn_rec *c, request_rec *r, SSL *ssl)
{
    ssl_filter_ctx_t *filter_ctx;
    apr_status_t rv = APR_SUCCESS;

    filter_ctx = apr_palloc(c->pool, sizeof(ssl_filter_ctx_t));

    filter_ctx->config          = myConnConfig(c);

    ap_add_output_filter(ssl_io_coalesce, NULL, r, c);

    filter_ctx->pOutputFilter   = ap_add_output_filter(ssl_io_filter,
                                                       filter_ctx, r, c);

#if MODSSL_USE_OPENSSL_PRE_1_1_API
    filter_ctx->pbioWrite       = BIO_new(&bio_filter_out_method);
#else
    filter_ctx->pbioWrite       = BIO_new(bio_filter_out_method);
#endif
    if(filter_ctx->pbioWrite == NULL) {
      return APR_ENOMEM;
    }
    BIO_set_data(filter_ctx->pbioWrite, (void *)bio_filter_out_ctx_new(filter_ctx, c));

    rv = ssl_io_input_add_filter(filter_ctx, c, r, ssl);
    if(rv != APR_SUCCESS) {
      return rv;
    }

    SSL_set_bio(ssl, filter_ctx->pbioRead, filter_ctx->pbioWrite);
    filter_ctx->pssl            = ssl;

    apr_pool_cleanup_register(c->pool, (void*)filter_ctx,
                              ssl_io_filter_cleanup, apr_pool_cleanup_null);
    /* ... */
    return APR_SUCCESS;
}

modules/ssl/ssl_engine_io.c lines 2265–2303

Besides attaching filters, three lines stand out.

Easy to get wrong

The virtual host at pre_connection time is the one chosen by IP and port, regardless of SNI. In the example below, even a connection made to b.example is logged as "Connection to child … established (server a.example:443)", because a.example is the first virtual host on the same *:8443. The switch to b.example happens during the handshake (section 6).

4. BIOs — the adapter between OpenSSL and the filter chain

OpenSSL can be set up to take a socket number and call read() and write() on it directly. In httpd, though, the socket belongs to the core filters CORE_IN and CORE. Other filters may sit in between (for example, mod_logio's input filter, which counts received bytes, is registered at AP_FTYPE_NETWORK - 1, between the SSL filter and CORE_IN, modules/loggers/mod_logio.c lines 342–343), and the event MPM handles sockets in non-blocking mode. If OpenSSL touched the socket directly, it would break that order.

So mod_ssl gives OpenSSL a BIO of its own making instead of a socket. A BIO's behaviour is defined by a function table (BIO_METHOD) holding "read function, write function, control function". mod_ssl fills the slots of that table with its own functions.

void init_bio_methods(void)
{
    bio_filter_out_method = BIO_meth_new(BIO_TYPE_MEM, "APR output filter");
    BIO_meth_set_write(bio_filter_out_method, &bio_filter_out_write);
    BIO_meth_set_ctrl(bio_filter_out_method, &bio_filter_out_ctrl);
    BIO_meth_set_create(bio_filter_out_method, &bio_filter_create);
    BIO_meth_set_destroy(bio_filter_out_method, &bio_filter_destroy);

    bio_filter_in_method = BIO_meth_new(BIO_TYPE_MEM, "APR input filter");
    BIO_meth_set_read(bio_filter_in_method, &bio_filter_in_read);
    BIO_meth_set_ctrl(bio_filter_in_method, &bio_filter_in_ctrl);   /* ctrl is never called */
    BIO_meth_set_create(bio_filter_in_method, &bio_filter_create);
    BIO_meth_set_destroy(bio_filter_in_method, &bio_filter_destroy);
}

modules/ssl/ssl_engine_io.c lines 607–620

This is the "swap behaviour via a table of function pointers" from part 2, only this time it's OpenSSL, not httpd, asking for it. Lines 576–600 of the same file have a version for old OpenSSL that fills in the struct directly (#if MODSSL_USE_OPENSSL_PRE_1_1_API); it does the same job.

Reading: filter → OpenSSL → BIO → back to the filters

A comment at the top of the file sums up the flow in one line.

 * The input chain is roughly:
 *
 * ssl_io_filter_input->ssl_io_input_read->SSL_read->...
 * ...->bio_filter_in_read->ap_get_brigade/next-httpd-filter

modules/ssl/ssl_engine_io.c lines 55–58

The read path. When the filter above asks for plaintext with ap_get_brigade, ssl_io_filter_input calls OpenSSL's SSL_read or SSL_accept, and OpenSSL asks its BIO for bytes with BIO_read. That calls bio_filter_in_read, the read function of mod_ssl's own BIO, which asks the filter below for ciphertext with ap_get_brigade. The ciphertext CORE_IN read from the socket goes back to OpenSSL as a char array, is decrypted, and is handed to the filter above as a plaintext bucket. Solid arrows are calls, dotted arrows are data coming back. OpenSSL (grey) is sandwiched between two mod_ssl functions (blue).

  1. When the filter above (the one reading the HTTP request) asks for plaintext with ap_get_brigade(), the SSL input filter ssl_io_filter_input() is called (line 1559).
  2. This filter calls OpenSSL's SSL_read() (inside ssl_io_input_read(), line 683).
  3. OpenSSL needs ciphertext to decrypt, so it reads from its BIO. That BIO's read function is mod_ssl's bio_filter_in_read().
  4. bio_filter_in_read() asks the next filter in the chain for ciphertext.
static int bio_filter_in_read(BIO *bio, char *in, int inlen)
{
    apr_size_t inl = inlen;
    bio_filter_in_ctx_t *inctx = (bio_filter_in_ctx_t *)BIO_get_data(bio);
    apr_read_type_e block = inctx->block;
    /* ... */
    if (APR_BRIGADE_EMPTY(inctx->bb)) {

        inctx->rc = ap_get_brigade(inctx->f->next, inctx->bb,
                                   AP_MODE_READBYTES, block,
                                   inl);
        /* ... */
    }

    inctx->rc = brigade_consume(inctx->bb, block, in, &inl);

    if (inctx->rc == APR_SUCCESS) {
        return (int)inl;
    }
    /* ... */
}

modules/ssl/ssl_engine_io.c lines 473–556 (middle shortened)

BIO_get_data(bio) retrieves the context hung on the BIO in section 3, and the function calls ap_get_brigade() on inctx->f (the SSL input filter itself)'s next, i.e. towards CORE_IN. brigade_consume() then flattens the buckets of the returned brigade into the char array in that OpenSSL handed over, and the function returns the number of bytes copied. This function is where the translation happens between the bucket-brigade world (part 11) and the OpenSSL world of "a buffer and a length".

  1. OpenSSL decrypts the ciphertext it received, and ssl_io_filter_input() wraps the plaintext in a bucket and sends it upward.

Writing: the same shape, in reverse

Writing goes the other way. The SSL output filter ssl_io_filter_output() (line 1923) reads the plaintext buckets it receives one by one and passes them through ssl_filter_write() → SSL_write() (line 2015, line 872). OpenSSL writes the encrypted result to its write BIO, and that BIO's write function, bio_filter_out_write(), wraps the bytes in a bucket and passes them to the filter below.

    e = apr_bucket_transient_create(in, inl, outctx->bb->bucket_alloc);
    APR_BRIGADE_INSERT_TAIL(outctx->bb, e);

modules/ssl/ssl_engine_io.c lines 215–216

A transient bucket "just points at someone else's memory for a moment". It points at OpenSSL's buffer without copying it, and if a filter further down needs to keep the data for later, it copies it then (the comment right above, lines 212–214). Next, bio_filter_out_pass() hands this brigade to the CORE filter with ap_pass_brigade() (line 238, lines 134–146).

The Coalescing filter (54), which exists only on the output side, sits right above the SSL filter and gathers small buckets. According to the source comment, the SSL output filter produces one TLS record (the unit of encryption) per bucket, so many small buckets mean needlessly many records and packets, and sending headers as a separate record reveals their size to an observer (lines 1687–1700).

Example: does OpenSSL really call httpd's filters?

To see this sandwich with our own eyes, I set a breakpoint on bio_filter_in_read() and sent one HTTPS request. I started httpd under gdb with -X (a debug option that runs it as a single process with no children), connected with curl, and printed the call stack where it stopped. The example was run with httpd 2.5.1-dev built unchanged from the reference commit (e4af30fe6d), in a Docker container (Ubuntu 24.04, OpenSSL 3.0.13). The whole example is the single file ex/u15-mod-ssl/run.sh (the config is in section 5).

#0  0x00007efe2609a81f in bio_filter_in_read () from /opt/httpd/modules/mod_ssl.so
#1  0x00007efe25ba4273 in ?? () from /lib/x86_64-linux-gnu/libcrypto.so.3
#2  0x00007efe25ba2727 in ?? () from /lib/x86_64-linux-gnu/libcrypto.so.3
#3  0x00007efe25ba2ebb in BIO_read () from /lib/x86_64-linux-gnu/libcrypto.so.3
#4  0x00007efe2601da27 in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#5  0x00007efe260212d9 in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#6  0x00007efe2601fe88 in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#7  0x00007efe2603e8e8 in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#8  0x00007efe260323a5 in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#9  0x00007efe2609e861 in ssl_io_filter_handshake () from /opt/httpd/modules/mod_ssl.so
#10 0x00007efe2609f9da in ssl_io_filter_input () from /opt/httpd/modules/mod_ssl.so
#11 0x0000555684490d8c in ap_get_brigade ()
#12 0x00007efe26088461 in ssl_hook_process_connection () from /opt/httpd/modules/mod_ssl.so
#13 0x0000555684489c11 in ap_run_process_connection ()
#14 0x00007efe261028b2 in process_socket () from /opt/httpd/modules/mod_mpm_event.so
#15 0x00007efe26106bc7 in worker_thread () from /opt/httpd/modules/mod_mpm_event.so
#16 0x0000555684486ff9 in thread_start ()
#17 0x00007efe26260b84 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#18 0x00007efe262edcc4 in clone () from /lib/x86_64-linux-gnu/libc.so.6

(Addresses differ on every run. ?? marks internal OpenSSL functions with no debug info.) Read it from the bottom up. An event MPM worker thread (#15) runs the process_connection hook (#13); mod_ssl's hook (#12) calls the input filters with ap_get_brigade() (#11); the SSL input filter (#10) goes through the handshake function (#9) into OpenSSL (#8–#4, libssl); and OpenSSL, via BIO_read() (#3), calls back into mod_ssl's bio_filter_in_read() (#0). The httpd → OpenSSL → httpd round trip shows up exactly as described.

There's one more thing to notice. The first read didn't happen in the code that reads HTTP requests but in ssl_hook_process_connection(), and the function inside it is not SSL_read() but the handshake (#9). That's the subject of the next section.

5. When does the handshake happen? — separately, just before the request is read

With the filters in place, the handshake would happen by itself on the first read. Every time the SSL input filter reads, it first calls ssl_io_filter_handshake() (ssl_engine_io.c lines 1617–1619), which returns immediately if the handshake is already done (lines 1197–1199) and otherwise calls SSL_accept() (line 1424). The output filter checks the handshake up front in the same way (lines 1956–1958).

mod_ssl doesn't wait for that, though. In the process_connection hook it triggers just the handshake first.

static int ssl_hook_process_connection(conn_rec* c)
{
    SSLConnRec *sslconn = myConnConfig(c);

    if (sslconn && !sslconn->disabled) {
        /* On an active SSL connection, let the input filters initialize
         * themselves which triggers the handshake, which again triggers
         * all kinds of useful things such as SNI and ALPN.
         */
        apr_bucket_brigade* temp;
        apr_status_t rv;

        temp = apr_brigade_create(c->pool, c->bucket_alloc);
        rv = ap_get_brigade(c->input_filters, temp,
                            AP_MODE_INIT, APR_BLOCK_READ, 0);
        apr_brigade_destroy(temp);
        /* ... on failure, get ready to close the connection ... */
    }
    
    return DECLINED;
}

modules/ssl/mod_ssl.c lines 717–755

The timeline of one connection. On accept, ap_update_vhost_given_ip sets c->base_server to a.example by IP and port; the pre_connection hook does SSL_new and inserts the SSL filters; the process_connection hook does one read with AP_MODE_INIT, and SSL_accept reads the ClientHello. ssl_callback_ClientHello sees SNI b.example, and init_vhost and ssl_find_vhost switch to b.example's certificate with SSL_set_SSL_CTX. When the handshake is done mod_ssl returns DECLINED, and the http module reads the request and picks r->server from Host. ssl_hook_ReadReq checks whether both virtual hosts' TLS setups match: if so the request is processed, otherwise 421 Misdirected Request is returned. The path of one connection. The three blue boxes in the middle (from the ClientHello callback on) are the SNI story of section 6.

Example: curl -vk with self-signed certificates

Inside the container I made self-signed certificates for a.example and b.example with openssl req -x509 ... (practice certificates signed by themselves, which browsers don't trust) and started two TLS virtual hosts on port 8443. Here is the relevant part of the config.

Listen 8443
LoadModule headers_module modules/mod_headers.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
LogLevel warn ssl:debug
SSLSessionCache shmcb:/tmp/ssl_scache(512000)

# Show values chosen by mod_ssl as response headers
#   %{...}s : mod_headers asks ap_ssl_var_lookup() directly
#   %{...}e : reads the environment variables (subprocess_env) filled in by the fixups phase
Header always set X-SSL-Protocol "%{SSL_PROTOCOL}s"
Header always set X-SSL-SNI      "%{SSL_TLS_SNI}s"
Header always set X-Env-HTTPS    "%{HTTPS}e"
Header always set X-Env-Protocol "%{SSL_PROTOCOL}e"

<VirtualHost *:8443>
  ServerName a.example
  DocumentRoot /tmp/a
  SSLEngine on
  SSLCertificateFile    /tmp/certs/a.example.crt
  SSLCertificateKeyFile /tmp/certs/a.example.key
</VirtualHost>
<VirtualHost *:8443>
  ServerName b.example
  DocumentRoot /tmp/b
  SSLEngine on
  SSLCertificateFile    /tmp/certs/b.example.crt
  SSLCertificateKeyFile /tmp/certs/b.example.key
  SSLOptions +StdEnvVars
</VirtualHost>

In curl -vk, -v shows the handshake and -k tells curl not to give up on a failed verification, even for a self-signed certificate. --resolve a.example:8443:127.0.0.1 points the name at the container itself. From the output I kept only the handshake lines and response headers.

=== A. curl -vk https://a.example:8443/index.txt (handshake lines) ===
* Connected to a.example (127.0.0.1) port 8443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=a.example
*  issuer: CN=a.example
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 200 OK
< X-SSL-Protocol: TLSv1.3
< X-SSL-SNI: a.example
< X-Env-HTTPS: on
< X-Env-Protocol: (null)
site A

Everything from Client hello to Finished happened inside the SSL_accept() we saw in the stack in section 4, and only after that does HTTP/1.1 200 OK arrive. ALPN: server accepted http/1.1 is because this container's config doesn't load the HTTP/2 module (mod_http2). We look at the four response headers in section 7.

Easy to get wrong: plain HTTP on the TLS port

If you connect to 8443 with http:// instead of https://, you might expect the connection to simply drop, but httpd sends back an HTML error page.

=== E. plain HTTP to the TLS port ===
HTTP/1.1 400 Bad Request
<p>Your browser sent a request that this server could not understand.<br />
Reason: You're speaking plain HTTP to an SSL-enabled server port.<br />
 Instead use the HTTPS scheme to access this URL, please.<br />
</p>

When SSL_accept() fails and OpenSSL reports "this looks like an HTTP request" (SSL_R_HTTP_REQUEST), the handshake function returns mod_ssl's own error code MODSSL_ERROR_HTTP_ON_HTTPS (ssl_engine_io.c lines 1449–1458). ssl_io_filter_error() then switches off the SSL filter for this connection so that it passes bytes straight through from now on, and leaves a note saying "a fake request line must be sent" (ssl_engine_io.c lines 981–999). When that note is present, the process_connection hook doesn't close the connection but hands it on to the http module (the shortened part of the code above, mod_ssl.c lines 734–751), and when the http module tries to read a request, the SSL input filter sends up a fake request line "GET / HTTP/1.0" and an empty line instead of the bytes the client sent (lines 945–951, 1582–1599). The http module treats it as an ordinary request and starts processing, and ssl_hook_ReadReq() on the post_read_request hook sees the note, attaches an error message and returns 400 (ssl_engine_kernel.c lines 166–177). Whatever the original request was, the response is a 400 for /. It's a workaround that invents a request just to show an error page, something only the filter design makes possible.

6. SNI — switching virtual host and certificate mid-handshake

With several virtual hosts on one port there's a problem. The server must send a certificate during the handshake, but the Host header that says which virtual host is meant can only be read after the handshake. That's why SSL_new() in section 3 starts with the default virtual host's (a.example's) SSL_CTX. The only clue to correct it is the SNI inside the ClientHello.

Where the callbacks are registered

While building the SSL_CTX at startup, mod_ssl tells OpenSSL "when you receive a ClientHello, call this function".

    if (!SSL_CTX_set_tlsext_servername_callback(mctx->ssl_ctx,
                          ssl_callback_ServerNameIndication) ||
        !SSL_CTX_set_tlsext_servername_arg(mctx->ssl_ctx, mctx)) {
        /* ... */
    }

#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
    /*
     * The ClientHello callback also allows to retrieve the SNI, but since it
     * runs at the earliest possible connection stage we can even set the TLS
     * protocol version(s) according to the selected (name-based-)vhost, which
     * is not possible at the SNI callback stage (due to OpenSSL internals).
     */
    SSL_CTX_set_client_hello_cb(mctx->ssl_ctx, ssl_callback_ClientHello, NULL);
#endif

modules/ssl/ssl_engine_init.c lines 780–798 (error handling shortened)

There are two callbacks: the long-standing SNI callback, and the ClientHello callback available since OpenSSL 1.1.1. As the comment says, the ClientHello callback is called earlier, early enough to change even the allowed TLS versions depending on the virtual host. Both callbacks end up in the same function, init_vhost(), which only does its work once per connection (vhost_found, ssl_engine_kernel.c lines 2256–2260). The example container's OpenSSL is 3.0.13, which satisfies the 1.1.1-or-later condition (#if), so both callbacks are registered, and as the example log below shows, the virtual host lookup happens only once per connection.

Finding the virtual host from SNI

The ClientHello callback pulls the host name out of the raw bytes of the SNI extension itself and passes it to init_vhost() (ssl_engine_kernel.c lines 2410–2467). init_vhost() uses the core function ap_vhost_iterate_given_conn() to hand the name-based virtual hosts on this connection's IP and port to ssl_find_vhost() one at a time (lines 2266–2267). The code that manages the virtual host list lives in the core (server/vhost.c, part 9); mod_ssl only judges "does this name match?".

static int ssl_find_vhost(void *servername, conn_rec *c, server_rec *s)
{
    SSLSrvConfigRec *sc;
    SSL *ssl;
    BOOL found;
    SSLConnRec *sslcon;

    found = ssl_util_vhost_matches(servername, s);

    /* set SSL_CTX (if matched) */
    sslcon = myConnConfig(c);
    if (found && (ssl = sslcon->ssl) &&
        (sc = mySrvConfig(s))) {
        SSL_CTX *ctx = SSL_set_SSL_CTX(ssl, sc->server->ssl_ctx);
        /* ... bring options, protocol versions, verification and session id context in line with the new ctx ... */
        sslcon->server = s;
        /* ... */
        return 1;
    }

    return 0;
}

modules/ssl/ssl_engine_kernel.c lines 2475–2559 (middle shortened)

If no name matches, it carries on with the handshake on the default (first) virtual host without even sending a warning (the comment at lines 2275–2293 explains why, citing RFC 6066).

Example: same port, different certificate

Same config as before, only the SNI changes.

=== B. same port, SNI = b.example ===
*  subject: CN=b.example
< HTTP/1.1 200 OK
< X-SSL-Protocol: TLSv1.3
< X-SSL-SNI: b.example
< X-Env-HTTPS: on
< X-Env-Protocol: TLSv1.3
site B

=== C. no SNI (connect by IP) ===
*  subject: CN=a.example
< HTTP/1.1 200 OK
< X-SSL-SNI: (null)
site A

B is on the same port 8443, but the certificate is CN=b.example. C connects by IP address, so curl sends no SNI (an IP address doesn't go in SNI). The default virtual host a.example's certificate goes out. I added Host: a.example, so the content is site A as well. mod_ssl's debug log records this (LogLevel ssl:debug, identical messages counted and grouped):

      1 AH01964: Connection to child 128 established (server a.example:443)
      1 AH01964: Connection to child 130 established (server a.example:443)
      1 AH01964: Connection to child 132 established (server a.example:443)
      1 AH01964: Connection to child 134 established (server a.example:443)
      1 AH01964: Connection to child 136 established (server a.example:443)
      2 AH02043: SSL virtual host for servername a.example found
      1 AH02043: SSL virtual host for servername b.example found
      1 AH02645: Server name not provided via TLS extension (using default/first virtual host)

(Connection numbers differ on every run.) The AH01964 from pre_connection says a.example for all five connections; the switch to b.example is the later AH02043, during the handshake. That's the "easy to get wrong" note from section 3, confirmed in the log. The fifth connection (plain HTTP) failed its handshake and so has no SNI line.

Easy to get wrong: SNI and Host differ → 421

SNI comes from the handshake, Host from the request. What if a client sends different values? I sent SNI a.example with Host: b.example.

=== D. SNI = a.example but Host: b.example ===
421

The r->server chosen from Host is b.example, but the TLS connection was made with a.example's certificate. ssl_hook_ReadReq() on the post_read_request hook compares the TLS setups of the two virtual hosts, and if they don't match it returns 421 Misdirected Request ("I can't answer for that host on this connection").

        /* Enforce SSL SNI vhost compatibility policy: the virtual
         * host selected for the connection (based on the SNI
         * extension) must have a "compatible" SSL configuration with
         * the one selected based on the Host: header. */
        if (!ssl_check_vhost_sni_policy(sc, hssc)) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02032)
                         "Hostname %s %s and hostname %s provided"
                         " via HTTP have no compatible SSL setup for policy '%s'",
                         servername ? servername : handshakeserver->server_hostname,
                         servername ? "provided via SNI" : "(default host as no SNI was provided)",
                          r->hostname, MODSSL_SNIVH_NAME(sc->mc->snivh_policy));
            return HTTP_MISDIRECTED_REQUEST;
        }

modules/ssl/ssl_engine_kernel.c lines 231–243

The log recorded "Hostname a.example provided via SNI and hostname b.example provided via HTTP have no compatible SSL setup for policy 'secure'" (AH02032). What counts as "matching" is set by the SSLVHostSNIPolicy directive, and the default is secure (ssl_engine_config.c line 64). Under secure, at startup each virtual host gets an MD5 digest over its certificate files, key files, protocols, ciphers, client verification settings and so on, and the check compares those digests (ssl_engine_init.c lines 330–405). The example's two virtual hosts have different certificate files, so they don't match. Conversely, as in C, no SNI plus a Host naming the default virtual host is fine.

Examples B, C and D side by side. In B, both SNI and Host are b.example. pre_connection starts with a.example's SSL_CTX (AH01964 server a.example:443), ssl_find_vhost switches it with SSL_set_SSL_CTX and logs AH02043 b.example found, the certificate is CN=b.example, and the response is 200 site B with X-SSL-SNI: b.example. In C, the connection is made by IP so there is no SNI, and Host is a.example. With no name to match it goes to the default (first) virtual host, logging AH02645 with certificate CN=a.example; Host is the default virtual host too, so the response is 200 site A with X-SSL-SNI: (null). In D, SNI is a.example and Host is b.example. The handshake matches a.example with certificate CN=a.example, but in ssl_hook_ReadReq r->server is b.example, and under SSLVHostSNIPolicy secure the different certificate files are not compatible, so the result is 421 Misdirected Request with AH02032 The example outputs and debug-log message numbers above, gathered per case. The virtual host changes during the handshake (AH02043); whether it agrees with Host is checked after the request is read.

7. SSL_* variables — fetched through the core's ap_ssl_* functions

Other modules need TLS information too. mod_rewrite decides redirects with %{HTTPS}, mod_log_config writes the protocol and cipher to the log, and mod_http2 behaves differently depending on whether the connection is TLS. But if these modules called mod_ssl's functions directly, they couldn't be built without mod_ssl.

Trunk's answer is a TLS question desk in the core. include/http_ssl.h declares it and server/ssl.c implements it. Here are a couple of the functions.

AP_DECLARE(int) ap_ssl_conn_is_ssl(conn_rec *c)
{
    int r = (ap_run_ssl_conn_is_ssl(c) == OK);
    if (r == 0 && module_ssl_is_https) {
        r = module_ssl_is_https(c);
    }
    return r;
}

server/ssl.c lines 84–91

AP_DECLARE(const char *) ap_ssl_var_lookup(apr_pool_t *p, server_rec *s,
                                           conn_rec *c, request_rec *r,
                                           const char *name)
{
    const char *val = ap_run_ssl_var_lookup(p, s, c, r, name);
    if (val == NULL && module_ssl_var_lookup) {
        val = module_ssl_var_lookup(p, s, c, r, name);
    }
    return val;
}

server/ssl.c lines 181–190

Both have the same shape. First run the hook (ap_run_ssl_*, both RUN_FIRST, lines 256–260), and if nobody answers, fall back to the old-style optional function. An optional function is an APR mechanism where "a module registers a function by name and other modules look it up by name"; if no module has registered it, it's NULL (we had a taste of it in part 6).

The core's TLS question desk. Modules such as mod_headers, mod_rewrite, mod_log_config and mod_http2 call ap_ssl_var_lookup and ap_ssl_conn_is_ssl in server/ssl.c. The core first checks whether a module registered on the ssl_* hooks answered, and if so uses that answer; mod_ssl is registered on the ssl_conn_is_ssl hook. If there's no answer it falls back to the old-style optional function, mod_ssl's ssl_var_lookup. At startup, ap_setup_ssl_optional_fns in core_post_config saves the old optional function and registers its own in its place. Other modules know only the core, as in the grey box at the top left. They compile without mod_ssl, and if no TLS module is present they get the answer "not TLS".

mod_ssl at this commit uses a mix of both paths.

void ssl_var_register(apr_pool_t *p)
{
    char *cp, *cp2;

    ap_hook_ssl_conn_is_ssl(ssl_conn_is_ssl, NULL, NULL, APR_HOOK_MIDDLE);
    APR_REGISTER_OPTIONAL_FN(ssl_get_tls_cb);
    APR_REGISTER_OPTIONAL_FN(ssl_var_lookup);
    APR_REGISTER_OPTIONAL_FN(ssl_ext_list);

modules/ssl/ssl_engine_vars.c lines 245–252

It answers "is this connection TLS?" on the new ssl_conn_is_ssl hook (OK if the connection has an SSL object, lines 69–73), but still offers variable values the old way, as the optional function ssl_var_lookup. The core, however, registers an optional function under the same name ssl_var_lookup. ap_setup_ssl_optional_fns(), called from the core's post_config hook (APR_HOOK_REALLY_FIRST), saves mod_ssl's previously registered function in module_ssl_var_lookup and registers its own function in its place (server/ssl.c lines 192–216, server/core.c line 5404, line 6161). So whether a module uses the new API or still looks up the optional function the old way, it ends up reaching mod_ssl by the same road.

The remaining hooks (ssl_add_cert_files, ssl_add_fallback_cert_files, ssl_answer_challenge, ssl_ocsp_*) point the other way: they are desks where mod_ssl asks and another module (mod_md) answers. At this commit, mod_md is the only module that hangs functions on them (mod_md.c lines 1591–1597).

2.4.x has it too. server/ssl.c and include/http_ssl.h aren't trunk-only; the 2.4.x branch has them, with the same list of functions (ap_ssl_conn_is_ssl, ap_ssl_var_lookup, ap_ssl_add_cert_files, ap_ssl_bind_outgoing and so on). According to the 2.4.x branch history, these files arrived in the 2.4.48 release (the commit message gives the reason as letting other TLS modules work "without impersonating mod_ssl"). So in module sources written for httpd older than 2.4.48, you'll only see the old style, such as APR_RETRIEVE_OPTIONAL_FN(ssl_var_lookup).

mod_headers' %{…}s uses this desk

In the example config's %{SSL_PROTOCOL}s, s is a format letter registered by mod_headers (mod_headers.c line 1068), and its function simply calls the core's ap_ssl_var_lookup().

static const char *header_request_ssl_var(request_rec *r, char *name)
{
    const char *val = ap_ssl_var_lookup(r->pool, r->server,
                                        r->connection, r, name);
    if (val && val[0])
        return unwrap_header(r->pool, val);
    else
        return "(null)";
}

modules/metadata/mod_headers.c lines 207–215

On mod_ssl's side, ssl_var_lookup() branches on the start of the name, and for SSL_PROTOCOL it calls OpenSSL's SSL_get_version() (ssl_engine_vars.c lines 491–493). The example's X-SSL-Protocol: TLSv1.3 is that value. The value is computed on the spot when asked.

Easy to get wrong: why %{SSL_PROTOCOL}e is (null)

In example A (a.example), X-Env-HTTPS was on but X-Env-Protocol was (null); in B (b.example) both had values. %{…}e doesn't ask mod_ssl; it reads the request's environment variable table (r->subprocess_env, the variables handed to things like CGI). That table is filled in by ssl_hook_Fixup() on the fixups hook.

    /* the always present HTTPS (=HTTP over SSL) flag! */
    apr_table_setn(env, "HTTPS", "on");

#ifdef HAVE_TLSEXT
    /* add content of SNI TLS extension (if supplied with ClientHello) */
    if ((servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name))) {
        apr_table_set(env, "SSL_TLS_SNI", servername);
    }
#endif
    /* ... */
    /* standard SSL environment variables */
    if (dc->nOptions & SSL_OPT_STDENVVARS) {
        modssl_var_extract_dns(env, ssl, r->pool);
        modssl_var_extract_san_entries(env, ssl, r->pool);

        for (i = 0; ssl_hook_Fixup_vars[i]; i++) {
            var = ssl_hook_Fixup_vars[i];
            val = ssl_var_lookup(r->pool, r->server, r->connection, r, var);
            if (!strIsEmpty(val)) {
                apr_table_setn(env, var, val);
            }
        }
    }

modules/ssl/ssl_engine_kernel.c lines 1425–1452

HTTPS=on always goes in, and SSL_TLS_SNI goes in whenever the client sent SNI, regardless of SSLOptions; but the dozens of standard variables including SSL_PROTOCOL (the list starting at line 1345) go in only when SSLOptions +StdEnvVars is on. Computing dozens of them on every request is expensive, so it's off by default. In the example, only b.example got +StdEnvVars. In summary:

Format Where it reads from Without StdEnvVars
%{SSL_PROTOCOL}s (mod_headers) ap_ssl_var_lookup() → mod_ssl computes it on the spot has a value
%{SSL_PROTOCOL}e (mod_headers) r->subprocess_env — the table filled by fixups (null)
%{HTTPS}e same table, always-set value on

This table is what gets passed to CGI scripts, so if SSL_PROTOCOL is empty in a CGI, +StdEnvVars is the place to check. On the other hand, if you only need the value in a log or a header, use a format that asks directly without +StdEnvVars (mod_headers' s, mod_log_config's %{…}x — mod_log_config.c lines 911–918, line 1902).

What this post leaves out

Here are just the names and locations of the big parts of mod_ssl this post didn't cover.

Summary

Concept One-line summary
The core and TLS The core has no TLS code. mod_ssl steps in through hooks and connection filters
ssl_init_Module() In post_config, builds an SSL_CTX (certificate, key, settings bundle) for each virtual host with SSLEngine on
ssl_hook_pre_connection() Per connection, SSL_new() from the default virtual host's SSL_CTX, then inserts the SSL filters
SSL filters CONNECTION+5 (55), so right above the core filters (60). Everything above sees only plaintext
Home-made BIOs OpenSSL's reads and writes come in through bio_filter_in_read() and bio_filter_out_write() and continue down the filter chain
Handshake timing Triggered first in process_connection with AP_MODE_INIT, then DECLINED hands over to the http module
SNI ClientHello callback → init_vhost() → SSL_set_SSL_CTX() swaps the certificate. c->base_server stays as it was
421 If the virtual host chosen by SNI and the one chosen by Host have different TLS setups, ssl_hook_ReadReq() refuses
ap_ssl_* The core's (server/ssl.c) TLS question desk. Hooks first, then the old optional functions. In 2.4.x since 2.4.48
SSL_* environment variables fixups always sets HTTPS, sets SSL_TLS_SNI whenever SNI was sent, and sets the rest only with SSLOptions +StdEnvVars

What's next

We've seen that TLS gets in as one layer of connection filter wrapped around the byte stream, plus a handful of hooks at startup, per connection and per request. The HTTP code above doesn't know it's there, and modules that want to know ask through the core's ap_ssl_* desk.

The last post in the series is about the place where all of this gets recorded: logs. We'll look at ap_log_error() for the error log, per-module log levels like the LogLevel ssl:debug used in this post's example, and how one LogFormat line in the access log turns into a table of functions. The %{…}x we met at the end of section 7 shows up there again. It continues in part 16: Logging — Access Logs, Error Logs, and Wrapping Up the Series.

The Spring series has no post on TLS. Still, the shape of "wrap the request and response from both sides, while the code inside doesn't know it's there" resembles the servlet filters in Spring part 20. Where a servlet filter wraps request objects, mod_ssl's filter works further down, wrapping the connection's bytes.

More posts