Skip to content

Commit

Permalink
implement renewal of pki certs with min_seconds_remaining argument
Browse files Browse the repository at this point in the history
  • Loading branch information
joemiller committed May 6, 2019
1 parent b5d6c0c commit 524a225
Show file tree
Hide file tree
Showing 7 changed files with 2,037 additions and 12 deletions.
66 changes: 62 additions & 4 deletions vault/resource_pki_secret_backend_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log"
"strings"
"time"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
Expand All @@ -12,10 +13,11 @@ import (

func pkiSecretBackendCertResource() *schema.Resource {
return &schema.Resource{
Create: pkiSecretBackendCertCreate,
Read: pkiSecretBackendCertRead,
Update: pkiSecretBackendCertUpdate,
Delete: pkiSecretBackendCertDelete,
Create: pkiSecretBackendCertCreate,
Read: pkiSecretBackendCertRead,
Update: pkiSecretBackendCertUpdate,
Delete: pkiSecretBackendCertDelete,
CustomizeDiff: pkiSecretBackendCertDiff,

Schema: map[string]*schema.Schema{
"backend": {
Expand Down Expand Up @@ -91,6 +93,18 @@ func pkiSecretBackendCertResource() *schema.Resource {
Description: "Flag to exclude CN from SANs.",
ForceNew: true,
},
"auto_renew": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "If enabled, a new certificate will be generated if the expiration is within min_seconds_remaining",
},
"min_seconds_remaining": {
Type: schema.TypeInt,
Optional: true,
Default: 604800,
Description: "Generate a new certificate when the expiration is within this number of seconds",
},
"certificate": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -122,6 +136,11 @@ func pkiSecretBackendCertResource() *schema.Resource {
Computed: true,
Description: "The serial number.",
},
"expiration": {
Type: schema.TypeInt,
Computed: true,
Description: "The certificate expiration.",
},
},
}
}
Expand Down Expand Up @@ -192,16 +211,55 @@ func pkiSecretBackendCertCreate(d *schema.ResourceData, meta interface{}) error
d.Set("private_key", resp.Data["private_key"])
d.Set("private_key_type", resp.Data["private_key_type"])
d.Set("serial_number", resp.Data["serial_number"])
d.Set("expiration", resp.Data["expiration"])

d.SetId(fmt.Sprintf("%s/%s/%s", backend, name, commonName))
return pkiSecretBackendCertRead(d, meta)
}

func pkiSecretBackendCertDiff(d *schema.ResourceDiff, meta interface{}) error {
if d.Id() == "" {
return nil
}

if !d.Get("auto_renew").(bool) {
return nil
}

expiration := d.Get("expiration").(int)
expireTime := time.Unix(int64(expiration), 0)

minSeconds := 0
if v, ok := d.GetOk("min_seconds_remaining"); ok {
minSeconds = v.(int)
}

renewTime := expireTime.Add(-time.Duration(minSeconds) * time.Second)
if time.Now().After(renewTime) {
log.Printf("[DEBUG] certificate %q is due for renewal, expires %s, renewal time %s", d.Id(), expireTime, renewTime)
if err := d.SetNewComputed("certificate"); err != nil {
return err
}
if err := d.SetNewComputed("private_key"); err != nil {
return err
}
return nil
}

log.Printf("[DEBUG] certificate %q is not due for renewal, expires %s, renewal time %s", d.Id(), expireTime, renewTime)
return nil
}

func pkiSecretBackendCertRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

func pkiSecretBackendCertUpdate(d *schema.ResourceData, m interface{}) error {
// If the certificate or private_key have been marked as changed by our custom diff function we need
// to create a new certificate and key
if d.HasChange("certificate") || d.HasChange("private_key") {
return pkiSecretBackendCertCreate(d, m)
}
return nil
}

