Add cross-signing mechanism to root rotation
This commit is contained in:
parent
bbfcb278e1
commit
43f13d5a0b
|
@ -10,10 +10,27 @@ import (
|
||||||
// an external CA that provides leaf certificate signing for
|
// an external CA that provides leaf certificate signing for
|
||||||
// given SpiffeIDServices.
|
// given SpiffeIDServices.
|
||||||
type CAProvider interface {
|
type CAProvider interface {
|
||||||
|
// Active root returns the currently active root CA for this
|
||||||
|
// provider. This should be a parent of the certificate returned by
|
||||||
|
// ActiveIntermediate()
|
||||||
ActiveRoot() (*structs.CARoot, error)
|
ActiveRoot() (*structs.CARoot, error)
|
||||||
|
|
||||||
|
// ActiveIntermediate returns the current signing cert used by this
|
||||||
|
// provider for generating SPIFFE leaf certs.
|
||||||
ActiveIntermediate() (*structs.CARoot, error)
|
ActiveIntermediate() (*structs.CARoot, error)
|
||||||
GenerateIntermediate() (*structs.CARoot, error)
|
|
||||||
|
// GenerateIntermediate returns a new intermediate signing cert, a
|
||||||
|
// cross-signing CSR for it and sets it to the active intermediate.
|
||||||
|
GenerateIntermediate() (*structs.CARoot, *x509.CertificateRequest, error)
|
||||||
|
|
||||||
|
// Sign signs a leaf certificate used by Connect proxies from a CSR.
|
||||||
Sign(*SpiffeIDService, *x509.CertificateRequest) (*structs.IssuedCert, error)
|
Sign(*SpiffeIDService, *x509.CertificateRequest) (*structs.IssuedCert, error)
|
||||||
//SignCA(*x509.CertificateRequest) (*structs.IssuedCert, error)
|
|
||||||
|
// SignCA signs a CA CSR and returns the resulting cross-signed cert.
|
||||||
|
SignCA(*x509.CertificateRequest) (string, error)
|
||||||
|
|
||||||
|
// Teardown performs any necessary cleanup that should happen when the provider
|
||||||
|
// is shut down permanently, such as removing a temporary PKI backend in Vault
|
||||||
|
// created for an intermediate CA.
|
||||||
Teardown() error
|
Teardown() error
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,20 +116,24 @@ func (s *ConnectCA) ConfigurationSet(
|
||||||
// to use a different root certificate.
|
// to use a different root certificate.
|
||||||
|
|
||||||
// If it's a config change that would trigger a rotation (different provider/root):
|
// If it's a config change that would trigger a rotation (different provider/root):
|
||||||
// -1. Create an instance of the provider described by the new config
|
// 1. Get the intermediate from the new provider
|
||||||
// 2. Get the intermediate from the new provider
|
// 2. Generate a CSR for the new intermediate, call SignCA on the old/current provider
|
||||||
// 3. Generate a CSR for the new intermediate, call SignCA on the old/current provider
|
|
||||||
// to get the cross-signed intermediate
|
// to get the cross-signed intermediate
|
||||||
// ~4. Get the active root for the new provider, append the intermediate from step 3
|
// 3. Get the active root for the new provider, append the intermediate from step 3
|
||||||
// to its list of intermediates
|
// to its list of intermediates
|
||||||
// -5. Update the roots and CA config in the state store at the same time, finally switching
|
_, csr, err := newProvider.GenerateIntermediate()
|
||||||
// to the new provider
|
|
||||||
// -6. Call teardown on the old provider, so it can clean up whatever it needs to
|
|
||||||
|
|
||||||
/*_, err := newProvider.ActiveIntermediate()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}*/
|
}
|
||||||
|
|
||||||
|
oldProvider := s.srv.getCAProvider()
|
||||||
|
xcCert, err := oldProvider.SignCA(csr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the cross signed cert to the new root's intermediates
|
||||||
|
newActiveRoot.Intermediates = []string{xcCert}
|
||||||
|
|
||||||
// Update the roots and CA config in the state store at the same time
|
// Update the roots and CA config in the state store at the same time
|
||||||
idx, roots, err := state.CARoots(nil)
|
idx, roots, err := state.CARoots(nil)
|
||||||
|
@ -160,7 +164,6 @@ func (s *ConnectCA) ConfigurationSet(
|
||||||
|
|
||||||
// If the config has been committed, update the local provider instance
|
// If the config has been committed, update the local provider instance
|
||||||
// and call teardown on the old provider
|
// and call teardown on the old provider
|
||||||
oldProvider := s.srv.getCAProvider()
|
|
||||||
s.srv.setCAProvider(newProvider)
|
s.srv.setCAProvider(newProvider)
|
||||||
|
|
||||||
if err := oldProvider.Teardown(); err != nil {
|
if err := oldProvider.Teardown(); err != nil {
|
||||||
|
@ -205,6 +208,7 @@ func (s *ConnectCA) Roots(
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
RootCert: r.RootCert,
|
RootCert: r.RootCert,
|
||||||
|
Intermediates: r.Intermediates,
|
||||||
RaftIndex: r.RaftIndex,
|
RaftIndex: r.RaftIndex,
|
||||||
Active: r.Active,
|
Active: r.Active,
|
||||||
}
|
}
|
||||||
|
@ -245,7 +249,9 @@ func (s *ConnectCA) Sign(
|
||||||
|
|
||||||
// todo(kyhavlov): more validation on the CSR before signing
|
// todo(kyhavlov): more validation on the CSR before signing
|
||||||
|
|
||||||
cert, err := s.srv.signConnectCert(serviceId, csr)
|
provider := s.srv.getCAProvider()
|
||||||
|
|
||||||
|
cert, err := provider.Sign(serviceId, csr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,23 +143,58 @@ func (c *ConsulCAProvider) ActiveRoot() (*structs.CARoot, error) {
|
||||||
return providerState.CARoot, nil
|
return providerState.CARoot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We aren't maintaining separate root/intermediate CAs for the builtin
|
||||||
|
// provider, so just return the root.
|
||||||
func (c *ConsulCAProvider) ActiveIntermediate() (*structs.CARoot, error) {
|
func (c *ConsulCAProvider) ActiveIntermediate() (*structs.CARoot, error) {
|
||||||
return c.ActiveRoot()
|
return c.ActiveRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConsulCAProvider) GenerateIntermediate() (*structs.CARoot, error) {
|
// We aren't maintaining separate root/intermediate CAs for the builtin
|
||||||
|
// provider, so just generate a CSR for the active root.
|
||||||
|
func (c *ConsulCAProvider) GenerateIntermediate() (*structs.CARoot, *x509.CertificateRequest, error) {
|
||||||
|
ca, err := c.ActiveIntermediate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
state := c.srv.fsm.State()
|
state := c.srv.fsm.State()
|
||||||
idx, providerState, err := state.CAProviderState(c.id)
|
_, providerState, err := state.CAProviderState(c.id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
_, config, err := state.CAConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ca, err := c.generateCA(providerState.PrivateKey, "", idx+1)
|
id := &connect.SpiffeIDSigning{ClusterID: config.ClusterSerial, Domain: "consul"}
|
||||||
if err != nil {
|
template := &x509.CertificateRequest{
|
||||||
return nil, err
|
URIs: []*url.URL{id.URI()},
|
||||||
}
|
}
|
||||||
|
|
||||||
return ca, nil
|
signer, err := connect.ParseSigner(providerState.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the CSR itself
|
||||||
|
var csrBuf bytes.Buffer
|
||||||
|
bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error creating CSR: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error encoding CSR: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err := connect.ParseCSR(csrBuf.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ca, csr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the state store entry for this provider instance.
|
// Remove the state store entry for this provider instance.
|
||||||
|
@ -194,7 +229,7 @@ func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.Ce
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the keyId for the cert from the signing public key.
|
// Create the keyId for the cert from the signing private key.
|
||||||
signer, err := connect.ParseSigner(providerState.PrivateKey)
|
signer, err := connect.ParseSigner(providerState.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -277,6 +312,74 @@ func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.Ce
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignCA returns an intermediate CA cert signed by the current active root.
|
||||||
|
func (c *ConsulCAProvider) SignCA(csr *x509.CertificateRequest) (string, error) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
// Get the provider state
|
||||||
|
state := c.srv.fsm.State()
|
||||||
|
_, providerState, err := state.CAProviderState(c.id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := connect.ParseSigner(providerState.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error parsing private key %q: %v", providerState.PrivateKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("Consul cross-signed CA %d", providerState.LeafIndex+1)
|
||||||
|
|
||||||
|
// The URI (SPIFFE compatible) for the cert
|
||||||
|
_, config, err := state.CAConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
id := &connect.SpiffeIDSigning{ClusterID: config.ClusterSerial, Domain: "consul"}
|
||||||
|
keyId, err := connect.KeyId(privKey.Public())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the CA cert
|
||||||
|
serialNum := &big.Int{}
|
||||||
|
serialNum.SetUint64(providerState.LeafIndex + 1)
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNum,
|
||||||
|
Subject: pkix.Name{CommonName: name},
|
||||||
|
URIs: csr.URIs,
|
||||||
|
Signature: csr.Signature,
|
||||||
|
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
||||||
|
PublicKey: csr.PublicKey,
|
||||||
|
PermittedDNSDomainsCritical: true,
|
||||||
|
PermittedDNSDomains: []string{id.URI().Hostname()},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
KeyUsage: x509.KeyUsageCertSign |
|
||||||
|
x509.KeyUsageCRLSign |
|
||||||
|
x509.KeyUsageDigitalSignature,
|
||||||
|
IsCA: true,
|
||||||
|
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
AuthorityKeyId: keyId,
|
||||||
|
SubjectKeyId: keyId,
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err := x509.CreateCertificate(
|
||||||
|
rand.Reader, &template, &template, privKey.Public(), privKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error generating CA certificate: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error encoding private key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// generatePrivateKey returns a new private key
|
// generatePrivateKey returns a new private key
|
||||||
func generatePrivateKey() (string, error) {
|
func generatePrivateKey() (string, error) {
|
||||||
var pk *ecdsa.PrivateKey
|
var pk *ecdsa.PrivateKey
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package consul
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/testrpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCAProvider_Bootstrap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
dir1, s1 := testServer(t)
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
provider := s1.getCAProvider()
|
||||||
|
|
||||||
|
root, err := provider.ActiveRoot()
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
state := s1.fsm.State()
|
||||||
|
_, activeRoot, err := state.CARootActive(nil)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(root.ID, activeRoot.ID)
|
||||||
|
assert.Equal(root.Name, activeRoot.Name)
|
||||||
|
assert.Equal(root.RootCert, activeRoot.RootCert)
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package consul
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -476,18 +475,6 @@ func (s *Server) setCAProvider(newProvider connect.CAProvider) {
|
||||||
s.caProvider = newProvider
|
s.caProvider = newProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// signConnectCert signs a cert for a service using the currently configured CA provider
|
|
||||||
func (s *Server) signConnectCert(service *connect.SpiffeIDService, csr *x509.CertificateRequest) (*structs.IssuedCert, error) {
|
|
||||||
s.caProviderLock.RLock()
|
|
||||||
defer s.caProviderLock.RUnlock()
|
|
||||||
|
|
||||||
cert, err := s.caProvider.Sign(service, csr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconcileReaped is used to reconcile nodes that have failed and been reaped
|
// reconcileReaped is used to reconcile nodes that have failed and been reaped
|
||||||
// from Serf but remain in the catalog. This is done by looking for unknown nodes with serfHealth checks registered.
|
// from Serf but remain in the catalog. This is done by looking for unknown nodes with serfHealth checks registered.
|
||||||
// We generate a "reap" event to cause the node to be cleaned up.
|
// We generate a "reap" event to cause the node to be cleaned up.
|
||||||
|
|
|
@ -31,6 +31,10 @@ type CARoot struct {
|
||||||
// RootCert is the PEM-encoded public certificate.
|
// RootCert is the PEM-encoded public certificate.
|
||||||
RootCert string
|
RootCert string
|
||||||
|
|
||||||
|
// Intermediates is a list of PEM-encoded intermediate certs to
|
||||||
|
// attach to any leaf certs signed by this CA.
|
||||||
|
Intermediates []string
|
||||||
|
|
||||||
// SigningCert is the PEM-encoded signing certificate and SigningKey
|
// SigningCert is the PEM-encoded signing certificate and SigningKey
|
||||||
// is the PEM-encoded private key for the signing certificate. These
|
// is the PEM-encoded private key for the signing certificate. These
|
||||||
// may actually be empty if the CA plugin in use manages these for us.
|
// may actually be empty if the CA plugin in use manages these for us.
|
||||||
|
|
Loading…
Reference in New Issue