open-consul/agent/consul/cluster_endpoint_test.go

618 lines
18 KiB
Go
Raw Normal View History

package consul
import (
"encoding/base64"
"fmt"
"io/ioutil"
"math/rand"
"net"
"os"
"path"
"testing"
"time"
"github.com/hashicorp/consul/agent/agentpb"
"github.com/hashicorp/consul/agent/agentpb/config"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/memberlist"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2/jwt"
)
func testJWTStandardClaims() jwt.Claims {
now := time.Now()
return jwt.Claims{
Subject: "consul",
Issuer: "consul",
Audience: jwt.Audience{"consul"},
NotBefore: jwt.NewNumericDate(now.Add(-1 * time.Second)),
Expiry: jwt.NewNumericDate(now.Add(10 * time.Minute)),
}
}
func signJWT(t *testing.T, privKey string, claims jwt.Claims, privateClaims interface{}) string {
t.Helper()
token, err := oidcauthtest.SignJWT(privKey, claims, privateClaims)
require.NoError(t, err)
return token
}
func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{}) string {
t.Helper()
return signJWT(t, privKey, testJWTStandardClaims(), claims)
}
// TestClusterAutoConfig is really an integration test of all the moving parts of the Cluster.AutoConfig RPC.
// Full testing of the individual parts will not be done in this test:
//
// * Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer)
// * Each of the individual config generation functions. These can be unit tested separately and many wont
// require a running test server.
func TestClusterAutoConfig(t *testing.T) {
type testCase struct {
request agentpb.AutoConfigRequest
expected agentpb.AutoConfigResponse
patchResponse func(t *testing.T, srv *Server, resp *agentpb.AutoConfigResponse)
err string
}
gossipKey := make([]byte, 32)
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
n, err := rand.Read(gossipKey)
require.NoError(t, err)
require.Equal(t, 32, n)
gossipKeyEncoded := base64.StdEncoding.EncodeToString(gossipKey)
// generate a test certificate for the server serving out the insecure RPC
cert, key, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
// generate a JWT signer
pub, priv, err := oidcauthtest.GenerateKey()
require.NoError(t, err)
_, altpriv, err := oidcauthtest.GenerateKey()
require.NoError(t, err)
cases := map[string]testCase{
"wrong-datacenter": {
request: agentpb.AutoConfigRequest{
Datacenter: "no-such-dc",
},
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
},
"unverifiable": {
request: agentpb.AutoConfigRequest{
Node: "test-node",
// this is signed using an incorrect private key
JWT: signJWTWithStandardClaims(t, altpriv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
},
"claim-assertion-failed": {
request: agentpb.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
},
err: "Permission denied: Failed JWT claim assertion",
},
"good": {
request: agentpb.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
},
expected: agentpb.AutoConfigResponse{
Config: &config.Config{
Datacenter: "dc1",
PrimaryDatacenter: "dc1",
NodeName: "test-node",
AutoEncrypt: &config.AutoEncrypt{
TLS: true,
},
ACL: &config.ACL{
Enabled: true,
PolicyTTL: "30s",
TokenTTL: "30s",
RoleTTL: "30s",
DisabledTTL: "0s",
DownPolicy: "extend-cache",
DefaultPolicy: "deny",
Tokens: &config.ACLTokens{
Agent: "patched-secret",
},
},
Gossip: &config.Gossip{
Encryption: &config.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
TLS: &config.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
},
},
},
patchResponse: func(t *testing.T, srv *Server, resp *agentpb.AutoConfigResponse) {
// we are expecting an ACL token but cannot check anything for equality
// so here we check that it was set and overwrite it
require.NotNil(t, resp.Config)
require.NotNil(t, resp.Config.ACL)
require.NotNil(t, resp.Config.ACL.Tokens)
require.NotEmpty(t, resp.Config.ACL.Tokens.Agent)
resp.Config.ACL.Tokens.Agent = "patched-secret"
// we don't know the expected join address until we start up the test server
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: srv.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
require.NotNil(t, resp.Config.Gossip)
require.Equal(t, []string{joinAddr.String()}, resp.Config.Gossip.RetryJoinLAN)
resp.Config.Gossip.RetryJoinLAN = nil
},
},
}
_, s, _ := testACLServerWithConfig(t, func(c *Config) {
c.Domain = "consul"
c.AutoConfigAuthzEnabled = true
c.AutoConfigAuthzAuthMethod = structs.ACLAuthMethod{
Name: "Auth Config Authorizer",
Type: "jwt",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Config: map[string]interface{}{
"BoundAudiences": []string{"consul"},
"BoundIssuer": "consul",
"JWTValidationPubKeys": []string{pub},
"ClaimMappings": map[string]string{
"consul_node_name": "node",
},
},
}
c.AutoConfigAuthzClaimAssertions = []string{
`value.node == "${node}"`,
}
c.AutoConfigAuthzAllowReuse = true
cafile := path.Join(c.DataDir, "cacert.pem")
err := ioutil.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
certfile := path.Join(c.DataDir, "cert.pem")
err = ioutil.WriteFile(certfile, []byte(cert), 0600)
require.NoError(t, err)
keyfile := path.Join(c.DataDir, "key.pem")
err = ioutil.WriteFile(keyfile, []byte(key), 0600)
require.NoError(t, err)
c.CAFile = cafile
c.CertFile = certfile
c.KeyFile = keyfile
c.VerifyOutgoing = true
c.VerifyIncoming = true
c.VerifyServerHostname = true
c.TLSMinVersion = "tls12"
c.TLSPreferServerCipherSuites = true
c.ConnectEnabled = true
c.AutoEncryptAllowTLS = true
c.SerfLANConfig.MemberlistConfig.GossipVerifyIncoming = true
c.SerfLANConfig.MemberlistConfig.GossipVerifyOutgoing = true
keyring, err := memberlist.NewKeyring(nil, gossipKey)
require.NoError(t, err)
c.SerfLANConfig.MemberlistConfig.Keyring = keyring
}, false)
conf := tlsutil.Config{
CAFile: s.config.CAFile,
VerifyServerHostname: s.config.VerifyServerHostname,
VerifyOutgoing: s.config.VerifyOutgoing,
Domain: s.config.Domain,
}
codec, err := insecureRPCClient(s, conf)
require.NoError(t, err)
waitForLeaderEstablishment(t, s)
for testName, tcase := range cases {
t.Run(testName, func(t *testing.T) {
var reply agentpb.AutoConfigResponse
err := msgpackrpc.CallWithCodec(codec, "Cluster.AutoConfig", &tcase.request, &reply)
if tcase.err != "" {
testutil.RequireErrorContains(t, err, tcase.err)
} else {
require.NoError(t, err)
if tcase.patchResponse != nil {
tcase.patchResponse(t, s, &reply)
}
require.Equal(t, tcase.expected, reply)
}
})
}
}
func TestClusterAutoConfig_baseConfig(t *testing.T) {
type testCase struct {
serverConfig Config
opts AutoConfigOptions
expected config.Config
err string
}
cases := map[string]testCase{
"ok": {
serverConfig: Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
},
opts: AutoConfigOptions{
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
expected: config.Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
},
"no-node-name": {
serverConfig: Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
},
err: "Cannot generate auto config response without a node name",
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cluster := Cluster{
srv: &Server{
config: &tcase.serverConfig,
},
}
var actual config.Config
err := cluster.baseConfig(tcase.opts, &actual)
if tcase.err == "" {
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
} else {
testutil.RequireErrorContains(t, err, tcase.err)
}
})
}
}
func TestClusterAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
dir := testutil.TempDir(t, "auto-config-tls-settings")
t.Cleanup(func() { os.RemoveAll(dir) })
cafile := path.Join(dir, "cacert.pem")
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
parseCiphers := func(t *testing.T, cipherStr string) []uint16 {
t.Helper()
ciphers, err := tlsutil.ParseCiphers(cipherStr)
require.NoError(t, err)
return ciphers
}
type testCase struct {
tlsConfig tlsutil.Config
expected config.Config
}
cases := map[string]testCase{
"secure": {
tlsConfig: tlsutil.Config{
VerifyOutgoing: true,
VerifyServerHostname: true,
TLSMinVersion: "tls12",
PreferServerCipherSuites: true,
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: config.Config{
TLS: &config.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
CipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
"less-secure": {
tlsConfig: tlsutil.Config{
VerifyOutgoing: true,
VerifyServerHostname: false,
TLSMinVersion: "tls10",
PreferServerCipherSuites: false,
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: config.Config{
TLS: &config.TLS{
VerifyOutgoing: true,
VerifyServerHostname: false,
MinVersion: "tls10",
PreferServerCipherSuites: false,
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
logger := testutil.Logger(t)
configurator, err := tlsutil.NewConfigurator(tcase.tlsConfig, logger)
require.NoError(t, err)
cluster := &Cluster{
srv: &Server{
tlsConfigurator: configurator,
},
}
var actual config.Config
err = cluster.updateTLSSettingsInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
type testCase struct {
conf memberlist.Config
expected config.Config
}
gossipKey := make([]byte, 32)
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
n, err := rand.Read(gossipKey)
require.NoError(t, err)
require.Equal(t, 32, n)
gossipKeyEncoded := base64.StdEncoding.EncodeToString(gossipKey)
keyring, err := memberlist.NewKeyring(nil, gossipKey)
require.NoError(t, err)
cases := map[string]testCase{
"encryption-required": {
conf: memberlist.Config{
Keyring: keyring,
GossipVerifyIncoming: true,
GossipVerifyOutgoing: true,
},
expected: config.Config{
Gossip: &config.Gossip{
Encryption: &config.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
},
},
"encryption-allowed": {
conf: memberlist.Config{
Keyring: keyring,
GossipVerifyIncoming: false,
GossipVerifyOutgoing: false,
},
expected: config.Config{
Gossip: &config.Gossip{
Encryption: &config.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: false,
VerifyOutgoing: false,
},
},
},
},
"encryption-disabled": {
// zero values all around - if no keyring is configured then the gossip
// encryption settings should not be set.
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cluster := Cluster{
srv: &Server{
config: DefaultConfig(),
},
}
cluster.srv.config.SerfLANConfig.MemberlistConfig = &tcase.conf
var actual config.Config
err := cluster.updateGossipEncryptionInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
type testCase struct {
serverConfig Config
expected config.Config
}
cases := map[string]testCase{
"auto_encrypt-enabled": {
serverConfig: Config{
ConnectEnabled: true,
AutoEncryptAllowTLS: true,
},
expected: config.Config{
AutoEncrypt: &config.AutoEncrypt{TLS: true},
},
},
"auto_encrypt-disabled": {
serverConfig: Config{
ConnectEnabled: true,
AutoEncryptAllowTLS: false,
},
expected: config.Config{
AutoEncrypt: &config.AutoEncrypt{TLS: false},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cluster := Cluster{
srv: &Server{
config: &tcase.serverConfig,
},
}
var actual config.Config
err := cluster.updateTLSCertificatesInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
type testCase struct {
patch func(c *Config)
expected config.Config
verify func(t *testing.T, c *config.Config)
err string
}
cases := map[string]testCase{
"enabled": {
patch: func(c *Config) {
c.ACLsEnabled = true
c.ACLPolicyTTL = 7 * time.Second
c.ACLRoleTTL = 10 * time.Second
c.ACLTokenTTL = 12 * time.Second
c.ACLDisabledTTL = 31 * time.Second
c.ACLDefaultPolicy = "allow"
c.ACLDownPolicy = "deny"
c.ACLEnableKeyListPolicy = true
},
expected: config.Config{
ACL: &config.ACL{
Enabled: true,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
Tokens: &config.ACLTokens{Agent: "verified"},
},
},
verify: func(t *testing.T, c *config.Config) {
t.Helper()
// the agent token secret is non-deterministically generated
// So we want to validate that one was set and overwrite with
// a value that the expected configurate wants.
require.NotNil(t, c)
require.NotNil(t, c.ACL)
require.NotNil(t, c.ACL.Tokens)
require.NotEmpty(t, c.ACL.Tokens.Agent)
c.ACL.Tokens.Agent = "verified"
},
},
"disabled": {
patch: func(c *Config) {
c.ACLsEnabled = false
c.ACLPolicyTTL = 7 * time.Second
c.ACLRoleTTL = 10 * time.Second
c.ACLTokenTTL = 12 * time.Second
c.ACLDisabledTTL = 31 * time.Second
c.ACLDefaultPolicy = "allow"
c.ACLDownPolicy = "deny"
c.ACLEnableKeyListPolicy = true
},
expected: config.Config{
ACL: &config.ACL{
Enabled: false,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
},
},
},
"local-tokens-disabled": {
patch: func(c *Config) {
c.PrimaryDatacenter = "somewhere else"
},
err: "Agent Auto Configuration requires local token usage to be enabled in this datacenter",
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
_, s, _ := testACLServerWithConfig(t, tcase.patch, false)
waitForLeaderEstablishment(t, s)
cluster := Cluster{srv: s}
var actual config.Config
err := cluster.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, &actual)
if tcase.err != "" {
testutil.RequireErrorContains(t, err, tcase.err)
} else {
require.NoError(t, err)
if tcase.verify != nil {
tcase.verify(t, &actual)
}
require.Equal(t, tcase.expected, actual)
}
})
}
}
func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
conf := testClusterConfig{
Datacenter: "primary",
Servers: 3,
}
nodes := newTestCluster(t, &conf)
cluster := Cluster{srv: nodes.Servers[0]}
var actual config.Config
err := cluster.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
var expected []string
for _, srv := range nodes.Servers {
expected = append(expected, fmt.Sprintf("127.0.0.1:%d", srv.config.SerfLANConfig.MemberlistConfig.BindPort))
}
require.NotNil(t, actual.Gossip)
require.ElementsMatch(t, expected, actual.Gossip.RetryJoinLAN)
}