consul/agent/config/builder_test.go
Derek Menteer 931cec42b3
Prevent serving TLS via ports.grpc (#15339)
Prevent serving TLS via ports.grpc

We remove the ability to run the ports.grpc in TLS mode to avoid
confusion and to simplify configuration. This breaking change
ensures that any user currently using ports.grpc in an encrypted
mode will receive an error message indicating that ports.grpc_tls
must be explicitly used.

The suggested action for these users is to simply swap their ports.grpc
to ports.grpc_tls in the configuration file. If both ports are defined,
or if the user has not configured TLS for grpc, then the error message
will not be printed.
2022-11-11 14:29:22 -06:00

539 lines
14 KiB
Go

package config
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/types"
)
func TestLoad(t *testing.T) {
// Basically just testing that injection of the extra
// source works.
devMode := true
builderOpts := LoadOpts{
// putting this in dev mode so that the config validates
// without having to specify a data directory
DevMode: &devMode,
DefaultConfig: FileSource{
Name: "test",
Format: "hcl",
Data: `node_name = "hobbiton"`,
},
Overrides: []Source{
FileSource{
Name: "overrides",
Format: "json",
Data: `{"check_reap_interval": "1ms"}`,
},
},
}
result, err := Load(builderOpts)
require.NoError(t, err)
require.Empty(t, result.Warnings)
cfg := result.RuntimeConfig
require.NotNil(t, cfg)
require.Equal(t, "hobbiton", cfg.NodeName)
require.Equal(t, 1*time.Millisecond, cfg.CheckReapInterval)
}
func TestShouldParseFile(t *testing.T) {
var testcases = []struct {
filename string
configFormat string
expected bool
}{
{filename: "config.json", expected: true},
{filename: "config.hcl", expected: true},
{filename: "config", configFormat: "hcl", expected: true},
{filename: "config.js", configFormat: "json", expected: true},
{filename: "config.yaml", expected: false},
}
for _, tc := range testcases {
name := fmt.Sprintf("filename=%s, format=%s", tc.filename, tc.configFormat)
t.Run(name, func(t *testing.T) {
require.Equal(t, tc.expected, shouldParseFile(tc.filename, tc.configFormat))
})
}
}
func TestNewBuilder_PopulatesSourcesFromConfigFiles(t *testing.T) {
paths := setupConfigFiles(t)
b, err := newBuilder(LoadOpts{ConfigFiles: paths})
require.NoError(t, err)
expected := []Source{
FileSource{Name: paths[0], Format: "hcl", Data: "content a"},
FileSource{Name: paths[1], Format: "json", Data: "content b"},
FileSource{Name: filepath.Join(paths[3], "a.hcl"), Format: "hcl", Data: "content a"},
FileSource{Name: filepath.Join(paths[3], "b.json"), Format: "json", Data: "content b"},
}
require.Equal(t, expected, b.Sources)
require.Len(t, b.Warnings, 2)
}
func TestNewBuilder_PopulatesSourcesFromConfigFiles_WithConfigFormat(t *testing.T) {
paths := setupConfigFiles(t)
b, err := newBuilder(LoadOpts{ConfigFiles: paths, ConfigFormat: "hcl"})
require.NoError(t, err)
expected := []Source{
FileSource{Name: paths[0], Format: "hcl", Data: "content a"},
FileSource{Name: paths[1], Format: "hcl", Data: "content b"},
FileSource{Name: paths[2], Format: "hcl", Data: "content c"},
FileSource{Name: filepath.Join(paths[3], "a.hcl"), Format: "hcl", Data: "content a"},
FileSource{Name: filepath.Join(paths[3], "b.json"), Format: "hcl", Data: "content b"},
FileSource{Name: filepath.Join(paths[3], "c.yaml"), Format: "hcl", Data: "content c"},
}
require.Equal(t, expected, b.Sources)
}
// TODO: this would be much nicer with gotest.tools/fs
func setupConfigFiles(t *testing.T) []string {
t.Helper()
path, err := os.MkdirTemp("", t.Name())
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(path) })
subpath := filepath.Join(path, "sub")
err = os.Mkdir(subpath, 0755)
require.NoError(t, err)
for _, dir := range []string{path, subpath} {
err = os.WriteFile(filepath.Join(dir, "a.hcl"), []byte("content a"), 0644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "b.json"), []byte("content b"), 0644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "c.yaml"), []byte("content c"), 0644)
require.NoError(t, err)
}
return []string{
filepath.Join(path, "a.hcl"),
filepath.Join(path, "b.json"),
filepath.Join(path, "c.yaml"),
subpath,
}
}
func TestLoad_NodeName(t *testing.T) {
type testCase struct {
name string
nodeName string
expectedWarn string
}
fn := func(t *testing.T, tc testCase) {
opts := LoadOpts{
FlagValues: Config{
NodeName: pString(tc.nodeName),
DataDir: pString("dir"),
},
}
patchLoadOptsShims(&opts)
result, err := Load(opts)
require.NoError(t, err)
require.Len(t, result.Warnings, 1)
require.Contains(t, result.Warnings[0], tc.expectedWarn)
}
var testCases = []testCase{
{
name: "invalid character - unicode",
nodeName: "🐼",
expectedWarn: `Node name "🐼" will not be discoverable via DNS due to invalid characters`,
},
{
name: "invalid character - slash",
nodeName: "thing/other/ok",
expectedWarn: `Node name "thing/other/ok" will not be discoverable via DNS due to invalid characters`,
},
{
name: "too long",
nodeName: strings.Repeat("a", 66),
expectedWarn: "due to it being too long.",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fn(t, tc)
})
}
}
func TestBuilder_unixPermissionsVal(t *testing.T) {
b, _ := newBuilder(LoadOpts{
FlagValues: Config{
NodeName: pString("foo"),
DataDir: pString("dir"),
},
})
goodmode := "666"
badmode := "9666"
patchLoadOptsShims(&b.opts)
require.NoError(t, b.err)
_ = b.unixPermissionsVal("local_bind_socket_mode", &goodmode)
require.NoError(t, b.err)
require.Len(t, b.Warnings, 0)
_ = b.unixPermissionsVal("local_bind_socket_mode", &badmode)
require.NotNil(t, b.err)
require.Contains(t, b.err.Error(), "local_bind_socket_mode: invalid mode")
require.Len(t, b.Warnings, 0)
}
func patchLoadOptsShims(opts *LoadOpts) {
if opts.hostname == nil {
opts.hostname = func() (string, error) {
return "thehostname", nil
}
}
if opts.getPrivateIPv4 == nil {
opts.getPrivateIPv4 = func() ([]*net.IPAddr, error) {
return []*net.IPAddr{ipAddr("10.0.0.1")}, nil
}
}
if opts.getPublicIPv6 == nil {
opts.getPublicIPv6 = func() ([]*net.IPAddr, error) {
return []*net.IPAddr{ipAddr("dead:beef::1")}, nil
}
}
}
func TestLoad_HTTPMaxConnsPerClientExceedsRLimit(t *testing.T) {
hcl := `
limits{
# We put a very high value to be sure to fail
# This value is more than max on Windows as well
http_max_conns_per_client = 16777217
}`
opts := LoadOpts{
DefaultConfig: FileSource{
Name: "test",
Format: "hcl",
Data: `
ae_interval = "1m"
data_dir="/tmp/00000000001979"
bind_addr = "127.0.0.1"
advertise_addr = "127.0.0.1"
datacenter = "dc1"
bootstrap = true
server = true
node_id = "00000000001979"
node_name = "Node-00000000001979"
`,
},
HCL: []string{hcl},
}
_, err := Load(opts)
require.Error(t, err)
assert.Contains(t, err.Error(), "but limits.http_max_conns_per_client: 16777217 needs at least 16777237")
}
func TestLoad_EmptyClientAddr(t *testing.T) {
type testCase struct {
name string
clientAddr *string
expectedWarningMessage *string
}
fn := func(t *testing.T, tc testCase) {
opts := LoadOpts{
FlagValues: Config{
ClientAddr: tc.clientAddr,
DataDir: pString("dir"),
},
}
patchLoadOptsShims(&opts)
result, err := Load(opts)
require.NoError(t, err)
if tc.expectedWarningMessage != nil {
require.Len(t, result.Warnings, 1)
require.Contains(t, result.Warnings[0], *tc.expectedWarningMessage)
}
}
var testCases = []testCase{
{
name: "empty string",
clientAddr: pString(""),
expectedWarningMessage: pString("client_addr is empty, client services (DNS, HTTP, HTTPS, GRPC) will not be listening for connections"),
},
{
name: "nil pointer",
clientAddr: nil, // defaults to 127.0.0.1
expectedWarningMessage: nil, // expecting no warnings
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fn(t, tc)
})
}
}
func TestBuilder_DurationVal_InvalidDuration(t *testing.T) {
b := builder{}
badDuration1 := "not-a-duration"
badDuration2 := "also-not"
b.durationVal("field1", &badDuration1)
b.durationVal("field1", &badDuration2)
require.Error(t, b.err)
require.Contains(t, b.err.Error(), "2 errors")
require.Contains(t, b.err.Error(), badDuration1)
require.Contains(t, b.err.Error(), badDuration2)
}
func TestBuilder_ServiceVal_MultiError(t *testing.T) {
b := builder{}
b.serviceVal(&ServiceDefinition{
Meta: map[string]string{"": "empty-key"},
Port: intPtr(12345),
SocketPath: strPtr("/var/run/socket.sock"),
Checks: []CheckDefinition{
{Interval: strPtr("bad-interval")},
},
Weights: &ServiceWeights{Passing: intPtr(-1)},
})
require.Error(t, b.err)
require.Contains(t, b.err.Error(), "4 errors")
require.Contains(t, b.err.Error(), "bad-interval")
require.Contains(t, b.err.Error(), "Key cannot be blank")
require.Contains(t, b.err.Error(), "Invalid weight")
require.Contains(t, b.err.Error(), "cannot have both socket path")
}
func TestBuilder_ServiceVal_with_Check(t *testing.T) {
b := builder{}
svc := b.serviceVal(&ServiceDefinition{
Name: strPtr("unbound"),
ID: strPtr("unbound"),
Port: intPtr(12345),
Checks: []CheckDefinition{
{
Interval: strPtr("5s"),
UDP: strPtr("localhost:53"),
},
},
})
require.NoError(t, b.err)
require.Equal(t, 1, len(svc.Checks))
require.Equal(t, "localhost:53", svc.Checks[0].UDP)
}
func intPtr(v int) *int {
return &v
}
func TestBuilder_tlsVersion(t *testing.T) {
b := builder{}
validTLSVersion := "TLSv1_3"
b.tlsVersion("tls.defaults.tls_min_version", &validTLSVersion)
deprecatedTLSVersion := "tls11"
b.tlsVersion("tls.defaults.tls_min_version", &deprecatedTLSVersion)
invalidTLSVersion := "tls9"
b.tlsVersion("tls.defaults.tls_min_version", &invalidTLSVersion)
require.Error(t, b.err)
require.Contains(t, b.err.Error(), "2 errors")
require.Contains(t, b.err.Error(), deprecatedTLSVersion)
require.Contains(t, b.err.Error(), invalidTLSVersion)
}
func TestBuilder_WarnGRPCTLS(t *testing.T) {
tests := []struct {
name string
hcl string
expectErr bool
}{
{
name: "success",
hcl: ``,
expectErr: false,
},
{
name: "grpc_tls is disabled but explicitly defined",
hcl: `
ports { grpc_tls = -1 }
tls { grpc { cert_file = "defined" }}
`,
// This behavior is a little strange, but it allows users
// to setup TLS and disable the port if they wish.
expectErr: false,
},
{
name: "grpc is disabled",
hcl: `
ports { grpc = -1 }
tls { grpc { cert_file = "defined" }}
`,
expectErr: false,
},
{
name: "grpc_tls is undefined with default manual cert",
hcl: `
tls { defaults { cert_file = "defined" }}
`,
expectErr: true,
},
{
name: "grpc_tls is undefined with manual cert",
hcl: `
tls { grpc { cert_file = "defined" }}
`,
expectErr: true,
},
{
name: "grpc_tls is undefined with auto encrypt",
hcl: `
auto_encrypt { tls = true }
tls { grpc { use_auto_cert = true }}
`,
expectErr: true,
},
{
name: "grpc_tls is undefined with auto config",
hcl: `
auto_config { enabled = true }
tls { grpc { use_auto_cert = true }}
`,
expectErr: true,
},
}
for _, tc := range tests {
// using dev mode skips the need for a data dir
// and enables both grpc ports by default.
devMode := true
builderOpts := LoadOpts{
DevMode: &devMode,
Overrides: []Source{
FileSource{
Name: "overrides",
Format: "hcl",
Data: tc.hcl,
},
},
}
_, err := Load(builderOpts)
if tc.expectErr {
require.Error(t, err)
require.Contains(t, err.Error(), "listener no longer supports TLS")
} else {
require.NoError(t, err)
}
}
}
func TestBuilder_tlsCipherSuites(t *testing.T) {
b := builder{}
validCipherSuites := strings.Join([]string{
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
}, ",")
b.tlsCipherSuites("tls.defaults.tls_cipher_suites", &validCipherSuites, types.TLSv1_2)
require.NoError(t, b.err)
unsupportedCipherSuites := strings.Join([]string{
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
}, ",")
b.tlsCipherSuites("tls.defaults.tls_cipher_suites", &unsupportedCipherSuites, types.TLSv1_2)
invalidCipherSuites := strings.Join([]string{
"cipherX",
}, ",")
b.tlsCipherSuites("tls.defaults.tls_cipher_suites", &invalidCipherSuites, types.TLSv1_2)
b.tlsCipherSuites("tls.defaults.tls_cipher_suites", &validCipherSuites, types.TLSv1_3)
require.Error(t, b.err)
require.Contains(t, b.err.Error(), "3 errors")
require.Contains(t, b.err.Error(), unsupportedCipherSuites)
require.Contains(t, b.err.Error(), invalidCipherSuites)
require.Contains(t, b.err.Error(), "cipher suites are not configurable")
}
func TestBuilder_parsePrefixFilter(t *testing.T) {
t.Run("Check that 1.12 rpc metrics are parsed correctly.", func(t *testing.T) {
type testCase struct {
name string
metricsPrefix string
prefixFilter []string
expectedAllowedPrefix []string
expectedBlockedPrefix []string
}
var testCases = []testCase{
{
name: "no prefix filter",
metricsPrefix: "somePrefix",
prefixFilter: []string{},
expectedAllowedPrefix: nil,
expectedBlockedPrefix: []string{"somePrefix.rpc.server.call"},
},
{
name: "operator enables 1.12 rpc metrics",
metricsPrefix: "somePrefix",
prefixFilter: []string{"+somePrefix.rpc.server.call"},
expectedAllowedPrefix: []string{"somePrefix.rpc.server.call"},
expectedBlockedPrefix: nil,
},
{
name: "operator enables 1.12 rpc metrics",
metricsPrefix: "somePrefix",
prefixFilter: []string{"-somePrefix.rpc.server.call"},
expectedAllowedPrefix: nil,
expectedBlockedPrefix: []string{"somePrefix.rpc.server.call"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := builder{}
telemetry := &Telemetry{
MetricsPrefix: &tc.metricsPrefix,
PrefixFilter: tc.prefixFilter,
}
allowedPrefix, blockedPrefix := b.parsePrefixFilter(telemetry)
require.Equal(t, tc.expectedAllowedPrefix, allowedPrefix)
require.Equal(t, tc.expectedBlockedPrefix, blockedPrefix)
})
}
})
}