Skip to content
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
89 changes: 89 additions & 0 deletions terraform/modules/acm_certificate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!-- BEGIN_TF_DOCS -->
<!--WARNING: GENERATED CONTENT with terraform-docs, e.g.
'terraform-docs --config "$(git rev-parse --show-toplevel)/.terraform-docs.yml" .'
Manually updating sections between TF_DOCS tags may be overwritten.
See https://terraform-docs.io/user-guide/configuration/ for more information.
-->
## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 5.0 |
| <a name="provider_tls"></a> [tls](#provider\_tls) | >= 4.0 |

<!--WARNING: GENERATED CONTENT with terraform-docs, e.g.
'terraform-docs --config "$(git rev-parse --show-toplevel)/.terraform-docs.yml" .'
Manually updating sections between TF_DOCS tags may be overwritten.
See https://terraform-docs.io/user-guide/configuration/ for more information.
-->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.6.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 5.0 |
| <a name="requirement_tls"></a> [tls](#requirement\_tls) | >= 4.0 |

<!--WARNING: GENERATED CONTENT with terraform-docs, e.g.
'terraform-docs --config "$(git rev-parse --show-toplevel)/.terraform-docs.yml" .'
Manually updating sections between TF_DOCS tags may be overwritten.
See https://terraform-docs.io/user-guide/configuration/ for more information.
-->
## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_platform"></a> [platform](#input\_platform) | Object representing the CDAP platform module. | <pre>object({<br/> app = string<br/> env = string<br/> primary_region = object({ name = string })<br/> service = string<br/> kms_alias_primary = object({<br/> target_key_arn = string<br/> })<br/> })</pre> | n/a | yes |
| <a name="input_enable_internal_endpoint"></a> [enable\_internal\_endpoint](#input\_enable\_internal\_endpoint) | Issue a PCA-backed certificate for the VPC-internal endpoint.<br/>Domain: <app>-<env>-<service>.internal<br/>Use for Lambda/ECS-to-ECS calls that do not need Zscaler or public access.<br/>Route 53 is NOT managed here — DNS for .internal is handled by CMS. | `bool` | `false` | no |
| <a name="input_enable_zscaler_endpoint"></a> [enable\_zscaler\_endpoint](#input\_enable\_zscaler\_endpoint) | Issue a PCA-backed certificate for the Zscaler-accessible endpoint.<br/>Domain: <app>-<env>-<service>.cmscloud.local<br/>Route 53 is NOT managed here — DNS for cmscloud.local is handled by CMS.<br/><br/>-------------------------------------------------------------------------<br/>CMS DOMAIN REGISTRATION — ACTION REQUIRED AFTER APPLY<br/>-------------------------------------------------------------------------<br/>After applying this module, submit a request to CMS to register:<br/> <app>-<env>-<service>.cmscloud.local<br/>and point it at the ALB DNS name from the alb module output.<br/>Use the zscaler\_domain output from this module for the request.<br/>------------------------------------------------------------------------- | `bool` | `false` | no |
| <a name="input_pca_ram_resource_share_name"></a> [pca\_ram\_resource\_share\_name](#input\_pca\_ram\_resource\_share\_name) | Name of the AWS RAM resource share providing access to the shared Private CA. Required when enable\_internal\_endpoint or enable\_zscaler\_endpoint is true. | `string` | `"pace-ca-g1"` | no |
| <a name="input_public_certificate"></a> [public\_certificate](#input\_public\_certificate) | PEM-encoded CMS-signed public certificate. Include via SOPS if provided by CMS. Set null to defer import while awaiting CMS signing. | `string` | `null` | no |
| <a name="input_public_certificate_chain"></a> [public\_certificate\_chain](#input\_public\_certificate\_chain) | PEM-encoded certificate chain. Optional — include via SOPS if provided by CMS with the signed certificate. | `string` | `null` | no |
| <a name="input_public_certificate_versions"></a> [public\_certificate\_versions](#input\_public\_certificate\_versions) | Set of active certificate versions. Add a new version number to generate a new<br/>key and CSR for renewal without deleting the previous version's parameters.<br/>Example: [1] → initial; [1, 2] → renewal in progress; [2] → old version cleaned up. | `set(number)` | <pre>[<br/> 1<br/>]</pre> | no |
| <a name="input_public_domain_name"></a> [public\_domain\_name](#input\_public\_domain\_name) | Domain name for the public endpoint. Must end in .cms.gov.<br/> -------------------------------------------------------------------------<br/> PUBLIC CERTIFICATE PROCESS — ACTION REQUIRED BEFORE CERT IS ACTIVE<br/> -------------------------------------------------------------------------<br/> 1. Run this module once without public\_certificate or public\_private\_key defined.<br/> 2. Follow output instructions to provide CMS with CSR in a zip file.<br/> 3. Once returned from CMS signed, encrypt the certificate, private key, and chain via SOPS.<br/> 4. Pass the sensitive values via SOPS into public\_certificate, public\_private\_key,<br/> and public\_certificate\_chain at module instantiation.<br/> 5. Re-apply — the module imports the cert into ACM automatically. | `string` | `null` | no |
| <a name="input_public_private_key"></a> [public\_private\_key](#input\_public\_private\_key) | PEM-encoded private key for the public certificate. Include via SOPS if provided by CMS. Set null to defer. | `string` | `null` | no |

<!--WARNING: GENERATED CONTENT with terraform-docs, e.g.
'terraform-docs --config "$(git rev-parse --show-toplevel)/.terraform-docs.yml" .'
Manually updating sections between TF_DOCS tags may be overwritten.
See https://terraform-docs.io/user-guide/configuration/ for more information.
-->
## Modules

No modules.

<!--WARNING: GENERATED CONTENT with terraform-docs, e.g.
'terraform-docs --config "$(git rev-parse --show-toplevel)/.terraform-docs.yml" .'
Manually updating sections between TF_DOCS tags may be overwritten.
See https://terraform-docs.io/user-guide/configuration/ for more information.
-->
## Resources

| Name | Type |
|------|------|
| [aws_acm_certificate.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource |
| [aws_acm_certificate.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource |
| [aws_acm_certificate_validation.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation) | resource |
| [aws_ssm_parameter.csr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.private_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [tls_cert_request.this](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/cert_request) | resource |
| [tls_private_key.this](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource |
| [aws_ram_resource_share.pace_ca](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ram_resource_share) | data source |
| [aws_route53_zone.internal](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source |
| [aws_route53_zone.zscaler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source |

<!--WARNING: GENERATED CONTENT with terraform-docs, e.g.
'terraform-docs --config "$(git rev-parse --show-toplevel)/.terraform-docs.yml" .'
Manually updating sections between TF_DOCS tags may be overwritten.
See https://terraform-docs.io/user-guide/configuration/ for more information.
-->
## Outputs

| Name | Description |
|------|-------------|
| <a name="output_csr_retrieval_instructions"></a> [csr\_retrieval\_instructions](#output\_csr\_retrieval\_instructions) | Instructions for retrieving the latest CSR and submitting to CMS. |
| <a name="output_internal_domain"></a> [internal\_domain](#output\_internal\_domain) | n/a |
| <a name="output_private_certificate_arn"></a> [private\_certificate\_arn](#output\_private\_certificate\_arn) | ARN of the PCA-issued certificate covering the internal and/or zscaler domains. Use as the primary cert on the ALB HTTPS listener. |
| <a name="output_public_certificate_arn"></a> [public\_certificate\_arn](#output\_public\_certificate\_arn) | ARN of the imported CMS-signed public certificate. Null if cert values have not yet been provided. |
| <a name="output_zscaler_domain"></a> [zscaler\_domain](#output\_zscaler\_domain) | n/a |
<!-- END_TF_DOCS -->
22 changes: 22 additions & 0 deletions terraform/modules/acm_certificate/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
locals {
hosted_zone_base_internal = "${var.platform.env}.${var.platform.app}.cmscloud.internal"
hosted_zone_base_zscaler = "${var.platform.env}.${var.platform.app}.cmscloud.local"
}

data "aws_ram_resource_share" "pace_ca" {
count = (var.enable_internal_endpoint || var.enable_zscaler_endpoint) ? 1 : 0
resource_owner = "OTHER-ACCOUNTS"
name = var.pca_ram_resource_share_name
}

data "aws_route53_zone" "internal" {
count = var.enable_internal_endpoint ? 1 : 0
name = local.hosted_zone_base_internal
private_zone = true
}

data "aws_route53_zone" "zscaler" {
count = var.enable_zscaler_endpoint ? 1 : 0
name = local.hosted_zone_base_zscaler
private_zone = true
}
116 changes: 116 additions & 0 deletions terraform/modules/acm_certificate/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
locals {
internal_domain = var.enable_internal_endpoint ? "${var.platform.service}.${trimsuffix(data.aws_route53_zone.internal[0].name, ".")}" : null
zscaler_domain = var.enable_zscaler_endpoint ? "${var.platform.service}.${trimsuffix(data.aws_route53_zone.zscaler[0].name, ".")}" : null

private_primary_domain = (
var.enable_internal_endpoint ? local.internal_domain :
var.enable_zscaler_endpoint ? local.zscaler_domain :
null
)

private_subject_alternative_names = (
var.enable_internal_endpoint && var.enable_zscaler_endpoint
) ? [local.zscaler_domain] : []
}

# -------------------------------------------------------
# PRIVATE: Issue from AWS Private CA
# -------------------------------------------------------
resource "aws_acm_certificate" "private" {
count = (var.enable_internal_endpoint || var.enable_zscaler_endpoint) ? 1 : 0

certificate_authority_arn = one(data.aws_ram_resource_share.pace_ca[0].resource_arns)
domain_name = local.private_primary_domain
subject_alternative_names = local.private_subject_alternative_names

tags = { Name = "${local.private_primary_domain}-private-cert" }

lifecycle {
prevent_destroy = true
create_before_destroy = true
}
}

# -------------------------------------------------------
# PUBLIC PATH: Import CMS-signed cert (developer note: use SOPS encrypted values)
# -------------------------------------------------------

resource "tls_private_key" "this" {
for_each = var.public_domain_name != null ? toset([for v in var.public_certificate_versions : tostring(v)]) : toset([])
algorithm = "RSA"
rsa_bits = 4096
}

resource "tls_cert_request" "this" {
for_each = var.public_domain_name != null ? toset([for v in var.public_certificate_versions : tostring(v)]) : toset([])
private_key_pem = tls_private_key.this[each.key].private_key_pem

subject {
country = "US"
province = "MD"
locality = "Rockville"
organization = "US Dept of Health and Human Services"
organizational_unit = "Centers for Medicare and Medicaid Services"
common_name = var.public_domain_name
}
}

# -------------------------------------------------------
# Store private key in SSM — encrypted with platform KMS key
# -------------------------------------------------------

resource "aws_ssm_parameter" "private_key" {
for_each = var.public_domain_name != null ? toset([for v in var.public_certificate_versions : tostring(v)]) : toset([])

name = "/${var.platform.app}/${var.platform.env}/${var.platform.service}/tls/v${each.key}/private-key"
type = "SecureString"
value = tls_private_key.this[each.key].private_key_pem
key_id = var.platform.kms_alias_primary.target_key_arn

tags = { Name = "${var.public_domain_name}-private-key" }

lifecycle {
# Prevent Terraform from overwriting the key if it already exists.
# The private key must remain stable — replacing it may invalidate signed certs.
ignore_changes = [value]
}
}

# -------------------------------------------------------
# Store CSR in SSM — plaintext is fine, CSRs are not sensitive
# Developers can retrieve this value and submit it to CMS for signing.
# -------------------------------------------------------

resource "aws_ssm_parameter" "csr" {
for_each = var.public_domain_name != null ? toset([for v in var.public_certificate_versions : tostring(v)]) : toset([])

name = "/${var.platform.app}/${var.platform.env}/${var.platform.service}/tls/v${each.key}/csr"
description = "Certificate Signing Request for ${var.public_domain_name}. Submit this to CMS for signing."
type = "String"
value = tls_cert_request.this[each.key].cert_request_pem

lifecycle {
ignore_changes = [value]
}
}


# Once cert information is provided via SOPS path, this will be set
resource "aws_acm_certificate" "public" {
count = (
var.public_domain_name != null &&
var.public_certificate != null &&
var.public_private_key != null
) ? 1 : 0

certificate_body = var.public_certificate
private_key = var.public_private_key
certificate_chain = var.public_certificate_chain

tags = { Name = var.public_domain_name }

lifecycle {
prevent_destroy = true
create_before_destroy = true
}
}
44 changes: 44 additions & 0 deletions terraform/modules/acm_certificate/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
locals {
_latest_cert_version = var.public_domain_name != null ? max(var.public_certificate_versions...) : null

csr_instructions = var.public_domain_name != null ? join("\n", [
"Latest CSR version: ${local._latest_cert_version}",
"",
"SSM path:",
" /${var.platform.app}/${var.platform.env}/${var.platform.service}/tls/v${local._latest_cert_version}/csr",
"",
"To retrieve and zip for CMS submission:",
" aws ssm get-parameter \\",
" --name \"/${var.platform.app}/${var.platform.env}/${var.platform.service}/tls/v${local._latest_cert_version}/csr\" \\",
" --query \"Parameter.Value\" \\",
" --output text > ${var.public_domain_name}.csr",
" zip ${var.public_domain_name}-v${local._latest_cert_version}-csr.zip ${var.public_domain_name}.csr",
"",
"After CMS signs the certificate, store the values via SOPS and re-apply.",
]) : null
}

output "private_certificate_arn" {
description = "ARN of the PCA-issued certificate covering the internal and/or zscaler domains. Use as the primary cert on the ALB HTTPS listener."
value = (var.enable_internal_endpoint || var.enable_zscaler_endpoint) ? aws_acm_certificate.private[0].arn : null
sensitive = true
}

output "internal_domain" {
value = var.enable_internal_endpoint ? local.internal_domain : null
}

output "zscaler_domain" {
value = var.enable_zscaler_endpoint ? local.zscaler_domain : null
}

output "public_certificate_arn" {
description = "ARN of the imported CMS-signed public certificate. Null if cert values have not yet been provided."
value = (var.public_domain_name != null && var.public_certificate != null && var.public_private_key != null) ? aws_acm_certificate.public[0].arn : null
sensitive = true
}

output "csr_retrieval_instructions" {
description = "Instructions for retrieving the latest CSR and submitting to CMS."
value = local.csr_instructions
}
Loading
Loading