Skip to content

TLS: add proxy.config.ssl.server.multicert.partial_reload#13373

Open
Jabrique wants to merge 1 commit into
apache:masterfrom
Jabrique:feat/ssl-multicert-partial-reload
Open

TLS: add proxy.config.ssl.server.multicert.partial_reload#13373
Jabrique wants to merge 1 commit into
apache:masterfrom
Jabrique:feat/ssl-multicert-partial-reload

Conversation

@Jabrique

@Jabrique Jabrique commented Jul 9, 2026

Copy link
Copy Markdown

When a traffic_ctl config reload encounters a broken entry in
ssl_multicert.yaml, the current behavior is all-or-nothing: the
entire new SSLCertLookup is thrown away and the old configuration stays
active. This is safe but operationally painful, a single bad cert
blocks all other cert updates from going live until the broken one is
fixed.

This PR adds proxy.config.ssl.server.multicert.partial_reload (INT,
default 0). When set to 1, a reload that finds one or more unloadable
certs still commits the rest of the certs rather than discarding
everything. Any skipped cert produces an ERROR in diags.log and
increments the new proxy.process.ssl.ssl_multicert_load_failures
counter, so the degraded state is never silent.

The default (0) keeps the existing strict/atomic behaviour; no
existing deployments are affected.

Implementation notes:

  • The commit gate is in SSLCertificateConfig::reconfigure(). The new
    partialCommit path only activates when configPartialReload == 1
    AND lookup->count() > 0. The count check uses SSLCertLookup::count()
    to verify that at least one real cert was successfully inserted into
    ssl_storage. This correctly handles both SNI-only deployments (no
    dest_ip: "*" entry) and the all-certs-fail case (count == 0 means
    there is nothing useful to commit). An earlier guard based on
    SSL_CTX_get0_certificate(lookup->ssl_default) was replaced because
    it silently disabled partial reload for SNI-only configurations, which
    always have a bare bootstrap context as ssl_default regardless of
    whether other certs loaded successfully.

  • retStatus is flipped to true after a partial commit only on
    live reloads
    (i.e. when configid != 0 at entry). At initial
    startup the lookup is always committed anyway (via the existing
    configid == 0 path), and retStatus must stay false so that
    startup() can still honour exit_on_load_fail. This distinction
    is captured via a const bool initialLoad = (configid == 0) that is
    read before configProcessor.set() mutates configid.

  • The new metric increment in _load_items() is guarded by a null
    pointer check. Unlike other counters that are only incremented from
    paths that execute after SSLInitializeStatistics(), _load_items()
    can be called during early startup before the stats block is fully
    initialized. The guard is intentional defensive programming, not an
    inconsistency.

  • This knob applies only to SSLCertificateConfig (TLS server certs).
    QUICCertConfig (HTTP/3) retains strict all-or-nothing semantics
    and is documented as a known limitation.

  • The comment block in reconfigure() above the hasAnyCert/partialCommit
    declarations explicitly documents both branches: count()==0 keeps
    retStatus false (reload reported as failed, lookup discarded); count()>0
    with partial_reload=1 flips retStatus to true after the partial commit.

Files changed:

File Change
src/records/RecordsConfig.cc Register new knob (RECU_DYNAMIC)
src/iocore/net/P_SSLConfig.h Add configPartialReload field
src/iocore/net/SSLConfig.cc Partial-commit gate in reconfigure() with startup guard
src/iocore/net/SSLStats.h/.cc New counter ssl_multicert_load_failures
src/iocore/net/SSLUtils.cc Increment counter on cert load failure (with guard + comment)
doc/admin-guide/files/records.yaml.en.rst Document new knob (:reloadable:, QUIC limitation note)
doc/admin-guide/monitoring/statistics/core/ssl.en.rst Document new metric
tests/gold_tests/tls/ssl_multicert_partial_reload.test.py New gold test

Testing:

Gold test ssl_multicert_partial_reload covers five scenarios:

  • Scenario A strict mode (default): 1 bad cert causes whole reload
    to fail, old config stays active. Verifies ssl_multicert_load_failures
    counter increments even in strict mode.
  • Scenario B partial mode with dest_ip: "*": bad cert skipped,
    new default cert (distinct CN reloaded.example.com) committed, proving
    new config was applied, not old one silently retained. Verifies ERROR in
    diags.log and ssl_multicert_load_failures counter >= 1.
  • Scenario C SNI-only partial reload: config without any
    dest_ip: "*" entry (CDN-style). Verifies partial reload succeeds and
    good SNI certs remain accessible.
  • Scenario D wildcard default fails but SNI cert succeeds: verifies
    partial commit still occurs (lookup->count() > 0 despite ssl_default
    being a bare bootstrap context).
  • Scenario E all certs fail with partial_reload=1: verifies reload
    is reported as failed (count()==0 keeps retStatus false), old config
    stays active, and failure counter still increments. This pins the behavior
    documented in the comment above hasAnyCert against accidental reversion.

No regressions against the existing ssl_multicert_loader gold test.

Known limitations / future work:

  • QUICCertConfig (HTTP/3) does not support partial reload yet.

