feat(inputs.gnmi): Allow to pass accepted cipher suites (#15256)

This commit is contained in:
Sven Rebhan 2024-05-06 14:19:10 -04:00 committed by GitHub
parent a78e02c3ae
commit 46fb0af4e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 303 additions and 59 deletions

View File

@ -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

View File

@ -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
}
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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!

View File

@ -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
}

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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