From a47e5e045c44d573cf74277e800fdde1b238ee61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20=C5=BDaromskis?= <89458604+julius-welink@users.noreply.github.com> Date: Mon, 27 Sep 2021 10:07:38 +0300 Subject: [PATCH 1/4] [ADDED] TLS connection rate limiter --- server/opts.go | 12 +++++++ server/rate_counter.go | 65 +++++++++++++++++++++++++++++++++++ server/rate_counter_test.go | 52 ++++++++++++++++++++++++++++ server/server.go | 34 +++++++++++++++++++ test/tls_test.go | 68 +++++++++++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 server/rate_counter.go create mode 100644 server/rate_counter_test.go diff --git a/server/opts.go b/server/opts.go index 044ce294..608caa00 100644 --- a/server/opts.go +++ b/server/opts.go @@ -249,6 +249,7 @@ type Options struct { TLSCaCert string `json:"-"` TLSConfig *tls.Config `json:"-"` TLSPinnedCerts PinnedCertSet `json:"-"` + TLSRateLimit int64 `json:"-"` AllowNonTLS bool `json:"-"` WriteDeadline time.Duration `json:"-"` MaxClosedClients int `json:"-"` @@ -513,6 +514,7 @@ type TLSConfigOpts struct { Map bool TLSCheckKnownURLs bool Timeout float64 + RateLimit int64 Ciphers []uint16 CurvePreferences []tls.CurveID PinnedCerts PinnedCertSet @@ -902,6 +904,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error o.TLSTimeout = tc.Timeout o.TLSMap = tc.Map o.TLSPinnedCerts = tc.PinnedCerts + o.TLSRateLimit = tc.RateLimit // Need to keep track of path of the original TLS config // and certs path for OCSP Stapling monitoring. @@ -3695,6 +3698,15 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error) return nil, &configErr{tk, "error parsing tls config, 'timeout' wrong type"} } tc.Timeout = at + case "connection_rate_limit": + at := int64(0) + switch mv := mv.(type) { + case int64: + at = mv + default: + return nil, &configErr{tk, "error parsing tls config, 'connection_rate_limit' wrong type"} + } + tc.RateLimit = at case "pinned_certs": ra, ok := mv.([]interface{}) if !ok { diff --git a/server/rate_counter.go b/server/rate_counter.go new file mode 100644 index 00000000..37b47dc7 --- /dev/null +++ b/server/rate_counter.go @@ -0,0 +1,65 @@ +// Copyright 2021-2022 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "sync" + "time" +) + +type rateCounter struct { + limit int64 + count int64 + blocked uint64 + end time.Time + interval time.Duration + mu sync.Mutex +} + +func newRateCounter(limit int64) *rateCounter { + return &rateCounter{ + limit: limit, + interval: time.Second, + } +} + +func (r *rateCounter) allow() bool { + now := time.Now() + + r.mu.Lock() + + if now.After(r.end) { + r.count = 0 + r.end = now.Add(r.interval) + } else { + r.count++ + } + allow := r.count < r.limit + if !allow { + r.blocked++ + } + + r.mu.Unlock() + + return allow +} + +func (r *rateCounter) countBlocked() uint64 { + r.mu.Lock() + blocked := r.blocked + r.blocked = 0 + r.mu.Unlock() + + return blocked +} diff --git a/server/rate_counter_test.go b/server/rate_counter_test.go new file mode 100644 index 00000000..80f4a14e --- /dev/null +++ b/server/rate_counter_test.go @@ -0,0 +1,52 @@ +// Copyright 2021-2022 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "testing" + "time" +) + +func TestRateCounter(t *testing.T) { + counter := newRateCounter(10) + counter.interval = 100 * time.Millisecond + + var i int + for i = 0; i < 10; i++ { + if !counter.allow() { + t.Errorf("counter should allow (iteration %d)", i) + } + } + for i = 0; i < 5; i++ { + if counter.allow() { + t.Errorf("counter should not allow (iteration %d)", i) + } + } + + blocked := counter.countBlocked() + if blocked != 5 { + t.Errorf("Expected blocked = 5, got %d", blocked) + } + + blocked = counter.countBlocked() + if blocked != 0 { + t.Errorf("Expected blocked = 0, got %d", blocked) + } + + time.Sleep(150 * time.Millisecond) + + if !counter.allow() { + t.Errorf("Expected true after current time window expired") + } +} diff --git a/server/server.go b/server/server.go index 6e3ded5e..5fc20022 100644 --- a/server/server.go +++ b/server/server.go @@ -259,6 +259,8 @@ type Server struct { rerrMu sync.Mutex rerrLast time.Time + connRateCounter *rateCounter + // If there is a system account configured, to still support the $G account, // the server will create a fake user and add it to the list of users. // Keep track of what that user name is for config reload purposes. @@ -365,6 +367,10 @@ func NewServer(opts *Options) (*Server, error) { httpReqStats: make(map[string]uint64), // Used to track HTTP requests } + if opts.TLSRateLimit > 0 { + s.connRateCounter = newRateCounter(opts.tlsConfigOpts.RateLimit) + } + // Trusted root operator keys. if !s.processTrustedKeys() { return nil, fmt.Errorf("Error processing trusted operator keys") @@ -513,6 +519,23 @@ func NewServer(opts *Options) (*Server, error) { return s, nil } +func (s *Server) logRejectedTLSConns() { + defer s.grWG.Done() + t := time.NewTicker(time.Second) + defer t.Stop() + for { + select { + case <-s.quitCh: + return + case <-t.C: + blocked := s.connRateCounter.countBlocked() + if blocked > 0 { + s.Warnf("Rejected %d connections due to TLS rate limiting", blocked) + } + } + } +} + // clusterName returns our cluster name which could be dynamic. func (s *Server) ClusterName() string { s.mu.Lock() @@ -1787,6 +1810,10 @@ func (s *Server) Start() { s.logPorts() } + if opts.TLSRateLimit > 0 { + s.startGoRoutine(s.logRejectedTLSConns) + } + // Wait for clients. s.AcceptLoop(clientListenReady) } @@ -2480,6 +2507,13 @@ func (s *Server) createClient(conn net.Conn) *client { // Check for TLS if !isClosed && tlsRequired { + if s.connRateCounter != nil && !s.connRateCounter.allow() { + c.mu.Unlock() + c.sendErr("Connection throttling is active. Please try again later.") + c.closeConnection(MaxConnectionsExceeded) + return nil + } + // If we have a prebuffer create a multi-reader. if len(pre) > 0 { c.nc = &tlsMixConn{c.nc, bytes.NewBuffer(pre)} diff --git a/test/tls_test.go b/test/tls_test.go index 6e92f827..ac17e722 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -1852,6 +1852,74 @@ func TestTLSPinnedCertsClient(t *testing.T) { nc.Close() } +type captureWarnLogger struct { + dummyLogger + receive chan string +} + +func newCaptureWarnLogger() *captureWarnLogger { + return &captureWarnLogger{ + receive: make(chan string, 100), + } +} + +func (l *captureWarnLogger) Warnf(format string, v ...interface{}) { + l.receive <- fmt.Sprintf(format, v...) +} + +func (l *captureWarnLogger) waitFor(expect string, timeout time.Duration) bool { + for { + select { + case msg := <-l.receive: + if strings.Contains(msg, expect) { + return true + } + case <-time.After(timeout): + return false + } + } +} + +func TestTLSConnectionRate(t *testing.T) { + config := ` + listen: "127.0.0.1:-1" + tls { + cert_file: "./configs/certs/server-cert.pem" + key_file: "./configs/certs/server-key.pem" + connection_rate_limit: 3 + } + ` + + confFileName := createConfFile(t, []byte(config)) + defer removeFile(t, confFileName) + + srv, _ := RunServerWithConfig(confFileName) + logger := newCaptureWarnLogger() + srv.SetLogger(logger, false, false) + defer srv.Shutdown() + + var err error + count := 0 + for count < 10 { + var nc *nats.Conn + nc, err = nats.Connect(srv.ClientURL(), nats.RootCAs("./configs/certs/ca.pem")) + + if err != nil { + break + } + nc.Close() + count++ + } + + if count != 3 { + t.Fatalf("Expected 3 connections per second, got %d (%v)", count, err) + } + + if !logger.waitFor("connections due to TLS rate limiting", time.Second) { + t.Fatalf("did not log 'TLS rate limiting' warning") + } +} + func TestTLSPinnedCertsRoute(t *testing.T) { tmplSeed := ` host: localhost From 78bbcd791fe913203f69a228cc3501005609b772 Mon Sep 17 00:00:00 2001 From: Matthias Hanel Date: Wed, 12 Jan 2022 21:56:05 -0500 Subject: [PATCH 2/4] [Adding] support for JS MaxBytesRequired Signed-off-by: Matthias Hanel --- go.mod | 2 +- go.sum | 4 +- server/accounts.go | 9 ++-- server/jwt_test.go | 2 +- .../nats-io/jwt/v2/account_claims.go | 13 ++--- .../github.com/nats-io/jwt/v2/creds_utils.go | 47 +++++++++++++++++++ .../nats-io/jwt/v2/decoder_account.go | 2 +- vendor/modules.txt | 2 +- 8 files changed, 65 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 6619a037..ae67a9a6 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/golang/protobuf v1.4.2 // indirect github.com/klauspost/compress v1.13.4 github.com/minio/highwayhash v1.0.1 - github.com/nats-io/jwt/v2 v2.2.0 + github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc github.com/nats-io/nkeys v0.3.0 github.com/nats-io/nuid v1.0.1 diff --git a/go.sum b/go.sum index 5136dc1b..cb6bbf29 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEE github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/nats-io/jwt/v2 v2.2.0 h1:Yg/4WFK6vsqMudRg91eBb7Dh6XeVcDMPHycDE8CfltE= -github.com/nats-io/jwt/v2 v2.2.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= +github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY= +github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc h1:SHr4MUUZJ/fAC0uSm2OzWOJYsHpapmR86mpw7q1qPXU= github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= diff --git a/server/accounts.go b/server/accounts.go index db5d6e52..e2a9ac22 100644 --- a/server/accounts.go +++ b/server/accounts.go @@ -3195,10 +3195,11 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim if ac.Limits.JetStreamLimits.DiskStorage != 0 || ac.Limits.JetStreamLimits.MemoryStorage != 0 { // JetStreamAccountLimits and jwt.JetStreamLimits use same value for unlimited a.jsLimits = &JetStreamAccountLimits{ - MaxMemory: ac.Limits.JetStreamLimits.MemoryStorage, - MaxStore: ac.Limits.JetStreamLimits.DiskStorage, - MaxStreams: int(ac.Limits.JetStreamLimits.Streams), - MaxConsumers: int(ac.Limits.JetStreamLimits.Consumer), + MaxMemory: ac.Limits.JetStreamLimits.MemoryStorage, + MaxStore: ac.Limits.JetStreamLimits.DiskStorage, + MaxStreams: int(ac.Limits.JetStreamLimits.Streams), + MaxConsumers: int(ac.Limits.JetStreamLimits.Consumer), + MaxBytesRequired: ac.Limits.JetStreamLimits.MaxBytesRequired, } } else if a.jsLimits != nil { // covers failed update followed by disable diff --git a/server/jwt_test.go b/server/jwt_test.go index 9c0f395a..a0a42386 100644 --- a/server/jwt_test.go +++ b/server/jwt_test.go @@ -4259,7 +4259,7 @@ func TestJWTJetStreamLimits(t *testing.T) { sysKp.Seed() sysCreds := genCredsFile(t, sysUserJwt, sysUSeed) // limits to apply and check - limits1 := jwt.JetStreamLimits{MemoryStorage: 1024 * 1024, DiskStorage: 2048 * 1024, Streams: 1, Consumer: 2} + limits1 := jwt.JetStreamLimits{MemoryStorage: 1024 * 1024, DiskStorage: 2048 * 1024, Streams: 1, Consumer: 2, MaxBytesRequired: true} // has valid limits that would fail when incorrectly applied twice limits2 := jwt.JetStreamLimits{MemoryStorage: 4096 * 1024, DiskStorage: 8192 * 1024, Streams: 3, Consumer: 4} // limits exceeding actual configured value of DiskStorage diff --git a/vendor/github.com/nats-io/jwt/v2/account_claims.go b/vendor/github.com/nats-io/jwt/v2/account_claims.go index 04065b6a..8568da16 100644 --- a/vendor/github.com/nats-io/jwt/v2/account_claims.go +++ b/vendor/github.com/nats-io/jwt/v2/account_claims.go @@ -51,15 +51,16 @@ func (n *NatsLimits) IsUnlimited() bool { } type JetStreamLimits struct { - MemoryStorage int64 `json:"mem_storage,omitempty"` // Max number of bytes stored in memory across all streams. (0 means disabled) - DiskStorage int64 `json:"disk_storage,omitempty"` // Max number of bytes stored on disk across all streams. (0 means disabled) - Streams int64 `json:"streams,omitempty"` // Max number of streams - Consumer int64 `json:"consumer,omitempty"` // Max number of consumer + MemoryStorage int64 `json:"mem_storage,omitempty"` // Max number of bytes stored in memory across all streams. (0 means disabled) + DiskStorage int64 `json:"disk_storage,omitempty"` // Max number of bytes stored on disk across all streams. (0 means disabled) + Streams int64 `json:"streams,omitempty"` // Max number of streams + Consumer int64 `json:"consumer,omitempty"` // Max number of consumers + MaxBytesRequired bool `json:"max_bytes_required,omitempty"` // Max bytes required by all Streams } // IsUnlimited returns true if all limits are unlimited func (j *JetStreamLimits) IsUnlimited() bool { - return *j == JetStreamLimits{NoLimit, NoLimit, NoLimit, NoLimit} + return *j == JetStreamLimits{NoLimit, NoLimit, NoLimit, NoLimit, false} } // OperatorLimits are used to limit access by an account @@ -188,7 +189,7 @@ func NewAccountClaims(subject string) *AccountClaims { c.Limits = OperatorLimits{ NatsLimits{NoLimit, NoLimit, NoLimit}, AccountLimits{NoLimit, NoLimit, true, NoLimit, NoLimit}, - JetStreamLimits{0, 0, 0, 0}} + JetStreamLimits{0, 0, 0, 0, false}} c.Subject = subject c.Mappings = Mapping{} return c diff --git a/vendor/github.com/nats-io/jwt/v2/creds_utils.go b/vendor/github.com/nats-io/jwt/v2/creds_utils.go index c532c887..94312ff5 100644 --- a/vendor/github.com/nats-io/jwt/v2/creds_utils.go +++ b/vendor/github.com/nats-io/jwt/v2/creds_utils.go @@ -21,6 +21,7 @@ import ( "fmt" "regexp" "strings" + "time" "github.com/nats-io/nkeys" ) @@ -216,3 +217,49 @@ func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) { } return kp, nil } + +// IssueUserJWT takes an account scoped signing key, account id, and use public key (and optionally a user's name, an expiration duration and tags) and returns a valid signed JWT. +// The scopedSigningKey, is a mandatory account scoped signing nkey pair to sign the generated jwt (note that it _must_ be a signing key attached to the account (and a _scoped_ signing key), not the account's private (seed) key). +// The accountId, is a mandatory public account nkey. Will return error when not set or not account nkey. +// The publicUserKey, is a mandatory public user nkey. Will return error when not set or not user nkey. +// The name, is an optional human-readable name. When absent, default to publicUserKey. +// The expirationDuration, is an optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. +// The tags, is an optional list of tags to be included in the JWT. +// +// Returns: +// string, resulting jwt. +// error, when issues arose. +func IssueUserJWT(scopedSigningKey nkeys.KeyPair, accountId string, publicUserKey string, name string, expirationDuration time.Duration, tags ...string) (string, error) { + + if !nkeys.IsValidPublicAccountKey(accountId) { + return "", errors.New("issueUserJWT requires an account key for the accountId parameter, but got " + nkeys.Prefix(accountId).String()) + } + + if !nkeys.IsValidPublicUserKey(publicUserKey) { + return "", errors.New("issueUserJWT requires an account key for the publicUserKey parameter, but got " + nkeys.Prefix(publicUserKey).String()) + } + + claim := NewUserClaims(publicUserKey) + claim.SetScoped(true) + + if expirationDuration != 0 { + claim.Expires = time.Now().Add(expirationDuration).UTC().Unix() + } + + claim.IssuerAccount = accountId + if name != "" { + claim.Name = name + } else { + claim.Name = publicUserKey + } + + claim.Subject = publicUserKey + claim.Tags = tags + + encoded, err := claim.Encode(scopedSigningKey) + if err != nil { + return "", errors.New("err encoding claim " + err.Error()) + } + + return encoded, nil +} diff --git a/vendor/github.com/nats-io/jwt/v2/decoder_account.go b/vendor/github.com/nats-io/jwt/v2/decoder_account.go index 607d9840..820982c9 100644 --- a/vendor/github.com/nats-io/jwt/v2/decoder_account.go +++ b/vendor/github.com/nats-io/jwt/v2/decoder_account.go @@ -73,7 +73,7 @@ func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) { a.Account.Exports = oa.v1NatsAccount.Exports a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits - a.Account.Limits.JetStreamLimits = JetStreamLimits{0, 0, 0, 0} + a.Account.Limits.JetStreamLimits = JetStreamLimits{0, 0, 0, 0, false} a.Account.SigningKeys = make(SigningKeys) for _, v := range oa.SigningKeys { a.Account.SigningKeys.Add(v) diff --git a/vendor/modules.txt b/vendor/modules.txt index c78f5afe..5a208311 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -6,7 +6,7 @@ github.com/klauspost/compress/s2 # github.com/minio/highwayhash v1.0.1 ## explicit github.com/minio/highwayhash -# github.com/nats-io/jwt/v2 v2.2.0 +# github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 ## explicit github.com/nats-io/jwt/v2 # github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc From 420a2ef514e52dafee7c94542d9aa8d266d68361 Mon Sep 17 00:00:00 2001 From: Derek Collison Date: Wed, 12 Jan 2022 20:19:45 -0800 Subject: [PATCH 3/4] When rebuilding the complete state need to do this in a go routine. We did this properly above but forgot this one. Signed-off-by: Derek Collison --- server/filestore.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/filestore.go b/server/filestore.go index bfbc468c..94b17839 100644 --- a/server/filestore.go +++ b/server/filestore.go @@ -3171,7 +3171,9 @@ checkCache: mb.mu.Unlock() var ld *LostStreamData if ld, err = mb.rebuildState(); ld != nil { - fs.rebuildState(ld) + // We do not know if fs is locked or not at this point. + // This should be an exceptional condition so do so in Go routine. + go fs.rebuildState(ld) } mb.mu.Lock() } From ce4e4b5d47a6ee4b942fa5f28e188f1c7b71cad7 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Wed, 12 Jan 2022 21:38:22 -0800 Subject: [PATCH 4/4] Start monitoring before JetStream Signed-off-by: Waldemar Quevedo --- server/server.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/server/server.go b/server/server.go index 6e3ded5e..a67c5196 100644 --- a/server/server.go +++ b/server/server.go @@ -1612,7 +1612,7 @@ func (s *Server) Start() { s.checkResolvePreloads() } - // Log the pid to a file + // Log the pid to a file. if opts.PidFile != _EMPTY_ { if err := s.logPid(); err != nil { s.Fatalf("Could not write pidfile: %v", err) @@ -1631,14 +1631,21 @@ func (s *Server) Start() { s.SetDefaultSystemAccount() } - // start up resolver machinery + // Start monitoring before enabling other subsystems of the + // server to be able to monitor during startup. + if err := s.StartMonitoring(); err != nil { + s.Fatalf("Can't start monitoring: %v", err) + return + } + + // Start up resolver machinery. if ar := s.AccountResolver(); ar != nil { if err := ar.Start(s); err != nil { s.Fatalf("Could not start resolver: %v", err) return } // In operator mode, when the account resolver depends on an external system and - // the system account is the bootstrapping account, start fetching it + // the system account is the bootstrapping account, start fetching it. if len(opts.TrustedOperators) == 1 && opts.SystemAccount != _EMPTY_ && opts.SystemAccount != DEFAULT_SYSTEM_ACCOUNT { _, isMemResolver := ar.(*MemAccResolver) if v, ok := s.accounts.Load(s.opts.SystemAccount); !isMemResolver && ok && v.(*Account).claimJWT == "" { @@ -1726,12 +1733,6 @@ func (s *Server) Start() { // Start OCSP Stapling monitoring for TLS certificates if enabled. s.startOCSPMonitoring() - // Start monitoring if needed. - if err := s.StartMonitoring(); err != nil { - s.Fatalf("Can't start monitoring: %v", err) - return - } - // Start up gateway if needed. Do this before starting the routes, because // we want to resolve the gateway host:port so that this information can // be sent to other routes.