diff --git a/CHANGELOG.md b/CHANGELOG.md index c965e97989..dde85cbb71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased * Add support for key_usage to `vault_pki_secret_backend_root_sign_intermediate` ([#2421])(https://github.com/hashicorp/terraform-provider-vault/pull/2421) +* Add new ephemeral resource `vault_transit_decrypt` ## 5.0.0 (May 21, 2025) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 29120da2d0..4afe7ace4e 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -526,6 +526,9 @@ const ( FieldDeletionTime = "deletion_time" FieldDestroyed = "destroyed" FieldDeleteAllVersions = "delete_all_versions" + FieldKey = "key" + FieldPlaintext = "plaintext" + FieldCiphertext = "ciphertext" /* ephemeral resource constants and write-only attributes diff --git a/internal/provider/fwprovider/provider.go b/internal/provider/fwprovider/provider.go index 8bc4b9b459..65506c6726 100644 --- a/internal/provider/fwprovider/provider.go +++ b/internal/provider/fwprovider/provider.go @@ -6,10 +6,11 @@ package fwprovider import ( "context" "fmt" + "regexp" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/ephemeral" ephemeralsecrets "github.com/hashicorp/terraform-provider-vault/internal/vault/secrets/ephemeral" - "regexp" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -231,6 +232,7 @@ func (p *fwprovider) EphemeralResources(_ context.Context) []func() ephemeral.Ep return []func() ephemeral.EphemeralResource{ ephemeralsecrets.NewKVV2EphemeralSecretResource, ephemeralsecrets.NewDBEphemeralSecretResource, + ephemeralsecrets.NewTransitDecryptEphemeralSecretResource, } } diff --git a/internal/vault/secrets/ephemeral/transit_decrypt.go b/internal/vault/secrets/ephemeral/transit_decrypt.go new file mode 100644 index 0000000000..60e3cbcb2f --- /dev/null +++ b/internal/vault/secrets/ephemeral/transit_decrypt.go @@ -0,0 +1,131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralsecrets + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/framework/base" + "github.com/hashicorp/terraform-provider-vault/internal/framework/client" + "github.com/hashicorp/terraform-provider-vault/internal/framework/errutil" + "github.com/hashicorp/terraform-provider-vault/internal/framework/model" +) + +var _ ephemeral.EphemeralResource = &TransitDecryptEphemeralSecretResource{} + +var NewTransitDecryptEphemeralSecretResource = func() ephemeral.EphemeralResource { + return &TransitDecryptEphemeralSecretResource{} +} + +type TransitDecryptEphemeralSecretResource struct { + base.EphemeralResourceWithConfigure +} + +type TransitDecryptEphemeralSecretModel struct { + base.BaseModelEphemeral + + Key types.String `tfsdk:"key"` + Backend types.String `tfsdk:"backend"` + Plaintext types.String `tfsdk:"plaintext"` + Context types.String `tfsdk:"context"` + Ciphertext types.String `tfsdk:"ciphertext"` +} + +type TransitDecryptEphemeralSecretAPIModel struct { + Plaintext string `json:"plaintext" mapstructure:"plaintext"` +} + +func (r *TransitDecryptEphemeralSecretResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + consts.FieldKey: schema.StringAttribute{ + MarkdownDescription: "Name of the decryption key to use.", + Required: true, + }, + consts.FieldBackend: schema.StringAttribute{ + MarkdownDescription: "The Transit secret backend the key belongs to.", + Required: true, + }, + consts.FieldPlaintext: schema.StringAttribute{ + MarkdownDescription: "Decrypted plain text", + Computed: true, + }, + consts.FieldContext: schema.StringAttribute{ + MarkdownDescription: "Specifies the context for key derivation", + Optional: true, + }, + consts.FieldCiphertext: schema.StringAttribute{ + MarkdownDescription: "Transit encrypted cipher text.", + Required: true, + }, + }, + MarkdownDescription: "Provides an ephemeral resource to decrypt a ciphertext from Vault using transit.", + } + + base.MustAddBaseEphemeralSchema(&resp.Schema) +} + +func (r *TransitDecryptEphemeralSecretResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_transit_decrypt" +} + +func (r *TransitDecryptEphemeralSecretResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data TransitDecryptEphemeralSecretModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + c, err := client.GetClient(ctx, r.Meta(), data.Namespace.ValueString()) + if err != nil { + resp.Diagnostics.AddError(errutil.ClientConfigureErr(err)) + } + + path := r.path(data.Backend.ValueString(), data.Key.ValueString()) + + payload := map[string]interface{}{ + "ciphertext": data.Ciphertext.ValueString(), + "context": base64.StdEncoding.EncodeToString([]byte(data.Context.ValueString())), + } + + secretResp, err := c.Logical().WriteWithContext(ctx, path, payload) + if err != nil { + resp.Diagnostics.AddError( + errutil.VaultReadErr(err), + ) + + return + } + if secretResp == nil { + resp.Diagnostics.AddError( + errutil.VaultReadResponseNil(), + ) + + return + } + + var decryptedResp TransitDecryptEphemeralSecretAPIModel + err = model.ToAPIModel(secretResp.Data, &decryptedResp) + if err != nil { + resp.Diagnostics.AddError("Unable to translate Vault response Data", err.Error()) + return + } + + plaintext, _ := base64.StdEncoding.DecodeString(decryptedResp.Plaintext) + + data.Plaintext = types.StringValue(string(plaintext)) + + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) +} + +func (r *TransitDecryptEphemeralSecretResource) path(backend, key string) string { + return fmt.Sprintf("/%s/decrypt/%s", backend, key) +} diff --git a/internal/vault/secrets/ephemeral/transit_decrypt_test.go b/internal/vault/secrets/ephemeral/transit_decrypt_test.go new file mode 100644 index 0000000000..a39ba220be --- /dev/null +++ b/internal/vault/secrets/ephemeral/transit_decrypt_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralsecrets_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/echoprovider" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-provider-vault/internal/providertest" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +// TestAccTransitDecrypt confirms that a transit encrypted +// secret is Correctly decrypted and read into the ephemeral resource +// +// Uses the Echo Provider to test values set in ephemeral resources +// see documentation here for more details: +// https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/ephemeral-resources#using-echo-provider-in-acceptance-tests +func TestAccTransitDecrypt(t *testing.T) { + testutil.SkipTestAcc(t) + mount := acctest.RandomWithPrefix("transit") + keyName := acctest.RandomWithPrefix("key") + secret := "password1" + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testutil.TestAccPreCheck(t) }, + // Include the provider we want to test + ProtoV5ProviderFactories: providertest.ProtoV5ProviderFactories, + // Include `echo` as a v6 provider from `terraform-plugin-testing` + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: testTransitSecretConfig(mount, keyName, secret), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.test_transit", tfjsonpath.New("data"), knownvalue.StringExact(secret)), + }, + }, + }, + }) +} + +func testTransitSecretConfig(mount, keyName, secret string) string { + return fmt.Sprintf(` +resource "vault_mount" "test" { + path = "%s" + type = "transit" +} + +resource "vault_transit_secret_backend_key" "test" { + name = "%s" + backend = vault_mount.test.path + deletion_allowed = true +} + +data "vault_transit_encrypt" "encrypted" { + backend = vault_mount.test.path + key = vault_transit_secret_backend_key.test.name + plaintext = "%s" +} + +ephemeral "vault_transit_decrypt" "decrypted" { + backend = vault_mount.test.path + key = vault_transit_secret_backend_key.test.name + ciphertext = data.vault_transit_encrypt.encrypted.ciphertext +} + +provider "echo" { + data = ephemeral.vault_transit_decrypt.decrypted.plaintext +} + +resource "echo" "test_transit" {} +`, mount, keyName, secret) +} diff --git a/website/docs/ephemeral-resources/transit_decrypt.html.md b/website/docs/ephemeral-resources/transit_decrypt.html.md new file mode 100644 index 0000000000..15675e569e --- /dev/null +++ b/website/docs/ephemeral-resources/transit_decrypt.html.md @@ -0,0 +1,58 @@ +--- +layout: "vault" +page_title: "Vault: ephemeral vault_transit_decrypt resource" +sidebar_current: "docs-vault-ephemeral-transit-decrypt" +description: |- + Decrypt ciphertext using a Vault Transit encryption key. +--- + +# vault_transit_decrypt + +This is a data source which can be used to decrypt ciphertext using a Vault Transit key. + +Decrypts an ephemeral cyphertext from the Vault Transit engine that is not stored in the remote TF state. +For more information, please refer to [the Vault documentation](https://developer.hashicorp.com/vault/docs/secrets/transit) +for the Transit engine. + +## Example Usage + +```hcl +resource "vault_mount" "transit" { + path = "transit" + type = "transit" +} + +resource "vault_transit_secret_backend_key" "my-key" { + name = "my-key" + backend = vault_mount.transit.path + deletion_allowed = true +} + +data "vault_transit_encrypt" "encrypted" { + backend = vault_mount.transit.path + key = vault_transit_secret_backend_key.my-key.name + plaintext = "foo" +} + +ephemeral "vault_transit_decrypt" "decrypted" { + backend = vault_mount.transit.path + key = vault_transit_secret_backend_key.my-key.name + ciphertext = data.vault_transit_encrypt.encrypted.ciphertext +} +``` + +## Argument Reference + +Each document configuration may have one or more `rule` blocks, which each accept the following arguments: + +- `key` - (Required) Specifies the name of the transit key to decrypt against. + +- `backend` - (Required) The path the transit secret backend is mounted at, with no leading or trailing `/`. + +- `ciphertext` - (Required) Ciphertext to be decoded. + +- `context` - (Optional) Context for key derivation. This is required if key derivation is enabled for this key. + +## Attributes Reference + +- `plaintext` - Decrypted plaintext returned from Vault diff --git a/website/vault.erb b/website/vault.erb index ed1a96b1fd..3e3d868fde 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -186,6 +186,9 @@ > vault_database_secret + > + vault_transit_decrypt +