diff --git a/CHANGELOG.md b/CHANGELOG.md index 7025aa3e0..4b1eb854b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ FEATURES: * Update `vault_database_secret_backend_connection`to support `password_authentication` for PostgreSQL, allowing to encrypt password before being passed to PostgreSQL ([#2371](https://github.com/hashicorp/terraform-provider-vault/pull/2371)) * Add support for `external_id` field for the `vault_aws_auth_backend_sts_role` resource ([#2370](https://github.com/hashicorp/terraform-provider-vault/pull/2370)) +* Add support for ACME configuration with the `vault_pki_secret_backend_config_acme` resource. Requires Vault 1.14+ ([#2157](https://github.com/hashicorp/terraform-provider-vault/pull/2157)). +* 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)) ## 4.5.0 (Nov 19, 2024) @@ -52,8 +55,10 @@ FEATURES: IMPROVEMENTS: * return a useful error when delete fails for the `vault_jwt_auth_backend_role` resource: ([#2232](https://github.com/hashicorp/terraform-provider-vault/pull/2232)) +BUGS: * Remove dependency on `github.com/hashicorp/vault` package: ([#2251](https://github.com/hashicorp/terraform-provider-vault/pull/2251)) * Add missing `custom_tags` and `secret_name_template` fields to `vault_secrets_sync_azure_destination` resource ([#2247](https://github.com/hashicorp/terraform-provider-vault/pull/2247)) +* Fix handling of 0 value within field `max_path_length` in `vault_pki_secret_backend_root_cert` and `vault_pki_secret_backend_root_sign_intermediate` resources ([#2253](https://github.com/hashicorp/terraform-provider-vault/pull/2253)) ## 4.2.0 (Mar 27, 2024) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 1fab9e141..7d85e88e7 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -438,7 +438,17 @@ const ( FieldMaxRetries = "max_retries" FieldSessionTags = "session_tags" FieldSelfManagedPassword = "self_managed_password" - + FieldAllowedIssuers = "allowed_issuers" + FieldAllowedRoles = "allowed_roles" + FieldAllowRoleExtKeyUsage = "allow_role_ext_key_usage" + FieldDefaultDirectoryPolicy = "default_directory_policy" + FieldDnsResolver = "dns_resolver" + FieldEabPolicy = "eab_policy" + FieldCnValidations = "cn_validations" + FieldsCreatedOn = "created_on" + FieldEabKey = "key" + FieldAcmeDirectory = "acme_directory" + FieldEabId = "eab_id" /* common environment variables */ diff --git a/vault/provider.go b/vault/provider.go index 1bb2c9962..1a1ced330 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -583,6 +583,10 @@ var ( Resource: UpdateSchemaResource(pkiSecretBackendCrlConfigResource()), PathInventory: []string{"/pki/config/crl"}, }, + "vault_pki_secret_backend_config_acme": { + Resource: UpdateSchemaResource(pkiSecretBackendConfigACMEResource()), + PathInventory: []string{"/pki/config/acme"}, + }, "vault_pki_secret_backend_config_ca": { Resource: UpdateSchemaResource(pkiSecretBackendConfigCAResource()), PathInventory: []string{"/pki/config/ca"}, @@ -639,6 +643,10 @@ var ( Resource: UpdateSchemaResource(pkiSecretBackendConfigIssuers()), PathInventory: []string{"/pki/config/issuers"}, }, + "vault_pki_secret_backend_acme_eab": { + Resource: UpdateSchemaResource(pkiSecretBackendAcmeEabResource()), + PathInventory: []string{"/pki/acme/new-eab"}, + }, "vault_quota_lease_count": { Resource: UpdateSchemaResource(quotaLeaseCountResource()), PathInventory: []string{"/sys/quotas/lease-count/{name}"}, diff --git a/vault/resource_pki_secret_backend_acme_eab.go b/vault/resource_pki_secret_backend_acme_eab.go new file mode 100644 index 000000000..3a61b97e1 --- /dev/null +++ b/vault/resource_pki_secret_backend_acme_eab.go @@ -0,0 +1,184 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +var requiredEabDataKeys = []string{"id", "key_type", "acme_directory", "key"} + +/* +ACME EAB (External Account Binding) tokens, that restrict ACME accounts from +being created anonymously +*/ +func pkiSecretBackendAcmeEabResource() *schema.Resource { + return &schema.Resource{ + Description: "Manages Vault PKI ACME EAB bindings", + CreateContext: provider.MountCreateContextWrapper(pkiSecretBackendCreateAcmeEab, provider.VaultVersion114), + ReadContext: pkiSecretBackendReadAcmeEab, + DeleteContext: pkiSecretBackendDeleteAcmeEab, + // There is no UpdateContext or ImportContext for EAB tokens as there is no read API available + + Schema: map[string]*schema.Schema{ + consts.FieldBackend: { + Type: schema.TypeString, + Required: true, + Description: "The PKI secret backend the resource belongs to", + ForceNew: true, + }, + consts.FieldIssuer: { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the issuer reference to use for directory path", + ForceNew: true, // If this is changed/set a new directory path will need to be calculated + }, + consts.FieldRole: { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the role to use for directory path", + ForceNew: true, // If this is changed/set a new directory path will need to be calculated + }, + // Response fields for EAB + consts.FieldEabId: { + Type: schema.TypeString, + Computed: true, + Description: "The identifier of a specific ACME EAB token", + }, + consts.FieldKeyType: { + Type: schema.TypeString, + Computed: true, + Description: "The key type of the EAB key", + }, + consts.FieldAcmeDirectory: { + Type: schema.TypeString, + Computed: true, + Description: "The ACME directory to which the key belongs", + }, + consts.FieldEabKey: { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The ACME EAB token", + }, + consts.FieldsCreatedOn: { + Type: schema.TypeString, + Computed: true, + Description: "An RFC3339 formatted date time when the EAB token was created", + }, + }, + } +} + +func pkiSecretBackendCreateAcmeEab(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return pkiSecretBackendReadAcmeEab(ctx, d, meta) +} + +func pkiSecretBackendReadAcmeEab(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + var issuer, role string + // These are optional inputs that will determine our path to use to create the EAB token + if issuerVal, ok := d.GetOk(consts.FieldIssuer); ok { + issuer = issuerVal.(string) + } + if roleVal, ok := d.GetOk(consts.FieldRole); ok { + role = roleVal.(string) + } + + backend := d.Get(consts.FieldBackend).(string) + path := pkiSecretBackendComputeAcmeDirectoryPath(backend, issuer, role) + + log.Printf("[DEBUG] Creating new ACME EAB token on PKI secret backend %q at path: %q", backend, path) + secret, err := client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) + if err != nil || secret == nil { + return diag.Errorf("error creating new ACME EAB on PKI secret backend %q at path %q: %s", backend, path, err) + } + + for _, reqKey := range requiredEabDataKeys { + if rawVal, ok := secret.Data[reqKey]; !ok { + return diag.Errorf("eab response missing required field: %q", reqKey) + } else { + if val, ok := rawVal.(string); !ok { + return diag.Errorf("eab response field: %q was not a string", reqKey) + } else if len(val) == 0 { + return diag.Errorf("eab response has empty required field: %q", reqKey) + } + } + } + + log.Printf("[DEBUG] Successfully created new ACME EAB token on backend %q at path %q", backend, path) + eabId := secret.Data["id"].(string) + fieldsToSet := map[string]string{ + consts.FieldEabId: eabId, + consts.FieldKeyType: secret.Data["key_type"].(string), + consts.FieldAcmeDirectory: secret.Data["acme_directory"].(string), + consts.FieldEabKey: secret.Data["key"].(string), + consts.FieldsCreatedOn: secret.Data["created_on"].(string), + } + + d.SetId(fmt.Sprintf("acme-eab:%s:%s", path, eabId)) + for key, val := range fieldsToSet { + if err := d.Set(key, val); err != nil { + return diag.FromErr(fmt.Errorf("failed setting field %q: %w", key, err)) + } + } + + return nil +} + +func pkiSecretBackendDeleteAcmeEab(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(data, meta) + if e != nil { + return diag.FromErr(e) + } + + backend := data.Get(consts.FieldBackend).(string) + eabId := data.Get(consts.FieldEabId).(string) + acmeDirectory := data.Get(consts.FieldAcmeDirectory).(string) + + log.Printf("[DEBUG] Deleting EAB token %q under path %q if unused", eabId, acmeDirectory) + path := pkiSecretBackendComputeAcmeEabPath(backend, eabId) + resp, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.FromErr(err) + } + _ = resp + log.Printf("[DEBUG] Deleted EAB token %q if it was unused", eabId) + return nil +} + +func pkiSecretBackendComputeAcmeEabPath(backend, eabId string) string { + trimmedBackend := strings.TrimPrefix(strings.TrimSpace(backend), "/") + trimmedEabId := strings.TrimSpace(eabId) + return fmt.Sprintf("%s/eab/%s", trimmedBackend, trimmedEabId) +} + +func pkiSecretBackendComputeAcmeDirectoryPath(backend string, issuer string, role string) string { + trimmedBackend := strings.TrimPrefix(strings.TrimSpace(backend), "/") + trimmedIssuer := strings.TrimSpace(issuer) + trimmedRole := strings.TrimSpace(role) + + switch { + case len(trimmedIssuer) > 0 && len(trimmedRole) > 0: + return fmt.Sprintf("%s/issuer/%s/roles/%s/acme/new-eab", trimmedBackend, trimmedIssuer, trimmedRole) + case len(trimmedIssuer) > 0: + return fmt.Sprintf("%s/issuer/%s/acme/new-eab", trimmedBackend, trimmedIssuer) + case len(trimmedRole) > 0: + return fmt.Sprintf("%s/roles/%s/acme/new-eab", trimmedBackend, trimmedRole) + default: + return fmt.Sprintf("%s/acme/new-eab", trimmedBackend) + } +} diff --git a/vault/resource_pki_secret_backend_acme_eab_test.go b/vault/resource_pki_secret_backend_acme_eab_test.go new file mode 100644 index 000000000..0b6f7df9c --- /dev/null +++ b/vault/resource_pki_secret_backend_acme_eab_test.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestAccPKISecretBackendAcmeEab(t *testing.T) { + t.Parallel() + + backend := acctest.RandomWithPrefix("tf-test-pki") + resourceType := "vault_pki_secret_backend_acme_eab" + resourceBackend := resourceType + ".test" + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion114) + }, + CheckDestroy: testCheckMountDestroyed(resourceType, consts.MountTypePKI, consts.FieldBackend), + Steps: []resource.TestStep{ + { + // Test EAB creation without a role and issuer + Config: testAccPKISecretBackendEAB(backend, "", ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, ""), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, ""), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "acme/directory"), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn), + ), + }, + { + // Test EAB creation with role and issuer + Config: testAccPKISecretBackendEAB(backend, "test-issuer", "test-role"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, "test-issuer"), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, "test-role"), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "issuer/test-issuer/roles/test-role/acme/directory"), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn), + ), + }, + { + // Test EAB creation with just an issuer + Config: testAccPKISecretBackendEAB(backend, "test-issuer", ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, "test-issuer"), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, ""), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "issuer/test-issuer/acme/directory"), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn), + ), + }, + { + // Test EAB creation with just a role + Config: testAccPKISecretBackendEAB(backend, "", "test-role"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, ""), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, "test-role"), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"), + resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "roles/test-role/acme/directory"), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey), + resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn), + ), + }, + { + // Test EAB deletion + Config: testAccPKISecretBackendEAB(backend, "", ""), + Destroy: true, + }, + }, + }) + +} + +func testAccPKISecretBackendEAB(path, issuer, role string) string { + return fmt.Sprintf(` +resource "vault_mount" "test" { + path = "%s" + type = "pki" + description = "PKI secret engine mount" +} + +resource "vault_pki_secret_backend_role" "test" { + backend = vault_mount.test.path + name = "test-role" +} + +resource "vault_pki_secret_backend_root_cert" "test" { + backend = vault_mount.test.path + type = "internal" + common_name = "test" + ttl = "86400" + issuer_name = "test-issuer" +} + +resource "vault_pki_secret_backend_acme_eab" "test" { + backend = vault_mount.test.path + issuer = "%s" + role = "%s" +} +`, path, issuer, role) +} + +func Test_pkiSecretBackendComputeAcmeDirectoryPath(t *testing.T) { + type args struct { + backend string + issuer string + role string + } + tests := []struct { + name string + args args + want string + }{ + {"issuer-with-role", args{"pki", "my-issuer", "my-role"}, "pki/issuer/my-issuer/roles/my-role/acme/new-eab"}, + {"only-issuer", args{"pki", "my-issuer", ""}, "pki/issuer/my-issuer/acme/new-eab"}, + {"only-role", args{"pki", "", "my-role"}, "pki/roles/my-role/acme/new-eab"}, + {"only-backend", args{"pki", "", ""}, "pki/acme/new-eab"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pkiSecretBackendComputeAcmeDirectoryPath(tt.args.backend, tt.args.issuer, tt.args.role); got != tt.want { + t.Errorf("pkiSecretBackendComputeAcmeDirectoryPath() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/vault/resource_pki_secret_backend_config_acme.go b/vault/resource_pki_secret_backend_config_acme.go new file mode 100644 index 000000000..a72e11467 --- /dev/null +++ b/vault/resource_pki_secret_backend_config_acme.go @@ -0,0 +1,209 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +var ( + pkiSecretBackendFromConfigACMERegex = regexp.MustCompile("^(.+)/config/acme$") + pkiAcmeFields = []string{ + consts.FieldEnabled, + consts.FieldDefaultDirectoryPolicy, + consts.FieldAllowedRoles, + consts.FieldAllowRoleExtKeyUsage, + consts.FieldAllowedIssuers, + consts.FieldEabPolicy, + consts.FieldDnsResolver, + } +) + +func pkiSecretBackendConfigACMEResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(pkiSecretBackendConfigACMECreate, provider.VaultVersion113), + ReadContext: provider.ReadContextWrapper(pkiSecretBackendConfigACMERead), + UpdateContext: pkiSecretBackendConfigACMEUpdate, + DeleteContext: pkiSecretBackendConfigACMEDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldBackend: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Full path where PKI backend is mounted.", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + consts.FieldEnabled: { + Type: schema.TypeBool, + Required: true, + Description: "Specifies whether ACME is enabled.", + }, + consts.FieldDefaultDirectoryPolicy: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies the policy to be used for non-role-qualified ACME requests.", + }, + consts.FieldAllowedRoles: { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: "Specifies which roles are allowed for use with ACME.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + consts.FieldAllowRoleExtKeyUsage: { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether the ExtKeyUsage field from a role is used.", + }, + consts.FieldAllowedIssuers: { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: "Specifies which issuers are allowed for use with ACME.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + consts.FieldEabPolicy: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies the policy to use for external account binding behaviour.", + }, + consts.FieldDnsResolver: { + Type: schema.TypeString, + Optional: true, + Description: "DNS resolver to use for domain resolution on this mount. " + + "Must be in the format :, with both parts mandatory.", + }, + }, + } +} + +func pkiSecretBackendConfigACMECreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + backend := d.Get(consts.FieldBackend).(string) + path := fmt.Sprintf("%s/config/acme", backend) + + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading acme config at %s, err=%s", path, err) + } + + if resp == nil { + return diag.Errorf("no acme config found at path %s", path) + } + + d.SetId(path) + + return pkiSecretBackendConfigACMEUpdate(ctx, d, meta) +} + +func pkiSecretBackendConfigACMEUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + + var patchRequired bool + data := map[string]interface{}{} + for _, k := range pkiAcmeFields { + if d.HasChange(k) { + data[k] = d.Get(k) + patchRequired = true + } + } + + if patchRequired { + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error writing data to %q, err=%s", path, err) + } + } + + return pkiSecretBackendConfigACMERead(ctx, d, meta) +} + +func pkiSecretBackendConfigACMERead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + if path == "" { + return diag.Errorf("no path set, id=%q", d.Id()) + } + + // get backend from full path + backend, err := pkiSecretBackendFromConfigACME(path) + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Reading %s from Vault", path) + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading from Vault: %s", err) + } + + if resp == nil { + return diag.Errorf("got nil response from Vault from path: %q", path) + } + + // set backend and issuerRef + if err := d.Set(consts.FieldBackend, backend); err != nil { + return diag.FromErr(err) + } + + for _, k := range pkiAcmeFields { + if err := d.Set(k, resp.Data[k]); err != nil { + return diag.Errorf("error setting state key %q for acme config, err=%s", + k, err) + } + } + + return nil +} + +func pkiSecretBackendConfigACMEDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func pkiSecretBackendFromConfigACME(path string) (string, error) { + if !pkiSecretBackendFromConfigACMERegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := pkiSecretBackendFromConfigACMERegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_pki_secret_backend_config_acme_test.go b/vault/resource_pki_secret_backend_config_acme_test.go new file mode 100644 index 000000000..b646f4ed9 --- /dev/null +++ b/vault/resource_pki_secret_backend_config_acme_test.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestPkiSecretBackendConfigACME_basic(t *testing.T) { + backend := acctest.RandomWithPrefix("pki-root") + resourceType := "vault_pki_secret_backend_config_acme" + resourceName := resourceType + ".test" + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion114) + }, + CheckDestroy: testCheckMountDestroyed(resourceType, consts.MountTypePKI, consts.FieldBackend), + Steps: []resource.TestStep{ + { + Config: testPkiSecretBackendConfigACME(backend, "sign-verbatim", "*", "*", "not-required", "", + false, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceName, consts.FieldEnabled, "false"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultDirectoryPolicy, "sign-verbatim"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedRoles+".0", "*"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedIssuers+".0", "*"), + resource.TestCheckResourceAttr(resourceName, consts.FieldEabPolicy, "not-required"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDnsResolver, ""), + ), + }, + { + Config: testPkiSecretBackendConfigACME(backend, "forbid", "test", "*", "new-account-required", + "1.1.1.1:8443", true, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceName, consts.FieldEnabled, "true"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultDirectoryPolicy, "forbid"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedRoles+".0", "test"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedIssuers+".0", "*"), + resource.TestCheckResourceAttr(resourceName, consts.FieldEabPolicy, "new-account-required"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDnsResolver, "1.1.1.1:8443"), + ), + }, + { + Config: testPkiSecretBackendConfigACME(backend, "role:test", "*", "*", "always-required", "", + true, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, backend), + resource.TestCheckResourceAttr(resourceName, consts.FieldEnabled, "true"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultDirectoryPolicy, "role:test"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedRoles+".0", "*"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowRoleExtKeyUsage, "true"), + resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedIssuers+".0", "*"), + resource.TestCheckResourceAttr(resourceName, consts.FieldEabPolicy, "always-required"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDnsResolver, ""), + ), + }, + testutil.GetImportTestStep(resourceName, false, nil), + }, + }) +} + +func testPkiSecretBackendConfigACME(path, default_directory_policy, allowed_roles, allowed_issuers, + eab_policy, dns_resolver string, enabled, allow_role_ext_key_usage bool) string { + return fmt.Sprintf(` +resource "vault_mount" "test" { + path = "%s" + type = "pki" +} + +resource "vault_pki_secret_backend_root_cert" "test" { + backend = vault_mount.test.path + type = "internal" + common_name = "test" + ttl = "86400" +} + +resource "vault_pki_secret_backend_config_cluster" "test" { + backend = vault_mount.test.path + path = "http://127.0.0.1:8200/v1/${vault_mount.test.path}" + aia_path = "http://127.0.0.1:8200/v1/${vault_mount.test.path}" +} + +resource "vault_pki_secret_backend_role" "test" { + backend = vault_pki_secret_backend_root_cert.test.backend + name = "test" +} + +resource "vault_pki_secret_backend_config_acme" "test" { + backend = vault_mount.test.path + enabled = "%t" + allowed_issuers = ["%s"] + allowed_roles = ["%s"] + allow_role_ext_key_usage = "%t" + default_directory_policy = "%s" + dns_resolver = "%s" + eab_policy = "%s" +}`, path, enabled, allowed_issuers, allowed_roles, allow_role_ext_key_usage, + default_directory_policy, dns_resolver, eab_policy) +} diff --git a/vault/resource_pki_secret_backend_role.go b/vault/resource_pki_secret_backend_role.go index 644fe2a00..a2a599403 100644 --- a/vault/resource_pki_secret_backend_role.go +++ b/vault/resource_pki_secret_backend_role.go @@ -50,6 +50,7 @@ var pkiSecretListFields = []string{ consts.FieldAllowedSerialNumbers, consts.FieldExtKeyUsage, consts.FieldExtKeyUsageOIDs, + consts.FieldCnValidations, } var pkiSecretBooleanFields = []string{ @@ -423,9 +424,15 @@ func pkiSecretBackendRoleResource() *schema.Resource { Required: false, Optional: true, Description: "Defines allowed Subject serial numbers.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + consts.FieldCnValidations: { + Type: schema.TypeList, + Required: false, + Optional: true, + Computed: true, + Description: "Specify validations to run on the Common Name field of the certificate.", + Elem: &schema.Schema{Type: schema.TypeString}, }, consts.FieldAllowedUserIds: { Type: schema.TypeList, diff --git a/vault/resource_pki_secret_backend_role_test.go b/vault/resource_pki_secret_backend_role_test.go index 9bc15c9ba..6293a786c 100644 --- a/vault/resource_pki_secret_backend_role_test.go +++ b/vault/resource_pki_secret_backend_role_test.go @@ -180,6 +180,9 @@ func TestPkiSecretBackendRole_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "not_before_duration", "45m"), resource.TestCheckResourceAttr(resourceName, "policy_identifiers.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy_identifiers.0", "1.2.3.4"), + resource.TestCheckResourceAttr(resourceName, "cn_validations.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "cn_validations.*", "email"), + resource.TestCheckTypeSetElemAttr(resourceName, "cn_validations.*", "hostname"), } resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, @@ -320,6 +323,8 @@ func TestPkiSecretBackendRole_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "policy_identifiers.0", "1.2.3.4"), resource.TestCheckResourceAttr(resourceName, "basic_constraints_valid_for_non_ca", "false"), resource.TestCheckResourceAttr(resourceName, "not_before_duration", "45m"), + resource.TestCheckResourceAttr(resourceName, "cn_validations.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "cn_validations.*", "disabled"), ), }, { @@ -391,6 +396,7 @@ resource "vault_pki_secret_backend_role" "test" { basic_constraints_valid_for_non_ca = false not_before_duration = "45m" allowed_serial_numbers = ["*"] + cn_validations = ["email", "hostname"] } `, path, name, roleTTL, maxTTL, extraConfig) } @@ -446,6 +452,7 @@ resource "vault_pki_secret_backend_role" "test" { basic_constraints_valid_for_non_ca = false not_before_duration = "45m" allowed_serial_numbers = ["*"] + cn_validations = ["disabled"] }`, path, name, policyIdentifiers) } diff --git a/vault/resource_pki_secret_backend_root_cert.go b/vault/resource_pki_secret_backend_root_cert.go index a6afc1755..87bec540d 100644 --- a/vault/resource_pki_secret_backend_root_cert.go +++ b/vault/resource_pki_secret_backend_root_cert.go @@ -394,8 +394,9 @@ func pkiSecretBackendRootCertCreate(_ context.Context, d *schema.ResourceData, m } data := map[string]interface{}{} + rawConfig := d.GetRawConfig() for _, k := range rootCertAPIFields { - if v, ok := d.GetOk(k); ok { + if v := d.Get(k); !rawConfig.GetAttr(k).IsNull() { data[k] = v } } diff --git a/vault/resource_pki_secret_backend_root_cert_test.go b/vault/resource_pki_secret_backend_root_cert_test.go index cd952f75d..2c1c7be0f 100644 --- a/vault/resource_pki_secret_backend_root_cert_test.go +++ b/vault/resource_pki_secret_backend_root_cert_test.go @@ -39,6 +39,7 @@ 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.Test(t, resource.TestCase{ @@ -263,6 +264,7 @@ resource "vault_pki_secret_backend_root_cert" "test" { country = "test" locality = "test" province = "test" + max_path_length = 0 } `, path) diff --git a/vault/resource_pki_secret_backend_root_sign_intermediate.go b/vault/resource_pki_secret_backend_root_sign_intermediate.go index 54c8a6026..f11848e59 100644 --- a/vault/resource_pki_secret_backend_root_sign_intermediate.go +++ b/vault/resource_pki_secret_backend_root_sign_intermediate.go @@ -263,8 +263,9 @@ func pkiSecretBackendRootSignIntermediateCreate(ctx context.Context, d *schema.R } data := map[string]interface{}{} + rawConfig := d.GetRawConfig() for _, k := range intermediateSignAPIFields { - if v, ok := d.GetOk(k); ok { + if v := d.Get(k); !rawConfig.GetAttr(k).IsNull() { data[k] = v } } diff --git a/vault/resource_pki_secret_backend_root_sign_intermediate_test.go b/vault/resource_pki_secret_backend_root_sign_intermediate_test.go index 18ffef5f5..d520936fe 100644 --- a/vault/resource_pki_secret_backend_root_sign_intermediate_test.go +++ b/vault/resource_pki_secret_backend_root_sign_intermediate_test.go @@ -4,6 +4,7 @@ package vault import ( + "crypto/x509" "encoding/base64" "encoding/pem" "fmt" @@ -249,6 +250,7 @@ func testCheckPKISecretRootSignIntermediate(res, path, commonName, format string resource.TestCheckResourceAttrSet(res, "serial_number"), assertPKICertificateBundle(res, format), assertPKICAChain(res), + assertCertificateAttributes(res), ) } @@ -315,6 +317,50 @@ func assertPKICAChain(res string) resource.TestCheckFunc { } } +func assertCertificateAttributes(res string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[res] + if !ok { + return fmt.Errorf("resource %q not found in the state", res) + } + format := rs.Primary.Attributes["format"] + var rawCert []byte + switch format { + case "pem", "pem_bundle": + pemCert := []byte(rs.Primary.Attributes["certificate"]) + b, _ := pem.Decode(pemCert) + if b == nil { + return fmt.Errorf("error decoding PEM certificate") + } + + rawCert = b.Bytes + case "der": + certAttr := rs.Primary.Attributes["certificate"] + var err error + rawCert, err = base64.StdEncoding.DecodeString(certAttr) + if err != nil { + return fmt.Errorf("error decoding der certificate: %w", err) + } + } + + crt, err := x509.ParseCertificate(rawCert) + if err != nil { + return fmt.Errorf("error parsing certificate: %w", err) + } + + expectedMaxPathLen, err := strconv.Atoi(rs.Primary.Attributes["max_path_length"]) + if err != nil { + return fmt.Errorf("error parsing max_path_length value as int: %w", err) + } + + if expectedMaxPathLen != crt.MaxPathLen { + return fmt.Errorf("expected MaxPathLen %d, actual %d", expectedMaxPathLen, crt.MaxPathLen) + } + + return nil + } +} + func testPkiSecretBackendRootSignIntermediateConfig_basic(rootPath, path, format string, revoke bool, issuerRef string) string { config := fmt.Sprintf(` resource "vault_mount" "test-root" { @@ -368,6 +414,7 @@ resource "vault_pki_secret_backend_root_sign_intermediate" "test" { locality = "San Francisco" province = "CA" revoke = %t + max_path_length = 0 `, rootPath, path, revoke) if format != "" { diff --git a/website/docs/r/pki_secret_backend_acme_eab.html.md b/website/docs/r/pki_secret_backend_acme_eab.html.md new file mode 100644 index 000000000..63939d159 --- /dev/null +++ b/website/docs/r/pki_secret_backend_acme_eab.html.md @@ -0,0 +1,61 @@ +--- +layout: "vault" +page_title: "Vault: vault_pki_secret_backend_acme_eab resource" +sidebar_current: "docs-vault-resource-pki-secret-backend-acme-eab" +description: |- + Creates ACME EAB tokens within the PKI Secret Backend for Vault. +--- + +# vault\_pki\_secret\_backend\_acme_eab + +Allows creating ACME EAB (External Account Binding) tokens and deleting unused ones. + +## Example Usage + +```hcl +resource "vault_mount" "test" { + path = "pki" + type = "pki" + description = "PKI secret engine mount" +} + +resource "vault_pki_secret_backend_acme_eab" "test" { + backend = vault_mount.test.path +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace of the target resource. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault/index.html#namespace). + *Available only for Vault Enterprise*. + +* `backend` - (Required) The path to the PKI secret backend to + create the EAB token within, with no leading or trailing `/`s. + +* `issuer` - (Optional) Create an EAB token that is specific to an issuer's ACME directory. + +* `role` - (Optional) Create an EAB token that is specific to a role's ACME directory. + +**NOTE**: Within Vault ACME there are different ACME directories which an EAB token is associated with; + + 1. Default directory (`pki/acme/`) - Do not specify a value for issuer nor role parameters. + 2. Issuer specific (`pki/issuer/:issuer_ref/acme/`) - Specify a value for the issuer parameter + 3. Role specific (`pki/roles/:role/acme/`) - Specify a value for the role parameter + 4. Issuer and Role specific (`pki/issuer/:issuer_ref/roles/:role/acme/`) - Specify a value for both the issuer and role parameters + +## Attributes Reference + +* `eab_id` - The identifier of a specific ACME EAB token +* `key_type` - The key type of the EAB key +* `acme_directory` - The ACME directory to which the key belongs +* `key` - The EAB token +* `created_on` - An RFC3339 formatted date time when the EAB token was created + +## Import + +As EAB tokens are only available on initial creation there is no possibility to +import or update this resource. diff --git a/website/docs/r/pki_secret_backend_config_acme.html.md b/website/docs/r/pki_secret_backend_config_acme.html.md new file mode 100644 index 000000000..b52a9f386 --- /dev/null +++ b/website/docs/r/pki_secret_backend_config_acme.html.md @@ -0,0 +1,81 @@ +--- +layout: "vault" +page_title: "Vault: vault_pki_secret_backend_config_acme resource" +sidebar_current: "docs-vault-resource-pki-secret-backend-config-acme" +description: |- + Sets the ACME configuration on a PKI Secret Backend for Vault. +--- + +# vault\_pki\_secret\_backend\_config\_acme + +Allows setting the ACME server configuration used by specified mount. + +## Example Usage + +```hcl +resource "vault_mount" "pki" { + path = "pki" + type = "pki" + default_lease_ttl_seconds = 3600 + max_lease_ttl_seconds = 86400 +} + +resource "vault_pki_secret_backend_config_cluster" "pki_config_cluster" { + backend = vault_mount.pki.path + path = "http://127.0.0.1:8200/v1/pki" + aia_path = "http://127.0.0.1:8200/v1/pki" +} + +resource "vault_pki_secret_backend_config_acme" "example" { + backend = vault_mount.pki.path + enabled = true + allowed_issuers = ["*"] + allowed_roles = ["*"] + allow_role_ext_key_usage = false + default_directory_policy = "sign-verbatim" + dns_resolver = "" + eab_policy = "not-required" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + *Available only for Vault Enterprise*. + +* `backend` - (Required) The path the PKI secret backend is mounted at, with no leading or trailing `/`s. + +* `enabled` - (Required) Specifies whether ACME is enabled. + +* `allowed_issuers` - (Optional) Specifies which issuers are allowed for use with ACME. + +* `allowed_roles` - (Optional) Specifies which roles are allowed for use with ACME. + +* `allow_role_ext_key_usage` - (Optional) Specifies whether the ExtKeyUsage field from a role is used. **Vault 1.14.1+** + +* `default_directory_policy` - (Optional) Specifies the policy to be used for non-role-qualified ACME requests. + Allowed values are `forbid`, `sign-verbatim`, `role:`, `external-policy` or `external-policy:`. + +* `dns_resolver` - (Optional) DNS resolver to use for domain resolution on this mount. + Must be in the format `:`, with both parts mandatory. + +* `eab_policy` - (Optional) Specifies the policy to use for external account binding behaviour. + Allowed values are `not-required`, `new-account-required` or `always-required`. + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +The ACME configuration can be imported using the resource's `id`. +In the case of the example above the `id` would be `pki/config/acme`, +where the `pki` component is the resource's `backend`, e.g. + +``` +$ terraform import vault_pki_secret_backend_config_acme.example pki/config/acme +``` diff --git a/website/docs/r/pki_secret_backend_role.html.md b/website/docs/r/pki_secret_backend_role.html.md index d25ab50d7..e7739937c 100644 --- a/website/docs/r/pki_secret_backend_role.html.md +++ b/website/docs/r/pki_secret_backend_role.html.md @@ -86,6 +86,8 @@ The following arguments are supported: * `client_flag` - (Optional) Flag to specify certificates for client use +* `cn_validations` - (Optional) Validations to run on the Common Name field of the certificate, choices: `email`, `hostname`, `disabled` + * `code_signing_flag` - (Optional) Flag to specify certificates for code signing use * `email_protection_flag` - (Optional) Flag to specify certificates for email protection use diff --git a/website/docs/r/ssh_secret_backend_role.html.md b/website/docs/r/ssh_secret_backend_role.html.md index a8acf1067..f7330667f 100644 --- a/website/docs/r/ssh_secret_backend_role.html.md +++ b/website/docs/r/ssh_secret_backend_role.html.md @@ -98,6 +98,10 @@ The following arguments are supported: * `not_before_duration` - (Optional) Specifies the duration by which to backdate the ValidAfter property. Uses [duration format strings](https://developer.hashicorp.com/vault/docs/concepts/duration-format). +* `allow_empty_principals` - (Optional) Allow signing certificates with no + valid principals (e.g. any valid principal). For backwards compatibility + only. The default of false is highly recommended. + ### Allowed User Key Configuration * `type` - (Required) The SSH public key type. diff --git a/website/vault.erb b/website/vault.erb index 4c356d567..bb237f845 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -549,10 +549,18 @@ vault_okta_auth_backend_user + > + pki_secret_backend_acme_eab + + > vault_pki_secret_backend_cert + > + vault_pki_secret_backend_config_acme + + > vault_pki_secret_backend_config_ca