From 33418afd3c1fab72ea62c5fe480af1d0b220ce7f Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Fri, 20 Apr 2018 20:39:51 -0700 Subject: [PATCH] Add cross-signing mechanism to root rotation --- agent/connect/ca_provider.go | 21 +++- agent/consul/connect_ca_endpoint.go | 40 ++++---- agent/consul/connect_ca_provider.go | 119 +++++++++++++++++++++-- agent/consul/connect_ca_provider_test.go | 34 +++++++ agent/consul/leader.go | 13 --- agent/structs/connect_ca.go | 4 + 6 files changed, 191 insertions(+), 40 deletions(-) create mode 100644 agent/consul/connect_ca_provider_test.go diff --git a/agent/connect/ca_provider.go b/agent/connect/ca_provider.go index dc70c6a58d..9a53d02a0d 100644 --- a/agent/connect/ca_provider.go +++ b/agent/connect/ca_provider.go @@ -10,10 +10,27 @@ import ( // an external CA that provides leaf certificate signing for // given SpiffeIDServices. 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) + + // ActiveIntermediate returns the current signing cert used by this + // provider for generating SPIFFE leaf certs. 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) - //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 } diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index 128c1493d1..9a3adeb991 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -116,20 +116,24 @@ func (s *ConnectCA) ConfigurationSet( // to use a different root certificate. // 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 - // 2. Get the intermediate from the new provider - // 3. Generate a CSR for the new intermediate, call SignCA on the old/current provider + // 1. Get the intermediate from the new provider + // 2. Generate a CSR for the new intermediate, call SignCA on the old/current provider // 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 - // -5. Update the roots and CA config in the state store at the same time, finally switching - // to the new provider - // -6. Call teardown on the old provider, so it can clean up whatever it needs to - - /*_, err := newProvider.ActiveIntermediate() + _, csr, err := newProvider.GenerateIntermediate() if err != nil { 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 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 // and call teardown on the old provider - oldProvider := s.srv.getCAProvider() s.srv.setCAProvider(newProvider) if err := oldProvider.Teardown(); err != nil { @@ -202,11 +205,12 @@ func (s *ConnectCA) Roots( // directly to the structure in the memdb store. reply.Roots[i] = &structs.CARoot{ - ID: r.ID, - Name: r.Name, - RootCert: r.RootCert, - RaftIndex: r.RaftIndex, - Active: r.Active, + ID: r.ID, + Name: r.Name, + RootCert: r.RootCert, + Intermediates: r.Intermediates, + RaftIndex: r.RaftIndex, + Active: r.Active, } if r.Active { @@ -245,7 +249,9 @@ func (s *ConnectCA) Sign( // 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 { return err } diff --git a/agent/consul/connect_ca_provider.go b/agent/consul/connect_ca_provider.go index b72a9ee36f..6f0508ce18 100644 --- a/agent/consul/connect_ca_provider.go +++ b/agent/consul/connect_ca_provider.go @@ -143,23 +143,58 @@ func (c *ConsulCAProvider) ActiveRoot() (*structs.CARoot, error) { 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) { 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() - idx, providerState, err := state.CAProviderState(c.id) + _, providerState, err := state.CAProviderState(c.id) 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) - if err != nil { - return nil, err + id := &connect.SpiffeIDSigning{ClusterID: config.ClusterSerial, Domain: "consul"} + template := &x509.CertificateRequest{ + 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. @@ -194,7 +229,7 @@ func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.Ce 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) if err != nil { return nil, err @@ -277,6 +312,74 @@ func (c *ConsulCAProvider) Sign(serviceId *connect.SpiffeIDService, csr *x509.Ce }, 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 func generatePrivateKey() (string, error) { var pk *ecdsa.PrivateKey diff --git a/agent/consul/connect_ca_provider_test.go b/agent/consul/connect_ca_provider_test.go new file mode 100644 index 0000000000..adad3acba0 --- /dev/null +++ b/agent/consul/connect_ca_provider_test.go @@ -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) +} diff --git a/agent/consul/leader.go b/agent/consul/leader.go index 8d62ca1aa2..91bacee2fa 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -1,7 +1,6 @@ package consul import ( - "crypto/x509" "fmt" "net" "strconv" @@ -476,18 +475,6 @@ func (s *Server) setCAProvider(newProvider connect.CAProvider) { 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 // 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. diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 1e2959dd1f..33c355fca3 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -31,6 +31,10 @@ type CARoot struct { // RootCert is the PEM-encoded public certificate. 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 // 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.