diff --git a/plugins/common/tls/client.conf b/plugins/common/tls/client.conf index c61b9e33a..46bc8d7a2 100644 --- a/plugins/common/tls/client.conf +++ b/plugins/common/tls/client.conf @@ -7,7 +7,16 @@ # tls_cert = "/path/to/certfile" ## Used for TLS client certificate authentication # tls_key = "/path/to/keyfile" + ## Password for the key file if it is encrypted + # tls_key_pwd = "" ## Send the specified TLS server name via SNI # tls_server_name = "kubernetes.example.com" + ## Minimal TLS version to accept by the client + # tls_min_version = "TLS12" + ## List of ciphers to accept, by default all secure ciphers will be accepted + ## See https://pkg.go.dev/crypto/tls#pkg-constants for supported values + # tls_cipher_suites = [] + ## Renegotiation method, "never", "once" or "freely" + # tls_renegotiation_method = "never" ## Use TLS but skip chain & host verification # insecure_skip_verify = false \ No newline at end of file diff --git a/plugins/common/tls/common.go b/plugins/common/tls/common.go index 1ceb20c3f..5d9a66186 100644 --- a/plugins/common/tls/common.go +++ b/plugins/common/tls/common.go @@ -1,6 +1,9 @@ package tls -import "crypto/tls" +import ( + "crypto/tls" + "sync" +) var tlsVersionMap = map[string]uint16{ "TLS10": tls.VersionTLS10, @@ -9,30 +12,23 @@ var tlsVersionMap = map[string]uint16{ "TLS13": tls.VersionTLS13, } -var tlsCipherMap = map[string]uint16{ - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, - "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, - "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, - "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, +var tlsCipherMapInit sync.Once +var tlsCipherMapSecure map[string]uint16 +var tlsCipherMapInsecure map[string]uint16 + +func init() { + tlsCipherMapInit.Do(func() { + // Initialize the secure suites + suites := tls.CipherSuites() + tlsCipherMapSecure = make(map[string]uint16, len(suites)) + for _, s := range suites { + tlsCipherMapSecure[s.Name] = s.ID + } + + suites = tls.InsecureCipherSuites() + tlsCipherMapInsecure = make(map[string]uint16, len(suites)) + for _, s := range suites { + tlsCipherMapInsecure[s.Name] = s.ID + } + }) } diff --git a/plugins/common/tls/config.go b/plugins/common/tls/config.go index c0dada9b1..aeb0de636 100644 --- a/plugins/common/tls/config.go +++ b/plugins/common/tls/config.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "os" - "strings" "go.step.sm/crypto/pemutil" @@ -19,15 +18,16 @@ const TLSMinVersionDefault = tls.VersionTLS12 // ClientConfig represents the standard client TLS config. type ClientConfig struct { - TLSCA string `toml:"tls_ca"` - TLSCert string `toml:"tls_cert"` - TLSKey string `toml:"tls_key"` - TLSKeyPwd string `toml:"tls_key_pwd"` - TLSMinVersion string `toml:"tls_min_version"` - InsecureSkipVerify bool `toml:"insecure_skip_verify"` - ServerName string `toml:"tls_server_name"` - RenegotiationMethod string `toml:"tls_renegotiation_method"` - Enable *bool `toml:"tls_enable"` + TLSCA string `toml:"tls_ca"` + TLSCert string `toml:"tls_cert"` + TLSKey string `toml:"tls_key"` + TLSKeyPwd string `toml:"tls_key_pwd"` + TLSMinVersion string `toml:"tls_min_version"` + TLSCipherSuites []string `toml:"tls_cipher_suites"` + InsecureSkipVerify bool `toml:"insecure_skip_verify"` + ServerName string `toml:"tls_server_name"` + RenegotiationMethod string `toml:"tls_renegotiation_method"` + Enable *bool `toml:"tls_enable"` SSLCA string `toml:"ssl_ca" deprecated:"1.7.0;use 'tls_ca' instead"` SSLCert string `toml:"ssl_cert" deprecated:"1.7.0;use 'tls_cert' instead"` @@ -136,6 +136,14 @@ func (c *ClientConfig) TLSConfig() (*tls.Config, error) { tlsConfig.ServerName = c.ServerName } + if len(c.TLSCipherSuites) != 0 { + cipherSuites, err := ParseCiphers(c.TLSCipherSuites) + if err != nil { + return nil, fmt.Errorf("could not parse client cipher suites: %w", err) + } + tlsConfig.CipherSuites = cipherSuites + } + return tlsConfig, nil } @@ -167,8 +175,7 @@ func (c *ServerConfig) TLSConfig() (*tls.Config, error) { if len(c.TLSCipherSuites) != 0 { cipherSuites, err := ParseCiphers(c.TLSCipherSuites) if err != nil { - return nil, fmt.Errorf( - "could not parse server cipher suites %s: %w", strings.Join(c.TLSCipherSuites, ","), err) + return nil, fmt.Errorf("could not parse server cipher suites: %w", err) } tlsConfig.CipherSuites = cipherSuites } diff --git a/plugins/common/tls/utils.go b/plugins/common/tls/utils.go index 25ef4a543..6fbdeb96e 100644 --- a/plugins/common/tls/utils.go +++ b/plugins/common/tls/utils.go @@ -1,22 +1,57 @@ package tls import ( + "errors" "fmt" "sort" "strings" ) +var ErrCipherUnsupported = errors.New("unsupported cipher") + +// InsecureCiphers returns the list of insecure ciphers among the list of given ciphers +func InsecureCiphers(ciphers []string) []string { + var insecure []string + + for _, c := range ciphers { + cipher := strings.ToUpper(c) + if _, ok := tlsCipherMapInsecure[cipher]; ok { + insecure = append(insecure, c) + } + } + + return insecure +} + +// Ciphers returns the list of supported ciphers +func Ciphers() (secure, insecure []string) { + for c := range tlsCipherMapSecure { + secure = append(secure, c) + } + + for c := range tlsCipherMapInsecure { + insecure = append(insecure, c) + } + + return secure, insecure +} + // ParseCiphers returns a `[]uint16` by received `[]string` key that represents ciphers from crypto/tls. // If some of ciphers in received list doesn't exists ParseCiphers returns nil with error func ParseCiphers(ciphers []string) ([]uint16, error) { suites := []uint16{} - for _, cipher := range ciphers { - v, ok := tlsCipherMap[cipher] + for _, c := range ciphers { + cipher := strings.ToUpper(c) + id, ok := tlsCipherMapSecure[cipher] if !ok { - return nil, fmt.Errorf("unsupported cipher %q", cipher) + idInsecure, ok := tlsCipherMapInsecure[cipher] + if !ok { + return nil, fmt.Errorf("%q %w", cipher, ErrCipherUnsupported) + } + id = idInsecure } - suites = append(suites, v) + suites = append(suites, id) } return suites, nil diff --git a/plugins/inputs/gnmi/README.md b/plugins/inputs/gnmi/README.md index 174c4733f..1a4bcac34 100644 --- a/plugins/inputs/gnmi/README.md +++ b/plugins/inputs/gnmi/README.md @@ -91,17 +91,29 @@ details on how to use them. ## Prefix tags from path keys with the path element # prefix_tag_key_with_path = false - ## enable client-side TLS and define CA to authenticate the device - # enable_tls = false - # tls_ca = "/etc/telegraf/ca.pem" + ## Optional client-side TLS to authenticate the device + ## Set to true/false to enforce TLS being enabled/disabled. If not set, + ## enable TLS only if any of the other options are specified. + # tls_enable = + ## Trusted root certificates for server + # tls_ca = "/path/to/cafile" + ## Used for TLS client certificate authentication + # tls_cert = "/path/to/certfile" + ## Used for TLS client certificate authentication + # tls_key = "/path/to/keyfile" + ## Password for the key file if it is encrypted + # tls_key_pwd = "" + ## Send the specified TLS server name via SNI + # tls_server_name = "kubernetes.example.com" ## Minimal TLS version to accept by the client # tls_min_version = "TLS12" + ## List of ciphers to accept, by default all secure ciphers will be accepted + ## See https://pkg.go.dev/crypto/tls#pkg-constants for supported values + # tls_cipher_suites = [] + ## Renegotiation method, "never", "once" or "freely" + # tls_renegotiation_method = "never" ## Use TLS but skip chain & host verification - # insecure_skip_verify = true - - ## define client-side TLS certificate & key to authenticate to the device - # tls_cert = "/etc/telegraf/cert.pem" - # tls_key = "/etc/telegraf/key.pem" + # insecure_skip_verify = false ## gNMI subscription prefix (optional, can usually be left empty) ## See: https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#222-paths @@ -229,3 +241,17 @@ to `subscription` to use the subscription path as `path` tag. Other devices might omit the prefix in updates altogether. Here setting `path_guessing_strategy` to `common path` can help to infer the `path` tag by using the part of the path that is common to all values in the update. + +### TLS handshake failure + +When receiving an error like + +```text +2024-01-01T00:00:00Z E! [inputs.gnmi] Error in plugin: failed to setup subscription: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: remote error: tls: handshake failure" +``` + +this might be due to insecure TLS configurations in the GNMI server. Please +check the minimum TLS version provided by the server as well as the cipher suite +used. You might want to use the `tls_min_version` or `tls_cipher_suites` setting +respectively to work-around the issue. Please be careful to not undermine the +security of the connection between the plugin and the device! diff --git a/plugins/inputs/gnmi/gnmi.go b/plugins/inputs/gnmi/gnmi.go index 8945c9ce5..1adef4dae 100644 --- a/plugins/inputs/gnmi/gnmi.go +++ b/plugins/inputs/gnmi/gnmi.go @@ -1,3 +1,4 @@ +//go:generate ../../../tools/config_includer/generator //go:generate ../../../tools/readme_config_includer/generator package gnmi @@ -196,6 +197,28 @@ func (c *GNMI) Init() error { } c.Log.Debugf("Internal alias mapping: %+v", c.internalAliases) + // Warn about configures insecure cipher suites + insecure := internaltls.InsecureCiphers(c.ClientConfig.TLSCipherSuites) + if len(insecure) > 0 { + c.Log.Warnf("Configured insecure cipher suites: %s", strings.Join(insecure, ",")) + } + + // Check the TLS configuration + if _, err := c.ClientConfig.TLSConfig(); err != nil { + if errors.Is(err, internaltls.ErrCipherUnsupported) { + secure, insecure := internaltls.Ciphers() + c.Log.Info("Supported secure ciphers:") + for _, name := range secure { + c.Log.Infof(" %s", name) + } + c.Log.Info("Supported insecure ciphers:") + for _, name := range insecure { + c.Log.Infof(" %s", name) + } + } + return err + } + return nil } diff --git a/plugins/inputs/gnmi/sample.conf b/plugins/inputs/gnmi/sample.conf index 30795898d..a72854fbe 100644 --- a/plugins/inputs/gnmi/sample.conf +++ b/plugins/inputs/gnmi/sample.conf @@ -44,17 +44,29 @@ ## Prefix tags from path keys with the path element # prefix_tag_key_with_path = false - ## enable client-side TLS and define CA to authenticate the device - # enable_tls = false - # tls_ca = "/etc/telegraf/ca.pem" + ## Optional client-side TLS to authenticate the device + ## Set to true/false to enforce TLS being enabled/disabled. If not set, + ## enable TLS only if any of the other options are specified. + # tls_enable = + ## Trusted root certificates for server + # tls_ca = "/path/to/cafile" + ## Used for TLS client certificate authentication + # tls_cert = "/path/to/certfile" + ## Used for TLS client certificate authentication + # tls_key = "/path/to/keyfile" + ## Password for the key file if it is encrypted + # tls_key_pwd = "" + ## Send the specified TLS server name via SNI + # tls_server_name = "kubernetes.example.com" ## Minimal TLS version to accept by the client # tls_min_version = "TLS12" + ## List of ciphers to accept, by default all secure ciphers will be accepted + ## See https://pkg.go.dev/crypto/tls#pkg-constants for supported values + # tls_cipher_suites = [] + ## Renegotiation method, "never", "once" or "freely" + # tls_renegotiation_method = "never" ## Use TLS but skip chain & host verification - # insecure_skip_verify = true - - ## define client-side TLS certificate & key to authenticate to the device - # tls_cert = "/etc/telegraf/cert.pem" - # tls_key = "/etc/telegraf/key.pem" + # insecure_skip_verify = false ## gNMI subscription prefix (optional, can usually be left empty) ## See: https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#222-paths diff --git a/plugins/inputs/gnmi/sample.conf.in b/plugins/inputs/gnmi/sample.conf.in new file mode 100644 index 000000000..11af0fac6 --- /dev/null +++ b/plugins/inputs/gnmi/sample.conf.in @@ -0,0 +1,118 @@ +# gNMI telemetry input plugin +[[inputs.gnmi]] + ## Address and port of the gNMI GRPC server + addresses = ["10.49.234.114:57777"] + + ## define credentials + username = "cisco" + password = "cisco" + + ## gNMI encoding requested (one of: "proto", "json", "json_ietf", "bytes") + # encoding = "proto" + + ## redial in case of failures after + # redial = "10s" + + ## gRPC Keepalive settings + ## See https://pkg.go.dev/google.golang.org/grpc/keepalive + ## The client will ping the server to see if the transport is still alive if it has + ## not see any activity for the given time. + ## If not set, none of the keep-alive setting (including those below) will be applied. + ## If set and set below 10 seconds, the gRPC library will apply a minimum value of 10s will be used instead. + # keepalive_time = "" + + ## Timeout for seeing any activity after the keep-alive probe was + ## sent. If no activity is seen the connection is closed. + # keepalive_timeout = "" + + ## gRPC Maximum Message Size + # max_msg_size = "4MB" + + ## Enable to get the canonical path as field-name + # canonical_field_names = false + + ## Remove leading slashes and dots in field-name + # trim_field_names = false + + ## Guess the path-tag if an update does not contain a prefix-path + ## Supported values are + ## none -- do not add a 'path' tag + ## common path -- use the common path elements of all fields in an update + ## subscription -- use the subscription path + # path_guessing_strategy = "none" + + ## Prefix tags from path keys with the path element + # prefix_tag_key_with_path = false + + ## Optional client-side TLS to authenticate the device +{{template "/plugins/common/tls/client.conf"}} + + ## gNMI subscription prefix (optional, can usually be left empty) + ## See: https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#222-paths + # origin = "" + # prefix = "" + # target = "" + + ## Vendor specific options + ## This defines what vendor specific options to load. + ## * Juniper Header Extension (juniper_header): some sensors are directly managed by + ## Linecard, which adds the Juniper GNMI Header Extension. Enabling this + ## allows the decoding of the Extension header if present. Currently this knob + ## adds component, component_id & sub_component_id as additional tags + # vendor_specific = [] + + ## Define additional aliases to map encoding paths to measurement names + # [inputs.gnmi.aliases] + # ifcounters = "openconfig:/interfaces/interface/state/counters" + + [[inputs.gnmi.subscription]] + ## Name of the measurement that will be emitted + name = "ifcounters" + + ## Origin and path of the subscription + ## See: https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#222-paths + ## + ## origin usually refers to a (YANG) data model implemented by the device + ## and path to a specific substructure inside it that should be subscribed + ## to (similar to an XPath). YANG models can be found e.g. here: + ## https://github.com/YangModels/yang/tree/master/vendor/cisco/xr + origin = "openconfig-interfaces" + path = "/interfaces/interface/state/counters" + + ## Subscription mode ("target_defined", "sample", "on_change") and interval + subscription_mode = "sample" + sample_interval = "10s" + + ## Suppress redundant transmissions when measured values are unchanged + # suppress_redundant = false + + ## If suppression is enabled, send updates at least every X seconds anyway + # heartbeat_interval = "60s" + + ## Tag subscriptions are applied as tags to other subscriptions. + # [[inputs.gnmi.tag_subscription]] + # ## When applying this value as a tag to other metrics, use this tag name + # name = "descr" + # + # ## All other subscription fields are as normal + # origin = "openconfig-interfaces" + # path = "/interfaces/interface/state" + # subscription_mode = "on_change" + # + # ## Match strategy to use for the tag. + # ## Tags are only applied for metrics of the same address. The following + # ## settings are valid: + # ## unconditional -- always match + # ## name -- match by the "name" key + # ## This resembles the previous 'tag-only' behavior. + # ## elements -- match by the keys in the path filtered by the path + # ## parts specified `elements` below + # ## By default, 'elements' is used if the 'elements' option is provided, + # ## otherwise match by 'name'. + # # match = "" + # + # ## For the 'elements' match strategy, at least one path-element name must + # ## be supplied containing at least one key to match on. Multiple path + # ## elements can be specified in any order. All given keys must be equal + # ## for a match. + # # elements = ["description", "interface"] diff --git a/plugins/inputs/http/README.md b/plugins/inputs/http/README.md index 54153232f..213ec7d77 100644 --- a/plugins/inputs/http/README.md +++ b/plugins/inputs/http/README.md @@ -77,8 +77,17 @@ to use them. # tls_cert = "/path/to/certfile" ## Used for TLS client certificate authentication # tls_key = "/path/to/keyfile" + ## Password for the key file if it is encrypted + # tls_key_pwd = "" ## Send the specified TLS server name via SNI # tls_server_name = "kubernetes.example.com" + ## Minimal TLS version to accept by the client + # tls_min_version = "TLS12" + ## List of ciphers to accept, by default all secure ciphers will be accepted + ## See https://pkg.go.dev/crypto/tls#pkg-constants for supported values + # tls_cipher_suites = [] + ## Renegotiation method, "never", "once" or "freely" + # tls_renegotiation_method = "never" ## Use TLS but skip chain & host verification # insecure_skip_verify = false diff --git a/plugins/inputs/http/sample.conf b/plugins/inputs/http/sample.conf index 5b79ede24..e55a7a677 100644 --- a/plugins/inputs/http/sample.conf +++ b/plugins/inputs/http/sample.conf @@ -48,8 +48,17 @@ # tls_cert = "/path/to/certfile" ## Used for TLS client certificate authentication # tls_key = "/path/to/keyfile" + ## Password for the key file if it is encrypted + # tls_key_pwd = "" ## Send the specified TLS server name via SNI # tls_server_name = "kubernetes.example.com" + ## Minimal TLS version to accept by the client + # tls_min_version = "TLS12" + ## List of ciphers to accept, by default all secure ciphers will be accepted + ## See https://pkg.go.dev/crypto/tls#pkg-constants for supported values + # tls_cipher_suites = [] + ## Renegotiation method, "never", "once" or "freely" + # tls_renegotiation_method = "never" ## Use TLS but skip chain & host verification # insecure_skip_verify = false