open-consul/agent/consul/auto_config_endpoint_test.go

1039 lines
30 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package consul
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net"
"net/url"
"os"
"path"
"testing"
"time"
msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
"github.com/hashicorp/memberlist"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/proto/private/pbautoconf"
"github.com/hashicorp/consul/proto/private/pbconfig"
"github.com/hashicorp/consul/proto/private/pbconnect"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types"
"gopkg.in/square/go-jose.v2/jwt"
)
type mockAutoConfigBackend struct {
mock.Mock
}
func (m *mockAutoConfigBackend) CreateACLToken(template *structs.ACLToken) (*structs.ACLToken, error) {
ret := m.Called(template)
// this handles converting an untyped nil to a typed nil
token, _ := ret.Get(0).(*structs.ACLToken)
return token, ret.Error(1)
}
func (m *mockAutoConfigBackend) DatacenterJoinAddresses(partition, segment string) ([]string, error) {
ret := m.Called(partition, segment)
// this handles converting an untyped nil to a typed nil
addrs, _ := ret.Get(0).([]string)
return addrs, ret.Error(1)
}
func (m *mockAutoConfigBackend) ForwardRPC(method string, req structs.RPCInfo, reply interface{}) (bool, error) {
ret := m.Called(method, req, reply)
return ret.Bool(0), ret.Error(1)
}
func (m *mockAutoConfigBackend) GetCARoots() (*structs.IndexedCARoots, error) {
ret := m.Called()
roots, _ := ret.Get(0).(*structs.IndexedCARoots)
return roots, ret.Error(1)
}
func (m *mockAutoConfigBackend) SignCertificate(csr *x509.CertificateRequest, id connect.CertURI) (*structs.IssuedCert, error) {
ret := m.Called(csr, id)
cert, _ := ret.Get(0).(*structs.IssuedCert)
return cert, ret.Error(1)
}
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)
}
// TestAutoConfigInitialConfiguration is really an integration test of all the moving parts of the AutoConfig.InitialConfiguration 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 should NOT
// require running test servers
func TestAutoConfigInitialConfiguration(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
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)
// this CSR is what gets sent in the request
csrID := connect.SpiffeIDAgent{
Host: "dummy.trustdomain",
Agent: "test-node",
Datacenter: "dc1",
}
csr, _ := connect.TestCSR(t, &csrID)
altCSRID := connect.SpiffeIDAgent{
Host: "dummy.trustdomain",
Agent: "alt",
Datacenter: "dc1",
}
altCSR, _ := connect.TestCSR(t, &altCSRID)
_, s, _ := testACLServerWithConfig(t, func(c *Config) {
c.TLSConfig.Domain = "consul"
c.AutoConfigAuthzEnabled = true
c.AutoConfigAuthzAuthMethod = structs.ACLAuthMethod{
Name: "Auth Config Authorizer",
Type: "jwt",
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
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 := os.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
certfile := path.Join(c.DataDir, "cert.pem")
err = os.WriteFile(certfile, []byte(cert), 0600)
require.NoError(t, err)
keyfile := path.Join(c.DataDir, "key.pem")
err = os.WriteFile(keyfile, []byte(key), 0600)
require.NoError(t, err)
c.TLSConfig.InternalRPC.CAFile = cafile
c.TLSConfig.InternalRPC.CertFile = certfile
c.TLSConfig.InternalRPC.KeyFile = keyfile
c.TLSConfig.InternalRPC.VerifyOutgoing = true
c.TLSConfig.InternalRPC.VerifyIncoming = true
c.TLSConfig.InternalRPC.VerifyServerHostname = true
c.TLSConfig.InternalRPC.TLSMinVersion = types.TLSv1_2
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)
// TODO: use s.config.TLSConfig directly instead of creating a new one?
conf := tlsutil.Config{
InternalRPC: tlsutil.ProtocolConfig{
CAFile: s.config.TLSConfig.InternalRPC.CAFile,
VerifyServerHostname: s.config.TLSConfig.InternalRPC.VerifyServerHostname,
VerifyOutgoing: s.config.TLSConfig.InternalRPC.VerifyOutgoing,
},
Domain: s.config.TLSConfig.Domain,
}
codec, err := insecureRPCClient(s, conf)
require.NoError(t, err)
waitForLeaderEstablishment(t, s)
roots, err := s.getCARoots(nil, s.fsm.State())
require.NoError(t, err)
pbroots, err := pbconnect.NewCARootsFromStructs(roots)
require.NoError(t, err)
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: s.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
// -------------------------------------------------------------------------
// Common test setup is now complete
// -------------------------------------------------------------------------
type testCase struct {
request *pbautoconf.AutoConfigRequest
expected *pbautoconf.AutoConfigResponse
patchResponse func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse)
err string
}
defaultEntMeta := structs.DefaultEnterpriseMetaInDefaultPartition()
cases := map[string]testCase{
"wrong-datacenter": {
request: &pbautoconf.AutoConfigRequest{
Datacenter: "no-such-dc",
},
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
},
"unverifiable": {
request: &pbautoconf.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",
},
"bad-req-node": {
request: &pbautoconf.AutoConfigRequest{
Node: "bad node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Invalid request field. node =",
},
"bad-req-segment": {
request: &pbautoconf.AutoConfigRequest{
Node: "test-node",
Segment: "bad segment",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Invalid request field. segment =",
},
"bad-req-partition": {
request: &pbautoconf.AutoConfigRequest{
Node: "test-node",
Partition: "bad partition",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Invalid request field. partition =",
},
"claim-assertion-failed": {
request: &pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
},
err: "Permission denied: Failed JWT claim assertion",
},
"bad-csr-id": {
request: &pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
CSR: altCSR,
},
err: "Spiffe ID agent name (alt) of the certificate signing request is not for the correct node (test-node)",
},
"good": {
request: &pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
CSR: csr,
},
expected: &pbautoconf.AutoConfigResponse{
CARoots: pbroots,
ExtraCACertificates: []string{cacert},
Config: &pbconfig.Config{
Datacenter: "dc1",
PrimaryDatacenter: "dc1",
NodeName: "test-node",
ACL: &pbconfig.ACL{
Enabled: true,
PolicyTTL: "30s",
TokenTTL: "30s",
RoleTTL: "30s",
DownPolicy: "extend-cache",
DefaultPolicy: "deny",
Tokens: &pbconfig.ACLTokens{
Agent: "patched-secret",
},
},
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
RetryJoinLAN: []string{joinAddr.String()},
},
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
},
},
},
patchResponse: func(t *testing.T, _ *Server, resp *pbautoconf.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"
require.NotNil(t, resp.Certificate)
require.NotEmpty(t, resp.Certificate.SerialNumber)
require.NotEmpty(t, resp.Certificate.CertPEM)
require.Empty(t, resp.Certificate.Service)
require.Empty(t, resp.Certificate.ServiceURI)
require.Equal(t, "test-node", resp.Certificate.Agent)
expectedID := connect.SpiffeIDAgent{
Host: roots.TrustDomain,
Agent: "test-node",
Partition: defaultEntMeta.PartitionOrDefault(),
Datacenter: "dc1",
}
require.Equal(t, expectedID.URI().String(), resp.Certificate.AgentURI)
// nil this out so we don't check it for equality
resp.Certificate = nil
},
},
}
for testName, tcase := range cases {
t.Run(testName, func(t *testing.T) {
reply := &pbautoconf.AutoConfigResponse{}
err := msgpackrpc.CallWithCodec(codec, "AutoConfig.InitialConfiguration", &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)
}
prototest.AssertDeepEqual(t, tcase.expected, reply)
}
})
}
}
func TestAutoConfig_baseConfig(t *testing.T) {
type testCase struct {
serverConfig Config
opts AutoConfigOptions
expected *pbautoconf.AutoConfigResponse
err string
}
cases := map[string]testCase{
"ok": {
serverConfig: Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
},
opts: AutoConfigOptions{
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.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) {
ac := AutoConfig{
config: &tcase.serverConfig,
}
actual := &pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.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 TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
dir := testutil.TempDir(t, "auto-config-tls-settings")
cafile := path.Join(dir, "cacert.pem")
err = os.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
type testCase struct {
tlsConfig tlsutil.Config
expected *pbautoconf.AutoConfigResponse
}
cases := map[string]testCase{
"secure": {
tlsConfig: tlsutil.Config{
InternalRPC: tlsutil.ProtocolConfig{
VerifyServerHostname: true,
VerifyOutgoing: true,
TLSMinVersion: types.TLSv1_2,
CAFile: cafile,
CipherSuites: []types.TLSCipherSuite{"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: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
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{
InternalRPC: tlsutil.ProtocolConfig{
VerifyServerHostname: false,
VerifyOutgoing: true,
TLSMinVersion: types.TLSv1_0,
CAFile: cafile,
CipherSuites: []types.TLSCipherSuite{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
},
},
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: false,
MinVersion: "tls10",
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)
ac := &AutoConfig{
tlsConfigurator: configurator,
}
actual := &pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err = ac.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 *pbautoconf.AutoConfigResponse
}
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: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
},
},
},
"encryption-allowed": {
conf: memberlist.Config{
Keyring: keyring,
GossipVerifyIncoming: false,
GossipVerifyOutgoing: false,
},
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.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.
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
cfg := DefaultConfig()
cfg.SerfLANConfig.MemberlistConfig = &tcase.conf
ac := AutoConfig{
config: cfg,
}
actual := &pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.updateGossipEncryptionInConfig(AutoConfigOptions{}, actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
now := time.Now()
later := now.Add(time.Hour)
// Generate a Test CA
ca := connect.TestCA(t, nil)
// roots will be returned by the mock backend
roots := structs.IndexedCARoots{
ActiveRootID: ca.ID,
TrustDomain: connect.TestClusterID + ".consul",
Roots: []*structs.CARoot{
ca,
},
}
// this CSR is what gets put into the opts for the
// function to look at an process
csrID := connect.SpiffeIDAgent{
Host: roots.TrustDomain,
Agent: "test",
Datacenter: "dc1",
}
csrStr, _ := connect.TestCSR(t, &csrID)
csr, err := connect.ParseCSR(csrStr)
require.NoError(t, err)
// fake certificate response for the backend
fakeCert := structs.IssuedCert{
SerialNumber: "1",
CertPEM: "not-currently-decoded",
ValidAfter: now,
ValidBefore: later,
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
RaftIndex: structs.RaftIndex{
ModifyIndex: 10,
CreateIndex: 10,
},
}
// translate the fake cert to the protobuf equivalent
// for embedding in expected results
pbcert, err := pbconnect.NewIssuedCertFromStructs(&fakeCert)
require.NoError(t, err)
// generate a CA certificate to use for specifying non-Connect
// certificates which come back differently in the response
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
// write out that ca cert to disk - it is unfortunate that
// this is necessary but creation of the tlsutil.Configurator
// will error if it cannot load the CA certificate from disk
dir := testutil.TempDir(t, "auto-config-tls-certificate")
cafile := path.Join(dir, "cacert.pem")
err = os.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
// translate the roots response to protobuf to be embedded
// into the expected results
pbroots, err := pbconnect.NewCARootsFromStructs(&roots)
require.NoError(t, err)
type testCase struct {
serverConfig Config
tlsConfig tlsutil.Config
opts AutoConfigOptions
expected *pbautoconf.AutoConfigResponse
}
cases := map[string]testCase{
"no-csr": {
serverConfig: Config{
ConnectEnabled: true,
},
tlsConfig: tlsutil.Config{
InternalRPC: tlsutil.ProtocolConfig{
VerifyServerHostname: true,
VerifyOutgoing: true,
TLSMinVersion: types.TLSv1_2,
CAFile: cafile,
CipherSuites: []types.TLSCipherSuite{"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: &pbautoconf.AutoConfigResponse{
CARoots: pbroots,
ExtraCACertificates: []string{cacert},
Config: &pbconfig.Config{},
},
},
"signed-certificate": {
serverConfig: Config{
ConnectEnabled: true,
},
tlsConfig: tlsutil.Config{
InternalRPC: tlsutil.ProtocolConfig{
VerifyServerHostname: true,
VerifyOutgoing: true,
TLSMinVersion: types.TLSv1_2,
CAFile: cafile,
CipherSuites: []types.TLSCipherSuite{"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"},
},
},
opts: AutoConfigOptions{
NodeName: "test",
CSR: csr,
SpiffeID: &csrID,
},
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{},
CARoots: pbroots,
ExtraCACertificates: []string{cacert},
Certificate: pbcert,
},
},
"connect-disabled": {
serverConfig: Config{
ConnectEnabled: false,
},
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
backend := &mockAutoConfigBackend{}
backend.On("GetCARoots").Return(&roots, nil)
backend.On("SignCertificate", tcase.opts.CSR, tcase.opts.SpiffeID).Return(&fakeCert, nil)
tlsConfigurator, err := tlsutil.NewConfigurator(tcase.tlsConfig, testutil.Logger(t))
require.NoError(t, err)
ac := AutoConfig{
config: &tcase.serverConfig,
tlsConfigurator: tlsConfigurator,
backend: backend,
}
actual := &pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err = ac.updateTLSCertificatesInConfig(tcase.opts, actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
}
}
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
type testCase struct {
config Config
expected *pbautoconf.AutoConfigResponse
expectACLToken bool
err error
}
const (
tokenAccessor = "b98761aa-c0ee-445b-9b0c-f54b56b47778"
tokenSecret = "1c96448a-ab04-4caa-982a-e8b095a111e2"
)
testDC := "dc1"
cases := map[string]testCase{
"enabled": {
config: Config{
Datacenter: testDC,
PrimaryDatacenter: testDC,
ACLsEnabled: true,
ACLResolverSettings: ACLResolverSettings{
ACLPolicyTTL: 7 * time.Second,
ACLRoleTTL: 10 * time.Second,
ACLTokenTTL: 12 * time.Second,
ACLDefaultPolicy: "allow",
ACLDownPolicy: "deny",
},
ACLEnableKeyListPolicy: true,
},
expectACLToken: true,
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
ACL: &pbconfig.ACL{
Enabled: true,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
Tokens: &pbconfig.ACLTokens{
Agent: tokenSecret,
},
},
},
},
},
"disabled": {
config: Config{
Datacenter: testDC,
PrimaryDatacenter: testDC,
ACLsEnabled: false,
ACLResolverSettings: ACLResolverSettings{
ACLPolicyTTL: 7 * time.Second,
ACLRoleTTL: 10 * time.Second,
ACLTokenTTL: 12 * time.Second,
ACLDefaultPolicy: "allow",
ACLDownPolicy: "deny",
},
ACLEnableKeyListPolicy: true,
},
expectACLToken: false,
expected: &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
ACL: &pbconfig.ACL{
Enabled: false,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
},
},
},
},
"local-tokens-disabled": {
config: Config{
Datacenter: testDC,
PrimaryDatacenter: "somewhere-else",
ACLsEnabled: true,
},
expectACLToken: true,
err: fmt.Errorf("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) {
backend := &mockAutoConfigBackend{}
expectedTemplate := &structs.ACLToken{
Description: `Auto Config Token for Node "something"`,
Local: true,
NodeIdentities: []*structs.ACLNodeIdentity{
{
NodeName: "something",
Datacenter: testDC,
},
},
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
}
testToken := &structs.ACLToken{
AccessorID: tokenAccessor,
SecretID: tokenSecret,
Description: `Auto Config Token for Node "something"`,
Local: true,
NodeIdentities: []*structs.ACLNodeIdentity{
{
NodeName: "something",
Datacenter: testDC,
},
},
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
}
if tcase.expectACLToken {
backend.On("CreateACLToken", expectedTemplate).Return(testToken, tcase.err).Once()
}
ac := AutoConfig{config: &tcase.config, backend: backend}
actual := &pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, actual)
if tcase.err != nil {
testutil.RequireErrorContains(t, err, tcase.err.Error())
} else {
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
}
backend.AssertExpectations(t)
})
}
}
func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
addrs := []string{"198.18.0.7:8300", "198.18.0.1:8300"}
backend := &mockAutoConfigBackend{}
backend.On("DatacenterJoinAddresses", "", "").Return(addrs, nil).Once()
ac := AutoConfig{backend: backend}
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.NotNil(t, actual.Config.Gossip)
require.ElementsMatch(t, addrs, actual.Config.Gossip.RetryJoinLAN)
backend.AssertExpectations(t)
}
func TestAutoConfig_parseAutoConfigCSR(t *testing.T) {
// createCSR copies the behavior of connect.CreateCSR with some
// customizations to allow for better unit testing.
createCSR := func(tmpl *x509.CertificateRequest, privateKey crypto.Signer) (string, error) {
connect.HackSANExtensionForCSR(tmpl)
bs, err := x509.CreateCertificateRequest(rand.Reader, tmpl, privateKey)
require.NoError(t, err)
var csrBuf bytes.Buffer
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
require.NoError(t, err)
return csrBuf.String(), nil
}
pk, _, err := connect.GeneratePrivateKey()
require.NoError(t, err)
agentURI := connect.SpiffeIDAgent{
Host: "test-host",
Datacenter: "tdc1",
Agent: "test-agent",
}.URI()
tests := []struct {
name string
setup func() string
expectErr string
}{
{
name: "err_garbage_data",
expectErr: "Failed to parse CSR",
setup: func() string { return "garbage" },
},
{
name: "err_not_one_uri",
expectErr: "CSR SAN contains an invalid number of URIs",
setup: func() string {
tmpl := &x509.CertificateRequest{
URIs: []*url.URL{agentURI, agentURI},
SignatureAlgorithm: connect.SigAlgoForKey(pk),
}
csr, err := createCSR(tmpl, pk)
require.NoError(t, err)
return csr
},
},
{
name: "err_email",
expectErr: "CSR SAN does not allow specifying email addresses",
setup: func() string {
tmpl := &x509.CertificateRequest{
URIs: []*url.URL{agentURI},
EmailAddresses: []string{"test@example.com"},
SignatureAlgorithm: connect.SigAlgoForKey(pk),
}
csr, err := createCSR(tmpl, pk)
require.NoError(t, err)
return csr
},
},
{
name: "err_spiffe_parse_uri",
expectErr: "Failed to parse the SPIFFE URI",
setup: func() string {
tmpl := &x509.CertificateRequest{
URIs: []*url.URL{connect.SpiffeIDAgent{}.URI()},
SignatureAlgorithm: connect.SigAlgoForKey(pk),
}
csr, err := createCSR(tmpl, pk)
require.NoError(t, err)
return csr
},
},
{
name: "err_not_agent",
expectErr: "SPIFFE ID is not an Agent ID",
setup: func() string {
spiffe := connect.SpiffeIDService{
Namespace: "tns",
Datacenter: "tdc1",
Service: "test-service",
}
tmpl := &x509.CertificateRequest{
URIs: []*url.URL{spiffe.URI()},
SignatureAlgorithm: connect.SigAlgoForKey(pk),
}
csr, err := createCSR(tmpl, pk)
require.NoError(t, err)
return csr
},
},
{
name: "success",
setup: func() string {
tmpl := &x509.CertificateRequest{
URIs: []*url.URL{agentURI},
SignatureAlgorithm: connect.SigAlgoForKey(pk),
}
csr, err := createCSR(tmpl, pk)
require.NoError(t, err)
return csr
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req, spif, err := parseAutoConfigCSR(tc.setup())
if tc.expectErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectErr)
} else {
require.NoError(t, err)
// TODO better verification of these
require.NotNil(t, req)
require.NotNil(t, spif)
}
})
}
}
func TestAutoConfig_invalidSegmentName(t *testing.T) {
invalid := []string{
"\n",
"\r",
"\t",
"`",
`'`,
`"`,
` `,
`a b`,
`a'b`,
`a or b`,
`a and b`,
`segment name`,
`segment"name`,
`"segment"name`,
`"segment" name`,
`segment'name'`,
}
valid := []string{
``,
`a`,
`a.b`,
`a.b.c`,
`a-b-c`,
`segment.name`,
}
for _, s := range invalid {
require.True(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s)
}
for _, s := range valid {
require.False(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s)
}
}