diff --git a/agent/connect/ca/provider.go b/agent/connect/ca/provider.go index 04c1317855..331d6911bc 100644 --- a/agent/connect/ca/provider.go +++ b/agent/connect/ca/provider.go @@ -27,6 +27,8 @@ type Provider interface { // true, calling this is an error. GenerateIntermediateCSR() (string, error) + // SetIntermediate sets the provider to use the given intermediate certificate + // as well as the root it was signed by. SetIntermediate(intermediatePEM, rootPEM string) error // ActiveIntermediate returns the current signing cert used by this provider diff --git a/agent/connect/ca/provider_consul.go b/agent/connect/ca/provider_consul.go index 6a7cc5b577..4a5e77c553 100644 --- a/agent/connect/ca/provider_consul.go +++ b/agent/connect/ca/provider_consul.go @@ -208,6 +208,8 @@ func (c *ConsulProvider) GenerateIntermediateCSR() (string, error) { return csr, nil } +// SetIntermediate validates that the given intermediate is for the right private key +// and writes the given intermediate and root certificates to the state. func (c *ConsulProvider) SetIntermediate(intermediatePEM, rootPEM string) error { _, providerState, err := c.getState() if err != nil { diff --git a/agent/connect/ca/provider_consul_test.go b/agent/connect/ca/provider_consul_test.go index 46d6bd78d6..4cfe338b7e 100644 --- a/agent/connect/ca/provider_consul_test.go +++ b/agent/connect/ca/provider_consul_test.go @@ -291,6 +291,12 @@ func TestConsulProvider_SignIntermediate(t *testing.T) { provider2 := &ConsulProvider{Delegate: delegate2} require.NoError(provider2.Configure(conf2.ClusterID, false, conf2.Config)) + testSignIntermediateCrossDC(t, provider1, provider2) +} + +func testSignIntermediateCrossDC(t *testing.T, provider1, provider2 Provider) { + require := require.New(t) + // Get the intermediate CSR from provider2. csrPEM, err := provider2.GenerateIntermediateCSR() require.NoError(err) diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index 00adbc3ca1..da7b3596fc 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -102,11 +102,92 @@ func (v *VaultProvider) GenerateRoot() error { return nil } +// GenerateIntermediateCSR creates a private key and generates a CSR +// for another datacenter's root to sign, overwriting the intermediate backend +// in the process. func (v *VaultProvider) GenerateIntermediateCSR() (string, error) { - return "", nil + if v.isRoot { + return "", fmt.Errorf("provider is the root certificate authority, " + + "cannot generate an intermediate CSR") + } + + return v.generateIntermediateCSR() } +func (v *VaultProvider) generateIntermediateCSR() (string, error) { + mounts, err := v.client.Sys().ListMounts() + if err != nil { + return "", err + } + + // Mount the backend if it isn't mounted already. + if _, ok := mounts[v.config.IntermediatePKIPath]; !ok { + err := v.client.Sys().Mount(v.config.IntermediatePKIPath, &vaultapi.MountInput{ + Type: "pki", + Description: "intermediate CA backend for Consul Connect", + Config: vaultapi.MountConfigInput{ + MaxLeaseTTL: "2160h", + }, + }) + + if err != nil { + return "", err + } + } + + // Create the role for issuing leaf certs if it doesn't exist yet + rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole + role, err := v.client.Logical().Read(rolePath) + if err != nil { + return "", err + } + spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"} + if role == nil { + _, err := v.client.Logical().Write(rolePath, map[string]interface{}{ + "allow_any_name": true, + "allowed_uri_sans": "spiffe://*", + "key_type": "any", + "max_ttl": v.config.LeafCertTTL.String(), + "require_cn": false, + }) + if err != nil { + return "", err + } + } + + // Generate a new intermediate CSR for the root to sign. + data, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{ + "common_name": "Vault CA Intermediate Authority", + "uri_sans": spiffeID.URI().String(), + }) + if err != nil { + return "", err + } + if data == nil || data.Data["csr"] == "" { + return "", fmt.Errorf("got empty value when generating intermediate CSR") + } + csr, ok := data.Data["csr"].(string) + if !ok { + return "", fmt.Errorf("csr result is not a string") + } + + return csr, nil +} + +// SetIntermediate writes the incoming intermediate and root certificates to the +// intermediate backend (as a chain). func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error { + if v.isRoot { + return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter") + } + + _, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{ + "certificate": fmt.Sprintf("%s\n%s", intermediatePEM, rootPEM), + }) + if err != nil { + return err + } + return nil } @@ -149,61 +230,14 @@ func (v *VaultProvider) getCA(path string) (string, error) { // necessary, then generates and signs a new CA CSR using the root PKI backend // and updates the intermediate backend to use that new certificate. func (v *VaultProvider) GenerateIntermediate() (string, error) { - mounts, err := v.client.Sys().ListMounts() + csr, err := v.generateIntermediateCSR() if err != nil { return "", err } - // Mount the backend if it isn't mounted already. - if _, ok := mounts[v.config.IntermediatePKIPath]; !ok { - err := v.client.Sys().Mount(v.config.IntermediatePKIPath, &vaultapi.MountInput{ - Type: "pki", - Description: "intermediate CA backend for Consul Connect", - Config: vaultapi.MountConfigInput{ - MaxLeaseTTL: "2160h", - }, - }) - - if err != nil { - return "", err - } - } - - // Create the role for issuing leaf certs if it doesn't exist yet - rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole - role, err := v.client.Logical().Read(rolePath) - if err != nil { - return "", err - } - spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"} - if role == nil { - _, err := v.client.Logical().Write(rolePath, map[string]interface{}{ - "allow_any_name": true, - "allowed_uri_sans": "spiffe://*", - "key_type": "any", - "max_ttl": v.config.LeafCertTTL.String(), - "require_cn": false, - }) - if err != nil { - return "", err - } - } - - // Generate a new intermediate CSR for the root to sign. - csr, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{ - "common_name": "Vault CA Intermediate Authority", - "uri_sans": spiffeID.URI().String(), - }) - if err != nil { - return "", err - } - if csr == nil || csr.Data["csr"] == "" { - return "", fmt.Errorf("got empty value when generating intermediate CSR") - } - // Sign the CSR with the root backend. intermediate, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{ - "csr": csr.Data["csr"], + "csr": csr, "format": "pem_bundle", }) if err != nil { @@ -257,8 +291,34 @@ func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) { return fmt.Sprintf("%s\n%s", cert, ca), nil } -func (v *VaultProvider) SignIntermediate(*x509.CertificateRequest) (string, error) { - return "", nil +// SignIntermediate returns a signed CA certificate with a path length constraint +// of 0 to ensure that the certificate cannot be used to generate further CA certs. +func (v *VaultProvider) SignIntermediate(csr *x509.CertificateRequest) (string, error) { + var pemBuf bytes.Buffer + err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}) + if err != nil { + return "", err + } + + // Sign the CSR with the root backend. + data, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{ + "csr": pemBuf.String(), + "format": "pem_bundle", + "max_path_length": 0, + }) + if err != nil { + return "", err + } + if data == nil || data.Data["certificate"] == "" { + return "", fmt.Errorf("got empty value when generating intermediate certificate") + } + + intermediate, ok := data.Data["certificate"].(string) + if !ok { + return "", fmt.Errorf("signed intermediate result is not a string") + } + + return intermediate, nil } // CrossSignCA takes a CA certificate and cross-signs it to form a trust chain diff --git a/agent/connect/ca/provider_vault_test.go b/agent/connect/ca/provider_vault_test.go index eee9e9544c..3b423fa6b3 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -16,10 +16,10 @@ import ( ) func testVaultCluster(t *testing.T) (*VaultProvider, *vault.Core, net.Listener) { - return testVaultClusterWithConfig(t, nil) + return testVaultClusterWithConfig(t, true, nil) } -func testVaultClusterWithConfig(t *testing.T, rawConf map[string]interface{}) (*VaultProvider, *vault.Core, net.Listener) { +func testVaultClusterWithConfig(t *testing.T, isRoot bool, rawConf map[string]interface{}) (*VaultProvider, *vault.Core, net.Listener) { if err := vault.AddTestLogicalBackend("pki", pki.Factory); err != nil { t.Fatal(err) } @@ -41,10 +41,12 @@ func testVaultClusterWithConfig(t *testing.T, rawConf map[string]interface{}) (* require := require.New(t) provider := &VaultProvider{} - require.NoError(provider.Configure("asdf", true, conf)) - require.NoError(provider.GenerateRoot()) - _, err := provider.GenerateIntermediate() - require.NoError(err) + require.NoError(provider.Configure("asdf", isRoot, conf)) + if isRoot { + require.NoError(provider.GenerateRoot()) + _, err := provider.GenerateIntermediate() + require.NoError(err) + } return provider, core, ln } @@ -100,7 +102,7 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) { t.Parallel() require := require.New(t) - provider, core, listener := testVaultClusterWithConfig(t, map[string]interface{}{ + provider, core, listener := testVaultClusterWithConfig(t, true, map[string]interface{}{ "LeafCertTTL": "1h", }) defer core.Shutdown() @@ -176,3 +178,32 @@ func TestVaultCAProvider_CrossSignCA(t *testing.T) { testCrossSignProviders(t, provider1, provider2) } + +func TestVaultProvider_SignIntermediate(t *testing.T) { + t.Parallel() + + provider1, core1, listener1 := testVaultCluster(t) + defer core1.Shutdown() + defer listener1.Close() + + provider2, core2, listener2 := testVaultClusterWithConfig(t, false, nil) + defer core2.Shutdown() + defer listener2.Close() + + testSignIntermediateCrossDC(t, provider1, provider2) +} + +func TestVaultProvider_SignIntermediateConsul(t *testing.T) { + t.Parallel() + + provider1, core1, listener1 := testVaultCluster(t) + defer core1.Shutdown() + defer listener1.Close() + + conf := testConsulCAConfig() + delegate := newMockDelegate(t, conf) + provider2 := &ConsulProvider{Delegate: delegate} + require.NoError(t, provider2.Configure(conf.ClusterID, false, conf.Config)) + + testSignIntermediateCrossDC(t, provider1, provider2) +}