Merge pull request #11910 from hashicorp/dnephin/ca-provider-interface-for-ica-in-primary

ca: add support for an external trusted CA
This commit is contained in:
Daniel Nephin 2022-02-22 13:14:52 -05:00 committed by GitHub
commit 771df290d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 596 additions and 190 deletions

3
.changelog/11910.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
ca: support using an external root CA with the vault CA provider
```

View File

@ -9,11 +9,7 @@ import (
"github.com/hashicorp/consul/agent/connect"
)
func validateSetIntermediate(
intermediatePEM, rootPEM string,
currentPrivateKey string, // optional
spiffeID *connect.SpiffeIDSigning,
) error {
func validateSetIntermediate(intermediatePEM, rootPEM string, spiffeID *connect.SpiffeIDSigning) error {
// Get the key from the incoming intermediate cert so we can compare it
// to the currently stored key.
intermediate, err := connect.ParseCert(intermediatePEM)
@ -21,26 +17,6 @@ func validateSetIntermediate(
return fmt.Errorf("error parsing intermediate PEM: %v", err)
}
if currentPrivateKey != "" {
privKey, err := connect.ParseSigner(currentPrivateKey)
if err != nil {
return err
}
// Compare the two keys to make sure they match.
b1, err := x509.MarshalPKIXPublicKey(intermediate.PublicKey)
if err != nil {
return err
}
b2, err := x509.MarshalPKIXPublicKey(privKey.Public())
if err != nil {
return err
}
if !bytes.Equal(b1, b2) {
return fmt.Errorf("intermediate cert is for a different private key")
}
}
// Validate the remaining fields and make sure the intermediate validates against
// the given root cert.
if !intermediate.IsCA {
@ -65,6 +41,32 @@ func validateSetIntermediate(
return nil
}
func validateIntermediateSignedByPrivateKey(intermediatePEM string, privateKey string) error {
intermediate, err := connect.ParseCert(intermediatePEM)
if err != nil {
return fmt.Errorf("error parsing intermediate PEM: %v", err)
}
privKey, err := connect.ParseSigner(privateKey)
if err != nil {
return err
}
// Compare the two keys to make sure they match.
b1, err := x509.MarshalPKIXPublicKey(intermediate.PublicKey)
if err != nil {
return err
}
b2, err := x509.MarshalPKIXPublicKey(privKey.Public())
if err != nil {
return err
}
if !bytes.Equal(b1, b2) {
return fmt.Errorf("intermediate cert is for a different private key")
}
return nil
}
func validateSignIntermediate(csr *x509.CertificateRequest, spiffeID *connect.SpiffeIDSigning) error {
// We explicitly _don't_ require that the CSR has a valid SPIFFE signing URI
// SAN because AWS PCA doesn't let us set one :(. We need to relax it here

View File

@ -135,6 +135,7 @@ type PrimaryProvider interface {
// the active intermediate. If multiple intermediates are needed to complete
// the chain from the signing certificate back to the active root, they should
// all by bundled here.
// TODO: replace with GenerateLeafSigningCert (https://github.com/hashicorp/consul/issues/12386)
GenerateIntermediate() (string, error)
// SignIntermediate will validate the CSR to ensure the trust domain in the
@ -171,14 +172,20 @@ type PrimaryProvider interface {
}
type SecondaryProvider interface {
// GenerateIntermediateCSR generates a CSR for an intermediate CA
// certificate, to be signed by the root of another datacenter. If IsPrimary was
// set to true with Configure(), calling this is an error.
// GenerateIntermediateCSR should return a CSR for an intermediate CA
// certificate. The intermediate CA will be signed by the primary CA and
// should be used by the provider to sign leaf certificates in the local
// datacenter.
//
// After the certificate is signed, SecondaryProvider.SetIntermediate will
// be called to store the intermediate CA.
GenerateIntermediateCSR() (string, error)
// SetIntermediate sets the provider to use the given intermediate certificate
// as well as the root it was signed by. This completes the initialization for
// a provider where IsPrimary was set to false in Configure().
// SetIntermediate is called to store a newly signed leaf signing certificate and
// the chain of certificates back to the root CA certificate.
//
// The provider should save the certificates and use them to
// Provider.Sign leaf certificates.
SetIntermediate(intermediatePEM, rootPEM string) error
}
@ -186,7 +193,12 @@ type SecondaryProvider interface {
//
// TODO: rename this struct
type RootResult struct {
// PEM encoded certificate that will be used as the primary CA.
// PEM encoded bundle of CA certificates. The first certificate must be the
// primary CA used to sign intermediates for secondary datacenters, and the
// last certificate must be the trusted CA.
//
// If there is only a single certificate in the bundle then it will be used
// as both the primary CA and the trusted CA.
PEM string
}

View File

@ -253,12 +253,10 @@ func (c *ConsulProvider) SetIntermediate(intermediatePEM, rootPEM string) error
return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter")
}
err = validateSetIntermediate(
intermediatePEM, rootPEM,
providerState.PrivateKey,
c.spiffeID,
)
if err != nil {
if err = validateSetIntermediate(intermediatePEM, rootPEM, c.spiffeID); err != nil {
return err
}
if err := validateIntermediateSignedByPrivateKey(intermediatePEM, providerState.PrivateKey); err != nil {
return err
}

View File

@ -279,7 +279,7 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
if err != nil {
return RootResult{}, err
}
_, err = v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
resp, err := v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
"uri_sans": v.spiffeID.URI().String(),
"key_type": v.config.PrivateKeyType,
@ -288,12 +288,10 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
if err != nil {
return RootResult{}, err
}
// retrieve the newly generated cert so that we can return it
// TODO: is this already available from the Local().Write() above?
rootPEM, err = v.getCA(v.config.RootPKIPath)
if err != nil {
return RootResult{}, err
var ok bool
rootPEM, ok = resp.Data["certificate"].(string)
if !ok {
return RootResult{}, fmt.Errorf("unexpected response from Vault: %v", resp.Data["certificate"])
}
default:
@ -302,7 +300,18 @@ func (v *VaultProvider) GenerateRoot() (RootResult, error) {
}
}
return RootResult{PEM: rootPEM}, nil
rootChain, err := v.getCAChain(v.config.RootPKIPath)
if err != nil {
return RootResult{}, err
}
// Workaround for a bug in the Vault PKI API.
// See https://github.com/hashicorp/vault/issues/13489
if rootChain == "" {
rootChain = rootPEM
}
return RootResult{PEM: rootChain}, nil
}
// GenerateIntermediateCSR creates a private key and generates a CSR
@ -402,8 +411,7 @@ func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error {
return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter")
}
// the private key is in vault, so we can't use it in this validation
err := validateSetIntermediate(intermediatePEM, rootPEM, "", v.spiffeID)
err := validateSetIntermediate(intermediatePEM, rootPEM, v.spiffeID)
if err != nil {
return err
}
@ -468,6 +476,29 @@ func (v *VaultProvider) getCA(path string) (string, error) {
return root, nil
}
// TODO: refactor to remove duplication with getCA
func (v *VaultProvider) getCAChain(path string) (string, error) {
req := v.client.NewRequest("GET", "/v1/"+path+"/ca_chain")
resp, err := v.client.RawRequest(req)
if resp != nil {
defer resp.Body.Close()
}
if resp != nil && resp.StatusCode == http.StatusNotFound {
return "", ErrBackendNotMounted
}
if err != nil {
return "", err
}
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
root := EnsureTrailingNewline(string(raw))
return root, nil
}
// GenerateIntermediate mounts the configured intermediate PKI backend if
// necessary, then generates and signs a new CA CSR using the root PKI backend
// and updates the intermediate backend to use that new certificate.
@ -529,12 +560,7 @@ func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) {
if !ok {
return "", fmt.Errorf("certificate was not a string")
}
ca, ok := response.Data["issuing_ca"].(string)
if !ok {
return "", fmt.Errorf("issuing_ca was not a string")
}
return EnsureTrailingNewline(cert) + EnsureTrailingNewline(ca), nil
return EnsureTrailingNewline(cert), nil
}
// SignIntermediate returns a signed CA certificate with a path length constraint

View File

@ -20,11 +20,11 @@ const (
DefaultIntermediateCertTTL = 24 * 365 * time.Hour
)
func pemEncodeKey(key []byte, blockType string) (string, error) {
func pemEncode(value []byte, blockType string) (string, error) {
var buf bytes.Buffer
if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: key}); err != nil {
return "", fmt.Errorf("error encoding private key: %s", err)
if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: value}); err != nil {
return "", fmt.Errorf("error encoding value %v: %s", blockType, err)
}
return buf.String(), nil
}
@ -38,7 +38,7 @@ func generateRSAKey(keyBits int) (crypto.Signer, string, error) {
}
bs := x509.MarshalPKCS1PrivateKey(pk)
pemBlock, err := pemEncodeKey(bs, "RSA PRIVATE KEY")
pemBlock, err := pemEncode(bs, "RSA PRIVATE KEY")
if err != nil {
return nil, "", err
}
@ -73,7 +73,7 @@ func generateECDSAKey(keyBits int) (crypto.Signer, string, error) {
return nil, "", fmt.Errorf("error marshaling ECDSA private key: %s", err)
}
pemBlock, err := pemEncodeKey(bs, "EC PRIVATE KEY")
pemBlock, err := pemEncode(bs, "EC PRIVATE KEY")
if err != nil {
return nil, "", err
}

View File

@ -56,6 +56,21 @@ func ParseLeafCerts(pemValue string) (*x509.Certificate, *x509.CertPool, error)
return leaf, intermediates, nil
}
// CertSubjects can be used in debugging to return the subject of each
// certificate in the PEM bundle. Each subject is separated by a newline.
func CertSubjects(pem string) string {
certs, err := parseCerts(pem)
if err != nil {
return err.Error()
}
var buf strings.Builder
for _, cert := range certs {
buf.WriteString(cert.Subject.String())
buf.WriteString("\n")
}
return buf.String()
}
// ParseCerts parses the all x509 certificates from a PEM-encoded value.
// The first returned cert is a leaf cert and any other ones are intermediates.
//
@ -90,21 +105,10 @@ func parseCerts(pemValue string) ([]*x509.Certificate, error) {
return out, nil
}
// CalculateCertFingerprint parses the x509 certificate from a PEM-encoded value
// and calculates the SHA-1 fingerprint.
func CalculateCertFingerprint(pemValue string) (string, error) {
// The _ result below is not an error but the remaining PEM bytes.
block, _ := pem.Decode([]byte(pemValue))
if block == nil {
return "", fmt.Errorf("no PEM-encoded data found")
}
if block.Type != "CERTIFICATE" {
return "", fmt.Errorf("first PEM-block should be CERTIFICATE type")
}
hash := sha1.Sum(block.Bytes)
return HexString(hash[:]), nil
// CalculateCertFingerprint calculates the SHA-1 fingerprint from the cert bytes.
func CalculateCertFingerprint(cert []byte) string {
hash := sha1.Sum(cert)
return HexString(hash[:])
}
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key

View File

@ -112,10 +112,7 @@ func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int, ttl ti
t.Fatalf("error encoding private key: %s", err)
}
result.RootCert = buf.String()
result.ID, err = CalculateCertFingerprint(result.RootCert)
if err != nil {
t.Fatalf("error generating CA ID fingerprint: %s", err)
}
result.ID = CalculateCertFingerprint(bs)
result.SerialNumber = uint64(sn.Int64())
result.NotBefore = template.NotBefore.UTC()
result.NotAfter = template.NotAfter.UTC()

View File

@ -253,28 +253,24 @@ func (c *CAManager) initializeCAConfig() (*structs.CAConfiguration, error) {
return config, nil
}
// parseCARoot returns a filled-in structs.CARoot from a raw PEM value.
func parseCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) {
id, err := connect.CalculateCertFingerprint(pemValue)
// newCARoot returns a filled-in structs.CARoot from a raw PEM value.
func newCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) {
primaryCert, err := connect.ParseCert(pemValue)
if err != nil {
return nil, fmt.Errorf("error parsing root fingerprint: %v", err)
return nil, err
}
rootCert, err := connect.ParseCert(pemValue)
if err != nil {
return nil, fmt.Errorf("error parsing root cert: %v", err)
}
keyType, keyBits, err := connect.KeyInfoFromCert(rootCert)
keyType, keyBits, err := connect.KeyInfoFromCert(primaryCert)
if err != nil {
return nil, fmt.Errorf("error extracting root key info: %v", err)
}
return &structs.CARoot{
ID: id,
Name: fmt.Sprintf("%s CA Root Cert", strings.Title(provider)),
SerialNumber: rootCert.SerialNumber.Uint64(),
SigningKeyID: connect.EncodeSigningKeyID(rootCert.SubjectKeyId),
ID: connect.CalculateCertFingerprint(primaryCert.Raw),
Name: fmt.Sprintf("%s CA Primary Cert", strings.Title(provider)),
SerialNumber: primaryCert.SerialNumber.Uint64(),
SigningKeyID: connect.EncodeSigningKeyID(primaryCert.SubjectKeyId),
ExternalTrustDomain: clusterID,
NotBefore: rootCert.NotBefore,
NotAfter: rootCert.NotAfter,
NotBefore: primaryCert.NotBefore,
NotAfter: primaryCert.NotAfter,
RootCert: pemValue,
PrivateKeyType: keyType,
PrivateKeyBits: keyBits,
@ -435,7 +431,7 @@ func (c *CAManager) secondaryInitialize(provider ca.Provider, conf *structs.CACo
}
var roots structs.IndexedCARoots
if err := c.delegate.forwardDC("ConnectCA.Roots", c.serverConf.PrimaryDatacenter, &args, &roots); err != nil {
return err
return fmt.Errorf("failed to get CA roots from primary DC: %w", err)
}
c.secondarySetPrimaryRoots(roots)
@ -487,12 +483,12 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
return fmt.Errorf("error generating CA root certificate: %v", err)
}
rootCA, err := parseCARoot(root.PEM, conf.Provider, conf.ClusterID)
rootCA, err := newCARoot(root.PEM, conf.Provider, conf.ClusterID)
if err != nil {
return err
}
// Also create the intermediate CA, which is the one that actually signs leaf certs
// TODO: https://github.com/hashicorp/consul/issues/12386
interPEM, err := provider.GenerateIntermediate()
if err != nil {
return fmt.Errorf("error generating intermediate cert: %v", err)
@ -887,7 +883,7 @@ func (c *CAManager) primaryUpdateRootCA(newProvider ca.Provider, args *structs.C
}
newRootPEM := providerRoot.PEM
newActiveRoot, err := parseCARoot(newRootPEM, args.Config.Provider, args.Config.ClusterID)
newActiveRoot, err := newCARoot(newRootPEM, args.Config.Provider, args.Config.ClusterID)
if err != nil {
return err
}
@ -940,7 +936,7 @@ func (c *CAManager) primaryUpdateRootCA(newProvider ca.Provider, args *structs.C
// get a cross-signed certificate.
// 3. Take the active root for the new provider and append the intermediate from step 2
// to its list of intermediates.
// TODO: this cert is already parsed once in parseCARoot, could we remove the second parse?
// TODO: this cert is already parsed once in newCARoot, could we remove the second parse?
newRoot, err := connect.ParseCert(newRootPEM)
if err != nil {
return err
@ -980,6 +976,7 @@ func (c *CAManager) primaryUpdateRootCA(newProvider ca.Provider, args *structs.C
}
}
// TODO: https://github.com/hashicorp/consul/issues/12386
intermediate, err := newProvider.GenerateIntermediate()
if err != nil {
return err

View File

@ -12,6 +12,7 @@ import (
"fmt"
"math/big"
"net/url"
"strings"
"testing"
"time"
@ -82,7 +83,6 @@ func TestCAManager_Initialize_Vault_Secondary_SharedVault(t *testing.T) {
},
}
})
defer serverDC2.Shutdown()
joinWAN(t, serverDC2, serverDC1)
testrpc.WaitForActiveCARoot(t, serverDC2.RPC, "dc2", nil)
@ -98,14 +98,25 @@ func TestCAManager_Initialize_Vault_Secondary_SharedVault(t *testing.T) {
}
func verifyLeafCert(t *testing.T, root *structs.CARoot, leafCertPEM string) {
t.Helper()
roots := structs.IndexedCARoots{
ActiveRootID: root.ID,
Roots: []*structs.CARoot{root},
}
verifyLeafCertWithRoots(t, roots, leafCertPEM)
}
func verifyLeafCertWithRoots(t *testing.T, roots structs.IndexedCARoots, leafCertPEM string) {
t.Helper()
leaf, intermediates, err := connect.ParseLeafCerts(leafCertPEM)
require.NoError(t, err)
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(root.RootCert))
if !ok {
t.Fatalf("Failed to add root CA PEM to cert pool")
for _, r := range roots.Roots {
ok := pool.AppendCertsFromPEM([]byte(r.RootCert))
if !ok {
t.Fatalf("Failed to add root CA PEM to cert pool")
}
}
// verify with intermediates from leaf CertPEM
@ -118,10 +129,12 @@ func verifyLeafCert(t *testing.T, root *structs.CARoot, leafCertPEM string) {
// verify with intermediates from the CARoot
intermediates = x509.NewCertPool()
for _, intermediate := range root.IntermediateCerts {
c, err := connect.ParseCert(intermediate)
require.NoError(t, err)
intermediates.AddCert(c)
for _, r := range roots.Roots {
for _, intermediate := range r.IntermediateCerts {
c, err := connect.ParseCert(intermediate)
require.NoError(t, err)
intermediates.AddCert(c)
}
}
_, err = leaf.Verify(x509.VerifyOptions{
@ -618,7 +631,7 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) {
generateExternalRootCA(t, vclient)
meshRootPath := "pki-root"
primaryCert := setupPrimaryCA(t, vclient, meshRootPath)
primaryCert := setupPrimaryCA(t, vclient, meshRootPath, "")
_, s1 := testServerWithConfig(t, func(c *Config) {
c.CAConfig = &structs.CAConfiguration{
@ -628,14 +641,9 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) {
"Token": vault.RootToken,
"RootPKIPath": meshRootPath,
"IntermediatePKIPath": "pki-intermediate/",
// TODO: there are failures to init the CA system if these are not set
// to the values of the already initialized CA.
"PrivateKeyType": "ec",
"PrivateKeyBits": 256,
},
}
})
defer s1.Shutdown()
runStep(t, "check primary DC", func(t *testing.T) {
testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
@ -665,10 +673,6 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) {
"Token": vault.RootToken,
"RootPKIPath": meshRootPath,
"IntermediatePKIPath": "pki-secondary/",
// TODO: there are failures to init the CA system if these are not set
// to the values of the already initialized CA.
"PrivateKeyType": "ec",
"PrivateKeyBits": 256,
},
}
})
@ -704,10 +708,224 @@ func getLeafCert(t *testing.T, codec rpc.ClientCodec, trustDomain string, dc str
cert := structs.IssuedCert{}
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", &req, &cert)
require.NoError(t, err)
return cert.CertPEM
}
func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
ca.SkipIfVaultNotPresent(t)
vault := ca.NewTestVaultServer(t)
vclient := vault.Client()
rootPEM := generateExternalRootCA(t, vclient)
primaryCAPath := "pki-primary"
primaryCert := setupPrimaryCA(t, vclient, primaryCAPath, rootPEM)
_, serverDC1 := testServerWithConfig(t, func(c *Config) {
c.CAConfig = &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": primaryCAPath,
"IntermediatePKIPath": "pki-intermediate/",
},
}
})
testrpc.WaitForTestAgent(t, serverDC1.RPC, "dc1")
var origLeaf string
roots := structs.IndexedCARoots{}
runStep(t, "verify primary DC", func(t *testing.T) {
codec := rpcClient(t, serverDC1)
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 1)
require.Equal(t, primaryCert, roots.Roots[0].RootCert)
require.Contains(t, roots.Roots[0].RootCert, rootPEM)
leafCert := getLeafCert(t, codec, roots.TrustDomain, "dc1")
verifyLeafCert(t, roots.Active(), leafCert)
origLeaf = leafCert
})
_, serverDC2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.PrimaryDatacenter = "dc1"
c.CAConfig = &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": "should-be-ignored",
"IntermediatePKIPath": "pki-secondary/",
},
}
})
var origLeafSecondary string
runStep(t, "start secondary DC", func(t *testing.T) {
joinWAN(t, serverDC2, serverDC1)
testrpc.WaitForActiveCARoot(t, serverDC2.RPC, "dc2", nil)
codec := rpcClient(t, serverDC2)
roots = structs.IndexedCARoots{}
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 1)
leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc2")
verifyLeafCert(t, roots.Roots[0], leafPEM)
origLeafSecondary = leafPEM
})
runStep(t, "renew leaf signing CA in primary", func(t *testing.T) {
previous := serverDC1.caManager.getLeafSigningCertFromRoot(roots.Active())
renewLeafSigningCert(t, serverDC1.caManager, serverDC1.caManager.primaryRenewIntermediate)
codec := rpcClient(t, serverDC1)
roots = structs.IndexedCARoots{}
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 1)
require.Len(t, roots.Roots[0].IntermediateCerts, 2)
newCert := serverDC1.caManager.getLeafSigningCertFromRoot(roots.Active())
require.NotEqual(t, previous, newCert)
leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1")
verifyLeafCert(t, roots.Roots[0], leafPEM)
// original certs from old signing cert should still verify
verifyLeafCert(t, roots.Roots[0], origLeaf)
})
runStep(t, "renew leaf signing CA in secondary", func(t *testing.T) {
previous := serverDC2.caManager.getLeafSigningCertFromRoot(roots.Active())
renewLeafSigningCert(t, serverDC2.caManager, serverDC2.caManager.secondaryRequestNewSigningCert)
codec := rpcClient(t, serverDC2)
roots = structs.IndexedCARoots{}
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 1)
// one intermediate from primary, two from secondary
require.Len(t, roots.Roots[0].IntermediateCerts, 3)
newCert := serverDC1.caManager.getLeafSigningCertFromRoot(roots.Active())
require.NotEqual(t, previous, newCert)
leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc2")
verifyLeafCert(t, roots.Roots[0], leafPEM)
// original certs from old signing cert should still verify
verifyLeafCert(t, roots.Roots[0], origLeaf)
})
runStep(t, "rotate root by changing the provider", func(t *testing.T) {
codec := rpcClient(t, serverDC1)
req := &structs.CARequest{
Op: structs.CAOpSetConfig,
Config: &structs.CAConfiguration{
Provider: "consul",
},
}
var resp error
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", req, &resp)
require.NoError(t, err)
require.Nil(t, resp)
roots = structs.IndexedCARoots{}
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 2)
active := roots.Active()
require.Len(t, active.IntermediateCerts, 1)
leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1")
verifyLeafCert(t, roots.Active(), leafPEM)
// original certs from old root cert should still verify
verifyLeafCertWithRoots(t, roots, origLeaf)
// original certs from secondary should still verify
rootsSecondary := structs.IndexedCARoots{}
r := &structs.DCSpecificRequest{Datacenter: "dc2"}
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", r, &rootsSecondary)
require.NoError(t, err)
verifyLeafCertWithRoots(t, rootsSecondary, origLeafSecondary)
})
runStep(t, "rotate to a different external root", func(t *testing.T) {
setupPrimaryCA(t, vclient, "pki-primary-2/", rootPEM)
codec := rpcClient(t, serverDC1)
req := &structs.CARequest{
Op: structs.CAOpSetConfig,
Config: &structs.CAConfiguration{
Provider: "vault",
Config: map[string]interface{}{
"Address": vault.Addr,
"Token": vault.RootToken,
"RootPKIPath": "pki-primary-2/",
"IntermediatePKIPath": "pki-intermediate-2/",
},
},
}
var resp error
err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", req, &resp)
require.NoError(t, err)
require.Nil(t, resp)
roots = structs.IndexedCARoots{}
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots)
require.NoError(t, err)
require.Len(t, roots.Roots, 3)
active := roots.Active()
require.Len(t, active.IntermediateCerts, 2)
leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1")
verifyLeafCert(t, roots.Active(), leafPEM)
// original certs from old root cert should still verify
verifyLeafCertWithRoots(t, roots, origLeaf)
// original certs from secondary should still verify
rootsSecondary := structs.IndexedCARoots{}
r := &structs.DCSpecificRequest{Datacenter: "dc2"}
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", r, &rootsSecondary)
require.NoError(t, err)
verifyLeafCertWithRoots(t, rootsSecondary, origLeafSecondary)
})
}
// renewLeafSigningCert mimics RenewIntermediate. This is unfortunate, but
// necessary for now as there is no easy way to invoke that logic unconditionally.
// Currently, it requires patching values and polling for the operation to
// complete, which adds a lot of distractions to a test case.
// With this function we can instead unconditionally rotate the leaf signing cert
// synchronously.
func renewLeafSigningCert(t *testing.T, manager *CAManager, fn func(ca.Provider, *structs.CARoot) error) {
t.Helper()
provider, _ := manager.getCAProvider()
store := manager.delegate.State()
_, root, err := store.CARootActive(nil)
require.NoError(t, err)
activeRoot := root.Clone()
err = fn(provider, activeRoot)
require.NoError(t, err)
err = manager.persistNewRootAndConfig(provider, activeRoot, nil)
require.NoError(t, err)
manager.setCAProvider(provider, activeRoot)
}
func generateExternalRootCA(t *testing.T, client *vaultapi.Client) string {
t.Helper()
err := client.Sys().Mount("corp", &vaultapi.MountInput{
@ -725,10 +943,10 @@ func generateExternalRootCA(t *testing.T, client *vaultapi.Client) string {
"ttl": "2400h",
})
require.NoError(t, err, "failed to generate root")
return resp.Data["certificate"].(string)
return ca.EnsureTrailingNewline(resp.Data["certificate"].(string))
}
func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string) string {
func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string, rootPEM string) string {
t.Helper()
err := client.Sys().Mount(path, &vaultapi.MountInput{
Type: "pki",
@ -756,9 +974,13 @@ func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string) string {
})
require.NoError(t, err, "failed to sign intermediate")
var buf strings.Builder
buf.WriteString(ca.EnsureTrailingNewline(intermediate.Data["certificate"].(string)))
buf.WriteString(ca.EnsureTrailingNewline(rootPEM))
_, err = client.Logical().Write(path+"/intermediate/set-signed", map[string]interface{}{
"certificate": intermediate.Data["certificate"],
"certificate": buf.String(),
})
require.NoError(t, err, "failed to set signed intermediate")
return ca.EnsureTrailingNewline(intermediate.Data["certificate"].(string))
return ca.EnsureTrailingNewline(buf.String())
}

View File

@ -15,11 +15,13 @@ import (
msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
uuid "github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/connect/ca"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
)
@ -1246,74 +1248,122 @@ func TestConnectCA_ConfigurationSet_PersistsRoots(t *testing.T) {
})
}
func TestParseCARoot(t *testing.T) {
type test struct {
name string
pem string
wantSerial uint64
wantSigningKeyID string
wantKeyType string
wantKeyBits int
wantErr bool
func TestNewCARoot(t *testing.T) {
type testCase struct {
name string
pem string
expected *structs.CARoot
expectedErr string
}
// Test certs generated with
run := func(t *testing.T, tc testCase) {
root, err := newCARoot(tc.pem, "provider-name", "cluster-id")
if tc.expectedErr != "" {
testutil.RequireErrorContains(t, err, tc.expectedErr)
return
}
require.NoError(t, err)
assert.DeepEqual(t, root, tc.expected)
}
// Test certs can be generated with
// go run connect/certgen/certgen.go -out-dir /tmp/connect-certs -key-type ec -key-bits 384
// for various key types. This does limit the exposure to formats that might
// exist in external certificates which can be used as Connect CAs.
// Specifically many other certs will have serial numbers that don't fit into
// 64 bits but for reasons we truncate down to 64 bits which means our
// `SerialNumber` will not match the one reported by openssl. We should
// probably fix that at some point as it seems like a big footgun but it would
// be a breaking API change to change the type to not be a JSON number and
// JSON numbers don't even support the full range of a uint64...
tests := []test{
{"no cert", "", 0, "", "", 0, true},
// serial generated with:
// openssl x509 -noout -text
testCases := []testCase{
{
name: "default cert",
// Watchout for indentations they will break PEM format
pem: readTestData(t, "cert-with-ec-256-key.pem"),
// Based on `openssl x509 -noout -text` report from the cert
wantSerial: 8341954965092507701,
wantSigningKeyID: "97:4D:17:81:64:F8:B4:AF:05:E8:6C:79:C5:40:3B:0E:3E:8B:C0:AE:38:51:54:8A:2F:05:DB:E3:E8:E4:24:EC",
wantKeyType: "ec",
wantKeyBits: 256,
wantErr: false,
name: "no cert",
expectedErr: "no PEM-encoded data found",
},
{
name: "ec 384 cert",
// Watchout for indentations they will break PEM format
pem: readTestData(t, "cert-with-ec-384-key.pem"),
// Based on `openssl x509 -noout -text` report from the cert
wantSerial: 2935109425518279965,
wantSigningKeyID: "0B:A0:88:9B:DC:95:31:51:2E:3D:D4:F9:42:D0:6A:A0:62:46:82:D2:7C:22:E7:29:A9:AA:E8:A5:8C:CF:C7:42",
wantKeyType: "ec",
wantKeyBits: 384,
wantErr: false,
name: "type=ec bits=256",
pem: readTestData(t, "cert-with-ec-256-key.pem"),
expected: &structs.CARoot{
ID: "c9:1b:24:e0:89:63:1a:ba:22:01:f4:cf:bc:f1:c0:36:b2:6b:6c:3d",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 8341954965092507701,
SigningKeyID: "97:4d:17:81:64:f8:b4:af:05:e8:6c:79:c5:40:3b:0e:3e:8b:c0:ae:38:51:54:8a:2f:05:db:e3:e8:e4:24:ec",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2019, 10, 17, 11, 46, 29, 0, time.UTC),
NotAfter: time.Date(2029, 10, 17, 11, 46, 29, 0, time.UTC),
RootCert: readTestData(t, "cert-with-ec-256-key.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
},
{
name: "rsa 4096 cert",
// Watchout for indentations they will break PEM format
pem: readTestData(t, "cert-with-rsa-4096-key.pem"),
// Based on `openssl x509 -noout -text` report from the cert
wantSerial: 5186695743100577491,
wantSigningKeyID: "92:FA:CC:97:57:1E:31:84:A2:33:DD:9B:6A:A8:7C:FC:BE:E2:94:CA:AC:B3:33:17:39:3B:B8:67:9B:DC:C1:08",
wantKeyType: "rsa",
wantKeyBits: 4096,
wantErr: false,
name: "type=ec bits=384",
pem: readTestData(t, "cert-with-ec-384-key.pem"),
expected: &structs.CARoot{
ID: "29:69:c4:0f:aa:8f:bd:07:31:0d:51:3b:45:62:3d:c0:b2:fc:c6:3f",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 2935109425518279965,
SigningKeyID: "0b:a0:88:9b:dc:95:31:51:2e:3d:d4:f9:42:d0:6a:a0:62:46:82:d2:7c:22:e7:29:a9:aa:e8:a5:8c:cf:c7:42",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2019, 10, 17, 11, 55, 18, 0, time.UTC),
NotAfter: time.Date(2029, 10, 17, 11, 55, 18, 0, time.UTC),
RootCert: readTestData(t, "cert-with-ec-384-key.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 384,
},
},
{
name: "type=rsa bits=4096",
pem: readTestData(t, "cert-with-rsa-4096-key.pem"),
expected: &structs.CARoot{
ID: "3a:6a:e3:e2:2d:44:85:5a:e9:44:3b:ef:d2:90:78:83:7f:61:a2:84",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 5186695743100577491,
SigningKeyID: "92:fa:cc:97:57:1e:31:84:a2:33:dd:9b:6a:a8:7c:fc:be:e2:94:ca:ac:b3:33:17:39:3b:b8:67:9b:dc:c1:08",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2019, 10, 17, 11, 53, 15, 0, time.UTC),
NotAfter: time.Date(2029, 10, 17, 11, 53, 15, 0, time.UTC),
RootCert: readTestData(t, "cert-with-rsa-4096-key.pem"),
Active: true,
PrivateKeyType: "rsa",
PrivateKeyBits: 4096,
},
},
{
name: "two certs in pem",
pem: readTestData(t, "pem-with-two-certs.pem"),
expected: &structs.CARoot{
ID: "42:43:10:1f:71:6b:21:21:d1:10:49:d1:f0:41:78:8c:0a:77:ef:c0",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 17692800288680335732,
SigningKeyID: "9d:5c:27:43:ce:58:7b:ca:3e:7d:c4:fb:b6:2e:b7:13:e9:a1:68:3e",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2022, 1, 5, 23, 22, 12, 0, time.UTC),
NotAfter: time.Date(2022, 4, 7, 15, 22, 42, 0, time.UTC),
RootCert: readTestData(t, "pem-with-two-certs.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
},
{
name: "three certs in pem",
pem: readTestData(t, "pem-with-three-certs.pem"),
expected: &structs.CARoot{
ID: "42:43:10:1f:71:6b:21:21:d1:10:49:d1:f0:41:78:8c:0a:77:ef:c0",
Name: "Provider-Name CA Primary Cert",
SerialNumber: 17692800288680335732,
SigningKeyID: "9d:5c:27:43:ce:58:7b:ca:3e:7d:c4:fb:b6:2e:b7:13:e9:a1:68:3e",
ExternalTrustDomain: "cluster-id",
NotBefore: time.Date(2022, 1, 5, 23, 22, 12, 0, time.UTC),
NotAfter: time.Date(2022, 4, 7, 15, 22, 42, 0, time.UTC),
RootCert: readTestData(t, "pem-with-three-certs.pem"),
Active: true,
PrivateKeyType: "ec",
PrivateKeyBits: 256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root, err := parseCARoot(tt.pem, "consul", "cluster")
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantSerial, root.SerialNumber)
require.Equal(t, strings.ToLower(tt.wantSigningKeyID), root.SigningKeyID)
require.Equal(t, tt.wantKeyType, root.PrivateKeyType)
require.Equal(t, tt.wantKeyBits, root.PrivateKeyBits)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
})
}
}

View File

@ -9,4 +9,4 @@ VR0jBCQwIoAgl00XgWT4tK8F6Gx5xUA7Dj6LwK44UVSKLwXb4+jkJOwwPwYDVR0R
BDgwNoY0c3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1
NTU1LmNvbnN1bDAKBggqhkjOPQQDAgNIADBFAiEA/x2MeYU5vCk2hwP7zlrv7bx3
9zx5YSbn04sgP6sNK30CIEPfjxDGy6K2dPDckATboYkZVQ4CJpPd6WrgwQaHpWC9
-----END CERTIFICATE-----
-----END CERTIFICATE-----

View File

@ -11,4 +11,4 @@ MTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqGSM49
BAMDA2gAMGUCMBT0orKHSATvulb6nRxVHq3OWOfmVgHu8VUCq9yuyAu1AAy/przY
/U0ury3g8T4jhwIxAIoCqYwWSJMFb13DZAR3XY+aFssVP5+vzhlaulqtg+YqjpKP
KzuCBpS3yUyAwWDphg==
-----END CERTIFICATE-----
-----END CERTIFICATE-----

View File

@ -28,4 +28,4 @@ RPw2YW6oqaMmZ9Uehxym8RDWqyyFPg9S0C73MTK7FitIROLW88hWKSpDDhFck/32
FVaRL8cC0KVlMCFByL/o6u0AsRNCOux1q3BJEdmAh7VI84+SPgztHFkptR4VnlHZ
kKTj2Mj/OylHHwhe6AU9pbtAGM6DtcqSjmd4wrkRX8WJDd/F3RlYZ8WhOToOj9gP
ra4mUhGz/OlDg6vN9TSeVlb5Ap7c38KoCmmt2n+F/KUpe6V4L1QA5yfz0S8=
-----END CERTIFICATE-----
-----END CERTIFICATE-----

View File

@ -0,0 +1,48 @@
-----BEGIN CERTIFICATE-----
MIICUjCCATqgAwIBAgIUQjOIDzaM7bGW8bU69Yl0H0C4WXQwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQwNzE1MjI0MlowFTETMBEGA1UEAxMKcHJpbWFyeSBDQTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABEIcOmVSobge9pLDGh6rfyFg2+ilTFmo2ICv5vrgUfIZhi8O
fwYz5WGb7qBPRdMw9kP8BWH/lCrn2W3Ax3x2E+2jYzBhMA4GA1UdDwEB/wQEAwIB
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdXCdDzlh7yj59xPu2LrcT6aFo
PjAfBgNVHSMEGDAWgBS+x+IFMFb+hCJy1OQzcdzJuwDVhDANBgkqhkiG9w0BAQsF
AAOCAQEAWWBBoygbjUEtoueGuC+jHAlr+VOBwiQPJLQA+xtbCvWSn8yIx/M1RyhY
0/6WLMzhYA1lQAIze8CgKzqoGXXIcHif3PRZ3mRUMNdV/qGUv0oHZBzTKZVySOIm
MLIoq7WvyVdVNxyvRalhHxiQA1Hrh+zQKjXhVPM6dpG0duTNYit9kJCCeNDzRjWc
a/GgFyeeYMTheU3eBR6Vp2A8hy2h5xw82ul8YLwX0bCtcP12XAUzj3jFqwt6RLxW
Wc7rvsLfgimEfulQwo2WLPWZw8bJdnPvNcUFX8f2Zvqy0Jg6fELnxO+AdHnAnI9J
WtJr0ImA95Hw8gGTzmXOddYVGHuGLA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDHzCCAgegAwIBAgIUDaEOI5nsEt9abNBbJibbQt+VZQIwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQxNTIzMjI0MlowFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAut/Gbr3MvypzEmRDTl7HGaSoVIydNEZNPqDD
jh1lqMFywB4DujTmkWLYcPJJ0RTT2NsSakteti/e1DHCuBSU0t3Q3K1paTh8aVLx
eK0IKNlCWqX5d1aYzCNZsRjJuQgPX6p/xcNGS+RS27jmRWPpvm6n1JfMvYRa7fF+
HnKhGNO+hDbhkQO4s0V1U+unNhshKDhTW3mBLmAEb2OHLOEaUZtYSbqr1E9tYXgU
DiYRkeWUpQXJ6pE91fmcaZFG0SxkqWnhe7GUa6wbb/vROWph4A1ZVHympBtOYwoJ
eibcJjBZLrugZdix8kl8NDI7SuIM/P0x0m9WkNfhJ9vSgQXlaQIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvsfiBTBW
/oQictTkM3HcybsA1YQwHwYDVR0jBBgwFoAUvsfiBTBW/oQictTkM3HcybsA1YQw
DQYJKoZIhvcNAQELBQADggEBALUJWitOV4xAvfNB8Z20AQ+/hdXkWVgj1VBbd++v
+X88q1TnueKAExU5o87MCjh9jMxalZqSVN9MUbQ4Xa+tmkjayizdpFaw6TbbaMIB
Tgqq5ATXMnOdZd46QC764Po9R9+k9hk4dNIr5gk1ifXZDMy/7jSOVARvpwzr0cTx
flRCTgZbcK10freoU7a74/YjEpG0wggGlR4aRWfm90Im9JM3aI55zAYQFzduf56c
HXJDLgBtbOx/ceqVrkPdvYwP9Q34tKAMiheQ0G3tTxP3Xc87gh4UEDV02oHhcbqw
WSm+8zTfGUlchowPRdqKE66urWTep+BA9c8zUqDdoq5lE9s=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICGTCCAZ+gAwIBAgIIJhC6ZZyZ/lQwCgYIKoZIzj0EAwMwFDESMBAGA1UEAxMJ
VGVzdCBDQSAxMB4XDTIyMDEwNTIzNDMyNVoXDTMyMDEwNTIzNDMyNVowFDESMBAG
A1UEAxMJVGVzdCBDQSAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETEyAhuLLOcxy
z2UHI7ePcB5AXL1o6mLwVfzyeaGfqUevzrFcLQ7WPiypZJW1KhOW5Q2bRgcjE8y3
fN+B8D+KT4fPtaRLtUVX6aZ0LCROFdgWjVo2DCvCq5VQnCGjW8r0o4G9MIG6MA4G
A1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MCkGA1UdDgQiBCBU/reewmUW
iduB8xxfW5clyUmrMewrWwtJuWPA/tFvTTArBgNVHSMEJDAigCBU/reewmUWiduB
8xxfW5clyUmrMewrWwtJuWPA/tFvTTA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMTEx
MTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqGSM49
BAMDA2gAMGUCMA4V/Iemelne4ZB+0glmxoKV6OPQ64oKkkrcy+vo1t1RZ+7jntRx
mxAnY3S2m35boQIxAOARpY+qfR3U3JM+vMW9KO0/KqM+y1/uvIaOA0bQex2w8bfN
V+QjFUDmjTT1dLpc7A==
-----END CERTIFICATE-----

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIICUjCCATqgAwIBAgIUQjOIDzaM7bGW8bU69Yl0H0C4WXQwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQwNzE1MjI0MlowFTETMBEGA1UEAxMKcHJpbWFyeSBDQTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABEIcOmVSobge9pLDGh6rfyFg2+ilTFmo2ICv5vrgUfIZhi8O
fwYz5WGb7qBPRdMw9kP8BWH/lCrn2W3Ax3x2E+2jYzBhMA4GA1UdDwEB/wQEAwIB
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdXCdDzlh7yj59xPu2LrcT6aFo
PjAfBgNVHSMEGDAWgBS+x+IFMFb+hCJy1OQzcdzJuwDVhDANBgkqhkiG9w0BAQsF
AAOCAQEAWWBBoygbjUEtoueGuC+jHAlr+VOBwiQPJLQA+xtbCvWSn8yIx/M1RyhY
0/6WLMzhYA1lQAIze8CgKzqoGXXIcHif3PRZ3mRUMNdV/qGUv0oHZBzTKZVySOIm
MLIoq7WvyVdVNxyvRalhHxiQA1Hrh+zQKjXhVPM6dpG0duTNYit9kJCCeNDzRjWc
a/GgFyeeYMTheU3eBR6Vp2A8hy2h5xw82ul8YLwX0bCtcP12XAUzj3jFqwt6RLxW
Wc7rvsLfgimEfulQwo2WLPWZw8bJdnPvNcUFX8f2Zvqy0Jg6fELnxO+AdHnAnI9J
WtJr0ImA95Hw8gGTzmXOddYVGHuGLA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDHzCCAgegAwIBAgIUDaEOI5nsEt9abNBbJibbQt+VZQIwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMB4XDTIyMDEwNTIzMjIxMloXDTIy
MDQxNTIzMjI0MlowFzEVMBMGA1UEAxMMY29ycG9yYXRlIENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAut/Gbr3MvypzEmRDTl7HGaSoVIydNEZNPqDD
jh1lqMFywB4DujTmkWLYcPJJ0RTT2NsSakteti/e1DHCuBSU0t3Q3K1paTh8aVLx
eK0IKNlCWqX5d1aYzCNZsRjJuQgPX6p/xcNGS+RS27jmRWPpvm6n1JfMvYRa7fF+
HnKhGNO+hDbhkQO4s0V1U+unNhshKDhTW3mBLmAEb2OHLOEaUZtYSbqr1E9tYXgU
DiYRkeWUpQXJ6pE91fmcaZFG0SxkqWnhe7GUa6wbb/vROWph4A1ZVHympBtOYwoJ
eibcJjBZLrugZdix8kl8NDI7SuIM/P0x0m9WkNfhJ9vSgQXlaQIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvsfiBTBW
/oQictTkM3HcybsA1YQwHwYDVR0jBBgwFoAUvsfiBTBW/oQictTkM3HcybsA1YQw
DQYJKoZIhvcNAQELBQADggEBALUJWitOV4xAvfNB8Z20AQ+/hdXkWVgj1VBbd++v
+X88q1TnueKAExU5o87MCjh9jMxalZqSVN9MUbQ4Xa+tmkjayizdpFaw6TbbaMIB
Tgqq5ATXMnOdZd46QC764Po9R9+k9hk4dNIr5gk1ifXZDMy/7jSOVARvpwzr0cTx
flRCTgZbcK10freoU7a74/YjEpG0wggGlR4aRWfm90Im9JM3aI55zAYQFzduf56c
HXJDLgBtbOx/ceqVrkPdvYwP9Q34tKAMiheQ0G3tTxP3Xc87gh4UEDV02oHhcbqw
WSm+8zTfGUlchowPRdqKE66urWTep+BA9c8zUqDdoq5lE9s=
-----END CERTIFICATE-----

View File

@ -66,14 +66,15 @@ func (r IndexedCARoots) Active() *CARoot {
// CARoot represents a root CA certificate that is trusted.
type CARoot struct {
// ID is a globally unique ID (UUID) representing this CA root.
// ID is a globally unique ID (UUID) representing this CA chain. It is
// calculated from the SHA1 of the primary CA certificate.
ID string
// Name is a human-friendly name for this CA root. This value is
// opaque to Consul and is not used for anything internally.
Name string
// SerialNumber is the x509 serial number of the certificate.
// SerialNumber is the x509 serial number of the primary CA certificate.
SerialNumber uint64
// SigningKeyID is the connect.HexString encoded id of the public key that
@ -96,9 +97,12 @@ type CARoot struct {
// future flexibility.
ExternalTrustDomain string
// Time validity bounds.
// NotBefore is the x509.Certificate.NotBefore value of the primary CA
// certificate. This value should generally be a time in the past.
NotBefore time.Time
NotAfter time.Time
// NotAfter is the x509.Certificate.NotAfter value of the primary CA
// certificate. This is the time when the certificate will expire.
NotAfter time.Time
// RootCert is the PEM-encoded public certificate for the root CA. The
// certificate is the same for all federated clusters.

View File

@ -116,16 +116,25 @@ The configuration options are listed below.
- `RootPKIPath` / `root_pki_path` (`string: <required>`) - The path to
a PKI secrets engine for the root certificate. If the path does not
a PKI secrets engine for the root certificate.
If the path does not
exist, Consul will mount a new PKI secrets engine at the specified path with the
`RootCertTTL` value as the root certificate's TTL. If the `RootCertTTL` is not set,
a [`max_lease_ttl`](https://www.vaultproject.io/api/system/mounts#max_lease_ttl)
of 87600 hours, or 10 years is applied by default as of Consul 1.11 and later. Prior to Consul 1.11,
the root certificate TTL was set to 8760 hour, or 1 year, and was not configurable.
The root certificate will expire at the end of the specified period.
When WAN Federation is enabled, each secondary datacenter must use the same Vault cluster and share the same `root_pki_path`
with the primary datacenter.
with the primary datacenter.
To use an intermediate certificate as the primary CA in Consul initialize the
`RootPKIPath` in Vault with a PEM bundle. The first certificate in the bundle
must be the intermediate certificate that Consul will use as the primary CA.
The last certificate in the bundle must be a root certificate. The bundle
must contain a valid chain, where each certificate is followed by the certificate
that authorized it.
- `IntermediatePKIPath` / `intermediate_pki_path` (`string: <required>`) -
The path to a PKI secrets engine for the generated intermediate certificate.