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 encrypted SES SMTP password #19579

Merged
merged 4 commits into from
Jul 12, 2021
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
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.