Skip to content

feat: support separate client TLS certificates#27543

Open
gwossum wants to merge 18 commits into
master-1.xfrom
gw/clientCert
Open

feat: support separate client TLS certificates#27543
gwossum wants to merge 18 commits into
master-1.xfrom
gw/clientCert

Conversation

@gwossum

@gwossum gwossum commented Jul 15, 2026

Copy link
Copy Markdown
Member
  • Support for separate server and client certificates in dual
    client/server TLSConfigManager objects.
  • Fix issue with subscriber not updating TLS certificate path on reload.
  • Subscriber now reloads all TLS parameter, not just certificate.
  • Fix issue with subscriber not propagating new TLS configuration
    to existing clients.
  • Certificate monitoring will now certificate loaders using the same
    certificate to avoid overly noisy logs.
  • Server, client, and client/server TLSConfigManager objects are now
    distinct and provide better checks based on their role.
  • General refactoring of TLSConfigManager and support classes.

gwossum added 4 commits July 14, 2026 23:08
- Support for separate server and client certificates in dual
  client/server TLSConfigManager objects.
- Certificate monitoring will now certificate loaders using the same
  certificate to avoid overly noisy logs.
- Server, client, and client/server TLSConfigManager objects are now
  distinct and provide better checks based on their role.
- General refactoring of TLSConfigManager and support classes.
Fix subscriber service so it now does a full TLS configuration reload.
Previously the certificate was not being propagated to existing
subscriber clients. Also added doing a full TLS configuration reload for
all configs, including CAs.
Remove unused and misleading *tls.Config parameter from NewHTTPS.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the pkg/tlsconfig subsystem to support distinct server and client certificates (and roles) and integrates that into multiple services (subscriber/httpd/opentsdb), including improved runtime reload behavior and centralized certificate monitoring to reduce log noise.

Changes:

  • Introduces role-aware TLSConfigManager APIs (server-only, client-only, dual-role) with separate server/client certificate loaders and reconfiguration support.
  • Updates subscriber HTTPS dialing to resolve TLS configuration per connection (so reloads reach existing writers) and adds focused TLS reload tests.
  • Adds TLSCertMonitor to centralize expiration/validity monitoring across loaders and deduplicate warnings for shared cert/key pairs.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
