Skip to content

Commit

Permalink
Add PKI certificate revocation support (#1411)
Browse files Browse the repository at this point in the history
A new configuration option revoke can be applied to the
vault_pki_secret_backend_cert resource. In the case where it is set to
to true, the certificate will be revoked via Vault's PKI certificate
revocation API.

Co-authored-by: Jesús Marín <[email protected]>
  • Loading branch information
benashz and jesmg authored Apr 12, 2022
1 parent 7e00f7b commit 0a9e825
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 31 deletions.
30 changes: 30 additions & 0 deletions vault/resource_pki_secret_backend_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ func pkiSecretBackendCertResource() *schema.Resource {
Computed: true,
Description: "The certificate expiration.",
},
"revoke": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Revoke the certificate upon resource destruction.",
},
},
}
}
Expand Down Expand Up @@ -286,6 +292,30 @@ func pkiSecretBackendCertUpdate(d *schema.ResourceData, m interface{}) error {
}

func pkiSecretBackendCertDelete(d *schema.ResourceData, meta interface{}) error {
if d.Get("revoke").(bool) {
client := meta.(*api.Client)

backend := d.Get("backend").(string)
path := strings.Trim(backend, "/") + "/revoke"

serialNumber := d.Get("serial_number").(string)
commonName := d.Get("common_name").(string)
data := map[string]interface{}{
"serial_number": serialNumber,
}

log.Printf("[DEBUG] Revoking certificate %q with serial number %q on PKI secret backend %q",
commonName, serialNumber, backend)
_, err := client.Logical().Write(path, data)
if err != nil {
return fmt.Errorf("error revoking certificate %q with serial number %q for PKI secret backend %q: %w",
commonName, serialNumber, backend, err)
}
log.Printf("[DEBUG] Successfully revoked certificate %q with serial number %q on PKI secret backend %q",
commonName,
serialNumber, backend)
}

return nil
}

Expand Down
208 changes: 177 additions & 31 deletions vault/resource_pki_secret_backend_cert_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package vault

import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"testing"
"time"

"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
Expand All @@ -15,23 +20,76 @@ import (
"github.com/hashicorp/terraform-provider-vault/testutil"
)

type testPKICertStore struct {
cert string
expectRevoked bool
}

func TestPkiSecretBackendCert_basic(t *testing.T) {
rootPath := "pki-root-" + strconv.Itoa(acctest.RandInt())
intermediatePath := "pki-intermediate-" + strconv.Itoa(acctest.RandInt())

var store testPKICertStore

resourceName := "vault_pki_secret_backend_cert.test"
resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { testutil.TestAccPreCheck(t) },
CheckDestroy: testPkiSecretBackendCertDestroy,
Steps: []resource.TestStep{
{
Config: testPkiSecretBackendCertConfig_basic(rootPath, intermediatePath, true, false),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "backend", intermediatePath),
resource.TestCheckResourceAttr(resourceName, "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "ttl", "720h"),
resource.TestCheckResourceAttr(resourceName, "uri_sans.#", "1"),
resource.TestCheckResourceAttr(resourceName, "uri_sans.0", "spiffe://test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "revoke", "false"),
testCapturePKICert(resourceName, &store),
),
},
{
// remove the cert to test revocation flow (expect no revocation)
Config: testPkiSecretBackendCertConfig_basic(rootPath, intermediatePath, false, false),
Check: resource.ComposeTestCheckFunc(
testPKICertRevocation(intermediatePath, &store),
),
},
},
})
}