Expand Down
125 changes: 125 additions & 0 deletions vault/resource_pki_secret_backend_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"testing"
"time"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
Expand Down Expand Up @@ -132,5 +133,129 @@ resource "vault_pki_secret_backend_cert" "test" {
name = "${vault_pki_secret_backend_role.test.name}"
common_name = "cert.test.my.domain"
ttl = "720h"
min_seconds_remaining = 60
}`, rootPath, intermediatePath)
}

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

resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testPkiSecretBackendCertDestroy,
Steps: []resource.TestStep{
{
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"),
),
},
{
Config: testPkiSecretBackendCertConfig_renew(rootPath),
PlanOnly: true,
},
{
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"),
),
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"),
),
},
},
})
}

func testPkiSecretBackendCertConfig_renew(rootPath string) string {
return fmt.Sprintf(`
resource "vault_pki_secret_backend" "test-root" {
path = "%s"
description = "test root"
default_lease_ttl_seconds = "8640000"
max_lease_ttl_seconds = "8640000"
}
resource "vault_pki_secret_backend_root_cert" "test" {
depends_on = [ "vault_pki_secret_backend.test-root" ]
backend = "${vault_pki_secret_backend.test-root.path}"
type = "internal"
common_name = "my.domain"
ttl = "86400"
format = "pem"
private_key_format = "der"
key_type = "rsa"
key_bits = 4096
ou = "test"
organization = "test"
country = "test"
locality = "test"
province = "test"
}
resource "vault_pki_secret_backend_role" "test" {
depends_on = [ "vault_pki_secret_backend_root_cert.test" ]
backend = "${vault_pki_secret_backend.test-root.path}"
name = "test"
allowed_domains = ["test.my.domain"]
allow_subdomains = true
max_ttl = "3600"
key_usage = ["DigitalSignature", "KeyAgreement", "KeyEncipherment"]
}
resource "vault_pki_secret_backend_cert" "test" {
depends_on = [ "vault_pki_secret_backend_role.test" ]
backend = "${vault_pki_secret_backend.test-root.path}"
name = "${vault_pki_secret_backend_role.test.name}"
common_name = "cert.test.my.domain"
ttl = "1h"
auto_renew = true
min_seconds_remaining = "3595"
}`, rootPath)
}

func testPkiSecretBackendCertWaitUntilRenewal(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}

expiration, err := strconv.Atoi(rs.Primary.Attributes["expiration"])
if err != nil {
return fmt.Errorf("Invalid expiration value: %s", err)
}

minSecondsRemain, err := strconv.Atoi(rs.Primary.Attributes["min_seconds_remaining"])
if err != nil {
return fmt.Errorf("Invalid min_seconds_remaining value: %s", err)
}

secondsUntilRenewal := (expiration - (int(time.Now().Unix()) + minSecondsRemain))
time.Sleep(time.Duration(secondsUntilRenewal+1) * time.Second)

return nil
}
}
63 changes: 59 additions & 4 deletions vault/resource_pki_secret_backend_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log"
"strings"
"time"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
Expand All @@ -12,10 +13,11 @@ import (

func pkiSecretBackendSignResource() *schema.Resource {
return &schema.Resource{
Create: pkiSecretBackendSignCreate,
Read: pkiSecretBackendSignRead,
Update: pkiSecretBackendSignUpdate,
Delete: pkiSecretBackendSignDelete,
Create: pkiSecretBackendSignCreate,
Read: pkiSecretBackendSignRead,
Update: pkiSecretBackendSignUpdate,
Delete: pkiSecretBackendSignDelete,
CustomizeDiff: pkiSecretBackendSignDiff,

Schema: map[string]*schema.Schema{
"backend": {
Expand Down Expand Up @@ -98,6 +100,18 @@ func pkiSecretBackendSignResource() *schema.Resource {
Description: "Flag to exclude CN from SANs.",
ForceNew: true,
},
"auto_renew": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "If enabled, a new certificate will be generated if the expiration is within min_seconds_remaining",
},
"min_seconds_remaining": {
Type: schema.TypeInt,
Optional: true,
Default: 604800,
Description: "Generate a new certificate when the expiration is within this number of seconds",
},
"certificate": {
Type: schema.TypeString,
Computed: true,
Expand All @@ -118,6 +132,11 @@ func pkiSecretBackendSignResource() *schema.Resource {
Computed: true,
Description: "The serial.",
},
"expiration": {
Type: schema.TypeInt,
Computed: true,
Description: "The certificate expiration.",
},
},
}
}
Expand Down Expand Up @@ -194,16 +213,52 @@ func pkiSecretBackendSignCreate(d *schema.ResourceData, meta interface{}) error
d.Set("issuing_ca", resp.Data["issuing_ca"])
d.Set("ca_chain", resp.Data["ca_chain"])
d.Set("serial", resp.Data["serial"])
d.Set("expiration", resp.Data["expiration"])

d.SetId(fmt.Sprintf("%s/%s/%s", backend, name, commonName))
return pkiSecretBackendSignRead(d, meta)
}

func pkiSecretBackendSignDiff(d *schema.ResourceDiff, meta interface{}) error {
if d.Id() == "" {
return nil
}

if !d.Get("auto_renew").(bool) {
return nil
}

expiration := d.Get("expiration").(int)
expireTime := time.Unix(int64(expiration), 0)

minSeconds := 0
if v, ok := d.GetOk("min_seconds_remaining"); ok {
minSeconds = v.(int)
}

renewTime := expireTime.Add(-time.Duration(minSeconds) * time.Second)
if time.Now().After(renewTime) {
log.Printf("[DEBUG] certificate %q is due for renewal, expires %s, renewal time %s", d.Id(), expireTime, renewTime)
if err := d.SetNewComputed("certificate"); err != nil {
return err
}
return nil
}

log.Printf("[DEBUG] certificate %q is not due for renewal, expires %s, renewal time %s", d.Id(), expireTime, renewTime)
return nil
}

func pkiSecretBackendSignRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

func pkiSecretBackendSignUpdate(d *schema.ResourceData, m interface{}) error {
// If the certificate has been marked as changed by our custom diff function we need
// to create a new certificate
if d.HasChange("certificate") {
return pkiSecretBackendSignCreate(d, m)
}
return nil
}

Expand Down
Loading

0 comments on commit 524a225

Please sign in to comment.