Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PKI certificate revocation support #1411

Merged
merged 4 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.