mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1655 lines
49 KiB
1655 lines
49 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package ca
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"runtime/pprof"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
vaultapi "github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/api/auth/gcp"
|
|
vaultconst "github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
)
|
|
|
|
const pkiTestPolicyBase = `
|
|
path "sys/mounts"
|
|
{
|
|
capabilities = ["read"]
|
|
}
|
|
path "sys/mounts/%[1]s"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}
|
|
path "sys/mounts/%[2]s"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}
|
|
path "sys/mounts/%[2]s/tune"
|
|
{
|
|
capabilities = ["update"]
|
|
}
|
|
path "%[1]s/*"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}
|
|
path "%[2]s/*"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}`
|
|
|
|
func pkiTestPolicy(rootPath, intermediatePath string) string {
|
|
return fmt.Sprintf(pkiTestPolicyBase, rootPath, intermediatePath)
|
|
}
|
|
|
|
func TestVaultCAProvider_ParseVaultCAConfig(t *testing.T) {
|
|
cases := map[string]struct {
|
|
rawConfig map[string]interface{}
|
|
expConfig *structs.VaultCAProviderConfig
|
|
isPrimary bool
|
|
expError string
|
|
}{
|
|
"no token and no auth method provided": {
|
|
rawConfig: map[string]interface{}{},
|
|
expError: "must provide a Vault token or configure a Vault auth method",
|
|
},
|
|
"both token and auth method provided": {
|
|
rawConfig: map[string]interface{}{"Token": "test", "AuthMethod": map[string]interface{}{"Type": "test"}},
|
|
expError: "only one of Vault token or Vault auth method can be provided, but not both",
|
|
},
|
|
"primary no root PKI path": {
|
|
rawConfig: map[string]interface{}{"Token": "test", "IntermediatePKIPath": "test"},
|
|
isPrimary: true,
|
|
expError: "must provide a valid path to a root PKI backend",
|
|
},
|
|
"secondary no root PKI path": {
|
|
rawConfig: map[string]interface{}{"Token": "test", "IntermediatePKIPath": "test"},
|
|
isPrimary: false,
|
|
expConfig: &structs.VaultCAProviderConfig{
|
|
CommonCAProviderConfig: defaultCommonConfig(),
|
|
Token: "test",
|
|
IntermediatePKIPath: "test/",
|
|
},
|
|
},
|
|
"no root intermediate path": {
|
|
rawConfig: map[string]interface{}{"Token": "test", "RootPKIPath": "test"},
|
|
expError: "must provide a valid path for the intermediate PKI backend",
|
|
},
|
|
"adds a slash to RootPKIPath and IntermediatePKIPath": {
|
|
isPrimary: true,
|
|
rawConfig: map[string]interface{}{"Token": "test", "RootPKIPath": "test", "IntermediatePKIPath": "test"},
|
|
expConfig: &structs.VaultCAProviderConfig{
|
|
CommonCAProviderConfig: defaultCommonConfig(),
|
|
Token: "test",
|
|
RootPKIPath: "test/",
|
|
IntermediatePKIPath: "test/",
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, c := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
config, err := ParseVaultCAConfig(c.rawConfig, c.isPrimary)
|
|
if c.expError != "" {
|
|
require.EqualError(t, err, c.expError)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expConfig, config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_configureVaultAuthMethod(t *testing.T) {
|
|
cases := map[string]struct {
|
|
expLoginPath string
|
|
params map[string]interface{}
|
|
expError string
|
|
hasLDG bool
|
|
}{
|
|
"alicloud": {expLoginPath: "auth/alicloud/login", params: map[string]any{"role": "test-role", "region": "test-region"}, hasLDG: true},
|
|
"approle": {expLoginPath: "auth/approle/login", params: map[string]any{"role_id_file_path": "test-path"}, hasLDG: true},
|
|
"aws": {expLoginPath: "auth/aws/login", params: map[string]interface{}{"type": "iam"}, hasLDG: true},
|
|
"azure": {expLoginPath: "auth/azure/login", params: map[string]interface{}{"role": "test-role", "resource": "test-resource"}, hasLDG: true},
|
|
"cf": {expLoginPath: "auth/cf/login"},
|
|
"github": {expLoginPath: "auth/github/login"},
|
|
"gcp": {expLoginPath: "auth/gcp/login", params: map[string]interface{}{"type": "iam", "role": "test-role"}},
|
|
"jwt": {expLoginPath: "auth/jwt/login", params: map[string]any{"role": "test-role", "path": "test-path"}, hasLDG: true},
|
|
"kerberos": {expLoginPath: "auth/kerberos/login"},
|
|
"kubernetes": {expLoginPath: "auth/kubernetes/login", params: map[string]interface{}{"role": "test-role"}, hasLDG: true},
|
|
"ldap": {expLoginPath: "auth/ldap/login/foo", params: map[string]interface{}{"username": "foo"}},
|
|
"oci": {expLoginPath: "auth/oci/login/foo", params: map[string]interface{}{"role": "foo"}},
|
|
"okta": {expLoginPath: "auth/okta/login/foo", params: map[string]interface{}{"username": "foo"}},
|
|
"radius": {expLoginPath: "auth/radius/login/foo", params: map[string]interface{}{"username": "foo"}},
|
|
"cert": {expLoginPath: "auth/cert/login"},
|
|
"token": {expError: "'token' auth method is not supported via auth method configuration; please provide the token with the 'token' parameter in the CA configuration"},
|
|
"userpass": {expLoginPath: "auth/userpass/login/foo", params: map[string]interface{}{"username": "foo"}},
|
|
"unsupported": {expError: "auth method \"unsupported\" is not supported"},
|
|
}
|
|
|
|
for authMethodType, c := range cases {
|
|
t.Run(authMethodType, func(t *testing.T) {
|
|
authMethod := &structs.VaultAuthMethod{
|
|
Type: authMethodType,
|
|
Params: c.params,
|
|
}
|
|
authIF, err := configureVaultAuthMethod(authMethod)
|
|
if c.expError != "" {
|
|
require.EqualError(t, err, c.expError)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, authIF)
|
|
|
|
switch authMethodType {
|
|
case VaultAuthMethodTypeGCP:
|
|
_ = authIF.(*gcp.GCPAuth)
|
|
default:
|
|
auth := authIF.(*VaultAuthClient)
|
|
require.Equal(t, authMethod, auth.AuthMethod)
|
|
require.Equal(t, c.expLoginPath, auth.LoginPath)
|
|
require.Equal(t, c.hasLDG, auth.LoginDataGen != nil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_VaultTLSConfig(t *testing.T) {
|
|
config := &structs.VaultCAProviderConfig{
|
|
CAFile: "/capath/ca.pem",
|
|
CAPath: "/capath/",
|
|
CertFile: "/certpath/cert.pem",
|
|
KeyFile: "/certpath/key.pem",
|
|
TLSServerName: "server.name",
|
|
TLSSkipVerify: true,
|
|
}
|
|
tlsConfig := vaultTLSConfig(config)
|
|
require.Equal(t, config.CAFile, tlsConfig.CACert)
|
|
require.Equal(t, config.CAPath, tlsConfig.CAPath)
|
|
require.Equal(t, config.CertFile, tlsConfig.ClientCert)
|
|
require.Equal(t, config.KeyFile, tlsConfig.ClientKey)
|
|
require.Equal(t, config.TLSServerName, tlsConfig.TLSServerName)
|
|
require.Equal(t, config.TLSSkipVerify, tlsConfig.Insecure)
|
|
}
|
|
|
|
func TestVaultCAProvider_Configure(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
testcases := []struct {
|
|
name string
|
|
rawConfig map[string]any
|
|
expectedValue func(t *testing.T, v *VaultProvider)
|
|
}{
|
|
{
|
|
name: "DefaultConfig",
|
|
rawConfig: map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
},
|
|
expectedValue: func(t *testing.T, v *VaultProvider) {
|
|
headers := v.client.Headers()
|
|
require.Equal(t, "", headers.Get(vaultconst.NamespaceHeaderName))
|
|
require.Equal(t, "pki-root/", v.config.RootPKIPath)
|
|
require.Equal(t, "pki-intermediate/", v.config.IntermediatePKIPath)
|
|
},
|
|
},
|
|
{
|
|
name: "TestConfigWithNamespace",
|
|
rawConfig: map[string]any{
|
|
"namespace": "ns1",
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
},
|
|
expectedValue: func(t *testing.T, v *VaultProvider) {
|
|
h := v.client.Headers()
|
|
require.Equal(t, "ns1", h.Get(vaultconst.NamespaceHeaderName))
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testcase := range testcases {
|
|
t.Run(testcase.name, func(t *testing.T) {
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, testcase.rawConfig)
|
|
testcase.expectedValue(t, provider)
|
|
})
|
|
}
|
|
}
|
|
|
|
// This test must not run in parallel
|
|
func TestVaultCAProvider_ConfigureFailureGoroutineLeakCheck(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := NewVaultProvider(hclog.New(&hclog.LoggerOptions{Name: "ca.vault"}))
|
|
|
|
t.Run("error on Configure does not leak renewal routine", func(t *testing.T) {
|
|
config := map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "badbadbad/",
|
|
}
|
|
cfg := vaultProviderConfig(t, testVault.Addr, token, config)
|
|
|
|
err := provider.Configure(cfg)
|
|
require.Error(t, err)
|
|
|
|
retry.RunWith(retry.TwoSeconds(), t, func(r *retry.R) {
|
|
profile := pprof.Lookup("goroutine")
|
|
sb := strings.Builder{}
|
|
require.NoError(r, profile.WriteTo(&sb, 2))
|
|
require.NotContains(r, sb.String(),
|
|
"created by github.com/hashicorp/consul/agent/connect/ca.(*VaultProvider).Configure",
|
|
"found renewal goroutine leak")
|
|
// If this test is failing because you added a new goroutine to
|
|
// (*VaultProvider).Configure AND that goroutine should persist
|
|
// even if Configure errored, then you should change the checked
|
|
// string to (*VaultProvider).renewToken.
|
|
})
|
|
})
|
|
|
|
t.Run("successful Configure starts renewal routine", func(t *testing.T) {
|
|
config := map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
}
|
|
cfg := vaultProviderConfig(t, testVault.Addr, token, config)
|
|
|
|
require.NoError(t, provider.Configure(cfg))
|
|
|
|
retry.RunWith(retry.TwoSeconds(), t, func(r *retry.R) {
|
|
profile := pprof.Lookup("goroutine")
|
|
sb := strings.Builder{}
|
|
require.NoError(r, profile.WriteTo(&sb, 2))
|
|
r.Log(sb.String())
|
|
require.Contains(r, sb.String(),
|
|
"created by github.com/hashicorp/consul/agent/connect/ca.(*VaultProvider).Configure",
|
|
"expected renewal goroutine, got none")
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestVaultCAProvider_SecondaryActiveIntermediate(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, false, testVault.Addr, token, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
cert, err := provider.ActiveLeafSigningCert()
|
|
require.Empty(t, cert)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestVaultCAProvider_RenewToken(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
// Create a token with a short TTL to be renewed by the provider.
|
|
ttl := 1 * time.Second
|
|
tcr := &vaultapi.TokenCreateRequest{
|
|
TTL: ttl.String(),
|
|
}
|
|
secret, err := testVault.client.Auth().Token().Create(tcr)
|
|
require.NoError(t, err)
|
|
providerToken := secret.Auth.ClientToken
|
|
|
|
_ = createVaultProvider(t, true, testVault.Addr, providerToken, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
// Check the last renewal time.
|
|
secret, err = testVault.client.Auth().Token().Lookup(providerToken)
|
|
require.NoError(t, err)
|
|
firstRenewal, err := secret.Data["last_renewal_time"].(json.Number).Int64()
|
|
require.NoError(t, err)
|
|
|
|
// Retry past the TTL and make sure the token has been renewed.
|
|
retry.Run(t, func(r *retry.R) {
|
|
secret, err = testVault.client.Auth().Token().Lookup(providerToken)
|
|
require.NoError(r, err)
|
|
lastRenewal, err := secret.Data["last_renewal_time"].(json.Number).Int64()
|
|
require.NoError(r, err)
|
|
require.Greater(r, lastRenewal, firstRenewal)
|
|
})
|
|
}
|
|
|
|
func TestVaultCAProvider_RenewTokenStopWatcherOnConfigure(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
// Create a token with a short TTL to be renewed by the provider.
|
|
ttl := 1 * time.Second
|
|
tcr := &vaultapi.TokenCreateRequest{
|
|
TTL: ttl.String(),
|
|
}
|
|
secret, err := testVault.client.Auth().Token().Create(tcr)
|
|
require.NoError(t, err)
|
|
providerToken := secret.Auth.ClientToken
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, providerToken, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
// overwrite stopWatcher to set flag on stop for testing
|
|
// be sure that original stopWatcher gets called to avoid goroutine leak
|
|
gotStopped := uint32(0)
|
|
realStop := provider.stopWatcher
|
|
provider.stopWatcher = func() {
|
|
atomic.StoreUint32(&gotStopped, 1)
|
|
realStop()
|
|
}
|
|
|
|
// Check the last renewal time.
|
|
secret, err = testVault.client.Auth().Token().Lookup(providerToken)
|
|
require.NoError(t, err)
|
|
firstRenewal, err := secret.Data["last_renewal_time"].(json.Number).Int64()
|
|
require.NoError(t, err)
|
|
|
|
// Wait past the TTL and make sure the token has been renewed.
|
|
retry.Run(t, func(r *retry.R) {
|
|
secret, err = testVault.client.Auth().Token().Lookup(providerToken)
|
|
require.NoError(r, err)
|
|
lastRenewal, err := secret.Data["last_renewal_time"].(json.Number).Int64()
|
|
require.NoError(r, err)
|
|
require.Greater(r, lastRenewal, firstRenewal)
|
|
})
|
|
|
|
providerConfig := vaultProviderConfig(t, testVault.Addr, providerToken, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
require.NoError(t, provider.Configure(providerConfig))
|
|
require.Equal(t, uint32(1), atomic.LoadUint32(&gotStopped))
|
|
}
|
|
|
|
func TestVaultCAProvider_Bootstrap(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
type testcase struct {
|
|
name string
|
|
caConfig map[string]any
|
|
certFunc func(*VaultProvider) (string, error)
|
|
backendPath string
|
|
rootCaCreation bool
|
|
expectedRootCertTTL string
|
|
}
|
|
|
|
run := func(t *testing.T, tc testcase) {
|
|
t.Parallel()
|
|
|
|
tc.caConfig["RootPKIPath"] = "pki-root/"
|
|
tc.caConfig["IntermediatePKIPath"] = "pki-intermediate/"
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, tc.caConfig)
|
|
client := testVault.client
|
|
|
|
cert, err := tc.certFunc(provider)
|
|
require.NoError(t, err)
|
|
resp, err := client.Logical().ReadRaw(tc.backendPath + "ca/pem")
|
|
require.NoError(t, err)
|
|
bytes, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, cert, string(bytes)+"\n")
|
|
|
|
// Should be a valid CA cert
|
|
parsed, err := connect.ParseCert(cert)
|
|
require.NoError(t, err)
|
|
require.True(t, parsed.IsCA)
|
|
require.Len(t, parsed.URIs, 1)
|
|
require.Equal(t, fmt.Sprintf("spiffe://%s.consul", provider.clusterID), parsed.URIs[0].String())
|
|
|
|
// test that the root cert ttl as applied
|
|
if tc.rootCaCreation {
|
|
rootCertTTL, err := time.ParseDuration(tc.expectedRootCertTTL)
|
|
require.NoError(t, err)
|
|
expectedNotAfter := time.Now().Add(rootCertTTL).UTC()
|
|
|
|
require.WithinDuration(t, expectedNotAfter, parsed.NotAfter, 10*time.Minute, "expected parsed cert ttl to be the same as the value configured")
|
|
}
|
|
}
|
|
|
|
cases := []testcase{
|
|
{
|
|
name: "default-root-cert-ttl",
|
|
caConfig: map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
},
|
|
certFunc: func(provider *VaultProvider) (string, error) {
|
|
root, err := provider.GenerateCAChain()
|
|
return root, err
|
|
},
|
|
backendPath: "pki-root/",
|
|
rootCaCreation: true,
|
|
expectedRootCertTTL: structs.DefaultRootCertTTL,
|
|
},
|
|
{
|
|
name: "custom-root-cert-ttl",
|
|
caConfig: map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
"RootCertTTL": "8761h",
|
|
},
|
|
certFunc: func(provider *VaultProvider) (string, error) {
|
|
return provider.ActiveLeafSigningCert()
|
|
},
|
|
backendPath: "pki-intermediate/",
|
|
rootCaCreation: false,
|
|
expectedRootCertTTL: "8761h",
|
|
},
|
|
}
|
|
|
|
// Verify the root and intermediate certs match the ones in the vault backends
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertCorrectKeyType(t *testing.T, want, certPEM string) {
|
|
t.Helper()
|
|
|
|
cert, err := connect.ParseCert(certPEM)
|
|
require.NoError(t, err)
|
|
|
|
switch want {
|
|
case "ec":
|
|
require.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm)
|
|
case "rsa":
|
|
require.Equal(t, x509.RSA, cert.PublicKeyAlgorithm)
|
|
default:
|
|
t.Fatal("test doesn't support key type")
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_SignLeaf(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
run := func(t *testing.T, tc KeyTestCase) {
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.KeyType,
|
|
"PrivateKeyBits": tc.KeyBits,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
spiffeService := &connect.SpiffeIDService{
|
|
Host: "node1",
|
|
Namespace: "default",
|
|
Datacenter: "dc1",
|
|
Service: "foo",
|
|
}
|
|
|
|
rootPEM, err := provider.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
assertCorrectKeyType(t, tc.KeyType, rootPEM)
|
|
|
|
intPEM, err := provider.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
assertCorrectKeyType(t, tc.KeyType, intPEM)
|
|
|
|
// Generate a leaf cert for the service.
|
|
var firstSerial uint64
|
|
{
|
|
raw, _ := connect.TestCSR(t, spiffeService)
|
|
|
|
csr, err := connect.ParseCSR(raw)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := provider.Sign(csr)
|
|
require.NoError(t, err)
|
|
|
|
parsed, err := connect.ParseCert(cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, parsed.URIs[0], spiffeService.URI())
|
|
firstSerial = parsed.SerialNumber.Uint64()
|
|
|
|
// Ensure the cert is valid now and expires within the correct limit.
|
|
now := time.Now()
|
|
require.True(t, parsed.NotAfter.Sub(now) < time.Hour)
|
|
require.True(t, parsed.NotBefore.Before(now))
|
|
|
|
// Make sure we can validate the cert as expected.
|
|
require.NoError(t, connect.ValidateLeaf(rootPEM, cert, []string{intPEM}))
|
|
requireTrailingNewline(t, cert)
|
|
}
|
|
|
|
// Generate a new cert for another service and make sure
|
|
// the serial number is unique.
|
|
spiffeService.Service = "bar"
|
|
{
|
|
raw, _ := connect.TestCSR(t, spiffeService)
|
|
|
|
csr, err := connect.ParseCSR(raw)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := provider.Sign(csr)
|
|
require.NoError(t, err)
|
|
|
|
parsed, err := connect.ParseCert(cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, parsed.URIs[0], spiffeService.URI())
|
|
require.NotEqual(t, firstSerial, parsed.SerialNumber.Uint64())
|
|
|
|
// Ensure the cert is valid now and expires within the correct limit.
|
|
require.True(t, time.Until(parsed.NotAfter) < time.Hour)
|
|
require.True(t, parsed.NotBefore.Before(time.Now()))
|
|
|
|
// Make sure we can validate the cert as expected.
|
|
require.NoError(t, connect.ValidateLeaf(rootPEM, cert, []string{intPEM}))
|
|
}
|
|
}
|
|
|
|
for _, tc := range KeyTestCases {
|
|
t.Run(tc.Desc, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_CrossSignCA(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
tests := CASigningKeyTypeCases()
|
|
|
|
run := func(t *testing.T, tc CASigningKeyTypes, withSudo, expectFailure bool) {
|
|
t.Parallel()
|
|
|
|
testVault1 := NewTestVaultServer(t)
|
|
|
|
attr1 := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
WithSudo: withSudo,
|
|
}
|
|
token1 := CreateVaultTokenWithAttrs(t, testVault1.client, attr1)
|
|
|
|
provider1 := createVaultProvider(t, true, testVault1.Addr, token1, map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.SigningKeyType,
|
|
"PrivateKeyBits": tc.SigningKeyBits,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
testutil.RunStep(t, "init", func(t *testing.T) {
|
|
rootPEM, err := provider1.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
assertCorrectKeyType(t, tc.SigningKeyType, rootPEM)
|
|
|
|
intPEM, err := provider1.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
assertCorrectKeyType(t, tc.SigningKeyType, intPEM)
|
|
})
|
|
|
|
testVault2 := NewTestVaultServer(t)
|
|
|
|
attr2 := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
WithSudo: false, // irrelevant for the new CA provider
|
|
}
|
|
token2 := CreateVaultTokenWithAttrs(t, testVault2.client, attr2)
|
|
|
|
provider2 := createVaultProvider(t, true, testVault2.Addr, token2, map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.CSRKeyType,
|
|
"PrivateKeyBits": tc.CSRKeyBits,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
testutil.RunStep(t, "swap", func(t *testing.T) {
|
|
rootPEM, err := provider2.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
assertCorrectKeyType(t, tc.CSRKeyType, rootPEM)
|
|
|
|
intPEM, err := provider2.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
assertCorrectKeyType(t, tc.CSRKeyType, intPEM)
|
|
|
|
if expectFailure {
|
|
testCrossSignProvidersShouldFail(t, provider1, provider2)
|
|
} else {
|
|
testCrossSignProviders(t, provider1, provider2)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.Desc, func(t *testing.T) {
|
|
t.Run("without sudo", func(t *testing.T) {
|
|
run(t, tc, false, true)
|
|
})
|
|
t.Run("with sudo", func(t *testing.T) {
|
|
run(t, tc, true, false)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultProvider_SignIntermediate(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
tests := CASigningKeyTypeCases()
|
|
|
|
run := func(t *testing.T, tc CASigningKeyTypes) {
|
|
t.Parallel()
|
|
|
|
testVault1 := NewTestVaultServer(t)
|
|
|
|
attr1 := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token1 := CreateVaultTokenWithAttrs(t, testVault1.client, attr1)
|
|
|
|
provider1 := createVaultProvider(t, true, testVault1.Addr, token1, map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.SigningKeyType,
|
|
"PrivateKeyBits": tc.SigningKeyBits,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
testVault2 := NewTestVaultServer(t)
|
|
|
|
attr2 := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token2 := CreateVaultTokenWithAttrs(t, testVault2.client, attr2)
|
|
|
|
provider2 := createVaultProvider(t, false, testVault2.Addr, token2, map[string]any{
|
|
"LeafCertTTL": "1h",
|
|
"PrivateKeyType": tc.CSRKeyType,
|
|
"PrivateKeyBits": tc.CSRKeyBits,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.Desc, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultProvider_SignIntermediateConsul(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
// primary = Vault, secondary = Consul
|
|
t.Run("pri=vault,sec=consul", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testVault1 := NewTestVaultServer(t)
|
|
|
|
attr1 := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token1 := CreateVaultTokenWithAttrs(t, testVault1.client, attr1)
|
|
|
|
provider1 := createVaultProvider(t, true, testVault1.Addr, token1, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
conf := testConsulCAConfig()
|
|
delegate := newMockDelegate(t, conf)
|
|
provider2 := TestConsulProvider(t, delegate)
|
|
cfg := testProviderConfig(conf)
|
|
cfg.IsPrimary = false
|
|
cfg.Datacenter = "dc2"
|
|
require.NoError(t, provider2.Configure(cfg))
|
|
|
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
})
|
|
|
|
// primary = Consul, secondary = Vault
|
|
t.Run("pri=consul,sec=vault", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
conf := testConsulCAConfig()
|
|
delegate := newMockDelegate(t, conf)
|
|
provider1 := TestConsulProvider(t, delegate)
|
|
require.NoError(t, provider1.Configure(testProviderConfig(conf)))
|
|
_, err := provider1.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
|
|
// Ensure that we don't configure vault to try and mint leafs that
|
|
// outlive their CA during the test (which hard fails in vault).
|
|
intermediateCertTTL := getIntermediateCertTTL(t, conf)
|
|
leafCertTTL := intermediateCertTTL - 4*time.Hour
|
|
|
|
overrideConf := map[string]any{
|
|
"LeafCertTTL": []uint8(leafCertTTL.String()),
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
}
|
|
|
|
testVault2 := NewTestVaultServer(t)
|
|
|
|
attr2 := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token2 := CreateVaultTokenWithAttrs(t, testVault2.client, attr2)
|
|
|
|
provider2 := createVaultProvider(t, false, testVault2.Addr, token2, overrideConf)
|
|
|
|
testSignIntermediateCrossDC(t, provider1, provider2)
|
|
})
|
|
}
|
|
|
|
func TestVaultProvider_Cleanup(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
t.Run("provider-change", func(t *testing.T) {
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
// ensure that the intermediate PKI mount exists
|
|
mounts, err := provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
|
|
// call cleanup with a provider change - this should cause removal of the mount
|
|
require.NoError(t, provider.Cleanup(true, nil))
|
|
|
|
// verify the mount was removed
|
|
mounts, err = provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.NotContains(t, mounts, provider.config.IntermediatePKIPath)
|
|
})
|
|
|
|
t.Run("pki-path-change", func(t *testing.T) {
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
// ensure that the intermediate PKI mount exists
|
|
mounts, err := provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
|
|
// call cleanup with an intermediate pki path change - this should cause removal of the mount
|
|
require.NoError(t, provider.Cleanup(false, map[string]any{
|
|
"Address": testVault.Addr,
|
|
"Token": token,
|
|
"RootPKIPath": "pki-root/",
|
|
//
|
|
"IntermediatePKIPath": "pki-intermediate-2/",
|
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
|
"LeafCertTTL": []uint8("72h"),
|
|
}))
|
|
|
|
// verify the mount was removed
|
|
mounts, err = provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.NotContains(t, mounts, provider.config.IntermediatePKIPath)
|
|
})
|
|
|
|
t.Run("pki-path-unchanged", func(t *testing.T) {
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
// ensure that the intermediate PKI mount exists
|
|
mounts, err := provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
|
|
// call cleanup with no config changes - this should not cause removal of the intermediate pki path
|
|
require.NoError(t, provider.Cleanup(false, map[string]any{
|
|
"Address": testVault.Addr,
|
|
"Token": token,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
|
"LeafCertTTL": []uint8("72h"),
|
|
}))
|
|
|
|
// verify the mount was NOT removed
|
|
mounts, err = provider.client.Sys().ListMounts()
|
|
require.NoError(t, err)
|
|
require.Contains(t, mounts, provider.config.IntermediatePKIPath)
|
|
})
|
|
}
|
|
|
|
func TestVaultProvider_ConfigureWithAuthMethod(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
cases := []struct {
|
|
authMethodType string
|
|
configureAuthMethodFunc func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{}
|
|
}{
|
|
{
|
|
authMethodType: "userpass",
|
|
configureAuthMethodFunc: func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{} {
|
|
_, err := vaultClient.Logical().Write("/auth/userpass/users/test",
|
|
map[string]interface{}{"password": "foo", "policies": "pki"})
|
|
require.NoError(t, err)
|
|
return map[string]interface{}{
|
|
"Type": "userpass",
|
|
"Params": map[string]interface{}{
|
|
"username": "test",
|
|
"password": "foo",
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
authMethodType: "approle",
|
|
configureAuthMethodFunc: func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{} {
|
|
_, err := vaultClient.Logical().Write("auth/approle/role/my-role",
|
|
map[string]interface{}{"token_policies": "pki"})
|
|
require.NoError(t, err)
|
|
resp, err := vaultClient.Logical().Read("auth/approle/role/my-role/role-id")
|
|
require.NoError(t, err)
|
|
roleID := resp.Data["role_id"]
|
|
|
|
resp, err = vaultClient.Logical().Write("auth/approle/role/my-role/secret-id", nil)
|
|
require.NoError(t, err)
|
|
secretID := resp.Data["secret_id"]
|
|
|
|
return map[string]interface{}{
|
|
"Type": "approle",
|
|
"Params": map[string]interface{}{
|
|
"role_id": roleID,
|
|
"secret_id": secretID,
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.authMethodType, func(t *testing.T) {
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
err := testVault.Client().Sys().EnableAuthWithOptions(c.authMethodType, &vaultapi.EnableAuthOptions{Type: c.authMethodType})
|
|
require.NoError(t, err)
|
|
|
|
err = testVault.Client().Sys().PutPolicy("pki", pkiTestPolicy("pki-root", "pki-intermediate"))
|
|
require.NoError(t, err)
|
|
|
|
authMethodConf := c.configureAuthMethodFunc(t, testVault.Client())
|
|
|
|
conf := map[string]any{
|
|
"Address": testVault.Addr,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
"AuthMethod": authMethodConf,
|
|
}
|
|
|
|
provider := NewVaultProvider(hclog.New(nil))
|
|
|
|
cfg := ProviderConfig{
|
|
ClusterID: connect.TestClusterID,
|
|
Datacenter: "dc1",
|
|
IsPrimary: true,
|
|
RawConfig: conf,
|
|
}
|
|
t.Cleanup(provider.Stop)
|
|
err = provider.Configure(cfg)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, provider.client.Token())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultProvider_RotateAuthMethodToken(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
err := testVault.Client().Sys().PutPolicy("pki", pkiTestPolicy("pki-root", "pki-intermediate"))
|
|
require.NoError(t, err)
|
|
|
|
err = testVault.Client().Sys().EnableAuthWithOptions("approle", &vaultapi.EnableAuthOptions{Type: "approle"})
|
|
require.NoError(t, err)
|
|
|
|
_, err = testVault.Client().Logical().Write("auth/approle/role/my-role",
|
|
map[string]interface{}{
|
|
"token_ttl": "2s",
|
|
"token_explicit_max_ttl": "2s",
|
|
"token_policies": "pki",
|
|
})
|
|
require.NoError(t, err)
|
|
resp, err := testVault.Client().Logical().Read("auth/approle/role/my-role/role-id")
|
|
require.NoError(t, err)
|
|
roleID := resp.Data["role_id"]
|
|
|
|
resp, err = testVault.Client().Logical().Write("auth/approle/role/my-role/secret-id", nil)
|
|
require.NoError(t, err)
|
|
secretID := resp.Data["secret_id"]
|
|
|
|
conf := map[string]any{
|
|
"Address": testVault.Addr,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
"AuthMethod": map[string]interface{}{
|
|
"Type": "approle",
|
|
"Params": map[string]interface{}{
|
|
"role_id": roleID,
|
|
"secret_id": secretID,
|
|
},
|
|
},
|
|
}
|
|
|
|
provider := NewVaultProvider(hclog.New(nil))
|
|
|
|
cfg := ProviderConfig{
|
|
ClusterID: connect.TestClusterID,
|
|
Datacenter: "dc1",
|
|
IsPrimary: true,
|
|
RawConfig: conf,
|
|
}
|
|
t.Cleanup(provider.Stop)
|
|
err = provider.Configure(cfg)
|
|
require.NoError(t, err)
|
|
token := provider.client.Token()
|
|
require.NotEmpty(t, token)
|
|
|
|
// Check that the token is rotated after max_ttl time has passed.
|
|
require.Eventually(t, func() bool {
|
|
return provider.client.Token() != token
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func TestVaultProvider_ReconfigureIntermediateTTL(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
// Set up a standard policy without any sys/mounts/pki-intermediate/tune permissions.
|
|
policy := `
|
|
path "sys/mounts"
|
|
{
|
|
capabilities = ["read"]
|
|
}
|
|
path "sys/mounts/pki-root"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}
|
|
path "sys/mounts/pki-intermediate"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}
|
|
path "pki-root/*"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}
|
|
path "pki-intermediate/*"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list"]
|
|
}`
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
err := testVault.Client().Sys().PutPolicy("pki", policy)
|
|
require.NoError(t, err)
|
|
|
|
tcr := &vaultapi.TokenCreateRequest{
|
|
Policies: []string{"pki"},
|
|
}
|
|
secret, err := testVault.client.Auth().Token().Create(tcr)
|
|
require.NoError(t, err)
|
|
providerToken := secret.Auth.ClientToken
|
|
|
|
makeProviderConfWithTTL := func(ttl string) ProviderConfig {
|
|
conf := map[string]any{
|
|
"Address": testVault.Addr,
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
"Token": providerToken,
|
|
"IntermediateCertTTL": ttl,
|
|
}
|
|
cfg := ProviderConfig{
|
|
ClusterID: connect.TestClusterID,
|
|
Datacenter: "dc1",
|
|
IsPrimary: true,
|
|
RawConfig: conf,
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
provider := NewVaultProvider(hclog.New(nil))
|
|
|
|
// Set up the initial provider config
|
|
t.Cleanup(provider.Stop)
|
|
err = provider.Configure(makeProviderConfWithTTL("222h"))
|
|
require.NoError(t, err)
|
|
_, err = provider.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
_, err = provider.GenerateLeafSigningCert()
|
|
require.NoError(t, err)
|
|
|
|
// Attempt to update the ttl without permissions for the tune endpoint - shouldn't
|
|
// return an error.
|
|
err = provider.Configure(makeProviderConfWithTTL("333h"))
|
|
require.NoError(t, err)
|
|
|
|
// Intermediate TTL shouldn't have changed
|
|
mountConfig, err := testVault.Client().Sys().MountConfig("pki-intermediate")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 222*3600, mountConfig.MaxLeaseTTL)
|
|
|
|
// Update the policy and verify we can reconfigure the TTL properly.
|
|
policy += `
|
|
path "sys/mounts/pki-intermediate/tune"
|
|
{
|
|
capabilities = ["update"]
|
|
}`
|
|
err = testVault.Client().Sys().PutPolicy("pki", policy)
|
|
require.NoError(t, err)
|
|
|
|
err = provider.Configure(makeProviderConfWithTTL("333h"))
|
|
require.NoError(t, err)
|
|
|
|
mountConfig, err = testVault.Client().Sys().MountConfig("pki-intermediate")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 333*3600, mountConfig.MaxLeaseTTL)
|
|
}
|
|
|
|
func TestVaultCAProvider_GenerateIntermediate(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, true, testVault.Addr, token, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
orig, err := provider.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
|
|
// This test was created to ensure that our calls to Vault
|
|
// returns a new Intermediate certificate and further calls
|
|
// to ActiveLeafSigningCert return the same new cert.
|
|
newLeaf, err := provider.GenerateLeafSigningCert()
|
|
require.NoError(t, err)
|
|
|
|
newActive, err := provider.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, newLeaf, newActive)
|
|
require.NotEqual(t, orig, newLeaf)
|
|
}
|
|
|
|
func TestVaultCAProvider_AutoTidyExpiredIssuers(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
provider := createVaultProvider(t, true, testVault.Addr, token,
|
|
map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
version := strings.Split(vaultTestVersion, ".")
|
|
require.Len(t, version, 3)
|
|
minorVersion, err := strconv.Atoi(version[1])
|
|
require.NoError(t, err)
|
|
expIssSet, errStr := provider.autotidyIssuers("pki-intermediate/")
|
|
switch {
|
|
case minorVersion <= 11:
|
|
require.False(t, expIssSet)
|
|
require.Contains(t, errStr, "auto-tidy")
|
|
case minorVersion == 12:
|
|
require.False(t, expIssSet)
|
|
require.Contains(t, errStr, "tidy_expired_issuers")
|
|
default: // Consul 1.13+
|
|
require.True(t, expIssSet)
|
|
}
|
|
|
|
// check permission denied
|
|
expIssSet, errStr = provider.autotidyIssuers("pki-bad/")
|
|
require.False(t, expIssSet)
|
|
require.Contains(t, errStr, "permission denied")
|
|
}
|
|
|
|
func TestVaultCAProvider_DeletePreviousIssuerAndKey(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
provider := createVaultProvider(t, true, testVault.Addr, token,
|
|
map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
res, err := testVault.Client().Logical().List("pki-intermediate/issuers")
|
|
require.NoError(t, err)
|
|
// Why 2 issuers? There is always an initial issuer that
|
|
// gets created before we manage the lifecycle of issuers.
|
|
// Since we're asserting that the number doesn't grow
|
|
// this isn't too much of a concern.
|
|
//
|
|
// Note the key "keys" refers to the IDs of the resource based
|
|
// on the endpoint the response is from.
|
|
require.Len(t, res.Data["keys"], 2)
|
|
|
|
res, err = testVault.Client().Logical().List("pki-intermediate/keys")
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Data["keys"], 1)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
_, err := provider.GenerateLeafSigningCert()
|
|
require.NoError(t, err)
|
|
|
|
res, err := testVault.Client().Logical().List("pki-intermediate/issuers")
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Data["keys"], 2)
|
|
|
|
res, err = testVault.Client().Logical().List("pki-intermediate/keys")
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Data["keys"], 1)
|
|
}
|
|
}
|
|
|
|
func TestVaultCAProvider_GenerateIntermediate_inSecondary(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
t.Parallel()
|
|
|
|
// Primary DC will be a consul provider.
|
|
conf := testConsulCAConfig()
|
|
delegate := newMockDelegate(t, conf)
|
|
primaryProvider := TestConsulProvider(t, delegate)
|
|
require.NoError(t, primaryProvider.Configure(testProviderConfig(conf)))
|
|
_, err := primaryProvider.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
|
|
// Ensure that we don't configure vault to try and mint leafs that
|
|
// outlive their CA during the test (which hard fails in vault).
|
|
intermediateCertTTL := getIntermediateCertTTL(t, conf)
|
|
leafCertTTL := intermediateCertTTL - 4*time.Hour
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
attr := &VaultTokenAttributes{
|
|
RootPath: "pki-root",
|
|
IntermediatePath: "pki-intermediate",
|
|
ConsulManaged: true,
|
|
}
|
|
token := CreateVaultTokenWithAttrs(t, testVault.client, attr)
|
|
|
|
provider := createVaultProvider(t, false, testVault.Addr, token, map[string]any{
|
|
"LeafCertTTL": []uint8(leafCertTTL.String()),
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
|
|
var origIntermediate string
|
|
testutil.RunStep(t, "initialize secondary provider", func(t *testing.T) {
|
|
// Get the intermediate CSR from provider.
|
|
csrPEM, issuerID, err := provider.GenerateIntermediateCSR()
|
|
require.NoError(t, err)
|
|
csr, err := connect.ParseCSR(csrPEM)
|
|
require.NoError(t, err)
|
|
|
|
// Sign the CSR with primaryProvider.
|
|
intermediatePEM, err := primaryProvider.SignIntermediate(csr)
|
|
require.NoError(t, err)
|
|
rootPEM, err := primaryProvider.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
|
|
// Give the new intermediate to provider to use.
|
|
require.NoError(t, provider.SetIntermediate(intermediatePEM, rootPEM, issuerID))
|
|
|
|
origIntermediate, err = provider.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
testutil.RunStep(t, "renew secondary provider", func(t *testing.T) {
|
|
// Get the intermediate CSR from provider.
|
|
csrPEM, issuerID, err := provider.GenerateIntermediateCSR()
|
|
require.NoError(t, err)
|
|
csr, err := connect.ParseCSR(csrPEM)
|
|
require.NoError(t, err)
|
|
|
|
// Sign the CSR with primaryProvider.
|
|
intermediatePEM, err := primaryProvider.SignIntermediate(csr)
|
|
require.NoError(t, err)
|
|
rootPEM, err := primaryProvider.GenerateCAChain()
|
|
require.NoError(t, err)
|
|
|
|
// Give the new intermediate to provider to use.
|
|
require.NoError(t, provider.SetIntermediate(intermediatePEM, rootPEM, issuerID))
|
|
|
|
// This test was created to ensure that our calls to Vault
|
|
// returns a new Intermediate certificate and further calls
|
|
// to ActiveLeafSigningCert return the same new cert.
|
|
newActiveIntermediate, err := provider.ActiveLeafSigningCert()
|
|
require.NoError(t, err)
|
|
|
|
require.NotEqual(t, origIntermediate, newActiveIntermediate)
|
|
require.Equal(t, intermediatePEM, newActiveIntermediate)
|
|
})
|
|
}
|
|
|
|
func TestVaultCAProvider_VaultManaged(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
const vaultManagedPKIPolicy = `
|
|
path "/pki-root/" {
|
|
capabilities = [ "read" ]
|
|
}
|
|
|
|
path "/pki-root/root/sign-intermediate" {
|
|
capabilities = [ "update" ]
|
|
}
|
|
|
|
path "/pki-intermediate/*" {
|
|
capabilities = [ "create", "read", "update", "delete", "list" ]
|
|
}
|
|
|
|
path "auth/token/renew-self" {
|
|
capabilities = [ "update" ]
|
|
}
|
|
|
|
path "auth/token/lookup-self" {
|
|
capabilities = [ "read" ]
|
|
}
|
|
`
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
client := testVault.Client()
|
|
|
|
client.SetToken("root")
|
|
|
|
// Mount pki root externally
|
|
require.NoError(t, client.Sys().Mount("pki-root", &vaultapi.MountInput{
|
|
Type: "pki",
|
|
Description: "root CA backend for Consul Connect",
|
|
Config: vaultapi.MountConfigInput{
|
|
MaxLeaseTTL: "12m",
|
|
},
|
|
}))
|
|
_, err := client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{
|
|
"common_name": "testconsul",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Mount pki intermediate externally
|
|
require.NoError(t, client.Sys().Mount("pki-intermediate", &vaultapi.MountInput{
|
|
Type: "pki",
|
|
Description: "intermediate CA backend for Consul Connect",
|
|
Config: vaultapi.MountConfigInput{
|
|
MaxLeaseTTL: "6m",
|
|
},
|
|
}))
|
|
|
|
// Generate a policy and token for the VaultProvider to use
|
|
require.NoError(t, client.Sys().PutPolicy("consul-ca", vaultManagedPKIPolicy))
|
|
tcr := &vaultapi.TokenCreateRequest{
|
|
Policies: []string{"consul-ca"},
|
|
}
|
|
secret, err := testVault.client.Auth().Token().Create(tcr)
|
|
require.NoError(t, err)
|
|
providerToken := secret.Auth.ClientToken
|
|
|
|
// We want to test the provider.Configure() step
|
|
|
|
_ = createVaultProvider(t, true, testVault.Addr, providerToken, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
}
|
|
|
|
func TestVaultCAProvider_ConsulManaged(t *testing.T) {
|
|
SkipIfVaultNotPresent(t)
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
|
|
client := testVault.Client()
|
|
|
|
client.SetToken("root")
|
|
|
|
// We do not configure any mounts and instead let Consul
|
|
// be responsible for mounting root and intermediate PKI
|
|
|
|
// Generate a policy and token for the VaultProvider to use
|
|
require.NoError(t, client.Sys().PutPolicy("consul-ca", pkiTestPolicy("pki-root", "pki-intermediate")))
|
|
tcr := &vaultapi.TokenCreateRequest{
|
|
Policies: []string{"consul-ca"},
|
|
}
|
|
secret, err := testVault.client.Auth().Token().Create(tcr)
|
|
require.NoError(t, err)
|
|
providerToken := secret.Auth.ClientToken
|
|
|
|
// We want to test the provider.Configure() step
|
|
|
|
_ = createVaultProvider(t, true, testVault.Addr, providerToken, map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
})
|
|
}
|
|
|
|
func TestVaultCAProvider_EnterpriseNamespace(t *testing.T) {
|
|
SkipIfVaultNotPresent(t, vaultRequirements{Enterprise: true})
|
|
t.Parallel()
|
|
|
|
cases := map[string]struct {
|
|
namespaces map[string]string
|
|
}{
|
|
"no configured namespaces": {},
|
|
"only base namespace provided": {namespaces: map[string]string{"Namespace": "base-ns"}},
|
|
"only root namespace provided": {namespaces: map[string]string{"RootPKINamespace": "root-pki-ns"}},
|
|
"only intermediate namespace provided": {namespaces: map[string]string{"IntermediatePKINamespace": "int-pki-ns"}},
|
|
"base and root namespace provided": {
|
|
namespaces: map[string]string{
|
|
"Namespace": "base-ns",
|
|
"RootPKINamespace": "root-pki-ns",
|
|
},
|
|
},
|
|
"base and intermediate namespace provided": {
|
|
namespaces: map[string]string{
|
|
"Namespace": "base-ns",
|
|
"IntermediatePKINamespace": "int-pki-ns",
|
|
},
|
|
},
|
|
"root and intermediate namespace provided": {
|
|
namespaces: map[string]string{
|
|
"RootPKINamespace": "root-pki-ns",
|
|
"IntermediatePKINamespace": "int-pki-ns",
|
|
},
|
|
},
|
|
"all namespaces provided": {
|
|
namespaces: map[string]string{
|
|
"Namespace": "base-ns",
|
|
"RootPKINamespace": "root-pki-ns",
|
|
"IntermediatePKINamespace": "int-pki-ns",
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, c := range cases {
|
|
c := c
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testVault := NewTestVaultServer(t)
|
|
token := "root"
|
|
|
|
providerConfig := map[string]any{
|
|
"RootPKIPath": "pki-root/",
|
|
"IntermediatePKIPath": "pki-intermediate/",
|
|
}
|
|
for k, v := range c.namespaces {
|
|
providerConfig[k] = v
|
|
}
|
|
|
|
if len(c.namespaces) > 0 {
|
|
// If explicit namespaces are provided, try to create the provider before any of the namespaces
|
|
// have been created. Verify that the provider fails to initialize.
|
|
provider, err := createVaultProviderE(t, true, testVault.Addr, token, providerConfig)
|
|
require.Error(t, err)
|
|
require.NotNil(t, provider)
|
|
}
|
|
|
|
// Create the namespaces
|
|
client := testVault.Client()
|
|
client.SetToken(token)
|
|
|
|
for _, ns := range c.namespaces {
|
|
_, err := client.Logical().Write(fmt.Sprintf("/sys/namespaces/%s", ns), map[string]any{})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Verify that once the namespaces have been created we are able to initialize the provider.
|
|
provider, err := createVaultProviderE(t, true, testVault.Addr, token, providerConfig)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, provider)
|
|
})
|
|
}
|
|
}
|
|
|
|
func getIntermediateCertTTL(t *testing.T, caConf *structs.CAConfiguration) time.Duration {
|
|
t.Helper()
|
|
|
|
require.NotNil(t, caConf)
|
|
require.NotNil(t, caConf.Config)
|
|
|
|
iface, ok := caConf.Config["IntermediateCertTTL"]
|
|
require.True(t, ok)
|
|
|
|
ttlBytes, ok := iface.([]uint8)
|
|
require.True(t, ok)
|
|
|
|
ttlString := string(ttlBytes)
|
|
|
|
dur, err := time.ParseDuration(ttlString)
|
|
require.NoError(t, err)
|
|
return dur
|
|
}
|
|
|
|
func createVaultProvider(t *testing.T, isPrimary bool, addr, token string, rawConf map[string]any) *VaultProvider {
|
|
t.Helper()
|
|
|
|
provider, err := createVaultProviderE(t, isPrimary, addr, token, rawConf)
|
|
require.NoError(t, err)
|
|
|
|
return provider
|
|
}
|
|
|
|
func createVaultProviderE(t *testing.T, isPrimary bool, addr, token string, rawConf map[string]any) (*VaultProvider, error) {
|
|
t.Helper()
|
|
cfg := vaultProviderConfig(t, addr, token, rawConf)
|
|
|
|
provider := NewVaultProvider(hclog.New(nil))
|
|
|
|
if !isPrimary {
|
|
cfg.IsPrimary = false
|
|
cfg.Datacenter = "dc2"
|
|
}
|
|
|
|
t.Cleanup(provider.Stop)
|
|
if err := provider.Configure(cfg); err != nil {
|
|
return provider, err
|
|
}
|
|
if isPrimary {
|
|
if _, err := provider.GenerateCAChain(); err != nil {
|
|
return provider, err
|
|
}
|
|
if _, err := provider.GenerateLeafSigningCert(); err != nil {
|
|
return provider, err
|
|
}
|
|
}
|
|
|
|
return provider, nil
|
|
}
|
|
|
|
func vaultProviderConfig(t *testing.T, addr, token string, rawConf map[string]any) ProviderConfig {
|
|
t.Helper()
|
|
require.NotEmpty(t, rawConf, "config map is required with at least %q and %q set",
|
|
"RootPKIPath",
|
|
"IntermediatePKIPath")
|
|
|
|
conf := map[string]any{
|
|
"Address": addr,
|
|
"Token": token,
|
|
// Tests duration parsing after msgpack type mangling during raft apply.
|
|
"LeafCertTTL": []uint8("72h"),
|
|
}
|
|
|
|
hasRequired := false
|
|
if rawConf != nil {
|
|
_, ok1 := rawConf["RootPKIPath"]
|
|
_, ok2 := rawConf["IntermediatePKIPath"]
|
|
hasRequired = ok1 && ok2
|
|
}
|
|
if !hasRequired {
|
|
t.Fatalf("The caller must provide both %q and %q config settings to avoid an incidental collision",
|
|
"RootPKIPath",
|
|
"IntermediatePKIPath")
|
|
}
|
|
|
|
for k, v := range rawConf {
|
|
conf[k] = v
|
|
}
|
|
|
|
cfg := ProviderConfig{
|
|
ClusterID: connect.TestClusterID,
|
|
Datacenter: "dc1",
|
|
IsPrimary: true,
|
|
RawConfig: conf,
|
|
}
|
|
|
|
return cfg
|
|
}
|