Skip to content

Commit

Permalink
Merge pull request #19579 from bartoszj/f-aws_iam_access_key
Browse files Browse the repository at this point in the history
Add encrypted SES SMTP password
  • Loading branch information
YakDriver authored Jul 12, 2021
2 parents fcae320 + b72a76b commit a8eb4c7
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 84 deletions.
3 changes: 3 additions & 0 deletions .changelog/19579.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_iam_access_key: Add encrypted SES SMTP password
```
67 changes: 39 additions & 28 deletions aws/resource_aws_iam_access_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,26 @@ func resourceAwsIamAccessKey() *schema.Resource {
},

Schema: map[string]*schema.Schema{
"user": {
"create_date": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Computed: true,
},
"status": {
"encrypted_secret": {
Type: schema.TypeString,
Computed: true,
},
"encrypted_ses_smtp_password_v4": {
Type: schema.TypeString,
Computed: true,
},
"key_fingerprint": {
Type: schema.TypeString,
Computed: true,
},
"pgp_key": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Default: "Active",
ValidateFunc: validation.StringInSlice([]string{
iam.StatusTypeActive,
iam.StatusTypeInactive,
}, false),
},
"secret": {
Type: schema.TypeString,
Expand All @@ -74,22 +81,16 @@ func resourceAwsIamAccessKey() *schema.Resource {
Computed: true,
Sensitive: true,
},
"pgp_key": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
},
"create_date": {
Type: schema.TypeString,
Computed: true,
},
"encrypted_secret": {
Type: schema.TypeString,
Computed: true,
"status": {
Type: schema.TypeString,
Optional: true,
Default: iam.StatusTypeActive,
ValidateFunc: validation.StringInSlice(iam.StatusType_Values(), false),
},
"key_fingerprint": {
"user": {
Type: schema.TypeString,
Computed: true,
Required: true,
ForceNew: true,
},
},
}
Expand Down Expand Up @@ -117,6 +118,11 @@ func resourceAwsIamAccessKeyCreate(d *schema.ResourceData, meta interface{}) err
return fmt.Errorf("CreateAccessKey response did not contain a Secret Access Key as expected")
}

sesSMTPPasswordV4, err := sesSmtpPasswordFromSecretKeySigV4(createResp.AccessKey.SecretAccessKey, meta.(*AWSClient).region)
if err != nil {
return fmt.Errorf("error getting SES SigV4 SMTP Password from Secret Access Key: %s", err)
}

if v, ok := d.GetOk("pgp_key"); ok {
pgpKey := v.(string)
encryptionKey, err := encryption.RetrieveGPGKey(pgpKey)
Expand All @@ -130,17 +136,22 @@ func resourceAwsIamAccessKeyCreate(d *schema.ResourceData, meta interface{}) err

d.Set("key_fingerprint", fingerprint)
d.Set("encrypted_secret", encrypted)

_, encrypted, err = encryption.EncryptValue(encryptionKey, sesSMTPPasswordV4, "SES SMTP password")
if err != nil {
return err
}

d.Set("encrypted_ses_smtp_password_v4", encrypted)
} else {
if err := d.Set("secret", createResp.AccessKey.SecretAccessKey); err != nil {
return err
}
}

sesSMTPPasswordV4, err := sesSmtpPasswordFromSecretKeySigV4(createResp.AccessKey.SecretAccessKey, meta.(*AWSClient).region)
if err != nil {
return fmt.Errorf("error getting SES SigV4 SMTP Password from Secret Access Key: %s", err)
if err := d.Set("ses_smtp_password_v4", sesSMTPPasswordV4); err != nil {
return err
}
}
d.Set("ses_smtp_password_v4", sesSMTPPasswordV4)

if v, ok := d.GetOk("status"); ok && v.(string) == iam.StatusTypeInactive {
input := &iam.UpdateAccessKeyInput{
Expand Down
101 changes: 58 additions & 43 deletions aws/resource_aws_iam_access_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (

func TestAccAWSAccessKey_basic(t *testing.T) {
var conf iam.AccessKeyMetadata
rName := fmt.Sprintf("test-user-%d", acctest.RandInt())
resourceName := "aws_iam_access_key.test"
rName := acctest.RandomWithPrefix("tf-acc-test")

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand All @@ -29,25 +30,30 @@ func TestAccAWSAccessKey_basic(t *testing.T) {
{
Config: testAccAWSAccessKeyConfig(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf),
testAccCheckAWSAccessKeyExists(resourceName, &conf),
testAccCheckAWSAccessKeyAttributes(&conf, "Active"),
testAccCheckResourceAttrRfc3339("aws_iam_access_key.a_key", "create_date"),
resource.TestCheckResourceAttrSet("aws_iam_access_key.a_key", "secret"),
testAccCheckResourceAttrRfc3339(resourceName, "create_date"),
resource.TestCheckResourceAttrSet(resourceName, "secret"),
resource.TestCheckNoResourceAttr(resourceName, "encrypted_secret"),
resource.TestCheckNoResourceAttr(resourceName, "key_fingerprint"),
resource.TestCheckResourceAttrSet(resourceName, "ses_smtp_password_v4"),
resource.TestCheckNoResourceAttr(resourceName, "encrypted_ses_smtp_password_v4"),
),
},
{
ResourceName: "aws_iam_access_key.a_key",
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"encrypted_secret", "key_fingerprint", "pgp_key", "secret", "ses_smtp_password_v4"},
ImportStateVerifyIgnore: []string{"encrypted_secret", "key_fingerprint", "pgp_key", "secret", "ses_smtp_password_v4", "encrypted_ses_smtp_password_v4"},
},
},
})
}

func TestAccAWSAccessKey_encrypted(t *testing.T) {
var conf iam.AccessKeyMetadata
rName := fmt.Sprintf("test-user-%d", acctest.RandInt())
resourceName := "aws_iam_access_key.test"
rName := acctest.RandomWithPrefix("tf-acc-test")

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand All @@ -58,30 +64,30 @@ func TestAccAWSAccessKey_encrypted(t *testing.T) {
{
Config: testAccAWSAccessKeyConfig_encrypted(rName, testPubKey1),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf),
testAccCheckAWSAccessKeyExists(resourceName, &conf),
testAccCheckAWSAccessKeyAttributes(&conf, "Active"),
testDecryptSecretKeyAndTest("aws_iam_access_key.a_key", testPrivKey1),
resource.TestCheckNoResourceAttr(
"aws_iam_access_key.a_key", "secret"),
resource.TestCheckResourceAttrSet(
"aws_iam_access_key.a_key", "encrypted_secret"),
resource.TestCheckResourceAttrSet(
"aws_iam_access_key.a_key", "key_fingerprint"),
testDecryptSecretKeyAndTest(resourceName, testPrivKey1),
resource.TestCheckNoResourceAttr(resourceName, "secret"),
resource.TestCheckResourceAttrSet(resourceName, "encrypted_secret"),
resource.TestCheckResourceAttrSet(resourceName, "key_fingerprint"),
resource.TestCheckNoResourceAttr(resourceName, "ses_smtp_password_v4"),
resource.TestCheckResourceAttrSet(resourceName, "encrypted_ses_smtp_password_v4"),
),
},
{
ResourceName: "aws_iam_access_key.a_key",
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"encrypted_secret", "key_fingerprint", "pgp_key", "secret", "ses_smtp_password_v4"},
ImportStateVerifyIgnore: []string{"encrypted_secret", "key_fingerprint", "pgp_key", "secret", "ses_smtp_password_v4", "encrypted_ses_smtp_password_v4"},
},
},
})
}

func TestAccAWSAccessKey_Status(t *testing.T) {
func TestAccAWSAccessKey_status(t *testing.T) {
var conf iam.AccessKeyMetadata
rName := fmt.Sprintf("test-user-%d", acctest.RandInt())
resourceName := "aws_iam_access_key.test"
rName := acctest.RandomWithPrefix("tf-acc-test")

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand All @@ -92,28 +98,28 @@ func TestAccAWSAccessKey_Status(t *testing.T) {
{
Config: testAccAWSAccessKeyConfig_Status(rName, iam.StatusTypeInactive),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf),
resource.TestCheckResourceAttr("aws_iam_access_key.a_key", "status", iam.StatusTypeInactive),
testAccCheckAWSAccessKeyExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "status", iam.StatusTypeInactive),
),
},
{
ResourceName: "aws_iam_access_key.a_key",
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"encrypted_secret", "key_fingerprint", "pgp_key", "secret", "ses_smtp_password_v4"},
ImportStateVerifyIgnore: []string{"encrypted_secret", "key_fingerprint", "pgp_key", "secret", "ses_smtp_password_v4", "encrypted_ses_smtp_password_v4"},
},
{
Config: testAccAWSAccessKeyConfig_Status(rName, iam.StatusTypeActive),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf),
resource.TestCheckResourceAttr("aws_iam_access_key.a_key", "status", iam.StatusTypeActive),
testAccCheckAWSAccessKeyExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "status", iam.StatusTypeActive),
),
},
{
Config: testAccAWSAccessKeyConfig_Status(rName, iam.StatusTypeInactive),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf),
resource.TestCheckResourceAttr("aws_iam_access_key.a_key", "status", iam.StatusTypeInactive),
testAccCheckAWSAccessKeyExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "status", iam.StatusTypeInactive),
),
},
},
Expand Down Expand Up @@ -186,7 +192,7 @@ func testAccCheckAWSAccessKeyExists(n string, res *iam.AccessKeyMetadata) resour

func testAccCheckAWSAccessKeyAttributes(accessKeyMetadata *iam.AccessKeyMetadata, status string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if !strings.Contains(*accessKeyMetadata.UserName, "test-user") {
if !strings.Contains(*accessKeyMetadata.UserName, "tf-acc-test") {
return fmt.Errorf("Bad username: %s", *accessKeyMetadata.UserName)
}

Expand All @@ -205,14 +211,23 @@ func testDecryptSecretKeyAndTest(nAccessKey, key string) resource.TestCheckFunc
return fmt.Errorf("Not found: %s", nAccessKey)
}

password, ok := keyResource.Primary.Attributes["encrypted_secret"]
secret, ok := keyResource.Primary.Attributes["encrypted_secret"]
if !ok {
return errors.New("No secret in state")
}

password, ok := keyResource.Primary.Attributes["encrypted_ses_smtp_password_v4"]
if !ok {
return errors.New("No password in state")
}

// We can't verify that the decrypted password is correct, because we don't
// We can't verify that the decrypted secret or password is correct, because we don't
// have it. We can verify that decrypting it does not error
_, err := pgpkeys.DecryptBytes(password, key)
_, err := pgpkeys.DecryptBytes(secret, key)
if err != nil {
return fmt.Errorf("Error decrypting secret: %s", err)
}
_, err = pgpkeys.DecryptBytes(password, key)
if err != nil {
return fmt.Errorf("Error decrypting password: %s", err)
}
Expand All @@ -223,40 +238,40 @@ func testDecryptSecretKeyAndTest(nAccessKey, key string) resource.TestCheckFunc

func testAccAWSAccessKeyConfig(rName string) string {
return fmt.Sprintf(`
resource "aws_iam_user" "a_user" {
name = "%s"
resource "aws_iam_user" "test" {
name = %[1]q
}
resource "aws_iam_access_key" "a_key" {
user = aws_iam_user.a_user.name
resource "aws_iam_access_key" "test" {
user = aws_iam_user.test.name
}
`, rName)
}

func testAccAWSAccessKeyConfig_encrypted(rName, key string) string {
return fmt.Sprintf(`
resource "aws_iam_user" "a_user" {
name = "%s"
resource "aws_iam_user" "test" {
name = %[1]q
}
resource "aws_iam_access_key" "a_key" {
user = aws_iam_user.a_user.name
resource "aws_iam_access_key" "test" {
user = aws_iam_user.test.name
pgp_key = <<EOF
%s
%[2]s
EOF
}
`, rName, key)
}

func testAccAWSAccessKeyConfig_Status(rName string, status string) string {
return fmt.Sprintf(`
resource "aws_iam_user" "a_user" {
resource "aws_iam_user" "test" {
name = %[1]q
}
resource "aws_iam_access_key" "a_key" {
user = aws_iam_user.a_user.name
resource "aws_iam_access_key" "test" {
user = aws_iam_user.test.name
status = %[2]q
}
`, rName, status)
Expand Down
23 changes: 10 additions & 13 deletions website/docs/r/iam_access_key.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,21 @@ output "aws_iam_smtp_password_v4" {

The following arguments are supported:

* `user` - (Required) The IAM user to associate with this access key.
* `pgp_key` - (Optional) Either a base-64 encoded PGP public key, or a
keybase username in the form `keybase:some_person_that_exists`, for use
in the `encrypted_secret` output attribute.
* `status` - (Optional) The access key status to apply. Defaults to `Active`.
Valid values are `Active` and `Inactive`.
* `pgp_key` - (Optional) Either a base-64 encoded PGP public key, or a keybase username in the form `keybase:some_person_that_exists`, for use in the `encrypted_secret` output attribute.
* `status` - (Optional) Access key status to apply. Defaults to `Active`. Valid values are `Active` and `Inactive`.
* `user` - (Required) IAM user to associate with this access key.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

* `create_date` - Date and time in [RFC3339 format](https://tools.ietf.org/html/rfc3339#section-5.8) that the access key was created.
* `id` - The access key ID.
* `user` - The IAM user associated with this access key.
* `key_fingerprint` - The fingerprint of the PGP key used to encrypt the secret. This attribute is not available for imported resources.
* `secret` - The secret access key. This attribute is not available for imported resources. Note that this will be written to the state file. If you use this, please protect your backend state file judiciously. Alternatively, you may supply a `pgp_key` instead, which will prevent the secret from being stored in plaintext, at the cost of preventing the use of the secret key in automation.
* `encrypted_secret` - The encrypted secret, base64 encoded, if `pgp_key` was specified. This attribute is not available for imported resources. The encrypted secret may be decrypted using the command line, for example: `terraform output -raw encrypted_secret | base64 --decode | keybase pgp decrypt`.
* `ses_smtp_password_v4` - The secret access key converted into an SES SMTP password by applying [AWS's documented Sigv4 conversion algorithm](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html#smtp-credentials-convert). This attribute is not available for imported resources. As SigV4 is region specific, valid Provider regions are `ap-south-1`, `ap-southeast-2`, `eu-central-1`, `eu-west-1`, `us-east-1` and `us-west-2`. See current [AWS SES regions](https://docs.aws.amazon.com/general/latest/gr/rande.html#ses_region).
* `encrypted_secret` - Encrypted secret, base64 encoded, if `pgp_key` was specified. This attribute is not available for imported resources. The encrypted secret may be decrypted using the command line, for example: `terraform output -raw encrypted_secret | base64 --decode | keybase pgp decrypt`.
* `encrypted_ses_smtp_password_v4` - Encrypted SES SMTP password, base64 encoded, if `pgp_key` was specified. This attribute is not available for imported resources. The encrypted password may be decrypted using the command line, for example: `terraform output -raw encrypted_ses_smtp_password_v4 | base64 --decode | keybase pgp decrypt`.
* `id` - Access key ID.
* `key_fingerprint` - Fingerprint of the PGP key used to encrypt the secret. This attribute is not available for imported resources.
* `secret` - Secret access key. This attribute is not available for imported resources. Note that this will be written to the state file. If you use this, please protect your backend state file judiciously. Alternatively, you may supply a `pgp_key` instead, which will prevent the secret from being stored in plaintext, at the cost of preventing the use of the secret key in automation.
* `ses_smtp_password_v4` - Secret access key converted into an SES SMTP password by applying [AWS's documented Sigv4 conversion algorithm](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html#smtp-credentials-convert). This attribute is not available for imported resources. As SigV4 is region specific, valid Provider regions are `ap-south-1`, `ap-southeast-2`, `eu-central-1`, `eu-west-1`, `us-east-1` and `us-west-2`. See current [AWS SES regions](https://docs.aws.amazon.com/general/latest/gr/rande.html#ses_region).

## Import

Expand All @@ -94,4 +91,4 @@ IAM Access Keys can be imported using the identifier, e.g.
$ terraform import aws_iam_access_key.example AKIA1234567890
```

Resource attributes such as `encrypted_secret`, `key_fingerprint`, `pgp_key`, `secret`, and `ses_smtp_password_v4` are not available for imported resources as this information cannot be read from the IAM API.
Resource attributes such as `encrypted_secret`, `key_fingerprint`, `pgp_key`, `secret`, `ses_smtp_password_v4`, and `encrypted_ses_smtp_password_v4` are not available for imported resources as this information cannot be read from the IAM API.

0 comments on commit a8eb4c7

Please sign in to comment.