WL#17271: Dynamic reload of TLS certificates in Router

Affects: MySQL-Router-26.x   —   Status: Complete

Description
-----------

MySQL Router reads TLS material when its plugins start. Replacing or renewing a
certificate, private key, CA file, or CRL therefore currently requires a
Router restart before new TLS handshakes use the replacement files. A restart
disconnects all active client sessions. Administrators need to renew or replace
this material without an outage, while Router retains its known-good TLS state
when replacement files are incomplete or invalid.

User stories
------------

* As a Router administrator, I can replace TLS files at their configured
  locations and request a reload without restarting Router.
* As a Router administrator, I can use the same `mysqlrouter --reload-tls`
  command on supported platforms, while supplying the platform-specific target
  of the running Router instance.
* As a connected client, my existing connection is not disconnected by a TLS
  reload. A subsequently established TLS connection uses the replacement
  material after a successful reload.

Scope
-----

This worklog adds an asynchronous TLS-material reload to production Router
runtime components that own a TLS context or a configured TLS client state,
including:

* the `routing` plugin for inbound client-to-Router TLS termination;
* the `routing` plugin for outbound Router-to-destination TLS connections;
* the HTTPS listener provided by `http_server`; and
* `metadata_cache` connections that use configured client certificate, key,
  CA, or CRL files.

For each covered component, reload rereads only the configured certificate,
private key, CA, CA directory, CRL, and CRL directory paths. A component may
have no configured material of a given type; this is not an error. Reload uses
the paths accepted at Router startup and does not reread or apply the Router
configuration file.

Example: certificate replacement
--------------------------------

Router is configured with stable paths below a `current` symbolic link, for
example:

    [routing:example]
    client_ssl_cert=/etc/mysqlrouter/tls/current/router-cert.pem
    client_ssl_key=/etc/mysqlrouter/tls/current/router-key.pem

The `current` link points to the active date-versioned directory, for example:

    /etc/mysqlrouter/tls/current -> /etc/mysqlrouter/tls/2026-07-20

To renew the certificate, the administrator creates a new dated directory with
the replacement files, changes `current` to point to that directory, and then
requests reload:

    mysqlrouter --reload-tls --pid-file=/run/mysqlrouter/mysqlrouter.pid

The Router service account must be able to read the replacement files. If the
replacement material is invalid, the reload fails for the affected TLS owner
and Router continues to use its previously loaded TLS state.

Out of scope
------------

The following are outside the scope of this worklog:

* rereading `mysqlrouter.conf` or changing configured TLS paths, TLS modes,
  cipher suites, TLS versions, listener addresses, ports, or plugin loading;
* enabling TLS for a component that was configured without it;
* changing Router configuration through a file watcher;
* modifying TLS passthrough connections, for which Router does not terminate
  TLS or own a TLS context;
* reloading `mock_server`, which is a test tool and not a production Router
  runtime component; and
* changing existing client or server connections. Existing sessions continue
  to use the TLS state selected when their handshakes completed.

Dependencies
------------

The implementation depends on each covered TLS owner being able to construct
and validate replacement TLS state before making it visible to new
connections. The Windows implementation depends on the Router Windows Service
runtime and the Windows named-event APIs.
Functional and Non-Functional Requirements
==========================================

Reload request and public interface
-----------------------------------

FR1 - MySQL Router SHALL provide `--reload-tls` as the command-line operation
for requesting a TLS-material reload from a running Router instance without
starting another Router instance.

FR1.1 - On POSIX platforms, `--reload-tls` SHALL require exactly one of
`--pid=PID` and `--pid-file=PATH`.

FR1.2 - `--pid` SHALL accept a positive decimal process identifier.

FR1.3 - `--pid-file` SHALL name a readable file containing one positive
decimal process identifier.

FR1.4 - On Windows, `--reload-tls` SHALL accept `--service-name=NAME`. If the
option is omitted, it SHALL target the default MySQL Router service name.

