feat: NATS mTLS pubsub implementation (#26902)

This commit is contained in:
Callum Styan
2026-07-13 11:00:02 -07:00
committed by GitHub
parent 010d96c3cd
commit ad29777cb2
22 changed files with 1776 additions and 57 deletions
+41 -2
View File
@@ -206,6 +206,13 @@ type Options struct {
TLSCertificates []tls.Certificate
TailnetCoordinator tailnet.Coordinator
DERPServer *derp.Server
// ClusterHost is this replica's routable cluster address (IP or hostname),
// resolved from DeploymentValues.Cluster.Host, falling back to the DERP
// relay host for older HA deployments that predate the setting. It is used
// as the NATS cluster route host and, when it is an IP, the cluster mTLS
// leaf IP SAN. It is consumed by the NATS pubsub (AGPL) and, under
// enterprise HA, by replicasync.
ClusterHost string
// BaseDERPMap is used as the base DERP map for all clients and agents.
// Proxies are added to this list.
BaseDERPMap *tailcfg.DERPMap
@@ -306,7 +313,12 @@ type Options struct {
AppSigningKeyCache cryptokeys.SigningKeycache
AppEncryptionKeyCache cryptokeys.EncryptionKeycache
OIDCConvertKeyCache cryptokeys.SigningKeycache
Clock quartz.Clock
// NATSCACache serves the NATS cluster mTLS CA via the generic signing key
// cache for the nats_ca feature. SigningKey returns the active CA
// (a *NATSCA); VerifyingKey returns a specific CA by sequence. The key
// rotator is the sole creator of nats_ca rows, so this cache is read-only.
NATSCACache cryptokeys.SigningKeycache
Clock quartz.Clock
// WebPushDispatcher is a way to send notifications over Web Push.
WebPushDispatcher webpush.Dispatcher
@@ -608,10 +620,34 @@ func New(options *Options) *API {
updatesProvider := NewUpdatesProvider(options.Logger.Named("workspace_updates"), options.Pubsub, options.Database, options.Authorizer)
// The NATS cluster CA is only minted and served when NATS pubsub is in use.
// It is experiment-gated, so it is opted into rotation and backed by a real
// signing cache only when the experiment is enabled; otherwise the rotator
// leaves it alone and the cache is a noop, which still answers requests (the
// pubsub treats a missing CA as "mTLS off"). This avoids minting CA private
// keys on deployments that never run NATS clustering.
rotatedFeatures := cryptokeys.DefaultRotatedFeatures()
if experiments.Enabled(codersdk.ExperimentNATSPubsub) {
rotatedFeatures = append(rotatedFeatures, database.CryptoKeyFeatureNATSCA)
}
// Start a background process that rotates keys. We intentionally start this after the caches
// are created to force initial requests for a key to populate the caches. This helps catch
// bugs that may only occur when a key isn't precached in tests and the latency cost is minimal.
cryptokeys.StartRotator(ctx, options.Logger, options.Database)
cryptokeys.StartRotator(ctx, options.Logger, options.Database, cryptokeys.WithFeatures(rotatedFeatures))
// The NATS CA cache is read-only and depends on the rotator having minted
// the nats_ca CA, so it must be constructed after StartRotator.
if options.NATSCACache == nil {
if experiments.Enabled(codersdk.ExperimentNATSPubsub) {
options.NATSCACache, err = cryptokeys.NewSigningCache(ctx, options.Logger.Named("nats_ca_cache"), &cryptokeys.DBFetcher{DB: options.Database}, codersdk.CryptoKeyFeatureNATSCA)
if err != nil {
options.Logger.Fatal(ctx, "failed to instantiate NATS CA cache", slog.Error(err))
}
} else {
options.NATSCACache = cryptokeys.NoopSigningKeycache{}
}
}
// Ensure all system role permissions are current.
//nolint:gocritic // Startup reconciliation reads/writes system roles. There is
@@ -2413,6 +2449,9 @@ func (api *API) Close() error {
_ = api.OIDCConvertKeyCache.Close()
_ = api.AppSigningKeyCache.Close()
_ = api.AppEncryptionKeyCache.Close()
if api.NATSCACache != nil {
_ = api.NATSCACache.Close()
}
_ = api.UpdatesProvider.Close()
api.workspaceAgentConnWatcher.Close()
api.workspaceBuildOrchestrator.Close()