-
Notifications
You must be signed in to change notification settings - Fork 0
PLT-1658 Add ACM Certificate Module #440
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
Merged
Merged
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
950e22d
Establish ALB module that ingests ACM and sets up a TLS listener by d…
mianava 2f07e7b
Format
mianava f3d6a6b
Enable private and public certificate usage in existing hosted zones.
mianava 03eb72d
Generate a CSR for public certificates using tls
mianava 4666ff2
Merge branch 'main' into mia/acmcert/PLT-1658
mianava d8fc62f
Remove ALB from this PR
mianava d363640
Document ACM Certificate use.
mianava 2ab763f
Enable a hosted zone for cdap-test for evaluation.
mianava 00d28b5
Support versioned tls csrs
mianava 4123f68
Set up test file for acm tf test
mianava 6b2add3
Documentation.
mianava 4609c35
Formatting.
mianava 851e72f
Enable platform module use for cdap for testing.
mianava 6f16872
Move tf testing to its own path for local evaluation.
mianava 992ee11
Merge branch 'main' into mia/acmcert/PLT-1658
mianava f6ddbce
Remove validation as private cert does not require it.
mianava c4dddfb
Ensure github actions can manage Route53
mianava File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| locals { | ||
| hosted_zone_base_internal = "${var.platform.app}-${var.platform.env}.cmscloud.internal" | ||
| hosted_zone_base_zscaler = "${var.platform.app}-${var.platform.env}.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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| 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_domain_name_prefix}-private-cert" } | ||
|
|
||
| lifecycle { | ||
| prevent_destroy = true | ||
| create_before_destroy = true | ||
| } | ||
| } | ||
|
|
||
| resource "aws_acm_certificate_validation" "private" { | ||
| count = (var.enable_internal_endpoint || var.enable_zscaler_endpoint) ? 1 : 0 | ||
| certificate_arn = aws_acm_certificate.private[0].arn | ||
|
|
||
| timeouts { | ||
| create = "5m" | ||
| } | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # PUBLIC PATH: Import CMS-signed cert (developer note: use SOPS encrypted values) | ||
| # ------------------------------------------------------- | ||
|
|
||
| resource "tls_private_key" "this" { | ||
| for_each = var.public_domain_name != null ? var.public_certificate_versions : toset([]) | ||
| algorithm = "RSA" | ||
| rsa_bits = 4096 | ||
| } | ||
|
|
||
| resource "tls_cert_request" "this" { | ||
| for_each = var.public_domain_name != null ? var.public_certificate_versions : 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 | ||
| # It is read back by this module when importing the signed cert into ACM. | ||
| # ------------------------------------------------------- | ||
|
|
||
| resource "aws_ssm_parameter" "private_key" { | ||
| for_each = var.public_domain_name != null ? var.public_certificate_versions : toset([]) | ||
|
|
||
| name = "/${var.platform.app}/${var.platform.env}/${var.service_name}/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 invalidates 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 ? var.public_certificate_versions : toset([]) | ||
|
|
||
| name = "/${var.platform.app}/${var.platform.env}/${var.service_name}/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 = 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 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.service_name}/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.service_name}/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_validation.private[0].certificate_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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| variable "platform" { | ||
| description = "Object representing the CDAP platform module." | ||
| type = object({ | ||
| app = string | ||
| env = string | ||
| primary_region = object({ name = string }) | ||
| service = string | ||
| kms_alias_primary = object({ | ||
| target_key_arn = string | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # Internal endpoint (VPC-only, cmscloud.internal) | ||
| # ------------------------------------------------------- | ||
| variable "enable_internal_endpoint" { | ||
| type = bool | ||
| default = false | ||
| description = <<-EOT | ||
| Issue a PCA-backed certificate for the VPC-internal endpoint. | ||
| Domain: <app>-<env>-<service>.internal | ||
| Use for Lambda/ECS-to-ECS calls that do not need Zscaler or public access. | ||
| Route 53 is NOT managed here — DNS for .internal is handled by CMS. | ||
| EOT | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # Zscaler endpoint (cmscloud.local) | ||
| # ------------------------------------------------------- | ||
| variable "enable_zscaler_endpoint" { | ||
| type = bool | ||
| default = false | ||
| description = <<-EOT | ||
| Issue a PCA-backed certificate for the Zscaler-accessible endpoint. | ||
| Domain: <app>-<env>-<service>.cmscloud.local | ||
| Route 53 is NOT managed here — DNS for cmscloud.local is handled by CMS. | ||
|
|
||
| ------------------------------------------------------------------------- | ||
| CMS DOMAIN REGISTRATION — ACTION REQUIRED AFTER APPLY | ||
| ------------------------------------------------------------------------- | ||
| After applying this module, submit a request to CMS to register: | ||
| <app>-<env>-<service>.cmscloud.local | ||
| and point it at the ALB DNS name from the alb module output. | ||
| Use the zscaler_domain output from this module for the request. | ||
| ------------------------------------------------------------------------- | ||
| EOT | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # Public endpoint (*.cms.gov) | ||
| # ------------------------------------------------------- | ||
|
|
||
| variable "public_certificate_versions" { | ||
| type = set(number) | ||
| default = [1] | ||
| description = <<-EOT | ||
| Set of active certificate versions. Add a new version number to generate a new | ||
| key and CSR for renewal without deleting the previous version's parameters. | ||
| Example: [1] → initial; [1, 2] → renewal in progress; [2] → old version cleaned up. | ||
| EOT | ||
| } | ||
|
|
||
| variable "public_domain_name" { | ||
| type = string | ||
| default = null | ||
| description = <<-EOT | ||
| Domain name for the public endpoint. Must end in .cms.gov. | ||
| ------------------------------------------------------------------------- | ||
| PUBLIC CERTIFICATE PROCESS — ACTION REQUIRED BEFORE CERT IS ACTIVE | ||
| ------------------------------------------------------------------------- | ||
| 1. Run this module once without public_certificate or public_private_key defined. | ||
| 2. Follow output instructions to provide CMS with CSR in a zip file. | ||
| 3. Once returned from CMS signed, encrypt the certificate, private key, and chain via SOPS. | ||
| 4. Pass the sensitive values via SOPS into public_certificate, public_private_key, | ||
| and public_certificate_chain at module instantiation. | ||
| 5. Re-apply — the module imports the cert into ACM automatically. | ||
|
|
||
| EOT | ||
| validation { | ||
| condition = var.public_domain_name == null || endswith(var.public_domain_name, ".cms.gov") | ||
| error_message = "public_domain_name must end in .cms.gov." | ||
| } | ||
| } | ||
|
|
||
| # Cert values passed in directly — populated from platform module SOPS outputs at instantiation. | ||
| # Set to null to defer cert creation while awaiting CMS issuance. | ||
| variable "public_certificate" { | ||
| type = string | ||
| default = null | ||
| sensitive = true | ||
| description = "PEM-encoded CMS-signed public certificate. Include via SOPS if provided by CMS. Set null to defer import while awaiting CMS signing." | ||
| } | ||
|
|
||
| variable "public_private_key" { | ||
| type = string | ||
| default = null | ||
| sensitive = true | ||
| description = "PEM-encoded private key for the public certificate. Include via SOPS if provided by CMS. Set null to defer." | ||
| } | ||
|
|
||
| variable "public_certificate_chain" { | ||
| type = string | ||
| default = null | ||
| sensitive = true | ||
| description = "PEM-encoded certificate chain. Optional — include via SOPS if provided by CMS with the signed certificate." | ||
| } | ||
|
|
||
| variable "pca_ram_resource_share_name" { | ||
| type = string | ||
| default = "pace-ca-g1" | ||
| description = "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." | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| terraform { | ||
| required_version = ">= 1.6.0" | ||
|
|
||
| required_providers { | ||
| aws = { | ||
| source = "hashicorp/aws" | ||
| version = ">= 5.0" | ||
| } | ||
| tls = { | ||
| source = "hashicorp/tls" | ||
| version = ">= 4.0" | ||
| } | ||
| } | ||
| } |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| locals { | ||
| alb_name = var.name_override != null ? var.name_override : "${var.platform.app}-${var.platform.env}-${var.platform.service}-alb" | ||
|
|
||
| # Use explicitly provided subnets, or fall back to the platform's private subnets | ||
| subnet_ids = var.subnet_ids != null ? var.subnet_ids : [for s in var.platform.private_subnets : s.id] | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # Application Load Balancer | ||
| # ------------------------------------------------------- | ||
| resource "aws_lb" "this" { | ||
| name = local.alb_name | ||
| internal = var.internal | ||
| load_balancer_type = "application" | ||
| subnets = local.subnet_ids | ||
| security_groups = var.security_group_ids | ||
|
|
||
| tags = merge( | ||
| { Name = local.alb_name }, | ||
| var.platform.tags | ||
| ) | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # HTTPS Listener (port 443) | ||
| # Default action is a 404 fixed response — apps can attach | ||
| # their own listener rules with path/host conditions. | ||
| # ------------------------------------------------------- | ||
| resource "aws_lb_listener" "https" { | ||
| load_balancer_arn = aws_lb.this.arn | ||
| port = 443 | ||
| protocol = "HTTPS" | ||
| ssl_policy = var.ssl_policy | ||
| certificate_arn = var.acm_certificate_arn | ||
|
|
||
| default_action { | ||
| type = "fixed-response" | ||
| fixed_response { | ||
| content_type = "text/plain" | ||
| message_body = "Not Found" | ||
| status_code = "404" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| # ------------------------------------------------------- | ||
| # HTTP Listener (port 80) redirect | ||
| # ------------------------------------------------------- | ||
| resource "aws_lb_listener" "http_redirect" { | ||
| count = var.enable_http_redirect ? 1 : 0 | ||
|
|
||
| load_balancer_arn = aws_lb.this.arn | ||
| port = 80 | ||
| protocol = "HTTP" | ||
|
|
||
| default_action { | ||
| type = "redirect" | ||
| redirect { | ||
| port = "443" | ||
| protocol = "HTTPS" | ||
| status_code = "HTTP_301" | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.