Skip to content

Commit

Permalink
Add name constraints params for PKI certificate resources. (#2396)
Browse files Browse the repository at this point in the history
Add name constraints params for PKI certificate resources.

Add the new parameters used to create the name constaints extension:
  * excluded_dns_domains
  * permitted_ip_ranges
  * excluded_ip_ranges
  * permitted_email_addresses
  * excluded_email_addresses
  * permitted_uri_domains
  * excluded_uri_domains

To resources:
  * vault_pki_secret_backend_root_cert
  * vault_pki_secret_backend_root_sign_intermediate
  • Loading branch information
victorr authored Jan 28, 2025
1 parent 6af08b8 commit baf4ae8
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 19 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
## Unreleased

FEATURES:

* Update `vault_pki_secret_backend_root_cert` and `vault_pki_secret_backend_root_sign_intermediate` to support the new fields for the name constraints extension. Requires Vault 1.19+ ([#2396](https://github.com/hashicorp/terraform-provider-vault/pull/2396)).

BUGS:

* Do not panic on Vault PKI roles without the cn_validations field: ([#2398](https://github.com/hashicorp/terraform-provider-vault/pull/2398))


## 4.6.0 (Jan 15, 2025)

FEATURES:
Expand All @@ -15,7 +18,6 @@ FEATURES:
* Update `vault_pki_secret_backend_role` to support the `cn_validations` role field ([#1820](https://github.com/hashicorp/terraform-provider-vault/pull/1820)).
* Add new resource `vault_pki_secret_backend_acme_eab` to manage PKI ACME external account binding tokens. Requires Vault 1.14+. ([#2367](https://github.com/hashicorp/terraform-provider-vault/pull/2367))
* Add new data source and resource `vault_pki_secret_backend_config_cmpv2`. Requires Vault 1.18+. *Available only for Vault Enterprise* ([#2330](https://github.com/hashicorp/terraform-provider-vault/pull/2330))

IMPROVEMENTS:

* Support the event `subscribe` policy capability for `vault_policy_document` data source ([#2293](https://github.com/hashicorp/terraform-provider-vault/pull/2293))
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/coreos/pkg v0.0.0-20230601102743-20bbbf26f4d8
github.com/denisenkom/go-mssqldb v0.12.3
github.com/go-sql-driver/mysql v1.7.1
github.com/go-test/deep v1.1.0
github.com/google/uuid v1.6.0
github.com/gosimple/slug v1.13.1
github.com/hashicorp/consul/api v1.27.0
Expand Down
7 changes: 7 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,13 @@ const (
FieldMaxPathLength = "max_path_length"
FieldExcludeCNFromSans = "exclude_cn_from_sans"
FieldPermittedDNSDomains = "permitted_dns_domains"
FieldExcludedDNSDomains = "excluded_dns_domains"
FieldPermittedIPRanges = "permitted_ip_ranges"
FieldExcludedIPRanges = "excluded_ip_ranges"
FieldPermittedEmailAddresses = "permitted_email_addresses"
FieldExcludedEmailAddresses = "excluded_email_addresses"
FieldPermittedURIDomains = "permitted_uri_domains"
FieldExcludedURIDomains = "excluded_uri_domains"
FieldIssuerName = "issuer_name"
FieldUserIds = "user_ids"
FieldIssuerID = "issuer_id"
Expand Down
77 changes: 77 additions & 0 deletions vault/resource_pki_secret_backend_root_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,69 @@ func pkiSecretBackendRootCertResource() *schema.Resource {
Type: schema.TypeString,
},
},
consts.FieldExcludedDNSDomains: {
Type: schema.TypeList,
Optional: true,
Description: "List of domains for which certificates are not allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldPermittedIPRanges: {
Type: schema.TypeList,
Optional: true,
Description: "List of IP ranges for which certificates are allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldExcludedIPRanges: {
Type: schema.TypeList,
Optional: true,
Description: "List of IP ranges for which certificates are not allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldPermittedEmailAddresses: {
Type: schema.TypeList,
Optional: true,
Description: "List of email addresses for which certificates are allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldExcludedEmailAddresses: {
Type: schema.TypeList,
Optional: true,
Description: "List of email addresses for which certificates are not allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldPermittedURIDomains: {
Type: schema.TypeList,
Optional: true,
Description: "List of URI domains for which certificates are allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldExcludedURIDomains: {
Type: schema.TypeList,
Optional: true,
Description: "List of URI domains for which certificates are not allowed to be issued.",
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldOu: {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -393,6 +456,20 @@ func pkiSecretBackendRootCertCreate(_ context.Context, d *schema.ResourceData, m
}
}

// Whether name constraints fields (other than permitted_dns_domains), are supproted,
// See VAULT-32141.
isNameConstraintsExtensionSupported := provider.IsAPISupported(meta, provider.VaultVersion119)
if isNameConstraintsExtensionSupported {
rootCertStringArrayFields = append(rootCertStringArrayFields,
consts.FieldExcludedDNSDomains,
consts.FieldPermittedIPRanges,
consts.FieldExcludedIPRanges,
consts.FieldPermittedEmailAddresses,
consts.FieldExcludedEmailAddresses,
consts.FieldPermittedURIDomains,
consts.FieldExcludedURIDomains,
)
}
data := map[string]interface{}{}
rawConfig := d.GetRawConfig()
for _, k := range rootCertAPIFields {
Expand Down
175 changes: 167 additions & 8 deletions vault/resource_pki_secret_backend_root_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
package vault

import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/go-test/deep"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"reflect"
"strconv"
"testing"
Expand All @@ -19,11 +24,35 @@ import (

func TestPkiSecretBackendRootCertificate_basic(t *testing.T) {
path := "pki-" + strconv.Itoa(acctest.RandInt())

config := testPkiSecretBackendRootCertificateConfig_basic(path)
resourceName := "vault_pki_secret_backend_root_cert.test"
checks := []resource.TestCheckFunc{
resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, path),
resource.TestCheckResourceAttr(resourceName, consts.FieldType, "internal"),
resource.TestCheckResourceAttr(resourceName, consts.FieldCommonName, "test Root CA"),
resource.TestCheckResourceAttr(resourceName, consts.FieldTTL, "86400"),
resource.TestCheckResourceAttr(resourceName, consts.FieldFormat, "pem"),
resource.TestCheckResourceAttr(resourceName, consts.FieldPrivateKeyFormat, "der"),
resource.TestCheckResourceAttr(resourceName, consts.FieldKeyType, "rsa"),
resource.TestCheckResourceAttr(resourceName, consts.FieldKeyBits, "4096"),
resource.TestCheckResourceAttr(resourceName, consts.FieldOu, "test"),
resource.TestCheckResourceAttr(resourceName, consts.FieldOrganization, "test"),
resource.TestCheckResourceAttr(resourceName, consts.FieldCountry, "test"),
resource.TestCheckResourceAttr(resourceName, consts.FieldLocality, "test"),
resource.TestCheckResourceAttr(resourceName, consts.FieldProvince, "test"),
resource.TestCheckResourceAttrSet(resourceName, consts.FieldSerialNumber),
assertCertificateAttributes(resourceName),
}

store := &testPKICertStore{}
testPkiSecretBackendRootCertificate(t, path, config, resourceName, checks, nil)
}

// TestPkiSecretBackendRootCertificate_name_constraints is just like TestPkiSecretBackendRootCertificate_basic,
// but it uses the permitted_/excluded_ parameters for the name constraints extension.
func TestPkiSecretBackendRootCertificate_name_constraints(t *testing.T) {
path := "pki-" + strconv.Itoa(acctest.RandInt())
config := testPkiSecretBackendRootCertificateConfig_name_constraints(path)
resourceName := "vault_pki_secret_backend_root_cert.test"
checks := []resource.TestCheckFunc{
resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, path),
resource.TestCheckResourceAttr(resourceName, consts.FieldType, "internal"),
Expand All @@ -39,16 +68,107 @@ func TestPkiSecretBackendRootCertificate_basic(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, consts.FieldLocality, "test"),
resource.TestCheckResourceAttr(resourceName, consts.FieldProvince, "test"),
resource.TestCheckResourceAttrSet(resourceName, consts.FieldSerialNumber),
assertCertificateAttributes(resourceName),

resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedDNSDomains+".0", "example.com"),
resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedDNSDomains+".1", ".example.com"),
resource.TestCheckResourceAttr(resourceName, consts.FieldExcludedDNSDomains+".0", "bad.example.com"),

resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedIPRanges+".0", "192.0.2.0/24"),
resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedIPRanges+".1", "2001:db8::/32"),
resource.TestCheckResourceAttr(resourceName, consts.FieldExcludedIPRanges+".0", "192.0.3.0/24"),
resource.TestCheckResourceAttr(resourceName, consts.FieldExcludedIPRanges+".1", "2002::/16"),

resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedEmailAddresses+".0", "[email protected]"),
resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedEmailAddresses+".1", "[email protected]"),
resource.TestCheckResourceAttr(resourceName, consts.FieldExcludedEmailAddresses+".0", "[email protected]"),

resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedURIDomains+".0", "https://example.com"),
resource.TestCheckResourceAttr(resourceName, consts.FieldPermittedURIDomains+".1", "https://www.example.com"),
resource.TestCheckResourceAttr(resourceName, consts.FieldExcludedURIDomains+".0", "ftp://example.com"),
func(s *terraform.State) error {
return checkCertificateNameConstraints(resourceName, s)
},
}

testPkiSecretBackendRootCertificate(t, path, config, resourceName, checks, func(t *testing.T) {
SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion119)
})
}

func checkCertificateNameConstraints(resourceName string, s *terraform.State) error {
var cert *x509.Certificate
{
rs, err := testutil.GetResourceFromRootModule(s, resourceName)
if err != nil {
return err
}

attrs := rs.Primary.Attributes

// "pem" for resource_pki_secret_backend_root_cert root certs,
// "pem_bundle" for resource_pki_secret_backend_root_sign_intermediate.
if !(attrs["format"] == "pem" || attrs["format"] == "pem_bundle") {
return errors.New("test only valid for resources configured with the 'pem' or 'pem_bundle' format")
}

certPEM := attrs["certificate"]
if certPEM == "" {
return fmt.Errorf("certificate from state cannot be empty")
}

b, _ := pem.Decode([]byte(certPEM))
if err != nil {
return err
}

cert, err = x509.ParseCertificate(b.Bytes)
if err != nil {
return err
}
}
var failedChecks []error
check := func(fieldName string, actual []string, expected ...string) {
diff := deep.Equal(expected, actual)
if len(diff) > 0 {
failedChecks = append(failedChecks, fmt.Errorf("error in field %q: %v", fieldName, diff))
}
}

check(consts.FieldPermittedDNSDomains, cert.PermittedDNSDomains, "example.com", ".example.com")
check(consts.FieldExcludedDNSDomains, cert.ExcludedDNSDomains, "bad.example.com")
var ips []string
for _, ip := range cert.PermittedIPRanges {
ips = append(ips, ip.String())
}
check(consts.FieldPermittedIPRanges, ips, "192.0.2.0/24", "2001:db8::/32")
ips = nil
for _, ip := range cert.ExcludedIPRanges {
ips = append(ips, ip.String())
}
check(consts.FieldExcludedIPRanges, ips, "192.0.3.0/24", "2002::/16")
check(consts.FieldPermittedEmailAddresses, cert.PermittedEmailAddresses, "[email protected]", "[email protected]")
check(consts.FieldExcludedEmailAddresses, cert.ExcludedEmailAddresses, "[email protected]")
check(consts.FieldPermittedURIDomains, cert.PermittedURIDomains, "https://example.com", "https://www.example.com")
check(consts.FieldExcludedURIDomains, cert.ExcludedURIDomains, "ftp://example.com")

return errors.Join(failedChecks...)
}

func testPkiSecretBackendRootCertificate(t *testing.T, path string, config string, resourceName string, checks []resource.TestCheckFunc, preCheck func(t *testing.T)) {
store := &testPKICertStore{}

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
PreCheck: func() { testutil.TestAccPreCheck(t) },
CheckDestroy: testCheckMountDestroyed("vault_mount", consts.MountTypePKI, consts.FieldPath),
PreCheck: func() {
testutil.TestAccPreCheck(t)
if preCheck != nil {
preCheck(t)
}
},
CheckDestroy: testCheckMountDestroyed("vault_mount", consts.MountTypePKI, consts.FieldPath),
Steps: []resource.TestStep{
{
Config: testPkiSecretBackendRootCertificateConfig_basic(path),
Config: config,
Check: resource.ComposeTestCheckFunc(
append(checks,
testCapturePKICert(resourceName, store),
Expand All @@ -67,7 +187,7 @@ func TestPkiSecretBackendRootCertificate_basic(t *testing.T) {
t.Fatal(err)
}
},
Config: testPkiSecretBackendRootCertificateConfig_basic(path),
Config: config,
Check: resource.ComposeTestCheckFunc(
append(checks,
testPKICertReIssued(resourceName, store),
Expand Down Expand Up @@ -100,7 +220,7 @@ func TestPkiSecretBackendRootCertificate_basic(t *testing.T) {
t.Fatalf("empty response for write on path %s", genPath)
}
},
Config: testPkiSecretBackendRootCertificateConfig_basic(path),
Config: config,
Check: resource.ComposeTestCheckFunc(
append(checks,
testPKICertReIssued(resourceName, store),
Expand Down Expand Up @@ -271,6 +391,45 @@ resource "vault_pki_secret_backend_root_cert" "test" {
return config
}

func testPkiSecretBackendRootCertificateConfig_name_constraints(path string) string {
config := fmt.Sprintf(`
resource "vault_mount" "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" {
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"
permitted_dns_domains = ["example.com",".example.com"]
excluded_dns_domains = ["bad.example.com"]
permitted_ip_ranges = ["192.0.2.0/24", "2001:db8::/32"]
excluded_ip_ranges = ["192.0.3.0/24", "2002::/16"]
permitted_email_addresses = ["[email protected]","[email protected]"]
excluded_email_addresses = ["[email protected]"]
permitted_uri_domains = ["https://example.com", "https://www.example.com"]
excluded_uri_domains = ["ftp://example.com"]
}
`, path)

return config
}

func testPkiSecretBackendRootCertificateConfig_multiIssuerInternal(path, issuer, key string) string {
config := fmt.Sprintf(`
resource "vault_mount" "test" {
Expand Down
Loading

0 comments on commit baf4ae8

Please sign in to comment.