Skip to content

ssl.chain.expiry metrics do not update for dynamically registered SSL bundles #48144

@TerryTaoYY

Description

@TerryTaoYY

Bug report

Description

When SslMeterBinder is constructed, it only registers update handlers for the SSL bundles that already exist at construction time (those returned by sslBundles.getBundleNames()).

If an SslBundle is registered after the SslMeterBinder has been created, the register handler runs once and onBundleChange(bundleName) is invoked, but no update handler is ever attached for that bundle. Subsequent calls to SslBundleRegistry.updateBundle(bundleName, ...) do not trigger onBundleChange again, so the ssl.chain.expiry gauges never reflect refreshed certificates for dynamically registered bundles.

This looks like a bug in the metrics binding rather than an intentional limitation, as ssl.chain.expiry is documented as tracking the expiry of SSL bundle certificate chains.

Affects Version(s)

  • 4.0.0-RC2

How to reproduce

A minimal way to reproduce is:

  1. Create a DefaultSslBundleRegistry.
  2. Construct an SslMeterBinder with:
    • a mocked SslInfo that returns no bundles from getBundles()
    • the DefaultSslBundleRegistry
    • a fixed Clock
  3. Bind the SslMeterBinder to a MeterRegistry.
  4. After the binder has been constructed and bound, register a new bundle:
   sslBundleRegistry.registerBundle("dynamic", bundle);

This will call onBundleChange("dynamic") once.
5. Update the same bundle:

sslBundleRegistry.updateBundle("dynamic", bundle);
  1. Observe that SslInfo.getBundle("dynamic") is not called again and the ssl.chain.expiry gauges do not reflect the updated certificate chain.

This behavior can be reproduced with a unit test similar to:

// SslMeterBinderTests.shouldWatchUpdatesForBundlesRegisteredAfterConstruction
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
SslInfo sslInfo = mock(SslInfo.class);
when(sslInfo.getBundles()).thenReturn(List.of());

// configure mocks for bundle "dynamic" and its certificate chain...

SslMeterBinder binder = new SslMeterBinder(sslInfo, sslBundleRegistry, CLOCK);
SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
binder.bindTo(meterRegistry);

SslBundle bundle = mock(SslBundle.class);
sslBundleRegistry.registerBundle("dynamic", bundle);
sslBundleRegistry.updateBundle("dynamic", bundle);

// Fails with the current implementation: only 1 invocation
verify(sslInfo, atLeast(2)).getBundle("dynamic");

With the current implementation, the verification above fails with Mockito's "TooFewActualInvocations", showing that bundle updates do not trigger another onBundleChange.

Expected behavior

For dynamically registered bundles, I would expect ssl.chain.expiry metrics to be kept in sync with the underlying SSL bundle, just like for bundles that already exist when SslMeterBinder is constructed.

In practice, this means that when a new bundle is registered, an update handler should be attached for that bundle so that subsequent SslBundleRegistry.updateBundle(...) calls cause the corresponding ssl.chain.expiry gauges to be refreshed.

Actual behavior

  • For bundles present at binder construction time:

    • Both register and update handlers are attached.
    • ssl.chain.expiry is updated when the bundle is updated.
  • For bundles registered after binder construction:

    • Only the register handler runs once.
    • No update handler is attached for the new bundle.
    • SslBundleRegistry.updateBundle(...) does not trigger onBundleChange again.
    • ssl.chain.expiry remains at the value computed at registration time and does not reflect refreshed certificates.

Workaround

There is no straightforward workaround at the application level, since the registration and update callbacks are handled inside SslMeterBinder and SslBundles/SslBundleRegistry.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: supersededAn issue that has been superseded by another

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions