From d96a05f58f2291cd4dff38dfe23a08dbd53eab8b Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 5 Jan 2024 12:19:20 -0500 Subject: [PATCH] add acmpca --- acm/README.md | 4 +- acm/main.tf | 2 +- acmpca/.terraform-docs.yml | 1 + acmpca/certificate.tf | 110 +++++++++++++++++++++++++++++++ acmpca/data.acmpca-parameters.tf | 1 + acmpca/data.tf | 1 + acmpca/defaults.tf | 1 + acmpca/locals.tf | 9 +++ acmpca/main.tf | 49 ++++++++++++++ acmpca/output.tf | 37 +++++++++++ acmpca/prefixes.tf | 1 + acmpca/variables.common.tf | 1 + acmpca/variables.tf | 97 +++++++++++++++++++++++++++ acmpca/version.tf | 1 + acmpca/versions.tf | 1 + common/defaults.tf | 8 +++ common/versions.tf | 10 +-- 17 files changed, 326 insertions(+), 8 deletions(-) create mode 120000 acmpca/.terraform-docs.yml create mode 100644 acmpca/certificate.tf create mode 120000 acmpca/data.acmpca-parameters.tf create mode 120000 acmpca/data.tf create mode 120000 acmpca/defaults.tf create mode 100644 acmpca/locals.tf create mode 100644 acmpca/main.tf create mode 100644 acmpca/output.tf create mode 120000 acmpca/prefixes.tf create mode 120000 acmpca/variables.common.tf create mode 100644 acmpca/variables.tf create mode 120000 acmpca/version.tf create mode 120000 acmpca/versions.tf diff --git a/acm/README.md b/acm/README.md index ce93e9c..8b9dbd6 100644 --- a/acm/README.md +++ b/acm/README.md @@ -1,7 +1,7 @@ # About : aws-certificate/acm -This module creates and ACM certificate, using the general purpose (ca1) ACM-PCA in the local region. It will automatically +This module creates an ACM certificate, using the general purpose (ca1) ACM-PCA in the local region. It will automatically include the DNS name in the SAN. You may add additonal SAN fully qualified domain names, but only DNS names are supported in the SAN for an ACM certificate. @@ -54,8 +54,8 @@ the ARN if completed. You'll use the ARN for an AWS LB Listener. | Name | Version | |------|---------| +| [terraform](#requirement\_terraform) | >= 0.14 | | [aws](#requirement\_aws) | >= 5.0 | -| [http](#requirement\_http) | >= 2.1.0 | | [local](#requirement\_local) | >= 2.1.0 | | [null](#requirement\_null) | >= 3.1.0 | | [tls](#requirement\_tls) | >= 3.1.0 | diff --git a/acm/main.tf b/acm/main.tf index 3f9f339..6003b3d 100644 --- a/acm/main.tf +++ b/acm/main.tf @@ -1,7 +1,7 @@ /* * # About : aws-certificate/acm * -* This module creates and ACM certificate, using the general purpose (ca1) ACM-PCA in the local region. It will automatically +* This module creates an ACM certificate, using the general purpose (ca1) ACM-PCA in the local region. It will automatically * include the DNS name in the SAN. You may add additonal SAN fully qualified domain names, but only DNS names are supported * in the SAN for an ACM certificate. * diff --git a/acmpca/.terraform-docs.yml b/acmpca/.terraform-docs.yml new file mode 120000 index 0000000..f095125 --- /dev/null +++ b/acmpca/.terraform-docs.yml @@ -0,0 +1 @@ +../.terraform-docs.yml \ No newline at end of file diff --git a/acmpca/certificate.tf b/acmpca/certificate.tf new file mode 100644 index 0000000..226ead2 --- /dev/null +++ b/acmpca/certificate.tf @@ -0,0 +1,110 @@ +locals { + cert_dns = lower(var.certificate_dns) + cert_san = distinct([for f in compact(concat([local.cert_dns], var.certificate_san)) : lower(f)]) + + ca_mode = lookup(local._defaults["mode"], var.certificate_mode, null) + ca_type = lookup(local._defaults["template"], var.cerificate_type, null) + ca_settings = var.certificate_mode == "general" ? local.ca_longterm_settings : local.ca_shortterm_settings + + output_file_directory = var.output_file_directory != null ? var.output_file_directory : format("%v/%v", path.root, "certs") + +} + +resource "tls_private_key" "certificate" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_cert_request" "certificate" { + private_key_pem = tls_private_key.certificate.private_key_pem + dns_names = local.ca_cert_san + + subject { + common_name = local.ca_dns_name + country = lookup(var.certificate_subject_override, "c", local._defaults.certificate["c"]) + organization = lookup(var.certificate_subject_override, "o", local._defaults.certificate["o"]) + organizational_unit = lookup(var.certificate_subject_override, "ou", local._defaults.certificate["ou"]) + } +} + +resource "aws_acmpca_certificate" "certificate" { + certificate_authority_arn = local.ca_settings.arn + certificate_signing_request = tls_cert_request.certificate.cert_request_pem + signing_algorithm = "SHA384WITHRSA" + validity { + type = "DAYS" + value = var.validity_days + } + template_arn = local.certificate_settings.template_arns["SubordinateCACertificate_PathLen0/V1"] + lifecycle { + create_before_destroy = true + precondition { + condition = var.certificate_mode == "general" || (var.certificate_mode == "short" && var.certificate_type == "end-entity") + error_message = "certificate_mode and certificate_type conflict." + } + precondition { + condition = var.certificate_mode == "general" || (var.certificate_mode == "short" && var.validity_days <= 7) + error_message = "certificate_mode short must have validity <= 7 days." + } + } + + # tags = merge( + # local.base_tags, + # var.tags, + # { "boc:pki:mail" = var.contact_email }, + # ) +} + +locals { + certificate_tls_key = base64encode(tls_private_key.certificate.private_key_pem) + certificate_chain = replace(aws_acmpca_certificate.certificate.certificate_chain, "/\r/", "") + certificate_crt = aws_acmpca_certificate.certificate.certificate + certificate_tls_crt = base64encode(join("\n", [local.certificate_crt, local.certificate_chain])) +} + +resource "null_source" "output_directory" { + count = var.create_files && local.output_file_directory != null ? 1 : 0 + local-exec { + command = "test -d ${local.output_file_directory} || mkdir -p ${local.output_file_directory}" + } +} + +locals { + filename_key = var.key_filename != null ? var.key_filename : format("%v/%v.%v", local.output_file_directory, local.cert_dns, "key") + filename_csr = var.csr_filename != null ? var.csr_filename : format("%v/%v.%v", local.output_file_directory, local.cert_dns, "csr") + filename_crt = var.certificate_filename != null ? var.certificate_filename : format("%v/%v.%v", local.output_file_directory, local.cert_dns, "crt") + filename_chain = var.certificate_chain_filename != null ? var.certificate_chain_filename : format("%v/%v.%v", local.output_file_directory, local.cert_dns, "chain.crt") +} + +resource "local_sensitive_file" "certificate_key" { + count = var.create_files && local.output_file_directory != null ? 1 : 0 + filename = local.filename_key + file_permission = "0644" + directory_permission = "0755" + content = tls_private_key.certificate.private_key_pem +} + +resource "local_sensitive_file" "certificate_csr" { + count = var.create_files && local.output_file_directory != null ? 1 : 0 + filename = local.filename_csr + file_permission = "0644" + directory_permission = "0755" + content = tls_cert_request.certificate.cert_request_pem +} + +resource "local_sensitive_file" "certificate_cert" { + count = var.create_files && local.output_file_directory != null ? 1 : 0 + filename = local.filename_crt + file_permission = "0644" + directory_permission = "0755" + content = aws_acmpca_certificate.certificate.certificate +} + +resource "local_sensitive_file" "certificate_cert_chain" { + count = var.create_files && local.output_file_directory != null ? 1 : 0 + filename = local.filename_chain + file_permission = "0644" + directory_permission = "0755" + content = local.certificate_chain +} + diff --git a/acmpca/data.acmpca-parameters.tf b/acmpca/data.acmpca-parameters.tf new file mode 120000 index 0000000..5e95501 --- /dev/null +++ b/acmpca/data.acmpca-parameters.tf @@ -0,0 +1 @@ +../common//data.acmpca-parameters.tf \ No newline at end of file diff --git a/acmpca/data.tf b/acmpca/data.tf new file mode 120000 index 0000000..995624d --- /dev/null +++ b/acmpca/data.tf @@ -0,0 +1 @@ +../common/data.tf \ No newline at end of file diff --git a/acmpca/defaults.tf b/acmpca/defaults.tf new file mode 120000 index 0000000..a5556ac --- /dev/null +++ b/acmpca/defaults.tf @@ -0,0 +1 @@ +../common/defaults.tf \ No newline at end of file diff --git a/acmpca/locals.tf b/acmpca/locals.tf new file mode 100644 index 0000000..2bd4d7f --- /dev/null +++ b/acmpca/locals.tf @@ -0,0 +1,9 @@ +locals { + account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id + account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew" + + base_tags = { + "boc:tf_module_version" = local._module_version + "boc:created_by" = "terraform" + } +} diff --git a/acmpca/main.tf b/acmpca/main.tf new file mode 100644 index 0000000..f67176e --- /dev/null +++ b/acmpca/main.tf @@ -0,0 +1,49 @@ +/* +* # About : aws-certificate/acmpca +* +* This module creates an ACM certificate, using the general purpose (ca1) ACM-PCA or short term (ca2) in the local region. It will automatically +* include the DNS name in the SAN. You may add additonal SAN fully qualified domain names, URIs, or +* in the SAN for an ACM certificate. The [CLI documentation](https://docs.aws.amazon.com/cli/latest/reference/acm-pca/issue-certificate.html) indicates +* you can use any of the standard types for a SAN (DNS, URI, email, DNS, etc.) along with the [API](https://docs.aws.amazon.com/privateca/latest/APIReference/API_IssueCertificate.html) +* reference. Other documentation states otherwise (TBD -- find link). +* +* It expects an SSM parameter `/enterprise/pki/ca1` for general purpose and `/enterprise/pki/ca2` for short term CA to exist in the account (distributed to all OUs from a central account). +* If this parameter does not exist, this module will fail. +* +* It returns: +* +* # Usage +* This shows the module call with how you would use it. +* +* ```hcl +* module "cert" { +* source = "git@github.e.it.census.gov:terraform-modules/aws-certificates//acm" +* +* certificate_dns = "test.domain.census.gov" +* contact_email = "cio.engineering.alert.list@census.gov" +* +* ## optional +* ## add additional names to SAN +* # certificate_san = [ "otherdomain.domain.census.gov" ] +* } +* +* # associating it with the ALB listener +* resource "aws_lb_listener" "app_443" { +* count = module.cert.certificate_arn != null ? 1 : 0 +* load_balancer_arn = aws_lb.app.arn +* port = 443 +* protocol = "HTTPS" +* ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" +* certificate_arn = module.cert.certificate_arn +* +* default_action { +* type = "forward" +* target_group_arn = aws_lb_target_group.app.arn +* } +* } +* ``` +* +* The output value to look at is `certificate_arn`. This is null if the certificate is incomplete or failed to load into ACM, or +* the ARN if completed. You'll use the ARN for an AWS LB Listener. +*/ + diff --git a/acmpca/output.tf b/acmpca/output.tf new file mode 100644 index 0000000..43afa30 --- /dev/null +++ b/acmpca/output.tf @@ -0,0 +1,37 @@ +output "certificate_key" { + description = "PEM format RSA Key" + sensitive = true + value = tls_private_key.certificate.private_key_pem +} + +output "certificate_csr" { + description = "PEM format Certificate Signing Request" + sensitive = false + value = tls_cert_request.certificate.cert_request_pem +} + +output "certificate" { + description = "PEM format for signed certificate" + sensitive = false + value = aws_acmpca_certificate.certificate.certificate +} + +output "certificate_chain" { + description = "PEM format for certificate chain (issuer through root)" + sensitive = false + value = local.certificate_chain +} + +output "certificate_files" { + description = "Map of certificate file names" + sensitive = false + value = { + enabled = var.create_files + key = local.filename_key + csr = local.filename_csr + certificate = local.filename_crt + chain = local.filename_chain + } +} + + diff --git a/acmpca/prefixes.tf b/acmpca/prefixes.tf new file mode 120000 index 0000000..7e265d5 --- /dev/null +++ b/acmpca/prefixes.tf @@ -0,0 +1 @@ +../common/prefixes.tf \ No newline at end of file diff --git a/acmpca/variables.common.tf b/acmpca/variables.common.tf new file mode 120000 index 0000000..7439ed8 --- /dev/null +++ b/acmpca/variables.common.tf @@ -0,0 +1 @@ +../common/variables.common.tf \ No newline at end of file diff --git a/acmpca/variables.tf b/acmpca/variables.tf new file mode 100644 index 0000000..1391ea4 --- /dev/null +++ b/acmpca/variables.tf @@ -0,0 +1,97 @@ +variable "certificate_dns" { + description = "DNS Name to be used for the certificate. For ACM certificate, the subject and CN may not be customized." + type = string +} + +variable "certificate_san" { + description = "The Subject Alternate Names (SAN), a list of FQDNs to include in the ACM Certificate. Only DNS names are supported. See docs at https://docs.aws.amazon.com/cli/latest/reference/acm/request-certificate.html" + type = list(string) + default = [] +} + +variable "contact_email" { + description = "Email address in @census.gov of contact for the certificate. This is strongly recommended to be a group email address." + type = string +} + + +variable "certificate_cn" { + description = "CommonName (CN) to use for certificate, defaults in c=US,o=U.S. Census Bureau,ou=Servers. This will typically be the DNS name. Uses certificate_dns if not provided." + type = string + default = null +} + +variable "certificate_subject_overrides" { + description = "Map of c, o, and ou to override certificate signing request settings. Note that only a single OU is permitted." + type = map(string) + default = {} +} + +variable "validity_days" { + description = "Number of days for which the certificate is valid. For the short lived certificate, this must be <= 7" + type = number + default = 365 + + validation { + condition = var.validity_days > 0 + error_message = "validity_days must be larger than 0." + } +} + +variable "create_files" { + description = "Flag controlling the creation of output files for the key, CSR, and certificate and bundle." + type = bool + default = false +} + +variable "output_file_directory" { + description = "File path for resultant files when create_files is used. Defaults to path.root/certs" + type = string + default = null +} + +variable "key_filename" { + description = "Filename for RSA private key. Defaults to {certificate_dns}.key" + type = string + default = null +} + +variable "csr_filename" { + description = "Filename for Certificate Signing Request (CSR). Defaults to {certificate_dns}.csr" + type = string + default = null +} + +variable "certificate_filename" { + description = "Filename for Certificate. Defaults to {certificate_dns}.crt" + type = string + default = null +} + +variable "certificate_authority_mode" { + description = "String indicating whether to use the general purpose (general) or short lived (short) CA (general is ca1, short lived is ca2)" + type = string + default = "general" + + validation { + condition = contains(["general", "short"], var.certificate_authority_mode) + error_message = "certificate_authority_mode must be one of 'general' | 'short'." + } +} + +variable "certificate_authority_template" { + description = "String indicating which specific ACMPCA template to use" + type = string + default = null +} + +variable "certificate_type" { + description = "Selection of type of certificate, either end-entity or subordinate-ca. Note that the subordinate-ca type is not available for the short lived CA mode" + type = string + default = "end-entity" + + validation { + condition = contains(["end-entity", "subordinate-ca"], var.certificate_type) + error_message = "certificate_type must be one of 'end-entity' | 'subordinate-ca'." + } +} diff --git a/acmpca/version.tf b/acmpca/version.tf new file mode 120000 index 0000000..b83c5b7 --- /dev/null +++ b/acmpca/version.tf @@ -0,0 +1 @@ +../common/version.tf \ No newline at end of file diff --git a/acmpca/versions.tf b/acmpca/versions.tf new file mode 120000 index 0000000..41bb22f --- /dev/null +++ b/acmpca/versions.tf @@ -0,0 +1 @@ +../common/versions.tf \ No newline at end of file diff --git a/common/defaults.tf b/common/defaults.tf index d847e3b..021a016 100644 --- a/common/defaults.tf +++ b/common/defaults.tf @@ -5,5 +5,13 @@ locals { "o" = "U.S. Census Bureau", "ou" = "Servers", } + "template" = { + "end-entity" = "EndEntityCertificate/V1" + "subordinate-ca" = "SubordinateCACertificate_PathLen0/V1" + } + "mode" = { + "general" = "GENERAL_PURPOSE" + "short" = "SHORT_LIVED_CERTIFICATE" + } } } diff --git a/common/versions.tf b/common/versions.tf index 6850947..6ea32cd 100644 --- a/common/versions.tf +++ b/common/versions.tf @@ -1,5 +1,5 @@ terraform { - # required_version = ">= 0.13" + required_version = ">= 0.14" required_providers { aws = { source = "hashicorp/aws" @@ -17,9 +17,9 @@ terraform { source = "hashicorp/tls" version = ">= 3.1.0" } - http = { - source = "hashicorp/http" - version = ">= 2.1.0" - } + # http = { + # source = "hashicorp/http" + # version = ">= 2.1.0" + # } } }