FR1.5 - Options applicable only to another platform, both POSIX target
options together, or a missing required POSIX target option SHALL cause the
command to fail without requesting a reload.

FR1.6 - The command SHALL return success after the reload request is delivered
to the target Router. It SHALL not wait for TLS-file I/O or report the result
of the asynchronous reload operation.

FR1.7 - If the target cannot be found, the PID is invalid, the PID file cannot
be read, the caller lacks permission, or the Windows service reload event
cannot be opened, the command SHALL fail with a diagnostic identifying the
failed operation.

FR2 - Router already uses `SIGHUP` on POSIX platforms to reopen log files.
This worklog SHALL extend that signal handling to also request a TLS-material
reload.

FR2.1 - A TLS reload failure caused by `SIGHUP` SHALL not request Router
shutdown. This exception applies only to TLS reload; existing fatal handling
of a log reopen failure is unchanged.

FR3 - On Windows, a Router instance running as a Windows service SHALL expose
a named event dedicated to TLS reload.

FR3.1 - The event name SHALL identify the target Router service instance.

FR3.2 - The event SHALL be separate from service start and shutdown events.

FR3.3 - Only principals authorized to administer the target service SHALL be
granted permission to signal the event.

TLS reload behavior
-------------------

FR4 - A TLS reload SHALL reread only TLS material from paths accepted by the
running Router configuration at startup. It SHALL not reload the Router
configuration file.

FR5 - Each covered production TLS owner SHALL independently attempt to reload
its configured certificate, private key, CA, CA directory, CRL, and CRL
directory material, as applicable.

FR5.1 - A TLS owner SHALL validate all replacement material required for the
replacement context before making any of that context available to new
connections.

FR5.2 - If validation or loading fails for a TLS owner, that owner SHALL retain
its previous usable TLS state and SHALL log an error identifying the owner and
the failure.

FR5.3 - A failure in one TLS owner SHALL not prevent other TLS owners from
attempting their reloads.

FR6 - After a TLS owner successfully reloads, new TLS handshakes or new
outbound TLS connections created by that owner SHALL use its replacement TLS
state.

FR6.1 - Router SHALL not terminate, renegotiate, or otherwise alter existing
client connections, existing outbound connections, or handshakes already in
progress because of a TLS reload.

FR6.2 - Reused outbound connections may continue to use their pre-reload TLS
state until they are closed and a replacement connection is created.

FR7 - Router SHALL serialize TLS reload execution.

FR7.1 - Multiple requests received before a pending reload begins may be
coalesced into one reload.

FR7.2 - If a request is received while a reload is active, Router SHALL perform
another reload after the active reload completes.

Packaging and service-manager interfaces
-----------------------------------------

FR8 - The packaged systemd unit SHALL implement `systemctl reload
mysqlrouter.service` by invoking `mysqlrouter --reload-tls` for the service
main process.

FR9 - The packaged SysV init script `reload` action SHALL
invoke `mysqlrouter --reload-tls --pid-file=PATH`, using the configured Router
PID file, rather than stopping and starting Router.

Non-Functional Requirements
---------------------------

NF1 - Signal handling, Windows service-control callbacks, named-event
notification, and CLI request delivery SHALL not synchronously load or
validate TLS material.

NF2 - The implementation SHALL keep a previous TLS context alive until all
connections and handshakes that already acquired it no longer use it.

NF3 - TLS reload activity, including successful completion and per-owner
failure, SHALL be logged without exposing private-key contents.

NF4 - Default Router startup and runtime behavior is unchanged until a reload
request is received.
High-Level Specification / Interface Specification
===================================================

Overview
--------

MySQL Router adds a process-wide TLS reload coordinator. TLS owners register
a reload callback when their runtime state is initialized. A reload request is
accepted quickly and processed asynchronously by the coordinator. Each owner
constructs replacement TLS state from its startup configuration, validates it,
and publishes it only after validation succeeds.

The coordinator is independent from the existing log reopen worker. On POSIX,
a single `SIGHUP` request schedules both operations.

