Skip to content

Commit 8092e63

Browse files
feat: fold the TLS-first handshake into the NATS pub/sub tlsEnabled flag
1 parent e132125 commit 8092e63

6 files changed

Lines changed: 54 additions & 90 deletions

File tree

frontend/docs/content/docs/self-hosting/configuration-options.mdx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,8 @@ Variables marked with ⚠️ are conditionally required when specific features a
318318
| `SERVER_MSGQUEUE_PUBSUB_NATS_URL` | Pub/sub NATS seed URL(s), comma-separated for a cluster | |
319319
| `SERVER_MSGQUEUE_PUBSUB_NATS_USERNAME` | Pub/sub NATS username (sent as a connect option, so reconnects also authenticate) | |
320320
| `SERVER_MSGQUEUE_PUBSUB_NATS_PASSWORD` | Pub/sub NATS password (see username) | |
321-
| `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED` | Pub/sub NATS: when `true`, require verified TLS regardless of the URL scheme | `false` |
321+
| `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED` | Pub/sub NATS: verified TLS with a TLS-first handshake (server must set `handshake_first`) | `false` |
322322
| `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ROOT_CA_FILE` | Pub/sub NATS PEM CA bundle for a private CA; requires `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED=true` | |
323-
| `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_HANDSHAKE_FIRST` | Pub/sub NATS: TLS handshake before the server's `INFO` (server must set `handshake_first`); requires TLS | `false` |
324323
| `SERVER_MSGQUEUE_PUBSUB_NATS_SUBJECT_PREFIX` | Pub/sub NATS subject prefix, joined to topic names with `.` | `hatchet.pubsub` |
325324
| `SERVER_SINGLE_QUEUE_LIMIT` | Single queue limit | `100` |
326325

@@ -340,17 +339,16 @@ password separately. Credentials embedded in the URL are not reapplied when the
340339
client reconnects to a cluster peer it learned about through gossip.
341340

342341
Set `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED=true` to require verified TLS
343-
regardless of the URL scheme; without a CA file the server certificate is
344-
verified against the system roots. If the server's certificate is signed by a
345-
private CA, also point `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ROOT_CA_FILE` at a PEM
346-
CA bundle, which also applies when the client reconnects to rediscovered
347-
cluster peers. The CA file only takes effect with TLS enabled, so Hatchet
348-
refuses to start if it is set while `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED`
349-
is false. A `tls://` URL enables TLS with the system roots via the scheme
350-
alone. If the server sets `handshake_first` in its tls block (the TLS
351-
handshake happens before the server's `INFO` message), also set
352-
`SERVER_MSGQUEUE_PUBSUB_NATS_TLS_HANDSHAKE_FIRST=true`; both sides must agree
353-
on the mode or the connect fails.
342+
with a TLS-first handshake: the handshake happens before the server's `INFO`
343+
message, so the server must set `handshake_first` in its tls block, and a
344+
client with this flag fails to connect to an INFO-first TLS or plaintext
345+
server. Without a CA file the server certificate is verified against the
346+
system roots. If the server's certificate is signed by a private CA, also
347+
point `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ROOT_CA_FILE` at a PEM CA bundle, which
348+
also applies when the client reconnects to rediscovered cluster peers. The CA
349+
file only takes effect with TLS enabled, so Hatchet refuses to start if it is
350+
set while `SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED` is false. A `tls://` URL
351+
does standard INFO-first TLS with the system roots via the scheme alone.
354352

355353
Unlike the other backends, which isolate installations at the connection level
356354
(a RabbitMQ vhost, a Postgres database), NATS isolates by subject. Set

internal/msgqueue/nats/pubsub.go

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,13 @@ type PubSub struct {
2929
type PubSubOpt func(*PubSubOpts)
3030

3131
type PubSubOpts struct {
32-
l *zerolog.Logger
33-
url string
34-
username string
35-
password string
36-
tlsEnabled bool
37-
tlsRootCAFile string
38-
tlsHandshakeFirst bool
39-
subjectPrefix string
32+
l *zerolog.Logger
33+
url string
34+
username string
35+
password string
36+
tlsEnabled bool
37+
tlsRootCAFile string
38+
subjectPrefix string
4039
}
4140

4241
func defaultPubSubOpts() *PubSubOpts {
@@ -70,9 +69,12 @@ func WithPubSubPassword(password string) PubSubOpt {
7069
}
7170
}
7271

73-
// WithPubSubTLSEnabled requires verified TLS regardless of the URL scheme.
74-
// Without a CA file the server certificate is verified against the system
75-
// roots. False leaves TLS to the URL scheme (tls:// uses system roots).
72+
// WithPubSubTLSEnabled requires verified TLS with a TLS-first handshake: the
73+
// handshake happens before the server's INFO message, so the server must set
74+
// handshake_first in its tls block. A client with this flag fails to connect
75+
// to an INFO-first TLS or plaintext server. Without a CA file the server
76+
// certificate is verified against the system roots. False leaves TLS to the
77+
// URL scheme (tls:// does standard INFO-first TLS with system roots).
7678
func WithPubSubTLSEnabled(enabled bool) PubSubOpt {
7779
return func(opts *PubSubOpts) {
7880
opts.tlsEnabled = enabled
@@ -89,18 +91,6 @@ func WithPubSubTLSRootCAFile(path string) PubSubOpt {
8991
}
9092
}
9193

92-
// WithPubSubTLSHandshakeFirst performs the TLS handshake before the server's
93-
// INFO message, for servers with handshake_first in their tls block. Against
94-
// a server that sends INFO first, the client's TLS handshake reads the
95-
// plaintext INFO as a malformed handshake reply and fails the connect within
96-
// the connect timeout. Requires WithPubSubTLSEnabled(true); NewPubSub fails
97-
// fast otherwise.
98-
func WithPubSubTLSHandshakeFirst(handshakeFirst bool) PubSubOpt {
99-
return func(opts *PubSubOpts) {
100-
opts.tlsHandshakeFirst = handshakeFirst
101-
}
102-
}
103-
10494
// WithPubSubSubjectPrefix sets the NATS subject prefix (default
10595
// "hatchet.pubsub"). Empty falls back to the default. No trimming or
10696
// validation: a bad prefix fails loudly via nats ErrBadSubject at startup.
@@ -133,10 +123,6 @@ func NewPubSub(fs ...PubSubOpt) (func() error, *PubSub, error) {
133123
return nil, nil, fmt.Errorf("nats pubsub tlsRootCAFile is set but tlsEnabled is false; a private CA bundle only takes effect with tlsEnabled: true (SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED)")
134124
}
135125

136-
if opts.tlsHandshakeFirst && !opts.tlsEnabled {
137-
return nil, nil, fmt.Errorf("nats pubsub tlsHandshakeFirst is set but tlsEnabled is false; a TLS-first handshake only takes effect with tlsEnabled: true (SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED)")
138-
}
139-
140126
l := opts.l
141127

142128
connectOpts := []natsgo.Option{
@@ -187,6 +173,13 @@ func NewPubSub(fs ...PubSubOpt) (func() error, *PubSub, error) {
187173
// the server is verified against the system roots.
188174
connectOpts = append(connectOpts, natsgo.Secure(&tls.Config{MinVersion: tls.VersionTLS12}))
189175

176+
// tlsEnabled always means a TLS-first handshake (before the server's
177+
// INFO message); the server must set handshake_first in its tls
178+
// block. Against an INFO-first server the client's TLS handshake
179+
// reads the plaintext INFO as a malformed handshake reply and fails
180+
// the connect within the connect timeout.
181+
connectOpts = append(connectOpts, natsgo.TLSHandshakeFirst())
182+
190183
if opts.tlsRootCAFile != "" {
191184
// RootCAs keeps the TLS config set by Secure above and attaches
192185
// the CA pool via a callback, which also applies to rediscovered
@@ -195,10 +188,6 @@ func NewPubSub(fs ...PubSubOpt) (func() error, *PubSub, error) {
195188
// needed here.
196189
connectOpts = append(connectOpts, natsgo.RootCAs(opts.tlsRootCAFile))
197190
}
198-
199-
if opts.tlsHandshakeFirst {
200-
connectOpts = append(connectOpts, natsgo.TLSHandshakeFirst())
201-
}
202191
}
203192

204193
nc, err := natsgo.Connect(opts.url, connectOpts...)

internal/msgqueue/nats/pubsub_tls_test.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,6 @@ func TestNewPubSubTLSRootCAFileRequiresTLSEnabled(t *testing.T) {
2626
assert.Nil(t, ps)
2727
}
2828

29-
func TestNewPubSubTLSHandshakeFirstRequiresTLSEnabled(t *testing.T) {
30-
cleanup, ps, err := NewPubSub(
31-
WithPubSubURL("nats://127.0.0.1:4222"),
32-
WithPubSubTLSHandshakeFirst(true),
33-
)
34-
35-
require.Error(t, err)
36-
assert.Contains(t, err.Error(), "tlsHandshakeFirst")
37-
assert.Contains(t, err.Error(), "tlsEnabled")
38-
assert.Nil(t, cleanup)
39-
assert.Nil(t, ps)
40-
}
41-
4229
func TestNewPubSubTLSRootCAFileMissing(t *testing.T) {
4330
cleanup, ps, err := NewPubSub(
4431
WithPubSubURL("nats://127.0.0.1:4222"),

pkg/config/loader/loader.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,6 @@ func createPubSubV1(dc *database.Layer, cf *server.ServerConfigFile, l *zerolog.
10511051
natsmq.WithPubSubPassword(cf.MessageQueue.PubSub.NATS.Password),
10521052
natsmq.WithPubSubTLSEnabled(cf.MessageQueue.PubSub.NATS.TLSEnabled),
10531053
natsmq.WithPubSubTLSRootCAFile(cf.MessageQueue.PubSub.NATS.TLSRootCAFile),
1054-
natsmq.WithPubSubTLSHandshakeFirst(cf.MessageQueue.PubSub.NATS.TLSHandshakeFirst),
10551054
natsmq.WithPubSubSubjectPrefix(cf.MessageQueue.PubSub.NATS.SubjectPrefix),
10561055
natsmq.WithPubSubLogger(l),
10571056
)

pkg/config/loader/loader_pubsub_test.go

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ func TestPubSubSettingsInheritance(t *testing.T) {
2121
wantNatsURL string
2222
wantNatsUsername string
2323
wantNatsPassword string
24-
wantNatsTLSEnabled bool
25-
wantNatsTLSRootCAFile string
26-
wantNatsTLSHandshakeFirst bool
24+
wantNatsTLSEnabled bool
25+
wantNatsTLSRootCAFile string
2726
wantNatsSubjectPrefix string
2827
wantMaxPub int32
2928
wantMaxSub int32
@@ -120,24 +119,22 @@ func TestPubSubSettingsInheritance(t *testing.T) {
120119
wantMaxSub: 20,
121120
},
122121
{
123-
name: "nats pubsub with tls enabled, root ca file, and handshake first",
122+
name: "nats pubsub with tls enabled and root ca file",
124123
env: map[string]string{
125-
"SERVER_MSGQUEUE_KIND": "rabbitmq",
126-
"SERVER_MSGQUEUE_RABBITMQ_URL": "amqp://user:password@rabbit:5672/",
127-
"SERVER_MSGQUEUE_PUBSUB_KIND": "nats",
128-
"SERVER_MSGQUEUE_PUBSUB_NATS_URL": "nats://nats:4222",
129-
"SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED": "true",
130-
"SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ROOT_CA_FILE": "/etc/hatchet/nats-ca.pem",
131-
"SERVER_MSGQUEUE_PUBSUB_NATS_TLS_HANDSHAKE_FIRST": "true",
124+
"SERVER_MSGQUEUE_KIND": "rabbitmq",
125+
"SERVER_MSGQUEUE_RABBITMQ_URL": "amqp://user:password@rabbit:5672/",
126+
"SERVER_MSGQUEUE_PUBSUB_KIND": "nats",
127+
"SERVER_MSGQUEUE_PUBSUB_NATS_URL": "nats://nats:4222",
128+
"SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED": "true",
129+
"SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ROOT_CA_FILE": "/etc/hatchet/nats-ca.pem",
132130
},
133-
wantKind: "nats",
134-
wantURL: "amqp://user:password@rabbit:5672/",
135-
wantNatsURL: "nats://nats:4222",
136-
wantNatsTLSEnabled: true,
137-
wantNatsTLSRootCAFile: "/etc/hatchet/nats-ca.pem",
138-
wantNatsTLSHandshakeFirst: true,
139-
wantMaxPub: 10,
140-
wantMaxSub: 20,
131+
wantKind: "nats",
132+
wantURL: "amqp://user:password@rabbit:5672/",
133+
wantNatsURL: "nats://nats:4222",
134+
wantNatsTLSEnabled: true,
135+
wantNatsTLSRootCAFile: "/etc/hatchet/nats-ca.pem",
136+
wantMaxPub: 10,
137+
wantMaxSub: 20,
141138
},
142139
{
143140
name: "nats pubsub with subject prefix",
@@ -177,7 +174,6 @@ func TestPubSubSettingsInheritance(t *testing.T) {
177174
assert.Equal(t, tc.wantNatsPassword, cf.MessageQueue.PubSub.NATS.Password)
178175
assert.Equal(t, tc.wantNatsTLSEnabled, cf.MessageQueue.PubSub.NATS.TLSEnabled)
179176
assert.Equal(t, tc.wantNatsTLSRootCAFile, cf.MessageQueue.PubSub.NATS.TLSRootCAFile)
180-
assert.Equal(t, tc.wantNatsTLSHandshakeFirst, cf.MessageQueue.PubSub.NATS.TLSHandshakeFirst)
181177
assert.Equal(t, tc.wantNatsSubjectPrefix, cf.MessageQueue.PubSub.NATS.SubjectPrefix)
182178
})
183179
}

pkg/config/server/server.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -584,24 +584,20 @@ type PubSubNATSConfigFile struct {
584584
Username string `mapstructure:"username" json:"username,omitempty"`
585585
Password string `mapstructure:"password" json:"password,omitempty"`
586586

587-
// TLSEnabled requires verified TLS regardless of the URL scheme. Without
588-
// TLSRootCAFile the server certificate is verified against the system
589-
// roots. False leaves TLS to the URL scheme alone.
587+
// TLSEnabled requires verified TLS with a TLS-first handshake: the
588+
// handshake happens before the server's INFO message, so the server must
589+
// set handshake_first in its tls block. A client with this flag fails to
590+
// connect to an INFO-first TLS or plaintext server. Without TLSRootCAFile
591+
// the server certificate is verified against the system roots. False
592+
// leaves TLS to the URL scheme alone (tls:// does standard INFO-first
593+
// TLS with system roots).
590594
TLSEnabled bool `mapstructure:"tlsEnabled" json:"tlsEnabled,omitempty"`
591595

592596
// TLSRootCAFile is a PEM CA bundle used to verify a NATS server whose
593597
// certificate is signed by a private CA (it also applies to rediscovered
594598
// cluster peers). Requires TLSEnabled: true; startup fails otherwise.
595599
TLSRootCAFile string `mapstructure:"tlsRootCAFile" json:"tlsRootCAFile,omitempty"`
596600

597-
// TLSHandshakeFirst performs the TLS handshake before the server's INFO
598-
// message; the server must enable handshake_first in its tls block.
599-
// Against a server that sends INFO first, the client's TLS handshake
600-
// reads the plaintext INFO as a malformed handshake reply and fails the
601-
// connect within the connect timeout. Requires TLSEnabled: true; startup
602-
// fails otherwise.
603-
TLSHandshakeFirst bool `mapstructure:"tlsHandshakeFirst" json:"tlsHandshakeFirst,omitempty"`
604-
605601
// SubjectPrefix is prepended (with a trailing ".") to topic names.
606602
// Empty defaults to "hatchet.pubsub".
607603
SubjectPrefix string `mapstructure:"subjectPrefix" json:"subjectPrefix,omitempty"`
@@ -970,7 +966,6 @@ func BindAllEnv(v *viper.Viper) {
970966
_ = v.BindEnv("msgQueue.pubSub.nats.subjectPrefix", "SERVER_MSGQUEUE_PUBSUB_NATS_SUBJECT_PREFIX")
971967
_ = v.BindEnv("msgQueue.pubSub.nats.tlsEnabled", "SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ENABLED")
972968
_ = v.BindEnv("msgQueue.pubSub.nats.tlsRootCAFile", "SERVER_MSGQUEUE_PUBSUB_NATS_TLS_ROOT_CA_FILE")
973-
_ = v.BindEnv("msgQueue.pubSub.nats.tlsHandshakeFirst", "SERVER_MSGQUEUE_PUBSUB_NATS_TLS_HANDSHAKE_FIRST")
974969
_ = v.BindEnv("runtime.singleQueueLimit", "SERVER_SINGLE_QUEUE_LIMIT")
975970
_ = v.BindEnv("runtime.optimisticSchedulingEnabled", "SERVER_OPTIMISTIC_SCHEDULING_ENABLED")
976971
_ = v.BindEnv("runtime.optimisticSchedulingSlots", "SERVER_OPTIMISTIC_SCHEDULING_SLOTS")

0 commit comments

Comments
 (0)