services/subscriber/tlsreload_internal_test.go New internal tests validating subscriber TLS reload behavior (cert, CA pool, insecure flags, existing vs new connections).
services/subscriber/service.go Injects cert monitor, uses TLSManagerOpts, reconfigures TLS manager on reload, and switches HTTPS writers to DialTLSContext.
services/subscriber/service_test.go Updates tests to construct/clean up a shared cert monitor for the subscriber service.
services/subscriber/mtls_internal_test.go Updates mTLS tests to use the new TLS config manager APIs and per-connection dialing.
services/subscriber/http.go Switches HTTPS client configuration from fixed *tls.Config to DialTLSContext for per-connection TLS resolution.
services/subscriber/config.go Adds TLSManagerOpts() to centralize mapping from subscriber config to TLS manager options.
services/opentsdb/service.go Switches to NewServerTLSConfigManager and reconfigure-based reload.
services/opentsdb/service_test.go Updates tests for new service constructor signature and cert monitor usage.
services/httpd/service.go Switches to NewServerTLSConfigManager and reconfigure-based reload; injects cert monitor.
services/httpd/service_test.go Updates httpd tests to provide a cert monitor to the service.
services/httpd/config_test.go Updates config test to construct httpd service with cert monitor.
pkg/tlsconfig/tls_config_test.go Adds tests for parsing/validating TLS cipher/version config behavior.
pkg/tlsconfig/loadcert.go Extracts/introduces a LoadCertificate helper with permission checks and richer certificate metadata.
pkg/tlsconfig/loadcert_test.go Adds tests for LoadCertificate, loaded-certificate helpers, and expiration/prematurity logging behavior.
pkg/tlsconfig/configmanager.go Major refactor: role-aware managers, separate server/client loaders, DialContext, and PrepareReconfigure.
pkg/tlsconfig/clientcert_test.go Adds tests for separate client certificate behavior and reconfiguration semantics.
pkg/tlsconfig/certmonitor.go Introduces TLSCertMonitor to batch and deduplicate certificate issue warnings across loaders.
pkg/tlsconfig/certmonitor_test.go Adds tests for monitor defaults, trigger behavior, deduplication, and shutdown safety.
pkg/tlsconfig/certconfig.go Refactors TLSCertLoader to integrate with the shared monitor, roles, and new load/reconfigure flow.
pkg/tlsconfig/certconfig_test.go Updates cert loader tests to use the new monitor/role-based APIs and new logging behavior.
pkg/testing/helper/helpers.go Makes CheckedClose fail fast on nil closers to improve test safety.
cmd/influxd/run/server.go Adds a shared TLSCertMonitor to influxd server lifecycle and threads it into services; updates subscriber reload call.
client/v2/client.go Adds support for DialTLSContext on the HTTP transport so TLS can be resolved per connection (non-proxied HTTPS).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/opentsdb/service_test.go
Comment thread services/opentsdb/service_test.go Outdated
Comment thread pkg/tlsconfig/configmanager.go Outdated
Comment thread pkg/tlsconfig/configmanager.go
Comment thread pkg/tlsconfig/configmanager.go Outdated
Comment thread cmd/influxd/run/server.go Outdated
Comment on lines +633 to +636
// Reload the subscriber's client TLS certificate at its currently
// configured path so rotated certificates are picked up.
if s.Subscriber != nil {
if af, err := s.Subscriber.PrepareReloadTLSCertificates(); err == nil {
if af, err := s.Subscriber.PrepareReloadTLSCertificates(config.Subscriber); err == nil {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comment will be updated in next push.

gwossum added 3 commits July 15, 2026 11:59
- Add support for full httpd TLS configuration reloading.
- Add usage to production TLS managers for improved logging.
- Fix some existing test issues.
- Server certificates must now have the server use EKU set.
- Added options to turn the sanity check errors into warnings.
- Add checks for client certificate client usage EKU. This check is more
  subtle than the server certificate server usage EKU check in order to
  avoiding currently working configurations.

@devanbenz devanbenz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A few comments so far. Still working through the PR.

Comment thread pkg/tlsconfig/certmonitor.go Outdated
m.queuedCertLoaders = append(m.queuedCertLoaders, cl)
m.mu.Unlock()

m.resetTrigger(m.triggerDelayInterval)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is it okay that triggerDelayInterval is read outside of a mutex? https://github.com/influxdata/influxdb/pull/27543/changes#diff-311ffb824cadc6a7531617ae06abf39e0920666d6336ec3184814784adf3ee02R264 locks it during modification. Should we just do a defer unlock here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch! The lock is purposely released before sending on a channel to avoid a potential deadlock, which is why defer is not used. Normally I'd make a lambda for it, but I didn't like the way it looked when I was coding it. I've pushed changes which capture the mutex protected while the lock is held to avoid this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This has been fixed and pushed.

Comment thread pkg/tlsconfig/certmonitor.go Outdated
m.certExpirationAdvanced = expirationAdvanced
m.mu.Unlock()

m.resetTrigger(m.triggerDelayInterval)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just pushed a fix and comment explaining why the channel send is done outside the lock.

// can be replaced by:
// defer CheckedClose(t, c)()
func CheckedClose(t require.TestingT, c io.Closer) func() {
require.NotNil(t, c, "CheckedClose cannot be used with a nil io.Closer")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You'll never guess why I added it 😅

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.

I feel seen

m.certCheckInterval = checkInterval
m.mu.Unlock()

m.resetCheckInterval(checkInterval)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pushed fix and comment.

Comment thread pkg/tlsconfig/configmanager.go Outdated
gwossum added 5 commits July 15, 2026 16:07
Fixed NewDisabledTLSConfigManager so that the returned
TLSConfigManager can both dial and listen plaintext.
- Fix mutex protected fields access without lock.
- Add comments explaining why trigger channel sends occur
  outside of lock.
Renamed config options `ignore-sanity-checks` for subscriber and
opentsdb services to `ignore-cert-sanity-checks`. This makes it obvious
the sanity checks being ignored are for certificates and not general
sanity checks. This naming matches other projects.

@davidby-influx davidby-influx 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.

Partial review to unblock you

Comment thread pkg/tlsconfig/certconfig.go Outdated
require.NotNil(t, x509Cert)
require.NotNil(t, x509Cert.SerialNumber)
require.Equal(t, goodSerial, *x509Cert.SerialNumber)

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.

To Do

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

TODO is a standard programming convention and has built-in support in many tools, including GoLand (https://www.jetbrains.com/help/go/using-todo.html)

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.

sorry, I was unclear. I meant, should we do this TODO?

Comment thread pkg/tlsconfig/certconfig_test.go Outdated
Comment thread pkg/tlsconfig/certconfig_test.go Outdated
require.WithinDuration(t, notAfter, timeExpires, 2*time.Minute, "untilExpires varies more than expected")
logs.TakeAll() // dump all logs
}
time.Sleep(testWarnWaitTime)

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.

I'm always worried about arbitrary waiting in tests, which is in a few places in here. No simple trigger available (without production code changes)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This wait is required because the actually logging is queued up in another goroutine. The timeout on other goroutien is set to 0 should it should be almost instantaneous. I haven't had any failures yet.

Comment thread pkg/tlsconfig/certconfig_test.go
Comment thread pkg/tlsconfig/certconfig_test.go

// monitor periodically logs certificate errors with the currently registered
// TLSCertLoader objects.
func (m *TLSCertMonitor) monitorCerts(startWg *sync.WaitGroup, stopWg *sync.WaitGroup) {

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.

Are these waitgroups in the struct?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

They are. Passing them into the goroutine like this is a habit I've gotten into because it has some advantages in some places. Not here, though.

@davidby-influx davidby-influx 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.

Another partial review to keep you unblocked

Comment thread pkg/tlsconfig/certconfig.go

return err
c := &tlsCertLoaderConfig{}
*c = *cl.config

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.

Any need for a deeper copy?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's all strings, bools, and a pointer to the logger. No need for a deep copy. After needing a deep copy was going to cause me issues with the tlsConfigManagerConfig type, I made a conscious decision to make the config structs so they did not require deep copies, so they don't keep any slices or structs that are directly modified. It's all primitive data types and pointers to structs that are either conserved or completely overwritten.

Comment thread services/subscriber/service_test.go

require.NoError(t, m.Close())
require.NoError(t, m.Close())
m.WaitForMonitorStop()

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.

Should the WaitForMonitor be in between the two Close calls to show only one routine was started?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a test for idempotency. If we really wanted that test, it should be added to another test case. TestTLSCertMonitor_ClosedMonitorDoesNotBlock does kind of test that. I'm not too worried about starting up multiple goroutines because sync.Once is used to ensure we only startup one goroutine.

Comment thread pkg/tlsconfig/loadcert.go

if xc.IsExpired() {
log.Warn("Certificate is expired", zap.Time("NotAfter", xc.NotAfter))
} else if xc.IsPremature() {

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.

Do we want to log all the issues, not just the first encountered (i.e., eliminate the elses)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A certificate cannot simultaneously be not valid yet, about to expire, and expired.

Comment thread services/httpd/tlsreload_test.go Outdated
if err != nil {
return err
}
defer conn.Close()

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.

you know what I want here... The magic of CheckedClose!

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.

Or the error capture from the defer into the return value.

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.

That might be better

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is one of those situations where the failed handshake maybe closes the connection before you get to this Close, so you might get no error or an unpredictable error. This error doesn't seem to have any TLS indication, so the best we can do is say Close should not return an error if the overall handshake function does not return an error. This will be in the next push.

gwossum added 3 commits July 16, 2026 16:07
- Improve error checking in tests.
- Use th.CheckedClose in more places.
- Fix some spelling mistakes.

@davidby-influx davidby-influx 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.

One pass through all files. But I see changes have been made

// multiple times, but it will only start one monitor goroutine.
func (m *TLSCertMonitor) Open() error {
m.startOnce.Do(func() {
go m.monitorCerts(&m.monitorStartWg, &m.monitorStopWg)

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.

why pass in members of the receiver? do we ever use externa, ones (in tests, perhaps)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Habit. Sometimes it helps decouple things, but not in this case since the goroutine still accesses m for other reasons.

Comment thread cmd/influx/cli/cli_mtls_test.go Outdated
c.IgnoreSignals = true
c.ForceTTY = true

require.Error(t, c.Run())

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.

Do we know anything about the error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in latest push

@davidby-influx davidby-influx 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.

Super close! Quite the body of work!

I will leave this for @devanbenz to follow up on any more changes.

require.NotNil(t, x509Cert)
require.NotNil(t, x509Cert.SerialNumber)
require.Equal(t, goodSerial, *x509Cert.SerialNumber)

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.

sorry, I was unclear. I meant, should we do this TODO?

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.

4 participants