TLS owner matrix
----------------

| Component | TLS direction | Current TLS state | Reloaded material | Applies to
|
|---|---|---|---|---|
| `routing` | client to Router | inbound server TLS context | Router server
certificate/key and configured trust material | new client handshakes |
| `routing` | Router to destination | destination client TLS contexts | Router
client certificate/key and configured CA/CRL material | new destination
connections |
| `http_server` | HTTPS client to Router | server TLS context | HTTPS
certificate/key and configured trust material | new HTTPS handshakes |
| `metadata_cache` | Router to metadata server | client-library TLS state |
configured client certificate/key and trust material | newly created metadata
connections |

TLS passthrough is excluded because Router does not own its TLS state. The
`mock_server` test tool is excluded from this feature.

Command-line interface
----------------------

The new command-line operation is:

    mysqlrouter --reload-tls [target-option]

POSIX target options are:

    mysqlrouter --reload-tls --pid=PID
    mysqlrouter --reload-tls --pid-file=PATH

The command sends `SIGHUP` to the target process. It does not parse
`mysqlrouter.conf` and does not start a Router instance.

Windows target option is:

    mysqlrouter --reload-tls --service-name=NAME

When no service name is supplied, the command uses the default Router service
name. The command opens the named TLS reload event owned by that service and
signals it. The command succeeds when the request was delivered, not when the
background reload finishes.

POSIX signal interface
----------------------

Router already blocks ordinary signals in worker threads and processes
`SIGHUP` in its signal-handler thread. Its signal-handler map allows one
callback per signal number. The `SIGHUP` callback therefore schedules both:

* the existing log reopen request; and
* a TLS reload coordinator request.

The callback must not perform TLS I/O. The TLS coordinator worker coalesces
pending requests and guarantees a further reload if another request arrives
while a reload is active.

Windows service interface
-------------------------

When Router runs as a Windows service, service startup creates a named,
manual-reset TLS reload event that is distinct from the existing service exit
event. Its name is derived from the service name, allowing independently
installed Router services to be targeted separately. The event security
descriptor grants signal access only to authorized service administrators.

The reload coordinator waits for this event and resets it after accepting a
request. Service shutdown wakes and joins the coordinator before closing the
event handle.

Runtime behavior
----------------

For a TLS owner, reload is a prepare-and-publish operation:

1. Read the configured TLS files from the paths captured at Router startup.
2. Build a replacement TLS context or equivalent client TLS state.
3. Validate the replacement material, including certificate/key compatibility
   and all configured trust material.
4. Atomically publish the replacement for new handshakes or new connections.
5. Retain the previous state until no existing operation uses it.

If any preparation step fails, the owner retains its old state and logs the
error. Other owners continue with their own reload attempts.

Packaging integration
---------------------

The systemd unit adds an `ExecReload` command that invokes
`mysqlrouter --reload-tls --pid=$MAINPID`. The command delivers `SIGHUP` to
the main Router process. This makes the following command the supported
systemd interface:

    systemctl reload mysqlrouter.service

The existing systemd logrotate rule may retain its direct `SIGHUP` delivery or
use `systemctl reload`; both request log reopen and TLS reload.

The SysV init script implements `reload` and `force-reload` with
`mysqlrouter --reload-tls --pid-file=PATH`, rather than the current stop/start
behavior. The Windows MSI does not need to install a service automatically:
Router already provides service installation commands. It ships the new
`--reload-tls` CLI operation and its documentation.
Low-Level Design Specification
==============================

Lifecycle and coordinator
-------------------------

The TLS reload coordinator is initialized after all Router plugins have
started, when TLS owners have initialized their state. It is stopped and joined
before plugin TLS state is destroyed.

`router_app.cc` currently creates `LogReopenComponent` after plugin startup,
registers `SIGHUP`, and removes the handler after the first plugin finishes.
The TLS coordinator follows those lifetime boundaries but is separate from
`LogReopenComponent`: log-reopen and TLS-reload failures have different
shutdown semantics.

