Skip to content

Commit

Permalink
feat(pki): Add support for ACME configuration (#2157)
Browse files Browse the repository at this point in the history
* feat(pki): Add support for ACME configuration

* imprv(pki): Remove ValidateFunc, check for 1.14.1. Simplify StateContext. Return error message if response is nil, nil
  • Loading branch information
Viper61 authored Nov 25, 2024
1 parent 0cbe249 commit 879bb54
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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)).

## 4.5.0 (Nov 19, 2024)

Expand Down
6 changes: 6 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,12 @@ 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"

/*
common environment variables
Expand Down
4 changes: 4 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,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"},
Expand Down
209 changes: 209 additions & 0 deletions vault/resource_pki_secret_backend_config_acme.go
Original file line number Diff line number Diff line change
@@ -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 <host>:<port>, 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
}
113 changes: 113 additions & 0 deletions vault/resource_pki_secret_backend_config_acme_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 879bb54

Please sign in to comment.