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 name constraints params for PKI certificate resources. #2396

Merged
merged 3 commits into from
Jan 28, 2025
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
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
Loading