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 new resource vault_pki_secret_backend_acme_eab to manage ACME EAB tokens #2367

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

FEATURES:

* 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))

## 4.5.0 (Nov 19, 2024)

FEATURES:
Expand Down
4 changes: 4 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ const (
FieldMaxRetries = "max_retries"
FieldSessionTags = "session_tags"
FieldSelfManagedPassword = "self_managed_password"
FieldsCreatedOn = "created_on"
FieldEabKey = "key"
FieldAcmeDirectory = "acme_directory"
FieldEabId = "eab_id"

/*
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 @@ -631,6 +631,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}"},
Expand Down
181 changes: 181 additions & 0 deletions vault/resource_pki_secret_backend_acme_eab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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)
}
}
147 changes: 147 additions & 0 deletions vault/resource_pki_secret_backend_acme_eab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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)
}
})
}
}
Loading