From 624411dbedb2896400e1008cef8e14c3d23fdfe7 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Tue, 3 May 2022 16:08:22 -0400 Subject: [PATCH] Force new root CA creation on out of band changes --- testutil/testutl.go | 1 + util/util.go | 6 +- .../resource_pki_secret_backend_root_cert.go | 82 ++++++++++-- ...ource_pki_secret_backend_root_cert_test.go | 121 +++++++++++++----- 4 files changed, 166 insertions(+), 44 deletions(-) create mode 100644 testutil/testutl.go diff --git a/testutil/testutl.go b/testutil/testutl.go new file mode 100644 index 000000000..110b2e6a7 --- /dev/null +++ b/testutil/testutl.go @@ -0,0 +1 @@ +package testutil diff --git a/util/util.go b/util/util.go index eb85f63e5..9b309d69d 100644 --- a/util/util.go +++ b/util/util.go @@ -42,7 +42,11 @@ func ToStringArray(input []interface{}) []string { } func Is404(err error) bool { - return strings.Contains(err.Error(), "Code: 404") + return IsHTTPErrorCode(err, http.StatusNotFound) +} + +func IsHTTPErrorCode(err error, code int) bool { + return strings.Contains(err.Error(), fmt.Sprintf("Code: %d", code)) } func CalculateConflictsWith(self string, group []string) []string { diff --git a/vault/resource_pki_secret_backend_root_cert.go b/vault/resource_pki_secret_backend_root_cert.go index 49b281147..f317166d3 100644 --- a/vault/resource_pki_secret_backend_root_cert.go +++ b/vault/resource_pki_secret_backend_root_cert.go @@ -1,21 +1,59 @@ package vault import ( + "context" + "crypto/x509" + "encoding/pem" "fmt" + "io" "log" + "net/http" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/certutil" + + "github.com/hashicorp/terraform-provider-vault/util" ) func pkiSecretBackendRootCertResource() *schema.Resource { return &schema.Resource{ Create: pkiSecretBackendRootCertCreate, - Read: pkiSecretBackendRootCertRead, - Update: pkiSecretBackendRootCertUpdate, Delete: pkiSecretBackendRootCertDelete, + Update: func(data *schema.ResourceData, i interface{}) error { + return nil + }, + Read: func(data *schema.ResourceData, i interface{}) error { + return nil + }, + CustomizeDiff: func(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { + client := meta.(*api.Client) + cert, err := getCACertificate(client, d.Get("backend").(string)) + if err != nil { + return err + } + + if cert != nil { + key := "serial" + cur := d.Get(key).(string) + n := certutil.GetHexFormatted(cert.SerialNumber.Bytes(), ":") + if err := d.SetNew(key, n); err != nil { + return err + } + + o, _ := d.GetChange(key) + // don't force new on new resources + if o.(string) != "" && cur != n { + if err := d.ForceNew(key); err != nil { + return err + } + } + + } + return nil + }, Schema: map[string]*schema.Schema{ "backend": { @@ -177,7 +215,7 @@ func pkiSecretBackendRootCertResource() *schema.Resource { "certificate": { Type: schema.TypeString, Computed: true, - Description: "The certicate.", + Description: "The certificate.", }, "issuing_ca": { Type: schema.TypeString, @@ -281,15 +319,43 @@ func pkiSecretBackendRootCertCreate(d *schema.ResourceData, meta interface{}) er d.Set("serial", resp.Data["serial_number"]) d.SetId(path) - return pkiSecretBackendRootCertRead(d, meta) -} -func pkiSecretBackendRootCertRead(d *schema.ResourceData, meta interface{}) error { return nil } -func pkiSecretBackendRootCertUpdate(d *schema.ResourceData, m interface{}) error { - return nil +func getCACertificate(client *api.Client, mount string) (*x509.Certificate, error) { + path := fmt.Sprintf("/v1/%s/ca/pem", mount) + req := client.NewRequest(http.MethodGet, path) + req.ClientToken = "" + resp, err := client.RawRequest(req) + if err != nil { + if util.IsHTTPErrorCode(err, http.StatusNotFound) || util.IsHTTPErrorCode(err, http.StatusForbidden) { + return nil, nil + } + return nil, err + } + + if resp == nil { + return nil, fmt.Errorf("expected a response body, got nil response") + } + + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + log.Printf("[INFO] Reading current CA") + b, _ := pem.Decode(data) + if b != nil { + cert, err := x509.ParseCertificate(b.Bytes) + if err != nil { + return nil, err + } + return cert, nil + } + + return nil, nil } func pkiSecretBackendRootCertDelete(d *schema.ResourceData, meta interface{}) error { diff --git a/vault/resource_pki_secret_backend_root_cert_test.go b/vault/resource_pki_secret_backend_root_cert_test.go index b8810f0dd..00d7d7fda 100644 --- a/vault/resource_pki_secret_backend_root_cert_test.go +++ b/vault/resource_pki_secret_backend_root_cert_test.go @@ -17,6 +17,25 @@ import ( func TestPkiSecretBackendRootCertificate_basic(t *testing.T) { path := "pki-" + strconv.Itoa(acctest.RandInt()) + resourceName := "vault_pki_secret_backend_root_cert.test" + + checks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "backend", path), + resource.TestCheckResourceAttr(resourceName, "type", "internal"), + resource.TestCheckResourceAttr(resourceName, "common_name", "test Root CA"), + resource.TestCheckResourceAttr(resourceName, "ttl", "86400"), + resource.TestCheckResourceAttr(resourceName, "format", "pem"), + resource.TestCheckResourceAttr(resourceName, "private_key_format", "der"), + resource.TestCheckResourceAttr(resourceName, "key_type", "rsa"), + resource.TestCheckResourceAttr(resourceName, "key_bits", "4096"), + resource.TestCheckResourceAttr(resourceName, "ou", "test"), + resource.TestCheckResourceAttr(resourceName, "organization", "test"), + resource.TestCheckResourceAttr(resourceName, "country", "test"), + resource.TestCheckResourceAttr(resourceName, "locality", "test"), + resource.TestCheckResourceAttr(resourceName, "province", "test"), + resource.TestCheckResourceAttrSet(resourceName, "serial"), + } + resource.Test(t, resource.TestCase{ Providers: testProviders, PreCheck: func() { testutil.TestAccPreCheck(t) }, @@ -24,22 +43,52 @@ func TestPkiSecretBackendRootCertificate_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testPkiSecretBackendRootCertificateConfig_basic(path), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "backend", path), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "type", "internal"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "common_name", "test Root CA"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "ttl", "86400"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "format", "pem"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "private_key_format", "der"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "key_type", "rsa"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "key_bits", "4096"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "ou", "test"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "organization", "test"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "country", "test"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "locality", "test"), - resource.TestCheckResourceAttr("vault_pki_secret_backend_root_cert.test", "province", "test"), - resource.TestCheckResourceAttrSet("vault_pki_secret_backend_root_cert.test", "serial"), - ), + Check: resource.ComposeTestCheckFunc(checks...), + }, + { + PreConfig: func() { + client := testProvider.Meta().(*api.Client) + _, err := client.Logical().Delete(fmt.Sprintf("%s/root", path)) + if err != nil { + t.Fatal(err) + } + }, + Config: testPkiSecretBackendRootCertificateConfig_basic(path), + }, + { + // test unmounted backend + PreConfig: func() { + client := testProvider.Meta().(*api.Client) + if err := client.Sys().Unmount(path); err != nil { + t.Fatal(err) + } + }, + Config: testPkiSecretBackendRootCertificateConfig_basic(path), + }, + { + // test out of band update to the root CA + PreConfig: func() { + client := testProvider.Meta().(*api.Client) + _, err := client.Logical().Delete(fmt.Sprintf("%s/root", path)) + if err != nil { + t.Fatal(err) + } + genPath := pkiSecretBackendIntermediateSetSignedReadPath(path, "internal") + resp, err := client.Logical().Write(genPath, + map[string]interface{}{ + "common_name": "out-of-band", + }, + ) + if err != nil { + t.Fatal(err) + } + + if resp == nil { + t.Fatalf("empty response for write on path %s", genPath) + } + }, + Config: testPkiSecretBackendRootCertificateConfig_basic(path), + Check: resource.ComposeTestCheckFunc(checks...), }, }, }) @@ -69,30 +118,32 @@ func testPkiSecretBackendRootCertificateDestroy(s *terraform.State) error { } func testPkiSecretBackendRootCertificateConfig_basic(path string) string { - return fmt.Sprintf(` + config := fmt.Sprintf(` resource "vault_mount" "test" { - path = "%s" - type = "pki" - description = "test" + path = "%s" + type = "pki" + description = "test" default_lease_ttl_seconds = "86400" max_lease_ttl_seconds = "86400" } resource "vault_pki_secret_backend_root_cert" "test" { - depends_on = [ "vault_mount.test" ] - backend = vault_mount.test.path - type = "internal" - common_name = "test Root CA" - ttl = "86400" - format = "pem" - private_key_format = "der" - key_type = "rsa" - key_bits = 4096 + backend = vault_mount.test.path + type = "internal" + common_name = "test Root CA" + ttl = "86400" + format = "pem" + private_key_format = "der" + key_type = "rsa" + key_bits = 4096 exclude_cn_from_sans = true - ou = "test" - organization = "test" - country = "test" - locality = "test" - province = "test" -}`, path) + ou = "test" + organization = "test" + country = "test" + locality = "test" + province = "test" +} +`, path) + + return config }