func TestPkiSecretBackendCert_revoke(t *testing.T) {
rootPath := "pki-root-" + strconv.Itoa(acctest.RandInt())
intermediatePath := "pki-intermediate-" + strconv.Itoa(acctest.RandInt())

var store testPKICertStore

resourceName := "vault_pki_secret_backend_cert.test"
resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { testutil.TestAccPreCheck(t) },
CheckDestroy: testPkiSecretBackendCertDestroy,
Steps: []resource.TestStep{
{
Config: testPkiSecretBackendCertConfig_basic(rootPath, intermediatePath),
Config: testPkiSecretBackendCertConfig_basic(rootPath, intermediatePath, true, true),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "backend", intermediatePath),
resource.TestCheckResourceAttr(resourceName, "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "ttl", "720h"),
resource.TestCheckResourceAttr(resourceName, "uri_sans.#", "1"),
resource.TestCheckResourceAttr(resourceName, "uri_sans.0", "spiffe://test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "uri_sans.0", "spiffe://test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "revoke", "true"),
testCapturePKICert(resourceName, &store),
),
},
{
// remove the cert to test revocation flow (expect revocation)
Config: testPkiSecretBackendCertConfig_basic(rootPath, intermediatePath, false, false),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "backend", intermediatePath),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "ttl", "720h"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "uri_sans.#", "1"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "uri_sans.0", "spiffe://test.my.domain"),
testPKICertRevocation(intermediatePath, &store),
),
},
},
Expand Down Expand Up @@ -61,8 +119,9 @@ func testPkiSecretBackendCertDestroy(s *terraform.State) error {
return nil
}

func testPkiSecretBackendCertConfig_basic(rootPath string, intermediatePath string) string {
return fmt.Sprintf(`
func testPkiSecretBackendCertConfig_basic(rootPath, intermediatePath string, withCert, revoke bool) string {
fragments := []string{
fmt.Sprintf(`
resource "vault_mount" "test-root" {
path = "%s"
type = "pki"
Expand Down Expand Up @@ -133,21 +192,31 @@ resource "vault_pki_secret_backend_role" "test" {
max_ttl = "3600"
key_usage = ["DigitalSignature", "KeyAgreement", "KeyEncipherment"]
}
`, rootPath, intermediatePath),
}

if withCert {
fragments = append(fragments, fmt.Sprintf(`
resource "vault_pki_secret_backend_cert" "test" {
depends_on = [ "vault_pki_secret_backend_role.test" ]
backend = vault_mount.test-intermediate.path
name = vault_pki_secret_backend_role.test.name
common_name = "cert.test.my.domain"
uri_sans = ["spiffe://test.my.domain"]
ttl = "720h"
depends_on = ["vault_pki_secret_backend_role.test"]
backend = vault_mount.test-intermediate.path
name = vault_pki_secret_backend_role.test.name
common_name = "cert.test.my.domain"
uri_sans = ["spiffe://test.my.domain"]
ttl = "720h"
min_seconds_remaining = 60
}`, rootPath, intermediatePath)
revoke = %t
}
`, revoke))
}

return strings.Join(fragments, "\n")
}

