Skip to content

Commit

Permalink
feat(inputs.gnmi): Allow to pass accepted cipher suites (#15256)
Browse files Browse the repository at this point in the history
  • Loading branch information
srebhan authored May 6, 2024
1 parent a78e02c commit 46fb0af
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 59 deletions.
9 changes: 9 additions & 0 deletions plugins/common/tls/client.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 23 additions & 27 deletions plugins/common/tls/common.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package tls

import "crypto/tls"
import (
"crypto/tls"
"sync"
)

var tlsVersionMap = map[string]uint16{
"TLS10": tls.VersionTLS10,
Expand All @@ -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
}
})
}
31 changes: 19 additions & 12 deletions plugins/common/tls/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"os"
"strings"

"go.step.sm/crypto/pemutil"

Expand All @@ -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"`
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
43 changes: 39 additions & 4 deletions plugins/common/tls/utils.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
42 changes: 34 additions & 8 deletions plugins/inputs/gnmi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!
23 changes: 23 additions & 0 deletions plugins/inputs/gnmi/gnmi.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:generate ../../../tools/config_includer/generator
//go:generate ../../../tools/readme_config_includer/generator
package gnmi

Expand Down Expand Up @@ -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
}

Expand Down
28 changes: 20 additions & 8 deletions plugins/inputs/gnmi/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 46fb0af

Please sign in to comment.