provider/aws: New data provider to decrypt KMS secrets (#11460)

* Add a new data provider to decrypt AWS KMS secrets

* Address feedback

* Rename aws_kms_secrets to aws_kms_secret
* Add more examples to the documentation
This commit is contained in:
Borgstrom ♕ 2017-01-29 13:01:38 -08:00 committed by Paul Stack
parent 3913f06d46
commit 01a6bd7592
5 changed files with 285 additions and 0 deletions

View File

@ -0,0 +1,99 @@
package aws
import (
"encoding/base64"
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsKmsSecret() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsKmsSecretRead,
Schema: map[string]*schema.Schema{
"secret": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"payload": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"context": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"grant_tokens": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"__has_dynamic_attributes": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
// dataSourceAwsKmsSecretRead decrypts the specified secrets
func dataSourceAwsKmsSecretRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).kmsconn
secrets := d.Get("secret").(*schema.Set)
d.SetId(time.Now().UTC().String())
for _, v := range secrets.List() {
secret := v.(map[string]interface{})
// base64 decode the payload
payload, err := base64.StdEncoding.DecodeString(secret["payload"].(string))
if err != nil {
return fmt.Errorf("Invalid base64 value for secret '%s': %v", secret["name"].(string), err)
}
// build the kms decrypt params
params := &kms.DecryptInput{
CiphertextBlob: []byte(payload),
}
if context, exists := secret["context"]; exists {
params.EncryptionContext = make(map[string]*string)
for k, v := range context.(map[string]interface{}) {
params.EncryptionContext[k] = aws.String(v.(string))
}
}
if grant_tokens, exists := secret["grant_tokens"]; exists {
params.GrantTokens = make([]*string, 0)
for _, v := range grant_tokens.([]interface{}) {
params.GrantTokens = append(params.GrantTokens, aws.String(v.(string)))
}
}
// decrypt
resp, err := conn.Decrypt(params)
if err != nil {
return fmt.Errorf("Failed to decrypt '%s': %s", secret["name"].(string), err)
}
// Set the secret via the name
log.Printf("[DEBUG] aws_kms_secret - successfully decrypted secret: %s", secret["name"].(string))
d.UnsafeSetFieldRaw(secret["name"].(string), string(resp.Plaintext))
}
return nil
}

View File

@ -0,0 +1,96 @@
package aws
import (
"encoding/base64"
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSKmsSecretDataSource_basic(t *testing.T) {
// Run a resource test to setup our KMS key
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckAwsKmsSecretDataSourceKey,
Check: func(s *terraform.State) error {
encryptedPayload, err := testAccCheckAwsKmsSecretDataSourceCheckKeySetup(s)
if err != nil {
return err
}
// We run the actual test on our data source nested in the
// Check function of the KMS key so we can access the
// encrypted output, above, and so that the key will be
// deleted at the end of the test
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccCheckAwsKmsSecretDataSourceSecret, encryptedPayload),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.aws_kms_secret.testing", "secret_name", "PAYLOAD"),
),
},
},
})
return nil
},
},
},
})
}
func testAccCheckAwsKmsSecretDataSourceCheckKeySetup(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources["aws_kms_key.terraform_data_source_testing"]
if !ok {
return "", fmt.Errorf("Failed to setup a KMS key for data source testing!")
}
// Now that the key is setup encrypt a string using it
// XXX TODO: Set up and test with grants
params := &kms.EncryptInput{
KeyId: aws.String(rs.Primary.Attributes["arn"]),
Plaintext: []byte("PAYLOAD"),
EncryptionContext: map[string]*string{
"name": aws.String("value"),
},
}
kmsconn := testAccProvider.Meta().(*AWSClient).kmsconn
resp, err := kmsconn.Encrypt(params)
if err != nil {
return "", fmt.Errorf("Failed encrypting string with KMS for data source testing: %s", err)
}
return base64.StdEncoding.EncodeToString(resp.CiphertextBlob), nil
}
const testAccCheckAwsKmsSecretDataSourceKey = `
resource "aws_kms_key" "terraform_data_source_testing" {
description = "Testing the Terraform AWS KMS Secret data_source"
}
`
const testAccCheckAwsKmsSecretDataSourceSecret = `
data "aws_kms_secret" "testing" {
secret {
name = "secret_name"
payload = "%s"
context {
name = "value"
}
}
}
`

