From 4831b05c70afef71cf872f2bd2cc208a32ea6a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20=28=EC=A4=91=EA=B4=91=29=20Przybylski?= Date: Tue, 14 Sep 2021 07:04:35 -0700 Subject: [PATCH] Add support for importing self-signed certificate to ACM (#5) * Adding ACM support for secret store to self-signed-cert module * fix(module): Updating version to AWS provider in the example and fixing alias naming to meet conventions * fix(module): Missed a version * refactor(module): Switching from secret_store to certificate_backends to support storing secrets in multiple locations * test(module): Adding terratest for verifying arn. Changing name of secret_store* to certificate_backend * refactor(module): Update to readme and documentation * feat(module): Adjusting variable names for terratest * feat(module): Adding variable for certificate backends to example * Auto Format Co-authored-by: cloudpossebot <11232728+cloudpossebot@users.noreply.github.com> --- README.md | 21 +- acm.tf | 6 + asm.tf | 8 +- docs/terraform.md | 21 +- examples/acm/context.tf | 279 ++++++++++++++++++ examples/acm/fixtures.us-east-1.tfvars | 17 ++ examples/acm/main.tf | 70 +++++ examples/acm/outputs.tf | 4 + examples/acm/variables.tf | 35 +++ examples/acm/versions.tf | 14 + examples/complete/versions.tf | 2 +- examples/custom_secrets/asm.us-east-1.tfvars | 2 +- .../customer-managed-key.us-east-1.tfvars | 2 +- examples/custom_secrets/main.tf | 10 +- .../custom_secrets/no-store.us-east-1.tfvars | 4 +- examples/custom_secrets/ssm.us-east-1.tfvars | 2 +- examples/custom_secrets/variables.tf | 16 +- examples/custom_secrets/versions.tf | 2 +- examples/preexisting_key/main.tf | 2 +- examples/preexisting_key/variables.tf | 2 +- examples/preexisting_key/versions.tf | 2 +- main.tf | 17 +- outputs.tf | 9 +- ssm.tf | 8 +- test/src/Makefile | 1 + test/src/examples_acm_test.go | 81 +++++ test/src/examples_preexisting_key_test.go | 10 +- variables.tf | 44 +-- versions.tf | 2 +- 29 files changed, 614 insertions(+), 79 deletions(-) create mode 100644 acm.tf create mode 100644 examples/acm/context.tf create mode 100644 examples/acm/fixtures.us-east-1.tfvars create mode 100644 examples/acm/main.tf create mode 100644 examples/acm/outputs.tf create mode 100644 examples/acm/variables.tf create mode 100644 examples/acm/versions.tf create mode 100644 test/src/examples_acm_test.go diff --git a/README.md b/README.md index b1e311d..718019c 100644 --- a/README.md +++ b/README.md @@ -160,14 +160,14 @@ Available targets: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [aws](#requirement\_aws) | >= 3.0 | | [tls](#requirement\_tls) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 2.0 | +| [aws](#provider\_aws) | >= 3.0 | | [tls](#provider\_tls) | >= 3.0 | ## Modules @@ -180,6 +180,7 @@ Available targets: | Name | Type | |------|------| +| [aws_acm_certificate.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource | | [aws_secretsmanager_secret.certificate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret.private_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret_version.certificate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | @@ -195,9 +196,14 @@ Available targets: |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [allowed\_uses](#input\_allowed\_uses) | List of keywords each describing a use that is permitted for the issued certificate.
Must be one of of the values outlined in [self\_signed\_cert.allowed\_uses](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/self_signed_cert#allowed_uses). | `list(string)` | n/a | yes | -| [asm\_recovery\_window\_in\_days](#input\_asm\_recovery\_window\_in\_days) | Number of days that AWS Secrets Manager waits before it can delete the secret. This value can be `0` to force deletion without recovery or range from `7` to `30` days.

This value is ignored if `var.secrets_store_type` is not `ASM`, or if `var.secrets_store_enabled` is `false`. | `number` | `30` | no | +| [asm\_recovery\_window\_in\_days](#input\_asm\_recovery\_window\_in\_days) | Number of days that AWS Secrets Manager waits before it can delete the secret. This value can be `0` to force deletion without recovery or range from `7` to `30` days.

This value is ignored if `var.certificate_backends` is not `ASM`, or if `var.certificate_backend_enabled` is `false`. | `number` | `30` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [basic\_constraints](#input\_basic\_constraints) | The [basic constraints](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9) of the issued certificate.
Currently, only the `CA` constraint (which identifies whether the subject of the certificate is a CA) can be set.

Defaults to this certificate not being a CA. |
object({
ca = bool
})
|
{
"ca": false
}
| no | +| [certificate\_backend\_kms\_key\_id](#input\_certificate\_backend\_kms\_key\_id) | The KMD Key ID (ARN or ID) to use when encrypting either the AWS SSM Parameters or AWS Secrets Manager Secrets relating to the certificate.

If not specified, the Amazon-managed Key `alias/aws/ssm` will be used if `var.certificate_backends` contains `SSM`,
and `alias/aws/secretsmanager` will be used if `var.certificate_backends` is `ASM`. | `string` | `null` | no | +| [certificate\_backends](#input\_certificate\_backends) | The certificate backend to use when writing secrets related to the self-signed certificate.
The value specified can either be `SSM` (AWS Systems Manager Parameter Store), `ASM` (AWS Secrets Manager),
and/or `ACM` (AWS Certificate Manager).

Defaults to `SSM`. | `set(string)` |
[
"SSM"
]
| no | +| [certificate\_backends\_base64\_enabled](#input\_certificate\_backends\_base64\_enabled) | Enable or disable base64 encoding of secrets before writing them to the secrets store. | `bool` | `false` | no | +| [certificate\_backends\_enabled](#input\_certificate\_backends\_enabled) | Enable or disable writing to the secrets store. | `bool` | `true` | no | +| [certificate\_chain](#input\_certificate\_chain) | When using ACM as a certificate backend, some certificates store a certificate chain from a CA. This CA will come from another resource. | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | @@ -215,12 +221,8 @@ Available targets: | [private\_key\_ecdsa\_curve](#input\_private\_key\_ecdsa\_curve) | When `var.cert_key_algorithm` is `ECDSA`, the name of the elliptic curve to use. May be any one of `P224`, `P256`, `P384` or `P521`.

Ignored if `var.cert_key_algorithm` is not `ECDSA`, or if a preexisting private key is supplied via `var.private_key_contents`.

Defaults to the `tls` provider default. | `string` | `"P224"` | no | | [private\_key\_rsa\_bits](#input\_private\_key\_rsa\_bits) | When `var.cert_key_algorithm` is `RSA`, the size of the generated RSA key in bits.

Ignored if `var.cert_key_algorithm` is not `RSA`, or if a preexisting private key is supplied via `var.private_key_contents`.

Defaults to the `tls` provider default. | `number` | `2048` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [secret\_extensions](#input\_secret\_extensions) | The extensions use when writing secrets to the secret store.

Please refer to `var.secret_path_format` for information on how secret paths are computed. |
object({
certificate = string
private_key = string
})
|
{
"certificate": "pem",
"private_key": "key"
}
| no | -| [secret\_path\_format](#input\_secret\_path\_format) | The path format to use when writing secrets to the secret store.

The certificate secret path will be computed as `format(var.secret_path_format, var.name, var.secret_extensions.certificate)`
and the private key path as `format(var.secret_path_format, var.name, var.secret_extensions.private_key)`.

Thus by default, if `var.name`=`example-self-signed-cert`, then the resulting secret paths for the self-signed certificate's
PEM file and private key will be `/example-self-signed-cert.pem` and `/example-self-signed-cert.key`, respectively.

This variable can be overridden in order to create more specific secret store paths. | `string` | `"/%s.%s"` | no | -| [secrets\_store\_base64\_enabled](#input\_secrets\_store\_base64\_enabled) | Enable or disable base64 encoding of secrets before writing them to the secrets store. | `bool` | `false` | no | -| [secrets\_store\_enabled](#input\_secrets\_store\_enabled) | Enable or disable writing to the secrets store. | `bool` | `true` | no | -| [secrets\_store\_kms\_key\_id](#input\_secrets\_store\_kms\_key\_id) | The KMD Key ID (ARN or ID) to use when encrypting either the AWS SSM Parameters or AWS Secrets Manager Secrets relating to the certificate.

If not specified, the Amazon-managed Key `alias/aws/ssm` will be used if `var.secrets_store_type` is `SSM`,
and `alias/aws/secretsmanager` will be used if `var.secrets_store_type` is `ASM`. | `string` | `null` | no | -| [secrets\_store\_type](#input\_secrets\_store\_type) | The secret store type to use when writing secrets related to the self-signed certificate.
The value specified can either be `SSM` (AWS Systems Manager Parameter Store) or `ASM` (AWS Secrets Manager).

Defaults to `SSM`. | `string` | `"SSM"` | no | +| [secret\_extensions](#input\_secret\_extensions) | The extensions use when writing secrets to the certificate backend.

Please refer to `var.secret_path_format` for information on how secret paths are computed. |
object({
certificate = string
private_key = string
})
|
{
"certificate": "pem",
"private_key": "key"
}
| no | +| [secret\_path\_format](#input\_secret\_path\_format) | The path format to use when writing secrets to the certificate backend.

The certificate secret path will be computed as `format(var.secret_path_format, var.name, var.secret_extensions.certificate)`
and the private key path as `format(var.secret_path_format, var.name, var.secret_extensions.private_key)`.

Thus by default, if `var.name`=`example-self-signed-cert`, then the resulting secret paths for the self-signed certificate's
PEM file and private key will be `/example-self-signed-cert.pem` and `/example-self-signed-cert.key`, respectively.

This variable can be overridden in order to create more specific certificate backend paths. | `string` | `"/%s.%s"` | no | | [skid\_enabled](#input\_skid\_enabled) | Whether or not the subject key identifier (SKID) should be included in the certificate. | `bool` | `false` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [subject](#input\_subject) | The subject configuration for the certificate.
This should be a map that is compatible with [tls\_cert\_request.subject](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/cert_request#subject).

If `common_name` is omitted, it will be set as `module.this.id`. | `any` | `{}` | no | @@ -233,6 +235,7 @@ Available targets: | Name | Description | |------|-------------| +| [certificate\_arn](#output\_certificate\_arn) | ARN of certificate stored in ACM that other services may need to refer to. This is useful when the certificate is stored in ACM. | | [certificate\_key\_path](#output\_certificate\_key\_path) | Secrets store path containing the certificate private key file. | | [certificate\_pem](#output\_certificate\_pem) | Contents of the certificate PEM. | | [certificate\_pem\_path](#output\_certificate\_pem\_path) | Secrets store path containing the certificate PEM file. | diff --git a/acm.tf b/acm.tf new file mode 100644 index 0000000..e540dce --- /dev/null +++ b/acm.tf @@ -0,0 +1,6 @@ +resource "aws_acm_certificate" "default" { + count = local.acm_enabled ? 1 : 0 + private_key = local.tls_key + certificate_body = local.tls_certificate + certificate_chain = var.basic_constraints.ca ? var.certificate_chain : null +} diff --git a/asm.tf b/asm.tf index e7c3255..b35eaad 100644 --- a/asm.tf +++ b/asm.tf @@ -3,7 +3,7 @@ resource "aws_secretsmanager_secret" "certificate" { name = format(var.secret_path_format, module.this.name, var.secret_extensions.certificate) recovery_window_in_days = var.asm_recovery_window_in_days - kms_key_id = local.secrets_store_kms_key_id + kms_key_id = local.certificate_backend_kms_key_id tags = module.this.tags } @@ -12,7 +12,7 @@ resource "aws_secretsmanager_secret_version" "certificate" { count = local.asm_enabled ? 1 : 0 secret_id = join("", aws_secretsmanager_secret.certificate.*.name) - secret_string = var.secrets_store_base64_enabled ? base64encode(local.tls_certificate) : local.tls_certificate + secret_string = var.certificate_backends_base64_enabled ? base64encode(local.tls_certificate) : local.tls_certificate } resource "aws_secretsmanager_secret" "private_key" { @@ -20,7 +20,7 @@ resource "aws_secretsmanager_secret" "private_key" { name = format(var.secret_path_format, module.this.name, var.secret_extensions.private_key) recovery_window_in_days = var.asm_recovery_window_in_days - kms_key_id = local.secrets_store_kms_key_id + kms_key_id = local.certificate_backend_kms_key_id tags = module.this.tags } @@ -29,5 +29,5 @@ resource "aws_secretsmanager_secret_version" "private_key" { count = local.asm_enabled ? 1 : 0 secret_id = join("", aws_secretsmanager_secret.private_key.*.name) - secret_string = var.secrets_store_base64_enabled ? base64encode(local.tls_key) : local.tls_key + secret_string = var.certificate_backends_base64_enabled ? base64encode(local.tls_key) : local.tls_key } diff --git a/docs/terraform.md b/docs/terraform.md index 5395be4..ee2e16e 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -4,14 +4,14 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [aws](#requirement\_aws) | >= 3.0 | | [tls](#requirement\_tls) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 2.0 | +| [aws](#provider\_aws) | >= 3.0 | | [tls](#provider\_tls) | >= 3.0 | ## Modules @@ -24,6 +24,7 @@ | Name | Type | |------|------| +| [aws_acm_certificate.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource | | [aws_secretsmanager_secret.certificate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret.private_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret_version.certificate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | @@ -39,9 +40,14 @@ |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [allowed\_uses](#input\_allowed\_uses) | List of keywords each describing a use that is permitted for the issued certificate.
Must be one of of the values outlined in [self\_signed\_cert.allowed\_uses](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/self_signed_cert#allowed_uses). | `list(string)` | n/a | yes | -| [asm\_recovery\_window\_in\_days](#input\_asm\_recovery\_window\_in\_days) | Number of days that AWS Secrets Manager waits before it can delete the secret. This value can be `0` to force deletion without recovery or range from `7` to `30` days.

This value is ignored if `var.secrets_store_type` is not `ASM`, or if `var.secrets_store_enabled` is `false`. | `number` | `30` | no | +| [asm\_recovery\_window\_in\_days](#input\_asm\_recovery\_window\_in\_days) | Number of days that AWS Secrets Manager waits before it can delete the secret. This value can be `0` to force deletion without recovery or range from `7` to `30` days.

This value is ignored if `var.certificate_backends` is not `ASM`, or if `var.certificate_backend_enabled` is `false`. | `number` | `30` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [basic\_constraints](#input\_basic\_constraints) | The [basic constraints](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9) of the issued certificate.
Currently, only the `CA` constraint (which identifies whether the subject of the certificate is a CA) can be set.

Defaults to this certificate not being a CA. |
object({
ca = bool
})
|
{
"ca": false
}
| no | +| [certificate\_backend\_kms\_key\_id](#input\_certificate\_backend\_kms\_key\_id) | The KMD Key ID (ARN or ID) to use when encrypting either the AWS SSM Parameters or AWS Secrets Manager Secrets relating to the certificate.

If not specified, the Amazon-managed Key `alias/aws/ssm` will be used if `var.certificate_backends` contains `SSM`,
and `alias/aws/secretsmanager` will be used if `var.certificate_backends` is `ASM`. | `string` | `null` | no | +| [certificate\_backends](#input\_certificate\_backends) | The certificate backend to use when writing secrets related to the self-signed certificate.
The value specified can either be `SSM` (AWS Systems Manager Parameter Store), `ASM` (AWS Secrets Manager),
and/or `ACM` (AWS Certificate Manager).

Defaults to `SSM`. | `set(string)` |
[
"SSM"
]
| no | +| [certificate\_backends\_base64\_enabled](#input\_certificate\_backends\_base64\_enabled) | Enable or disable base64 encoding of secrets before writing them to the secrets store. | `bool` | `false` | no | +| [certificate\_backends\_enabled](#input\_certificate\_backends\_enabled) | Enable or disable writing to the secrets store. | `bool` | `true` | no | +| [certificate\_chain](#input\_certificate\_chain) | When using ACM as a certificate backend, some certificates store a certificate chain from a CA. This CA will come from another resource. | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | @@ -59,12 +65,8 @@ | [private\_key\_ecdsa\_curve](#input\_private\_key\_ecdsa\_curve) | When `var.cert_key_algorithm` is `ECDSA`, the name of the elliptic curve to use. May be any one of `P224`, `P256`, `P384` or `P521`.

Ignored if `var.cert_key_algorithm` is not `ECDSA`, or if a preexisting private key is supplied via `var.private_key_contents`.

Defaults to the `tls` provider default. | `string` | `"P224"` | no | | [private\_key\_rsa\_bits](#input\_private\_key\_rsa\_bits) | When `var.cert_key_algorithm` is `RSA`, the size of the generated RSA key in bits.

Ignored if `var.cert_key_algorithm` is not `RSA`, or if a preexisting private key is supplied via `var.private_key_contents`.

Defaults to the `tls` provider default. | `number` | `2048` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [secret\_extensions](#input\_secret\_extensions) | The extensions use when writing secrets to the secret store.

Please refer to `var.secret_path_format` for information on how secret paths are computed. |
object({
certificate = string
private_key = string
})
|
{
"certificate": "pem",
"private_key": "key"
}
| no | -| [secret\_path\_format](#input\_secret\_path\_format) | The path format to use when writing secrets to the secret store.

The certificate secret path will be computed as `format(var.secret_path_format, var.name, var.secret_extensions.certificate)`
and the private key path as `format(var.secret_path_format, var.name, var.secret_extensions.private_key)`.

Thus by default, if `var.name`=`example-self-signed-cert`, then the resulting secret paths for the self-signed certificate's
PEM file and private key will be `/example-self-signed-cert.pem` and `/example-self-signed-cert.key`, respectively.

This variable can be overridden in order to create more specific secret store paths. | `string` | `"/%s.%s"` | no | -| [secrets\_store\_base64\_enabled](#input\_secrets\_store\_base64\_enabled) | Enable or disable base64 encoding of secrets before writing them to the secrets store. | `bool` | `false` | no | -| [secrets\_store\_enabled](#input\_secrets\_store\_enabled) | Enable or disable writing to the secrets store. | `bool` | `true` | no | -| [secrets\_store\_kms\_key\_id](#input\_secrets\_store\_kms\_key\_id) | The KMD Key ID (ARN or ID) to use when encrypting either the AWS SSM Parameters or AWS Secrets Manager Secrets relating to the certificate.

If not specified, the Amazon-managed Key `alias/aws/ssm` will be used if `var.secrets_store_type` is `SSM`,
and `alias/aws/secretsmanager` will be used if `var.secrets_store_type` is `ASM`. | `string` | `null` | no | -| [secrets\_store\_type](#input\_secrets\_store\_type) | The secret store type to use when writing secrets related to the self-signed certificate.
The value specified can either be `SSM` (AWS Systems Manager Parameter Store) or `ASM` (AWS Secrets Manager).

Defaults to `SSM`. | `string` | `"SSM"` | no | +| [secret\_extensions](#input\_secret\_extensions) | The extensions use when writing secrets to the certificate backend.

Please refer to `var.secret_path_format` for information on how secret paths are computed. |
object({
certificate = string
private_key = string
})
|
{
"certificate": "pem",
"private_key": "key"
}
| no | +| [secret\_path\_format](#input\_secret\_path\_format) | The path format to use when writing secrets to the certificate backend.

The certificate secret path will be computed as `format(var.secret_path_format, var.name, var.secret_extensions.certificate)`
and the private key path as `format(var.secret_path_format, var.name, var.secret_extensions.private_key)`.

Thus by default, if `var.name`=`example-self-signed-cert`, then the resulting secret paths for the self-signed certificate's
PEM file and private key will be `/example-self-signed-cert.pem` and `/example-self-signed-cert.key`, respectively.

This variable can be overridden in order to create more specific certificate backend paths. | `string` | `"/%s.%s"` | no | | [skid\_enabled](#input\_skid\_enabled) | Whether or not the subject key identifier (SKID) should be included in the certificate. | `bool` | `false` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [subject](#input\_subject) | The subject configuration for the certificate.
This should be a map that is compatible with [tls\_cert\_request.subject](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/cert_request#subject).

If `common_name` is omitted, it will be set as `module.this.id`. | `any` | `{}` | no | @@ -77,6 +79,7 @@ | Name | Description | |------|-------------| +| [certificate\_arn](#output\_certificate\_arn) | ARN of certificate stored in ACM that other services may need to refer to. This is useful when the certificate is stored in ACM. | | [certificate\_key\_path](#output\_certificate\_key\_path) | Secrets store path containing the certificate private key file. | | [certificate\_pem](#output\_certificate\_pem) | Contents of the certificate PEM. | | [certificate\_pem\_path](#output\_certificate\_pem\_path) | Secrets store path containing the certificate PEM file. | diff --git a/examples/acm/context.tf b/examples/acm/context.tf new file mode 100644 index 0000000..5e0ef88 --- /dev/null +++ b/examples/acm/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/acm/fixtures.us-east-1.tfvars b/examples/acm/fixtures.us-east-1.tfvars new file mode 100644 index 0000000..5b79b08 --- /dev/null +++ b/examples/acm/fixtures.us-east-1.tfvars @@ -0,0 +1,17 @@ +region = "us-east-1" + +namespace = "eg" + +environment = "ue1" + +stage = "test" + +name = "self-signed-cert" + +certificate_backends_enabled = true + +certificate_backends = ["ACM"] + +basic_constraints = { + ca = true +} diff --git a/examples/acm/main.tf b/examples/acm/main.tf new file mode 100644 index 0000000..5d803fc --- /dev/null +++ b/examples/acm/main.tf @@ -0,0 +1,70 @@ +provider "aws" { + region = var.region +} + +module "self_signed_cert_ca" { + source = "../.." + + subject = { + common_name = module.this.id + organization = "Cloud Posse" + organizational_unit = "Engineering" + } + + validity = { + duration_hours = 730 + early_renewal_hours = 24 + } + + allowed_uses = [ + "crl_signing", + "cert_signing", + ] + + subject_alt_names = { + ip_addresses = null + dns_names = null + uris = null + } + + certificate_backends_enabled = var.certificate_backends_enabled + certificate_backends = var.certificate_backends + + basic_constraints = var.basic_constraints + + context = module.this.context +} + +module "self_signed_cert_server" { + source = "../.." + + subject = { + common_name = module.this.id + organization = "Cloud Posse" + organizational_unit = "Engineering" + } + + validity = { + duration_hours = 730 + early_renewal_hours = 24 + } + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] + + subject_alt_names = { + ip_addresses = ["10.10.10.10"] + dns_names = ["example.com"] + uris = ["https://example.com"] + } + + certificate_chain = module.self_signed_cert_ca.certificate_pem + + certificate_backends_enabled = var.certificate_backends_enabled + certificate_backends = var.certificate_backends + + context = module.this.context +} diff --git a/examples/acm/outputs.tf b/examples/acm/outputs.tf new file mode 100644 index 0000000..bd101e9 --- /dev/null +++ b/examples/acm/outputs.tf @@ -0,0 +1,4 @@ +output "certificate_arn" { + description = "ARN of certificate stored in ACM that other services may need to refer to. This is useful when the certificate is stored in ACM." + value = module.self_signed_cert_server.certificate_pem +} diff --git a/examples/acm/variables.tf b/examples/acm/variables.tf new file mode 100644 index 0000000..cd8ecd8 --- /dev/null +++ b/examples/acm/variables.tf @@ -0,0 +1,35 @@ +variable "region" { + description = "The region to deploy AWS resources to." + type = string +} + +variable "basic_constraints" { + description = <<-EOT + The [basic constraints](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9) of the issued certificate. + Currently, only the `CA` constraint (which identifies whether the subject of the certificate is a CA) can be set. + + Defaults to this certificate not being a CA. + EOT + type = object({ + ca = bool + }) + default = { + ca = false + } +} + +variable "certificate_backends_enabled" { + description = "Enable or disable writing to the secrets store." + type = bool +} + +variable "certificate_backends" { + description = <<-EOT + The certificate backend to use when writing secrets related to the self-signed certificate. + The value specified can either be `SSM` (AWS Systems Manager Parameter Store), `ASM` (AWS Secrets Manager), + and/or `ACM` (AWS Certificate Manager). + + Defaults to `SSM`. + EOT + type = set(string) +} \ No newline at end of file diff --git a/examples/acm/versions.tf b/examples/acm/versions.tf new file mode 100644 index 0000000..aef8d07 --- /dev/null +++ b/examples/acm/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 0.13.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 3.0" + } + } +} diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index c707ee7..aef8d07 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 3.0" } tls = { source = "hashicorp/tls" diff --git a/examples/custom_secrets/asm.us-east-1.tfvars b/examples/custom_secrets/asm.us-east-1.tfvars index c81ee26..7c96ec1 100644 --- a/examples/custom_secrets/asm.us-east-1.tfvars +++ b/examples/custom_secrets/asm.us-east-1.tfvars @@ -10,4 +10,4 @@ name = "self-signed-cert-asm" secret_path_format = "/test-asm/%s.%s" -secrets_store_type = "ASM" +certificate_backends = ["ASM"] diff --git a/examples/custom_secrets/customer-managed-key.us-east-1.tfvars b/examples/custom_secrets/customer-managed-key.us-east-1.tfvars index 5c25499..64e88b2 100644 --- a/examples/custom_secrets/customer-managed-key.us-east-1.tfvars +++ b/examples/custom_secrets/customer-managed-key.us-east-1.tfvars @@ -10,6 +10,6 @@ name = "self-signed-cert-cmk" secret_path_format = "/test-cmk/%s.%s" -secrets_store_type = "SSM" +certificate_backends = ["SSM"] create_cmk = true diff --git a/examples/custom_secrets/main.tf b/examples/custom_secrets/main.tf index d8ec3b6..7f6082c 100644 --- a/examples/custom_secrets/main.tf +++ b/examples/custom_secrets/main.tf @@ -29,14 +29,14 @@ module "self_signed_cert" { "server_auth" ] - secret_extensions = var.secret_extensions - secret_path_format = var.secret_path_format - secrets_store_type = var.secrets_store_type - secrets_store_enabled = var.secrets_store_enabled + secret_extensions = var.secret_extensions + secret_path_format = var.secret_path_format + certificate_backends = var.certificate_backends + certificate_backends_enabled = var.certificate_backends_enabled asm_recovery_window_in_days = 0 // otherwise Terratest won't be able to force destroy ASM secrets - secrets_store_kms_key_id = var.create_cmk ? join("", aws_kms_alias.cmk.*.name) : null + certificate_backend_kms_key_id = var.create_cmk ? join("", aws_kms_alias.cmk.*.name) : null context = module.this.context } diff --git a/examples/custom_secrets/no-store.us-east-1.tfvars b/examples/custom_secrets/no-store.us-east-1.tfvars index 331ba4f..346795c 100644 --- a/examples/custom_secrets/no-store.us-east-1.tfvars +++ b/examples/custom_secrets/no-store.us-east-1.tfvars @@ -10,6 +10,6 @@ name = "self-signed-cert-ssm-disabled" secret_path_format = "/ssm-test/%s.%s" -secrets_store_enabled = false +certificate_backends_enabled = false -secrets_store_type = "SSM" +certificate_backends = ["SSM"] diff --git a/examples/custom_secrets/ssm.us-east-1.tfvars b/examples/custom_secrets/ssm.us-east-1.tfvars index 51b50a1..24ab28f 100644 --- a/examples/custom_secrets/ssm.us-east-1.tfvars +++ b/examples/custom_secrets/ssm.us-east-1.tfvars @@ -10,4 +10,4 @@ name = "self-signed-cert-ssm" secret_path_format = "/test-ssm/%s.%s" -secrets_store_type = "SSM" +certificate_backends = ["SSM"] diff --git a/examples/custom_secrets/variables.tf b/examples/custom_secrets/variables.tf index 64bb0ee..2b1cc14 100644 --- a/examples/custom_secrets/variables.tf +++ b/examples/custom_secrets/variables.tf @@ -26,14 +26,20 @@ variable "secret_path_format" { type = string } -variable "secrets_store_enabled" { +variable "certificate_backends_enabled" { description = "Whether or not to write to the secrets store." type = bool default = true } -variable "secrets_store_type" { - description = "The secrets store type to use, `SSM` or `ASM`." - type = string - default = "SSM" +variable "certificate_backends" { + description = <<-EOT + The certificate backend to use when writing secrets related to the self-signed certificate. + The value specified can either be `SSM` (AWS Systems Manager Parameter Store), `ASM` (AWS Secrets Manager), + and/or `ACM` (AWS Certificate Manager). + + Defaults to `SSM`. + EOT + type = set(string) + default = ["SSM"] } diff --git a/examples/custom_secrets/versions.tf b/examples/custom_secrets/versions.tf index c707ee7..aef8d07 100644 --- a/examples/custom_secrets/versions.tf +++ b/examples/custom_secrets/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 3.0" } tls = { source = "hashicorp/tls" diff --git a/examples/preexisting_key/main.tf b/examples/preexisting_key/main.tf index 6c8c9d0..6030f08 100644 --- a/examples/preexisting_key/main.tf +++ b/examples/preexisting_key/main.tf @@ -19,7 +19,7 @@ module "self_signed_cert" { private_key_contents = var.private_key_contents private_key_algorithm = var.private_key_algorithm - secrets_store_base64_enabled = var.secrets_store_base64_enabled + certificate_backends_base64_enabled = var.certificate_backends_base64_enabled context = module.this.context } diff --git a/examples/preexisting_key/variables.tf b/examples/preexisting_key/variables.tf index 849222b..e16c661 100644 --- a/examples/preexisting_key/variables.tf +++ b/examples/preexisting_key/variables.tf @@ -13,7 +13,7 @@ variable "private_key_algorithm" { type = string } -variable "secrets_store_base64_enabled" { +variable "certificate_backends_base64_enabled" { description = "Enable or disable base64 encoding of secrets before writing them to the secrets store." type = bool default = false diff --git a/examples/preexisting_key/versions.tf b/examples/preexisting_key/versions.tf index c707ee7..aef8d07 100644 --- a/examples/preexisting_key/versions.tf +++ b/examples/preexisting_key/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 3.0" } tls = { source = "hashicorp/tls" diff --git a/main.tf b/main.tf index f424445..9de710f 100644 --- a/main.tf +++ b/main.tf @@ -1,12 +1,13 @@ locals { - enabled = module.this.enabled - create_private_key = local.enabled && try(length(var.private_key_contents), 0) == 0 - secrets_store_enabled = local.enabled && var.secrets_store_enabled - secrets_store_kms_key_id = try(length(var.secrets_store_kms_key_id), 0) > 0 ? var.secrets_store_kms_key_id : null - ssm_enabled = local.secrets_store_enabled && var.secrets_store_type == "SSM" - asm_enabled = local.secrets_store_enabled && var.secrets_store_type == "ASM" - tls_certificate = try(tls_self_signed_cert.default[0].cert_pem, null) - tls_key = try(tls_private_key.default[0].private_key_pem, var.private_key_contents) + enabled = module.this.enabled + create_private_key = local.enabled && try(length(var.private_key_contents), 0) == 0 + certificate_backends_enabled = local.enabled && var.certificate_backends_enabled + certificate_backend_kms_key_id = try(length(var.certificate_backend_kms_key_id), 0) > 0 ? var.certificate_backend_kms_key_id : null + ssm_enabled = local.certificate_backends_enabled && contains(var.certificate_backends, "SSM") + acm_enabled = local.certificate_backends_enabled && contains(var.certificate_backends, "ACM") + asm_enabled = local.certificate_backends_enabled && contains(var.certificate_backends, "ASM") + tls_certificate = try(tls_self_signed_cert.default[0].cert_pem, null) + tls_key = try(tls_private_key.default[0].private_key_pem, var.private_key_contents) } resource "tls_private_key" "default" { diff --git a/outputs.tf b/outputs.tf index abdb0be..f68acf0 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,14 +1,19 @@ output "certificate_key_path" { description = "Secrets store path containing the certificate private key file." - value = local.secrets_store_enabled ? coalesce(join("", aws_ssm_parameter.private_key.*.name), join("", aws_secretsmanager_secret.private_key.*.name)) : null + value = local.asm_enabled || local.ssm_enabled ? coalesce(join("", aws_ssm_parameter.private_key.*.name), join("", aws_secretsmanager_secret.private_key.*.name)) : null } output "certificate_pem_path" { description = "Secrets store path containing the certificate PEM file." - value = local.secrets_store_enabled ? coalesce(join("", aws_ssm_parameter.certificate.*.name), join("", aws_secretsmanager_secret.certificate.*.name)) : null + value = local.asm_enabled || local.ssm_enabled ? coalesce(join("", aws_ssm_parameter.certificate.*.name), join("", aws_secretsmanager_secret.certificate.*.name)) : null } output "certificate_pem" { description = "Contents of the certificate PEM." value = join("", tls_self_signed_cert.default.*.cert_pem) } + +output "certificate_arn" { + description = "ARN of certificate stored in ACM that other services may need to refer to. This is useful when the certificate is stored in ACM." + value = join("", aws_acm_certificate.default.*.arn) +} diff --git a/ssm.tf b/ssm.tf index dccdcfe..bd88844 100644 --- a/ssm.tf +++ b/ssm.tf @@ -3,8 +3,8 @@ resource "aws_ssm_parameter" "certificate" { name = format(var.secret_path_format, module.this.name, var.secret_extensions.certificate) type = "SecureString" - key_id = local.secrets_store_kms_key_id - value = var.secrets_store_base64_enabled ? base64encode(local.tls_certificate) : local.tls_certificate + key_id = local.certificate_backend_kms_key_id + value = var.certificate_backends_base64_enabled ? base64encode(local.tls_certificate) : local.tls_certificate tags = module.this.tags } @@ -14,8 +14,8 @@ resource "aws_ssm_parameter" "private_key" { name = format(var.secret_path_format, module.this.name, var.secret_extensions.private_key) type = "SecureString" - key_id = local.secrets_store_kms_key_id - value = var.secrets_store_base64_enabled ? base64encode(local.tls_key) : local.tls_key + key_id = local.certificate_backend_kms_key_id + value = var.certificate_backends_base64_enabled ? base64encode(local.tls_key) : local.tls_key tags = module.this.tags } diff --git a/test/src/Makefile b/test/src/Makefile index 935b3db..36a2138 100644 --- a/test/src/Makefile +++ b/test/src/Makefile @@ -16,6 +16,7 @@ init: ## Run tests test: init go mod download + go test -v -timeout 60m -run TestExamplesAcm go test -v -timeout 60m -run TestExamplesComplete go test -v -timeout 60m -run TestExamplesCustomSecrets go test -v -timeout 60m -run TestExamplesPreexistingKey diff --git a/test/src/examples_acm_test.go b/test/src/examples_acm_test.go new file mode 100644 index 0000000..8773ea1 --- /dev/null +++ b/test/src/examples_acm_test.go @@ -0,0 +1,81 @@ +package test + +import ( + "math/rand" + "strconv" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestExamplesAcm(t *testing.T) { + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: "../../examples/acm", + Upgrade: true, + // Variables to pass to our Terraform code using -var-file options + VarFiles: []string{"fixtures.us-east-1.tfvars"}, + } + + terraform.Init(t, terraformOptions) + // Run tests in parallel + t.Run("Disabled", testExamplesAcmDisabled) + t.Run("Enabled", testExamplesAcmEnabled) +} + +func testExamplesAcmDisabled(t *testing.T) { + t.Parallel() + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: "../../examples/acm", + Upgrade: true, + EnvVars: map[string]string{ + "TF_CLI_ARGS": "-state=terraform-disabled-test.tfstate", + }, + // Variables to pass to our Terraform code using -var-file options + VarFiles: []string{"fixtures.us-east-1.tfvars"}, + Vars: map[string]interface{}{ + "enabled": false, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.Apply(t, terraformOptions) +} + +func testExamplesAcmEnabled(t *testing.T) { + t.Parallel() + + rand.Seed(time.Now().UnixNano() + 1) // give a slightly different seed than the other parallel test + + attributes := []string{strconv.Itoa(rand.Intn(100000))} + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: "../../examples/acm", + Upgrade: true, + EnvVars: map[string]string{ + "TF_CLI_ARGS": "-state=terraform-enabled-test.tfstate", + }, + // Variables to pass to our Terraform code using -var-file options + VarFiles: []string{"fixtures.us-east-1.tfvars"}, + Vars: map[string]interface{}{ + "attributes": attributes, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.Apply(t, terraformOptions) + + certificateArn := terraform.Output(t, terraformOptions, "certificate_arn") + assert.NotNil(t, certificateArn) +} diff --git a/test/src/examples_preexisting_key_test.go b/test/src/examples_preexisting_key_test.go index 340748b..7a78e26 100644 --- a/test/src/examples_preexisting_key_test.go +++ b/test/src/examples_preexisting_key_test.go @@ -115,11 +115,11 @@ func testExamplesPreexistingKeyRSABase64(t *testing.T) { // Variables to pass to our Terraform code using -var-file options VarFiles: []string{"fixtures.us-east-1.tfvars"}, Vars: map[string]interface{}{ - "name": "self-signed-cert-existing-rsa-base64", - "attributes": attributes, - "private_key_contents": string(privateKeyPEM), - "private_key_algorithm": "RSA", - "secrets_store_base64_enabled": true, + "name": "self-signed-cert-existing-rsa-base64", + "attributes": attributes, + "private_key_contents": string(privateKeyPEM), + "private_key_algorithm": "RSA", + "certificate_backends_base64_enabled": true, }, } diff --git a/variables.tf b/variables.tf index 7a35f41..5f48343 100644 --- a/variables.tf +++ b/variables.tf @@ -130,13 +130,13 @@ variable "subject_alt_names" { } } -// Secrets Store Variables +// Certificate Backend Variables variable "asm_recovery_window_in_days" { description = <<-EOT Number of days that AWS Secrets Manager waits before it can delete the secret. This value can be `0` to force deletion without recovery or range from `7` to `30` days. - This value is ignored if `var.secrets_store_type` is not `ASM`, or if `var.secrets_store_enabled` is `false`. + This value is ignored if `var.certificate_backends` is not `ASM`, or if `var.certificate_backend_enabled` is `false`. EOT type = number default = 30 @@ -144,7 +144,7 @@ variable "asm_recovery_window_in_days" { variable "secret_extensions" { description = <<-EOT - The extensions use when writing secrets to the secret store. + The extensions use when writing secrets to the certificate backend. Please refer to `var.secret_path_format` for information on how secret paths are computed. EOT @@ -160,7 +160,7 @@ variable "secret_extensions" { variable "secret_path_format" { description = <<-EOT - The path format to use when writing secrets to the secret store. + The path format to use when writing secrets to the certificate backend. The certificate secret path will be computed as `format(var.secret_path_format, var.name, var.secret_extensions.certificate)` and the private key path as `format(var.secret_path_format, var.name, var.secret_extensions.private_key)`. @@ -168,7 +168,7 @@ variable "secret_path_format" { Thus by default, if `var.name`=`example-self-signed-cert`, then the resulting secret paths for the self-signed certificate's PEM file and private key will be `/example-self-signed-cert.pem` and `/example-self-signed-cert.key`, respectively. - This variable can be overridden in order to create more specific secret store paths. + This variable can be overridden in order to create more specific certificate backend paths. EOT type = string default = "/%s.%s" @@ -179,40 +179,50 @@ variable "secret_path_format" { } } -variable "secrets_store_base64_enabled" { +variable "certificate_backends_base64_enabled" { description = "Enable or disable base64 encoding of secrets before writing them to the secrets store." type = bool default = false } -variable "secrets_store_enabled" { +variable "certificate_backends_enabled" { description = "Enable or disable writing to the secrets store." type = bool default = true } -variable "secrets_store_kms_key_id" { +variable "certificate_backend_kms_key_id" { description = <<-EOT The KMD Key ID (ARN or ID) to use when encrypting either the AWS SSM Parameters or AWS Secrets Manager Secrets relating to the certificate. - If not specified, the Amazon-managed Key `alias/aws/ssm` will be used if `var.secrets_store_type` is `SSM`, - and `alias/aws/secretsmanager` will be used if `var.secrets_store_type` is `ASM`. + If not specified, the Amazon-managed Key `alias/aws/ssm` will be used if `var.certificate_backends` contains `SSM`, + and `alias/aws/secretsmanager` will be used if `var.certificate_backends` is `ASM`. EOT type = string default = null } -variable "secrets_store_type" { +variable "certificate_backends" { description = <<-EOT - The secret store type to use when writing secrets related to the self-signed certificate. - The value specified can either be `SSM` (AWS Systems Manager Parameter Store) or `ASM` (AWS Secrets Manager). + The certificate backend to use when writing secrets related to the self-signed certificate. + The value specified can either be `SSM` (AWS Systems Manager Parameter Store), `ASM` (AWS Secrets Manager), + and/or `ACM` (AWS Certificate Manager). Defaults to `SSM`. EOT - type = string - default = "SSM" + type = set(string) + default = ["SSM"] validation { - condition = contains(["SSM", "ASM"], var.secrets_store_type) - error_message = "Secrets store type one be one of: SSM, ASM." + condition = length(setintersection(["SSM", "ASM", "ACM"], var.certificate_backends)) > 0 + error_message = "Certificate backend must be one be one of: SSM, ASM, ACM." } } + +variable "certificate_chain" { + description = <<-EOT + When using ACM as a certificate backend, some certificates store a certificate chain from a CA. This CA will come from another resource. + EOT + + type = string + default = null +} \ No newline at end of file diff --git a/versions.tf b/versions.tf index c707ee7..aef8d07 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 3.0" } tls = { source = "hashicorp/tls"