`SignalHandler` stores one callback per signal number. The existing `SIGHUP`
callback must request both workers; a second callback cannot be registered.

Reload registration contract
----------------------------

Each covered TLS owner registers a callback with the coordinator. The callback
returns a success/failure result and a loggable error message. The owner keeps
its effective startup TLS settings, including paths and options needed to
construct an equivalent replacement context.

The coordinator runs callbacks outside its request-state lock. It logs each
owner result and continues after a callback failure.

Request coalescing
------------------

The coordinator has `idle`, `requested`, `active`, and `shutdown` states.

* A request in `idle` changes state to `requested` and wakes the worker.
* The worker changes `requested` to `active` and runs one reload pass.
* A request in `active` records a required follow-up pass.
* After a pass, the worker runs the follow-up pass when requested; otherwise,
  it returns to `idle`.
* Shutdown prevents new work and joins the worker.

Unlike the existing log reopen coalescing rule, TLS reload cannot discard a
request received during an active pass. This prevents a completed file
replacement from being missed.

TLS context publication
-----------------------

`routing_plugin.cc` creates inbound `TlsServerContext` objects and stores
`DestinationTlsContext` instances used to construct destination client
contexts. `http_server_plugin.cc` creates the HTTPS `TlsServerContext` owned
by its HTTP server context. These owners must use lifetime-safe shared
references or equivalent synchronization for context replacement.

A replacement context is completely configured before publication. Loading a
certificate or key into the currently published context is not permitted.
Publication occurs under owner synchronization. Existing operations retain the
prior context; new operations obtain the newly published context.

For destination context caches, reload invalidates cached client contexts only
after replacement configuration is validated. New destination connections then
obtain contexts from the new configuration. Existing destination connections
are not closed.

MySQL client channels
---------------------

The `metadata_cache` client-library TLS state retains its startup TLS file
settings and applies them to newly created metadata connections after reload.
The design does not force established metadata connections to reconnect.

Windows details
---------------

`nt_servc.cc` creates an unnamed manual-reset `hExitEvent` used only to end
`ServiceMain`. It must not be reused for reload. The service adds a separate,
named, manual-reset event and a handle for TLS reload.

The event name includes the service name. Its security descriptor grants
`EVENT_MODIFY_STATE` only to administrators and authorized service-management
principals. The service closes the reload event only after the coordinator
worker exits. The CLI uses `OpenEvent(EVENT_MODIFY_STATE, ...)` and
`SetEvent()`.

Packaging changes
-----------------

The following existing files are affected by the design:

* the Router systemd unit gains `ExecReload` using
  `mysqlrouter --reload-tls --pid=$MAINPID`;
* `support-files/mysqlrouter-log-rotate-systemd.in` may use `systemctl reload`
  instead of a direct `systemctl kill -s HUP`;
* `support-files/mysqlrouter-log-rotate.in` continues to use the PID file and
  `SIGHUP`; and
* `packaging/deb-in/mysql-router-packagesource.mysqlrouter.init` changes
  `reload` and `force-reload` from stop/start to
  `mysqlrouter --reload-tls --pid-file=PATH`.

Test plan
---------

Component and integration tests must cover:

* a valid replacement certificate/key pair, where a new inbound TLS
  connection observes the replacement certificate;
* configured CA and CRL reload where applicable;
* unreadable, missing, malformed, and certificate/key-mismatched replacement
  material, where new connections continue to use the old context;
* existing inbound and outbound TLS connections remaining usable across reload;
* new routing destination and MySQL client connections using replacement
  material;
* independent results from multiple TLS owners;
* multiple requests while reload is active, including a replacement between
  the two passes;
* `SIGHUP` reopening a rotated log file while TLS reload is requested;
* POSIX CLI validation, direct PID delivery, PID-file delivery, and failure
  diagnostics;
* systemd `reload` and SysV `reload`/`force-reload`; and
* Windows event naming, permission failure, custom service name, delivery, and
  cleanup during service shutdown.