View File

@ -184,6 +184,7 @@ func Provider() terraform.ResourceProvider {
"aws_vpc_endpoint": dataSourceAwsVpcEndpoint(),
"aws_vpc_endpoint_service": dataSourceAwsVpcEndpointService(),
"aws_vpc_peering_connection": dataSourceAwsVpcPeeringConnection(),
"aws_kms_secret": dataSourceAwsKmsSecret(),
},
ResourcesMap: map[string]*schema.Resource{

View File

@ -0,0 +1,86 @@
---
layout: "aws"
page_title: "AWS: aws_kms_secret"
sidebar_current: "docs-aws-datasource-kms-secret"
description: |-
Provides secret data encrypted with the KMS service
---
# aws\_kms\_secret
The KMS secret data source allows you to use data encrypted with the AWS KMS
service within your resource definitions.
## Note about encrypted data
Using this data provider will allow you to conceal secret data within your
resource definitions but does not take care of protecting that data in the
logging output, plan output or state output.
Please take care to secure your secret data outside of resource definitions.
## Example Usage
First, let's encrypt a password with KMS using the [AWS CLI
tools](http://docs.aws.amazon.com/cli/latest/reference/kms/encrypt.html). This
requires you to have your AWS CLI setup correctly, and you would replace the
key-id with your own.
```
$ echo 'master-password' > plaintext-password
$ aws kms encrypt \
> --key-id ab123456-c012-4567-890a-deadbeef123 \
> --plaintext fileb://plaintext-example \
> --encryption-context foo=bar \
> --output text --query CiphertextBlob
AQECAHgaPa0J8WadplGCqqVAr4HNvDaFSQ+NaiwIBhmm6qDSFwAAAGIwYAYJKoZIhvcNAQcGoFMwUQIBADBMBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDI+LoLdvYv8l41OhAAIBEIAfx49FFJCLeYrkfMfAw6XlnxP23MmDBdqP8dPp28OoAQ==
```
Now, take that output and add it to your resource definitions.
```
data "aws_kms_secret" "db" {
secret {
name = "master_password"
payload = "AQECAHgaPa0J8WadplGCqqVAr4HNvDaFSQ+NaiwIBhmm6qDSFwAAAGIwYAYJKoZIhvcNAQcGoFMwUQIBADBMBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDI+LoLdvYv8l41OhAAIBEIAfx49FFJCLeYrkfMfAw6XlnxP23MmDBdqP8dPp28OoAQ=="
context {
foo = "bar"
}
}
}
resource "aws_rds_cluster" "rds" {
master_username = "root"
master_password = "${data.aws_kms_secret.db.master_password}"
...
}
```
And your RDS cluster would have the root password set to "master-password"
## Argument Reference
The following arguments are supported:
* `secret` - (Required) One or more encrypted payload definitions from the KMS
service. See the Secret Definitions below.
### Secret Definitions
Each secret definition supports the following arguments:
* `name` - (Required) The name to export this secret under in the attributes.
* `payload` - (Required) Base64 encoded payload, as returned from a KMS encrypt
opertation.
* `context` - (Optional) An optional mapping that makes up the Encryption
Context for the secret.
* `grant_tokens` (Optional) An optional list of Grant Tokens for the secret.
For more information on `context` and `grant_tokens` see the [KMS
Concepts](http://docs.aws.amazon.com/kms/latest/developerguide/concepts.html)
## Attributes Reference
Each `secret` defined is exported under its `name` as a top-level attribute.

View File

@ -71,6 +71,9 @@
<li<%= sidebar_current("docs-aws-datasource-ip_ranges") %>>
<a href="/docs/providers/aws/d/ip_ranges.html">aws_ip_ranges</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-kms-secret") %>>
<a href="/docs/providers/aws/d/kms_secret.html">aws_kms_secret</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-prefix-list") %>>
<a href="/docs/providers/aws/d/prefix_list.html">aws_prefix_list</a>
</li>