func TestPkiSecretBackendCert_renew(t *testing.T) {
rootPath := "pki-root-" + strconv.Itoa(acctest.RandInt())

resourceName := "vault_pki_secret_backend_cert.test"
resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { testutil.TestAccPreCheck(t) },
Expand All @@ -156,11 +225,12 @@ func TestPkiSecretBackendCert_renew(t *testing.T) {
{
Config: testPkiSecretBackendCertConfig_renew(rootPath),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "backend", rootPath),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "ttl", "1h"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "min_seconds_remaining", "3595"),
resource.TestCheckResourceAttrSet("vault_pki_secret_backend_cert.test", "expiration"),
resource.TestCheckResourceAttr(resourceName, "backend", rootPath),
resource.TestCheckResourceAttr(resourceName, "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "ttl", "1h"),
resource.TestCheckResourceAttr(resourceName, "min_seconds_remaining", "3595"),
resource.TestCheckResourceAttr(resourceName, "revoke", "false"),
resource.TestCheckResourceAttrSet(resourceName, "expiration"),
),
},
{
Expand All @@ -170,23 +240,25 @@ func TestPkiSecretBackendCert_renew(t *testing.T) {
{
Config: testPkiSecretBackendCertConfig_renew(rootPath),
Check: resource.ComposeTestCheckFunc(
testPkiSecretBackendCertWaitUntilRenewal("vault_pki_secret_backend_cert.test"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "backend", rootPath),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "ttl", "1h"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "min_seconds_remaining", "3595"),
resource.TestCheckResourceAttrSet("vault_pki_secret_backend_cert.test", "expiration"),
testPkiSecretBackendCertWaitUntilRenewal(resourceName),
resource.TestCheckResourceAttr(resourceName, "backend", rootPath),
resource.TestCheckResourceAttr(resourceName, "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "ttl", "1h"),
resource.TestCheckResourceAttr(resourceName, "min_seconds_remaining", "3595"),
resource.TestCheckResourceAttr(resourceName, "revoke", "false"),
resource.TestCheckResourceAttrSet(resourceName, "expiration"),
),
ExpectNonEmptyPlan: true,
},
{
Config: testPkiSecretBackendCertConfig_renew(rootPath),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "backend", rootPath),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "ttl", "1h"),
resource.TestCheckResourceAttr("vault_pki_secret_backend_cert.test", "min_seconds_remaining", "3595"),
resource.TestCheckResourceAttrSet("vault_pki_secret_backend_cert.test", "expiration"),
resource.TestCheckResourceAttr(resourceName, "backend", rootPath),
resource.TestCheckResourceAttr(resourceName, "common_name", "cert.test.my.domain"),
resource.TestCheckResourceAttr(resourceName, "ttl", "1h"),
resource.TestCheckResourceAttr(resourceName, "min_seconds_remaining", "3595"),
resource.TestCheckResourceAttr(resourceName, "revoke", "false"),
resource.TestCheckResourceAttrSet(resourceName, "expiration"),
),
},
},
Expand Down Expand Up @@ -268,3 +340,77 @@ func testPkiSecretBackendCertWaitUntilRenewal(n string) resource.TestCheckFunc {
return nil
}
}

func testCapturePKICert(resourcePath string, store *testPKICertStore) resource.TestCheckFunc {
return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "vault_pki_secret_backend_cert" {
continue
}

store.cert = rs.Primary.Attributes["certificate"]
v, err := strconv.ParseBool(rs.Primary.Attributes["revoke"])
if err != nil {
return err
}
store.expectRevoked = v
return nil
}
return fmt.Errorf("certificate not found in state")
}
}

func testPKICertRevocation(path string, store *testPKICertStore) resource.TestCheckFunc {
return func(_ *terraform.State) error {
if store.cert == "" {
return fmt.Errorf("certificate in %#v is empty", store)
}

addr := testProvider.Meta().(*api.Client).Address()
url := fmt.Sprintf("%s/v1/%s/crl", addr, path)
c := cleanhttp.DefaultClient()
resp, err := c.Get(url)
if err != nil {
return err
}

if resp.StatusCode > http.StatusAccepted {
return fmt.Errorf("invalid response, %#v", resp)
}

defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

crl, err := x509.ParseCRL(body)
if err != nil {
return err
}

p, _ := pem.Decode([]byte(store.cert))
cert, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return err
}

for _, revoked := range crl.TBSCertList.RevokedCertificates {
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
if !store.expectRevoked {
return fmt.Errorf("cert unexpectedly revoked, serial number %v, revocations %#v",
cert.SerialNumber, crl.TBSCertList.RevokedCertificates)
}
return nil
}
}

if store.expectRevoked {
return fmt.Errorf("cert not revoked, serial number %v, revocations %#v",
cert.SerialNumber, crl.TBSCertList.RevokedCertificates)
}

return nil
}
}
4 changes: 4 additions & 0 deletions website/docs/r/pki_secret_backend_cert.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ The following arguments are supported:
* `min_seconds_remaining` - (Optional) Generate a new certificate when the expiration is within this number of seconds, default is 604800 (7 days)

* `auto_renew` - (Optional) If set to `true`, certs will be renewed if the expiration is within `min_seconds_remaining`. Default `false`

* `revoke` - If set to `true`, the certificate will be revoked on resource destruction.

## Attributes Reference

Expand All @@ -77,3 +79,5 @@ In addition to the fields above, the following attributes are exported:
* `serial_number` - The serial number

* `expiration` - The expiration date of the certificate in unix epoch format

* `revoke` - Boolean value denoting whether the certificate will be revoked on resource destruction.

0 comments on commit 0a9e825

Please sign in to comment.