Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

common/tls: Allow specifying SNI hostnames #7897

Merged
merged 4 commits into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/TLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ For client TLS support we have the following options:
# tls_key = "/etc/telegraf/key.pem"
## Skip TLS verification.
# insecure_skip_verify = false
## Send the specified TLS server name via SNI.
# tls_server_name = "foo.example.com"
```

### Server Configuration
Expand Down
7 changes: 6 additions & 1 deletion plugins/common/tls/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type ClientConfig struct {
TLSCert string `toml:"tls_cert"`
TLSKey string `toml:"tls_key"`
InsecureSkipVerify bool `toml:"insecure_skip_verify"`
ServerName string `toml:"tls_server_name"`

// Deprecated in 1.7; use TLS variables above
SSLCA string `toml:"ssl_ca"`
Expand Down Expand Up @@ -49,7 +50,7 @@ func (c *ClientConfig) TLSConfig() (*tls.Config, error) {
// want TLS, this will require using another option to determine. In the
// case of an HTTP plugin, you could use `https`. Other plugins may need
// the dedicated option `TLSEnable`.
if c.TLSCA == "" && c.TLSKey == "" && c.TLSCert == "" && !c.InsecureSkipVerify {
if c.TLSCA == "" && c.TLSKey == "" && c.TLSCert == "" && !c.InsecureSkipVerify && c.ServerName == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct. If you neither specify a CA nor a KEY or CERT but a ServerName then this check will not trigger but I doubt it makes sense to check the servername without validating the certificate!?

Copy link
Contributor Author

@antifuchs antifuchs Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ServerName directive does not affect only the host name in certificate checking: It's also the hostname requested via SNI in the TLS ClientHello message - that's the thing that I need to customize that prompted me to submit the PR in the first place (:

Even if that weren't the case, an empty TLS Config is documented to use the host's root CA set if no root CAs are provided, so setting the ServerName value on an otherwise-empty TLS config would be meaningful.

Copy link
Member

@srebhan srebhan Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please put a comment there linking to the docu you mentioned. Just for the next one asking himself why this should make sense!? Thanks for the explanation!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, so I re-read the TODO comment above & started auditing our code's usage of TLSConfig(), where I found out that the only case where a nil tls.Config matters (because the stdlib function tls.Client requires a non-nil value) is the x509_cert.go code... where we override the ServerName based on a configuration setting (incompatibly with the "common" ServerName setting now)!

So I'll have to fix the ServerName usage in x509_cert.go, but more importantly: I think we should remove that TODO, as it could be a pretty meaningful distinction in terms of "were there any settings that the operator customized" (but ultimately, it looks like there's not much of a difference). What do you think?

return nil, nil
}

Expand All @@ -73,6 +74,10 @@ func (c *ClientConfig) TLSConfig() (*tls.Config, error) {
}
}

if c.ServerName != "" {
tlsConfig.ServerName = c.ServerName
}

return tlsConfig, nil
}

Expand Down
17 changes: 13 additions & 4 deletions plugins/common/tls/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ var pki = testutil.NewPKI("../../../testutil/pki")

func TestClientConfig(t *testing.T) {
tests := []struct {
name string
client tls.ClientConfig
expNil bool
expErr bool
name string
client tls.ClientConfig
expNil bool
expErr bool
serverName string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serverName doesn't seem to be used in this test. Can you remove it please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, vestige of a previous idea for a test - Removing it now.

}{
{
name: "unset",
Expand Down Expand Up @@ -86,6 +87,14 @@ func TestClientConfig(t *testing.T) {
SSLKey: pki.ClientKeyPath(),
},
},
{
name: "set SNI server name",
client: tls.ClientConfig{
ServerName: "foo.example.com",
},
expNil: false,
expErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion plugins/inputs/http_response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ This input plugin checks HTTP/HTTPS connections.
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Use the given name as the SNI server name on each URL
# tls_server_name = ""

## HTTP Request Headers (all values must be strings)
# [inputs.http_response.headers]
Expand Down Expand Up @@ -91,7 +93,7 @@ This input plugin checks HTTP/HTTPS connections.
- response_string_match (int, 0 = mismatch / body read error, 1 = match)
- response_status_code_match (int, 0 = mismatch, 1 = match)
- http_response_code (int, response status code)
- result_type (string, deprecated in 1.6: use `result` tag and `result_code` field)
- result_type (string, deprecated in 1.6: use `result` tag and `result_code` field)
- result_code (int, [see below](#result--result_code))

#### `result` / `result_code`
Expand Down
4 changes: 2 additions & 2 deletions plugins/inputs/http_response/http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ var sampleConfig = `
# {'fake':'data'}
# '''

## Optional name of the field that will contain the body of the response.
## By default it is set to an empty String indicating that the body's content won't be added
## Optional name of the field that will contain the body of the response.
## By default it is set to an empty String indicating that the body's content won't be added
# response_body_field = ''

## Maximum allowed HTTP response body size in bytes.
Expand Down
38 changes: 38 additions & 0 deletions plugins/inputs/http_response/http_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -1266,3 +1267,40 @@ func TestStatusCodeAndStringMatchFail(t *testing.T) {
}
checkOutput(t, &acc, expectedFields, expectedTags, nil, nil)
}

func TestSNI(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "super-special-hostname.example.com", r.TLS.ServerName)
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

h := &HTTPResponse{
Log: testutil.Logger{},
URLs: []string{ts.URL + "/good"},
Method: "GET",
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
ClientConfig: tls.ClientConfig{
InsecureSkipVerify: true,
ServerName: "super-special-hostname.example.com",
},
}
var acc testutil.Accumulator
err := h.Gather(&acc)
require.NoError(t, err)
expectedFields := map[string]interface{}{
"http_response_code": http.StatusOK,
"result_type": "success",
"result_code": 0,
"response_time": nil,
"content_length": nil,
}
expectedTags := map[string]interface{}{
"server": nil,
"method": "GET",
"status_code": "200",
"result": "success",
}
absentFields := []string{"response_string_match"}
checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil)
}