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

feat(pki): Add support for ACME configuration #2157

Merged
merged 2 commits into from
Nov 25, 2024
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
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{
Viper61 marked this conversation as resolved.
Show resolved Hide resolved
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