@Jabrique Jabrique marked this pull request as draft July 9, 2026 07:43
@Jabrique Jabrique force-pushed the feat/ssl-multicert-partial-reload branch from 324571d to a632611 Compare July 9, 2026 08:09
@Jabrique Jabrique marked this pull request as ready for review July 9, 2026 08:21
@Jabrique Jabrique force-pushed the feat/ssl-multicert-partial-reload branch from a632611 to 71a042c Compare July 9, 2026 09:55
@JosiahWI JosiahWI added this to the 11.0.0 milestone Jul 9, 2026
@bryancall bryancall requested a review from Copilot July 9, 2026 16:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds an operator-controlled “partial commit” mode for TLS server multicert reloads so that a single bad ssl_multicert.yaml entry doesn’t block unrelated certificate updates, while still surfacing the degraded state via ERROR logging and a new metric.

Changes:

  • Add proxy.config.ssl.server.multicert.partial_reload (dynamic, default 0) and gate partial-commit behavior in SSLCertificateConfig::reconfigure().
  • Introduce proxy.process.ssl.ssl_multicert_load_failures and increment it on per-item certificate load failures.
  • Add documentation and a new gold test covering strict vs partial reload behavior (including SNI-only and “all fail” cases).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/gold_tests/tls/ssl_multicert_partial_reload.test.py New gold test exercising strict vs partial reload behavior and validating metric/log outcomes.
src/records/RecordsConfig.cc Registers the new proxy.config.ssl.server.multicert.partial_reload runtime knob.
src/iocore/net/P_SSLConfig.h Adds configPartialReload to SSL config parameters.
src/iocore/net/SSLConfig.cc Implements the partial-commit gate in SSLCertificateConfig::reconfigure() with a startup-vs-reload distinction.
src/iocore/net/SSLStats.h Declares the new ssl_multicert_load_failures counter pointer in the SSL stats block.
src/iocore/net/SSLStats.cc Creates the new proxy.process.ssl.ssl_multicert_load_failures metric in SSLInitializeStatistics().
src/iocore/net/SSLUtils.cc Increments the new metric on per-cert load failure (with a startup guard).
doc/admin-guide/files/records.yaml.en.rst Documents the new knob, reload semantics, and QUIC limitation note.
doc/admin-guide/monitoring/statistics/core/ssl.en.rst Documents the new ssl_multicert_load_failures counter.

Comment on lines +200 to +202
# The bad cert must still produce an ERROR log entry (partial does not silence errors)
ts_partial.Disk.diags_log.Content = Testers.IncludesExpression('ERROR', 'bad cert should still produce an error log entry')

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. 'ERROR' is too broad. One note on the suggested fix though "Failed to load certificate 'does_not_exist.pem'" is from errata.note(), which goes to the JSONRPC output, not diags.log. What actually shows up in the log is from SSLError():

failed to load certificate secret for .../does_not_exist.pem with key path ...

Comment thread src/iocore/net/SSLUtils.cc Outdated
Comment on lines +1966 to +1968
// Guard required: _load_items() runs before SSLInitializeStatistics() during
// early startup in some configurations (e.g. tests). Other counters are
// incremented only from paths that cannot execute before stats init.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

When this knob is set to 1, a live traffic_ctl config reload that
hits one or more certificate load failures now commits the partial
SSLCertLookup (all certs that loaded cleanly) rather than discarding
the entire new configuration and keeping the old one.

The commit gate in reconfigure() uses lookup->count() to verify that
at least one cert was successfully inserted into ssl_storage before
committing the partial result. This correctly handles both SNI-only
deployments (no dest_ip: "*" entry, which always have a bare bootstrap
context as ssl_default) and the all-certs-fail case (count == 0 means
nothing useful to commit).

lookup is committed unconditionally via the configid==0 path anyway,
and retStatus must stay false so that startup() can still honour
exit_on_load_fail. The initial value of configid is captured before
configProcessor.set() mutates it.

Any skipped cert still produces an ERROR entry in diags.log and
increments the new proxy.process.ssl.ssl_multicert_load_failures
counter, so the degraded state is never silent. The counter is also
incremented in strict mode (0) for use as an alert signal.

The null-check guard on the counter increment in SSLUtils.cc is
required because SSLCertificateConfig::startup() (which calls
_load_items()) always runs before SSLInitializeStatistics() in the
normal startup path, so the counter pointer is nullptr during the initial cert load.

Default is 0, preserving the existing strict/atomic behavior.
This knob applies only to SSLCertificateConfig. QUICCertConfig
retains strict semantics and is documented as a known limitation.

Add a gold test covering five scenarios:
- A: strict mode, bad cert, reload fails, old config retained,
     counter increments even in strict mode.
- B: partial mode + wildcard default, bad cert skipped, new cert
     committed (verified by distinct CN), specific SSLError()
     message confirmed in diags.log, counter >= 1.
- C: SNI-only (no dest_ip: "*"), partial reload succeeds and good
     SNI cert remains accessible.
- D: wildcard default fails, SNI cert succeeds, partial commit still
     occurs (count()>0 despite ssl_default being bare context).
- E: all certs fail with partial_reload=1, reload reported as failed
     (count()==0 keeps retStatus false), old config stays active,
     counter increments. Pins the count()==0 behavior against
     accidental reversion.
@Jabrique Jabrique force-pushed the feat/ssl-multicert-partial-reload branch from 71a042c to b8ab221 Compare July 10, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants