From 0f82c97e015016fe08372ee147a26ebdcabb940b Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 9 Jan 2024 13:00:15 -0500 Subject: [PATCH] Adding Mercury.TLS field CertPath for node communication with web servers + load balancers over TLS (#11492) * feature/transmission_key: adding Transmission.TLS field CertPath * [Transmission.TLS] --> [Mercury.TLS] --- core/config/docs/core.toml | 5 +++ core/config/mercury_config.go | 5 +++ core/config/toml/types.go | 25 +++++++++++ core/config/toml/types_test.go | 45 +++++++++++++++++++ core/services/chainlink/config_mercury.go | 12 +++++ .../services/chainlink/config_mercury_test.go | 14 ++++++ core/services/chainlink/config_test.go | 6 +++ .../testdata/config-empty-effective.toml | 3 ++ .../chainlink/testdata/config-full.toml | 3 ++ .../config-multi-chain-effective.toml | 3 ++ core/services/ocr2/delegate.go | 1 + .../testdata/config-empty-effective.toml | 3 ++ core/web/resolver/testdata/config-full.toml | 3 ++ .../config-multi-chain-effective.toml | 3 ++ docs/CHANGELOG.md | 3 +- docs/CONFIG.md | 13 ++++++ testdata/scripts/node/validate/default.txtar | 3 ++ .../disk-based-logging-disabled.txtar | 3 ++ .../validate/disk-based-logging-no-dir.txtar | 3 ++ .../node/validate/disk-based-logging.txtar | 3 ++ testdata/scripts/node/validate/invalid.txtar | 3 ++ testdata/scripts/node/validate/valid.txtar | 3 ++ testdata/scripts/node/validate/warnings.txtar | 3 ++ 23 files changed, 167 insertions(+), 1 deletion(-) diff --git a/core/config/docs/core.toml b/core/config/docs/core.toml index 953f1feabd8..3cb6bb0aaa7 100644 --- a/core/config/docs/core.toml +++ b/core/config/docs/core.toml @@ -574,3 +574,8 @@ MaxStaleAge = "1h" # Default # LatestReportDeadline controls how long to wait for a response from the # mercury server before retrying. Setting this to zero will wait indefinitely. LatestReportDeadline = "5s" # Default + +# Mercury.TLS controls client settings for when the node talks to traditional web servers or load balancers. +[Mercury.TLS] +# CertFile is the path to a PEM file of trusted root certificate authority certificates +CertFile = "/path/to/client/certs.pem" # Example diff --git a/core/config/mercury_config.go b/core/config/mercury_config.go index e530af6338f..6a483c593e3 100644 --- a/core/config/mercury_config.go +++ b/core/config/mercury_config.go @@ -12,7 +12,12 @@ type MercuryCache interface { LatestReportDeadline() time.Duration } +type MercuryTLS interface { + CertFile() string +} + type Mercury interface { Credentials(credName string) *ocr2models.MercuryCredentials Cache() MercuryCache + TLS() MercuryTLS } diff --git a/core/config/toml/types.go b/core/config/toml/types.go index e944b44853b..93fcef32611 100644 --- a/core/config/toml/types.go +++ b/core/config/toml/types.go @@ -1304,12 +1304,37 @@ func (mc *MercuryCache) setFrom(f *MercuryCache) { } } +type MercuryTLS struct { + CertFile *string +} + +func (m *MercuryTLS) setFrom(f *MercuryTLS) { + if v := f.CertFile; v != nil { + m.CertFile = v + } +} + +func (m *MercuryTLS) ValidateConfig() (err error) { + if *m.CertFile != "" { + if !isValidFilePath(*m.CertFile) { + err = multierr.Append(err, configutils.ErrInvalid{Name: "CertFile", Value: *m.CertFile, Msg: "must be a valid file path"}) + } + } + return +} + type Mercury struct { Cache MercuryCache `toml:",omitempty"` + TLS MercuryTLS `toml:",omitempty"` } func (m *Mercury) setFrom(f *Mercury) { m.Cache.setFrom(&f.Cache) + m.TLS.setFrom(&f.TLS) +} + +func (m *Mercury) ValidateConfig() (err error) { + return m.TLS.ValidateConfig() } type MercuryCredentials struct { diff --git a/core/config/toml/types_test.go b/core/config/toml/types_test.go index e16d3a864da..d29da688e33 100644 --- a/core/config/toml/types_test.go +++ b/core/config/toml/types_test.go @@ -531,5 +531,50 @@ func TestTracing_ValidateMode(t *testing.T) { } } +func TestMercuryTLS_ValidateTLSCertPath(t *testing.T) { + tests := []struct { + name string + tlsCertPath *string + wantErr bool + errMsg string + }{ + { + name: "valid file path", + tlsCertPath: ptr("/etc/ssl/certs/cert.pem"), + wantErr: false, + }, + { + name: "relative file path", + tlsCertPath: ptr("certs/cert.pem"), + wantErr: false, + }, + { + name: "excessively long file path", + tlsCertPath: ptr(strings.Repeat("z", 4097)), + wantErr: true, + errMsg: "CertFile: invalid value (" + strings.Repeat("z", 4097) + "): must be a valid file path", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mercury := &Mercury{ + TLS: MercuryTLS{ + CertFile: tt.tlsCertPath, + }, + } + + err := mercury.ValidateConfig() + + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + // ptr is a utility function for converting a value to a pointer to the value. func ptr[T any](t T) *T { return &t } diff --git a/core/services/chainlink/config_mercury.go b/core/services/chainlink/config_mercury.go index 61e48d340e4..49f1cf0a5f4 100644 --- a/core/services/chainlink/config_mercury.go +++ b/core/services/chainlink/config_mercury.go @@ -24,6 +24,14 @@ func (m *mercuryCacheConfig) LatestReportDeadline() time.Duration { return m.c.LatestReportDeadline.Duration() } +type mercuryTLSConfig struct { + c toml.MercuryTLS +} + +func (m *mercuryTLSConfig) CertFile() string { + return *m.c.CertFile +} + type mercuryConfig struct { c toml.Mercury s toml.MercurySecrets @@ -47,3 +55,7 @@ func (m *mercuryConfig) Credentials(credName string) *models.MercuryCredentials func (m *mercuryConfig) Cache() config.MercuryCache { return &mercuryCacheConfig{c: m.c.Cache} } + +func (m *mercuryConfig) TLS() config.MercuryTLS { + return &mercuryTLSConfig{c: m.c.TLS} +} diff --git a/core/services/chainlink/config_mercury_test.go b/core/services/chainlink/config_mercury_test.go index 58019a91557..71dfb2a01ce 100644 --- a/core/services/chainlink/config_mercury_test.go +++ b/core/services/chainlink/config_mercury_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/models" + + "github.com/smartcontractkit/chainlink/v2/core/config/toml" ) const ( @@ -34,3 +36,15 @@ func TestMercuryConfig(t *testing.T) { assert.Equal(t, &models.MercuryCredentials{URL: "https://chain1.link", Username: "username1", Password: "password1"}, m.Credentials("cred1")) assert.Equal(t, &models.MercuryCredentials{URL: "https://chain2.link", Username: "username2", Password: "password2"}, m.Credentials("cred2")) } + +func TestMercuryTLS(t *testing.T) { + certPath := "/path/to/cert.pem" + transmission := toml.Mercury{ + TLS: toml.MercuryTLS{ + CertFile: &certPath, + }, + } + cfg := mercuryConfig{c: transmission} + + assert.Equal(t, certPath, cfg.TLS().CertFile()) +} diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index f0c88db65fe..10ab00e8bd0 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -666,6 +666,9 @@ func TestConfig_Marshal(t *testing.T) { MaxStaleAge: models.MustNewDuration(101 * time.Second), LatestReportDeadline: models.MustNewDuration(102 * time.Second), }, + TLS: toml.MercuryTLS{ + CertFile: ptr("/path/to/cert.pem"), + }, } for _, tt := range []struct { @@ -1097,6 +1100,9 @@ URL = 'http://stark.node' LatestReportTTL = '1m40s' MaxStaleAge = '1m41s' LatestReportDeadline = '1m42s' + +[Mercury.TLS] +CertFile = '/path/to/cert.pem' `}, {"full", full, fullTOML}, {"multi-chain", multiChain, multiChainTOML}, diff --git a/core/services/chainlink/testdata/config-empty-effective.toml b/core/services/chainlink/testdata/config-empty-effective.toml index 9efad05c03a..cc4dddcf8af 100644 --- a/core/services/chainlink/testdata/config-empty-effective.toml +++ b/core/services/chainlink/testdata/config-empty-effective.toml @@ -227,3 +227,6 @@ TLSCertPath = '' LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' + +[Mercury.TLS] +CertFile = '' diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index c6387e4ef67..da8b68cfdb1 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -238,6 +238,9 @@ LatestReportTTL = '1m40s' MaxStaleAge = '1m41s' LatestReportDeadline = '1m42s' +[Mercury.TLS] +CertFile = '/path/to/cert.pem' + [[EVM]] ChainID = '1' Enabled = false diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index 40c18f28eb9..d5352a685c4 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -228,6 +228,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 519a7a58f5d..37235437de1 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -190,6 +190,7 @@ type jobPipelineConfig interface { type mercuryConfig interface { Credentials(credName string) *models.MercuryCredentials Cache() coreconfig.MercuryCache + TLS() coreconfig.MercuryTLS } type thresholdConfig interface { diff --git a/core/web/resolver/testdata/config-empty-effective.toml b/core/web/resolver/testdata/config-empty-effective.toml index 9efad05c03a..cc4dddcf8af 100644 --- a/core/web/resolver/testdata/config-empty-effective.toml +++ b/core/web/resolver/testdata/config-empty-effective.toml @@ -227,3 +227,6 @@ TLSCertPath = '' LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' + +[Mercury.TLS] +CertFile = '' diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index 45504d62596..957ffdc968e 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -238,6 +238,9 @@ LatestReportTTL = '1m40s' MaxStaleAge = '1m41s' LatestReportDeadline = '1m42s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' Enabled = false diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index 40c18f28eb9..d5352a685c4 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -228,6 +228,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 36ee68412ca..2ba80927256 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -64,7 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Two new prom metrics for mercury, nops should consider adding alerting on these: - `mercury_insufficient_blocks_count` - `mercury_zero_blocks_count` - +- Added new `Mercury.TLS` TOML config field `CertFile` for configuring transport credentials when the node acts as a client and initiates a TLS handshake. + ### Changed - `PromReporter` no longer directly reads txm related status from the db, and instead uses the txStore API. diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 2bb32b58252..421b0ea9dad 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -1589,6 +1589,19 @@ LatestReportDeadline = "5s" # Default LatestReportDeadline controls how long to wait for a response from the mercury server before retrying. Setting this to zero will wait indefinitely. +## Mercury.TLS +```toml +[Mercury.TLS] +CertFile = "/path/to/client/certs.pem" # Example +``` +Mercury.TLS controls client settings for when the node talks to traditional web servers or load balancers. + +### CertFile +```toml +CertFile = "/path/to/client/certs.pem" # Example +``` +CertFile is the path to a PEM file of trusted root certificate authority certificates + ## EVM EVM defaults depend on ChainID: diff --git a/testdata/scripts/node/validate/default.txtar b/testdata/scripts/node/validate/default.txtar index c9f86e43b29..32a7e5f71b3 100644 --- a/testdata/scripts/node/validate/default.txtar +++ b/testdata/scripts/node/validate/default.txtar @@ -240,6 +240,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + Invalid configuration: invalid secrets: 2 errors: - Database.URL: empty: must be provided and non-empty - Password.Keystore: empty: must be provided and non-empty diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index 1ae283c8a8a..9cca8764eed 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -284,6 +284,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index dcb2d05ce39..d1658a1772d 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -284,6 +284,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index fee173d3083..18ac99db785 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -284,6 +284,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index 5acbad83f8c..f048e35547e 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -274,6 +274,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index b54fc899bbd..47c63e4c03e 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -281,6 +281,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + [[EVM]] ChainID = '1' AutoCreateKey = true diff --git a/testdata/scripts/node/validate/warnings.txtar b/testdata/scripts/node/validate/warnings.txtar index 710e29d4983..c6daa829ddb 100644 --- a/testdata/scripts/node/validate/warnings.txtar +++ b/testdata/scripts/node/validate/warnings.txtar @@ -263,6 +263,9 @@ LatestReportTTL = '1s' MaxStaleAge = '1h0m0s' LatestReportDeadline = '5s' +[Mercury.TLS] +CertFile = '' + # Configuration warning: Tracing.TLSCertPath: invalid value (something): must be empty when Tracing.Mode is 'unencrypted' Valid configuration.