09ae0ab94a
This field was never user-configurable. We always overwrote the value with 120s from NonUserSource. However, we also never copied the value from RuntimeConfig to consul.Config, So the value in NonUserSource was always ignored, and we used the default value of 30s set by consul.DefaultConfig. All of this code is an unnecessary distraction because a user can not actually configure this value. This commit removes the fields and uses a constant value instad. Someone attempting to set acl.disabled_ttl in their config will now get an error about an unknown field, but previously the value was completely ignored, so the new behaviour seems more correct. We have to keep this field in the AutoConfig response for backwards compatibility, but the value will be ignored by the client, so it doesn't really matter what value we set.
852 lines
25 KiB
Go
852 lines
25 KiB
Go
package consul
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"net"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/memberlist"
|
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
|
"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/pbautoconf"
|
|
"github.com/hashicorp/consul/proto/pbconfig"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/hashicorp/consul/tlsutil"
|
|
|
|
"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(segment string) ([]string, error) {
|
|
ret := m.Called(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 := 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.TLSConfig.CAFile = cafile
|
|
c.TLSConfig.CertFile = certfile
|
|
c.TLSConfig.KeyFile = keyfile
|
|
c.TLSConfig.VerifyOutgoing = true
|
|
c.TLSConfig.VerifyIncoming = true
|
|
c.TLSConfig.VerifyServerHostname = true
|
|
c.TLSConfig.TLSMinVersion = "tls12"
|
|
c.TLSConfig.PreferServerCipherSuites = 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)
|
|
|
|
// TODO: use s.config.TLSConfig directly instead of creating a new one?
|
|
conf := tlsutil.Config{
|
|
CAFile: s.config.TLSConfig.CAFile,
|
|
VerifyServerHostname: s.config.TLSConfig.VerifyServerHostname,
|
|
VerifyOutgoing: s.config.TLSConfig.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 := translateCARootsToProtobuf(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
|
|
}
|
|
|
|
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",
|
|
},
|
|
"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",
|
|
PreferServerCipherSuites: true,
|
|
},
|
|
},
|
|
},
|
|
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",
|
|
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) {
|
|
var 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)
|
|
}
|
|
require.Equal(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 parseCiphers(t *testing.T, cipherStr string) []uint16 {
|
|
t.Helper()
|
|
ciphers, err := tlsutil.ParseCiphers(cipherStr)
|
|
require.NoError(t, err)
|
|
return ciphers
|
|
}
|
|
|
|
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 = ioutil.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{
|
|
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: pbautoconf.AutoConfigResponse{
|
|
Config: &pbconfig.Config{
|
|
TLS: &pbconfig.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: pbautoconf.AutoConfigResponse{
|
|
Config: &pbconfig.Config{
|
|
TLS: &pbconfig.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)
|
|
|
|
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 := translateIssuedCertToProtobuf(&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 = ioutil.WriteFile(cafile, []byte(cacert), 0600)
|
|
require.NoError(t, err)
|
|
|
|
// translate the roots response to protobuf to be embedded
|
|
// into the expected results
|
|
pbroots, err := translateCARootsToProtobuf(&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{
|
|
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: pbautoconf.AutoConfigResponse{
|
|
CARoots: pbroots,
|
|
ExtraCACertificates: []string{cacert},
|
|
Config: &pbconfig.Config{},
|
|
},
|
|
},
|
|
"signed-certificate": {
|
|
serverConfig: Config{
|
|
ConnectEnabled: true,
|
|
},
|
|
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"),
|
|
},
|
|
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)
|
|
}
|