diff --git a/terraform/modules/acm_certificate/README.md b/terraform/modules/acm_certificate/README.md
new file mode 100644
index 00000000..504a3d6b
--- /dev/null
+++ b/terraform/modules/acm_certificate/README.md
@@ -0,0 +1,89 @@
+
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | >= 5.0 |
+| [tls](#provider\_tls) | >= 4.0 |
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.6.0 |
+| [aws](#requirement\_aws) | >= 5.0 |
+| [tls](#requirement\_tls) | >= 4.0 |
+
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [platform](#input\_platform) | Object representing the CDAP platform module. |
object({
app = string
env = string
primary_region = object({ name = string })
service = string
kms_alias_primary = object({
target_key_arn = string
})
}) | n/a | yes |
+| [enable\_internal\_endpoint](#input\_enable\_internal\_endpoint) | Issue a PCA-backed certificate for the VPC-internal endpoint.
Domain: --.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. | `bool` | `false` | no |
+| [enable\_zscaler\_endpoint](#input\_enable\_zscaler\_endpoint) | Issue a PCA-backed certificate for the Zscaler-accessible endpoint.
Domain: --.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:
--.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.
------------------------------------------------------------------------- | `bool` | `false` | no |
+| [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 |
+| [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 |
+| [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 |
+| [public\_certificate\_versions](#input\_public\_certificate\_versions) | 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. | `set(number)` | [
1
]
| no |
+| [public\_domain\_name](#input\_public\_domain\_name) | 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. | `string` | `null` | no |
+| [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 |
+
+
+## Modules
+
+No modules.
+
+
+## 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 |
+
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [csr\_retrieval\_instructions](#output\_csr\_retrieval\_instructions) | Instructions for retrieving the latest CSR and submitting to CMS. |
+| [internal\_domain](#output\_internal\_domain) | n/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. |
+| [public\_certificate\_arn](#output\_public\_certificate\_arn) | ARN of the imported CMS-signed public certificate. Null if cert values have not yet been provided. |
+| [zscaler\_domain](#output\_zscaler\_domain) | n/a |
+
\ No newline at end of file
diff --git a/terraform/modules/acm_certificate/data.tf b/terraform/modules/acm_certificate/data.tf
new file mode 100644
index 00000000..5c5e3ec3
--- /dev/null
+++ b/terraform/modules/acm_certificate/data.tf
@@ -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
+}
diff --git a/terraform/modules/acm_certificate/main.tf b/terraform/modules/acm_certificate/main.tf
new file mode 100644
index 00000000..aa6735d6
--- /dev/null
+++ b/terraform/modules/acm_certificate/main.tf
@@ -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
+ }
+}
diff --git a/terraform/modules/acm_certificate/outputs.tf b/terraform/modules/acm_certificate/outputs.tf
new file mode 100644
index 00000000..5e4d8a1b
--- /dev/null
+++ b/terraform/modules/acm_certificate/outputs.tf
@@ -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
+}
diff --git a/terraform/modules/acm_certificate/variables.tf b/terraform/modules/acm_certificate/variables.tf
new file mode 100644
index 00000000..7a64f5d4
--- /dev/null
+++ b/terraform/modules/acm_certificate/variables.tf
@@ -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: --.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: --.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:
+ --.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."
+}
diff --git a/terraform/modules/acm_certificate/versions.tf b/terraform/modules/acm_certificate/versions.tf
new file mode 100644
index 00000000..0728cbb5
--- /dev/null
+++ b/terraform/modules/acm_certificate/versions.tf
@@ -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"
+ }
+ }
+}
diff --git a/terraform/modules/platform/variables.tf b/terraform/modules/platform/variables.tf
index c1acd7f2..1f1f812d 100644
--- a/terraform/modules/platform/variables.tf
+++ b/terraform/modules/platform/variables.tf
@@ -2,7 +2,7 @@ variable "app" {
description = "The short name for the delivery team or ADO."
type = string
validation {
- condition = contains(["ab2d", "bcda", "dpc"], var.app)
+ condition = contains(["ab2d", "bcda", "dpc", "cdap"], var.app)
error_message = "Invalid short var.app (application). Must be one of ab2d, bcda, or dpc."
}
}
diff --git a/terraform/services/github-actions-role/main.tf b/terraform/services/github-actions-role/main.tf
index 36c6b343..3ebbe950 100644
--- a/terraform/services/github-actions-role/main.tf
+++ b/terraform/services/github-actions-role/main.tf
@@ -431,7 +431,8 @@ data "aws_iam_policy_document" "github_actions_policy" {
"route53:GetChange",
"route53:GetHostedZone",
"route53:ListHostedZones",
- "route53:ListResourceRecordSets"
+ "route53:ListResourceRecordSets",
+ "route53:ListTagsForResource"
]
resources = ["*"]
}
diff --git a/terraform/services/hosted_zones/README.md b/terraform/services/hosted_zones/README.md
new file mode 100644
index 00000000..4fcb44e3
--- /dev/null
+++ b/terraform/services/hosted_zones/README.md
@@ -0,0 +1,73 @@
+
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | 5.100.0 |
+
+
+## Requirements
+
+No requirements.
+
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [app](#input\_app) | The application name (ab2d, bcda, dpc, cdap) | `string` | n/a | yes |
+| [env](#input\_env) | The application environment (dev, test, mgmt, sbx, sandbox, prod) | `string` | n/a | yes |
+
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [platform](#module\_platform) | ../../modules/platform | n/a |
+
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_route53_zone.internal](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zone) | resource |
+| [aws_route53_zone.zscaler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zone) | resource |
+
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [cms\_dns\_registration\_instructions](#output\_cms\_dns\_registration\_instructions) | Rendered instructions for registering this hosted zone with CMS DNS/networking. |
+| [internal\_hosted\_zone\_id](#output\_internal\_hosted\_zone\_id) | The Route53 Hosted Zone ID that allows developer access. Provide this to CMS DNS/networking team for zone delegation or discovery registration. |
+| [internal\_hosted\_zone\_name](#output\_internal\_hosted\_zone\_name) | The fully qualified domain name of the hosted zone accessible by VPC only. |
+| [internal\_hosted\_zone\_name\_servers](#output\_internal\_hosted\_zone\_name\_servers) | Name servers assigned to this hosted zone by Route53. |
+| [zscaler\_hosted\_zone\_id](#output\_zscaler\_hosted\_zone\_id) | The Route53 Hosted Zone ID that allows developer access. Provide this to CMS DNS/networking team for zone delegation or discovery registration. |
+| [zscaler\_hosted\_zone\_name](#output\_zscaler\_hosted\_zone\_name) | The fully qualified domain name of the zscaler-friendly hosted zone. |
+| [zscaler\_hosted\_zone\_name\_servers](#output\_zscaler\_hosted\_zone\_name\_servers) | Name servers assigned to this hosted zone by Route53. Required for public zone NS delegation — provide these to the CMS DNS team. |
+
\ No newline at end of file
diff --git a/terraform/services/hosted_zones/conf.sh b/terraform/services/hosted_zones/conf.sh
new file mode 100644
index 00000000..6e81d649
--- /dev/null
+++ b/terraform/services/hosted_zones/conf.sh
@@ -0,0 +1 @@
+TARGET_ENVS="cdap-test"
diff --git a/terraform/services/hosted_zones/main.tf b/terraform/services/hosted_zones/main.tf
new file mode 100644
index 00000000..7f892e6f
--- /dev/null
+++ b/terraform/services/hosted_zones/main.tf
@@ -0,0 +1,25 @@
+module "platform" {
+ providers = { aws = aws, aws.secondary = aws.secondary }
+
+ source = "../../modules/platform"
+ app = var.app
+ env = var.env
+ root_module = "https://github.com/CMSgov/cdap/tree/main/terraform/services/hosted-zones"
+ service = "hosted-zones"
+}
+
+resource "aws_route53_zone" "internal" {
+ name = "${var.env}.${var.app}.cmscloud.internal"
+
+ vpc {
+ vpc_id = module.platform.vpc_id
+ }
+}
+
+resource "aws_route53_zone" "zscaler" {
+ name = "${var.env}.${var.app}.cmscloud.local"
+
+ vpc {
+ vpc_id = module.platform.vpc_id
+ }
+}
diff --git a/terraform/services/hosted_zones/outputs.tf b/terraform/services/hosted_zones/outputs.tf
new file mode 100644
index 00000000..ff63baa9
--- /dev/null
+++ b/terraform/services/hosted_zones/outputs.tf
@@ -0,0 +1,58 @@
+locals {
+ zscaler_hosted_zone_instructions = <<-EOT
+ ============================================================
+ *** ACTION REQUIRED — CMS DNS Registration ***
+ ============================================================
+ Zone Name : ${aws_route53_zone.zscaler.name}
+ Zone ID : ${aws_route53_zone.zscaler.zone_id}
+ Name Servers : ${join(", ", aws_route53_zone.zscaler.name_servers)}
+ ============================================================
+
+ For hosted zones that allow access via Zscaler, you must coordinate with the CMS DNS/networking
+ team to register this zone. Provide them with:
+
+ 1. The information above
+ 2. The AWS Account ID and region where the zone is managed
+ 3. The intended use to be Zscaler resolution of internal endpoints.
+
+ Upon completion, verify the Zscaler VPC association
+ has been completed on the hosted zone configuration and that the Zscaler team has validated DNS forwarding
+ for this zone's domain suffix.
+ ============================================================
+ EOT
+}
+
+output "zscaler_hosted_zone_id" {
+ description = "The Route53 Hosted Zone ID that allows developer access. Provide this to CMS DNS/networking team for zone delegation or discovery registration."
+ value = aws_route53_zone.zscaler.zone_id
+}
+
+output "zscaler_hosted_zone_name" {
+ description = "The fully qualified domain name of the zscaler-friendly hosted zone."
+ value = aws_route53_zone.zscaler.name
+}
+
+output "zscaler_hosted_zone_name_servers" {
+ description = "Name servers assigned to this hosted zone by Route53. Required for public zone NS delegation — provide these to the CMS DNS team."
+ value = aws_route53_zone.zscaler.name_servers
+}
+
+output "cms_dns_registration_instructions" {
+ description = "Rendered instructions for registering this hosted zone with CMS DNS/networking."
+ value = local.zscaler_hosted_zone_instructions
+}
+
+output "internal_hosted_zone_id" {
+ description = "The Route53 Hosted Zone ID that allows developer access. Provide this to CMS DNS/networking team for zone delegation or discovery registration."
+ value = aws_route53_zone.internal.zone_id
+}
+
+output "internal_hosted_zone_name" {
+ description = "The fully qualified domain name of the hosted zone accessible by VPC only."
+ value = aws_route53_zone.internal.name
+}
+
+output "internal_hosted_zone_name_servers" {
+ description = "Name servers assigned to this hosted zone by Route53."
+ value = aws_route53_zone.internal.name_servers
+}
diff --git a/terraform/services/hosted_zones/tofu.tf b/terraform/services/hosted_zones/tofu.tf
new file mode 100644
index 00000000..087183da
--- /dev/null
+++ b/terraform/services/hosted_zones/tofu.tf
@@ -0,0 +1,20 @@
+provider "aws" {
+ region = "us-east-1"
+ default_tags {
+ tags = module.platform.default_tags
+ }
+}
+
+provider "aws" {
+ alias = "secondary"
+ region = "us-west-2"
+ default_tags {
+ tags = module.platform.default_tags
+ }
+}
+
+terraform {
+ backend "s3" {
+ key = "hosted_zones/terraform.tfstate"
+ }
+}
diff --git a/terraform/services/hosted_zones/variables.tf b/terraform/services/hosted_zones/variables.tf
new file mode 100644
index 00000000..336758ac
--- /dev/null
+++ b/terraform/services/hosted_zones/variables.tf
@@ -0,0 +1,17 @@
+variable "app" {
+ description = "The application name (ab2d, bcda, dpc, cdap)"
+ type = string
+ validation {
+ condition = contains(["ab2d", "bcda", "dpc", "cdap"], var.app)
+ error_message = "Valid value for app is ab2d, bcda, dpc, or cdap."
+ }
+}
+
+variable "env" {
+ description = "The application environment (dev, test, mgmt, sbx, sandbox, prod)"
+ type = string
+ validation {
+ condition = contains(["dev", "test", "mgmt", "sbx", "sandbox", "prod"], var.env)
+ error_message = "Valid value for env is dev, test, mgmt, sbx, sandbox, or prod."
+ }
+}
diff --git a/terraform/services/tftesting/acm_certificate/README.md b/terraform/services/tftesting/acm_certificate/README.md
new file mode 100644
index 00000000..9237edfd
--- /dev/null
+++ b/terraform/services/tftesting/acm_certificate/README.md
@@ -0,0 +1 @@
+This service is used when evaluating the acm_certificate module. Destroy all resources upon completion of relevant ticket.
\ No newline at end of file
diff --git a/terraform/services/tftesting/acm_certificate/main.tf b/terraform/services/tftesting/acm_certificate/main.tf
new file mode 100644
index 00000000..4ad496eb
--- /dev/null
+++ b/terraform/services/tftesting/acm_certificate/main.tf
@@ -0,0 +1,23 @@
+module "platform" {
+ providers = { aws = aws, aws.secondary = aws.secondary }
+
+ source = "../../../modules/platform"
+ app = "cdap"
+ env = "test"
+ root_module = "https://github.com/CMSgov/cdap/tree/main/terraform/services/tftesting/acm_certificate"
+ service = "tf-test-acm-certificate"
+}
+
+module "private_acm_certificate" {
+ source = "../../../modules/acm_certificate"
+ platform = module.platform
+ enable_internal_endpoint = true
+ enable_zscaler_endpoint = true
+}
+
+module "public_acm_certificate" {
+ source = "../../../modules/acm_certificate"
+ platform = module.platform
+ public_domain_name = "tftesting-acm-certificate.cdap.cms.gov"
+}
+
diff --git a/terraform/services/tftesting/acm_certificate/tofu.tf b/terraform/services/tftesting/acm_certificate/tofu.tf
new file mode 100644
index 00000000..9834ea58
--- /dev/null
+++ b/terraform/services/tftesting/acm_certificate/tofu.tf
@@ -0,0 +1,20 @@
+provider "aws" {
+ region = "us-east-1"
+ default_tags {
+ tags = module.platform.default_tags
+ }
+}
+
+provider "aws" {
+ alias = "secondary"
+ region = "us-west-2"
+ default_tags {
+ tags = module.platform.default_tags
+ }
+}
+
+terraform {
+ backend "s3" {
+ key = "tftesting/acm_certificate/terraform.tfstate"
+ }
+}
diff --git a/terraform/services/tftesting/acm_certificate/variables.tf b/terraform/services/tftesting/acm_certificate/variables.tf
new file mode 100644
index 00000000..e69de29b