Skip to content

Instantly share code, notes, and snippets.

@atrull
Last active October 4, 2023 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atrull/555b39aa305a93d5b8200cced3c7cda1 to your computer and use it in GitHub Desktop.
Save atrull/555b39aa305a93d5b8200cced3c7cda1 to your computer and use it in GitHub Desktop.
##
## Complete end to end mTLS in Cloudflare with a Vault-based CA.. and signing client certs with that
##
# This assumes you have a CA already setup in vault - I recommend a root and an intermediate
#
# We assume that you have an existing vault state that incorporates these outputs like these:
# output "prod_ca_cert" {
# value = "${vault_pki_secret_backend_root_sign_intermediate.intermediate.certificate}\n${tls_self_signed_cert.root_ca.cert_pem}"
# }
# output "prod_ca_int_path" {
# value = vault_mount.ca_int.path
# }
# output "prod_ca_int_client_signing_role" {
# value = vault_pki_secret_backend_role.role-client-cert.name
# }
# output "prod_ca_int_server_signing_role" {
# value = vault_pki_secret_backend_role.role-server-cert.name
# }
# used for details for the CA process
data "terraform_remote_state" "vault_secrets" {
backend = "s3" # etc
config = {
# your bits here
}
}
# we get the zone id from domain
data "cloudflare_zones" "ourcorp_com" {
filter {
name = "ourcorp.com"
}
}
# we get the env
data "external" "env" { program = ["jq", "-n", "env"] }
# We upload our CA cert to Cloudflare
resource "cloudflare_mtls_certificate" "prod_ca_cert" {
account_id = var.ourcorp_cloudflare_account_id
name = "OurCorp Prod CA"
certificates = data.terraform_remote_state.vault_secrets.outputs.prod_ca_cert
ca = true
}
# curl2 style hostname association - we associate the hostname with the CA cert so that we can get mTLS working
# we do it this way because the current cloudflare provider doesn't have a resource to do this that I can find.
data "curl2" "associate_ca_cert_with_login2_staging_ourcorp_com" {
http_method = "PUT"
uri = "https://api.cloudflare.com/client/v4/zones/${data.cloudflare_zones.ourcorp_com.zones[0].id}/certificate_authorities/hostname_associations"
auth_type = "Bearer"
bearer_token = data.external.env.result.CLOUDFLARE_API_TOKEN
json = jsonencode(
{
hostnames = ["login2.staging.ourcorp.com"],
mtls_certificate_id = cloudflare_mtls_certificate.prod_ca_cert.id
}
)
}
# null_resource style hostname association - probably more trustworthy but uglier
# https://developers.cloudflare.com/api/operations/client-certificate-for-a-zone-put-hostname-associations
resource "null_resource" "associate_ca_cert_with_login2_staging_ourcorp_com" {
triggers = {
always_run = "${timestamp()}"
}
provisioner "local-exec" {
Bootstrap script called with private_ip of each node in the cluster
command = <<EOT
curl --request PUT \
--url https://api.cloudflare.com/client/v4/zones/${data.cloudflare_zones.ourcorp_com.zones[0].id}/certificate_authorities/hostname_associations \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ${data.external.env.result.CLOUDFLARE_API_TOKEN}' \
--data '{ "hostnames": [ "login2.staging.ourcorp.com" ], "mtls_certificate_id": "${cloudflare_mtls_certificate.prod_ca_cert.id}" }'
EOT
}
}
# WAF rule... to enforce the client cert validity on the specific url
resource "cloudflare_ruleset" "firewall_waf_ourcorp_com" {
zone_id = data.cloudflare_zones.ourcorp_com.zones[0].id
name = "ourcorp.com Custom Firewall WAF"
description = "Zone-level WAF Custom Rules config for ourcorp.com"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
description = "mTLS-enforced authentication to login2 for Ourcorp"
enabled = true
expression = "((not cf.tls_client_auth.cert_verified or cf.tls_client_auth.cert_revoked) and (http.request.full_uri eq \"https://login2.staging.ourcorp.com\"))"
action_parameters {
response {
content = "Please provide a valid client certificate"
content_type = "text/plain"
status_code = 403
}
}
}
}
# Testing Client Cert Access
# client cert
resource "vault_pki_secret_backend_cert" "ourcorp_client_testing" {
backend = data.terraform_remote_state.vault_secrets.outputs.prod_ca_int_path
name = data.terraform_remote_state.vault_secrets.outputs.prod_ca_int_client_signing_role
common_name = "ourcorp-client-testing"
ttl = 2592000 # shortlived testing - 1 month cert
}
# local certs for ssl test
resource "local_sensitive_file" "ourcorp_client_testing_cert" {
content = vault_pki_secret_backend_cert.ourcorp_client_testing.certificate
filename = "${path.module}/ourcorp_client_testing_cert.pem"
}
resource "local_sensitive_file" "ourcorp_client_testing_private_key" {
content = vault_pki_secret_backend_cert.ourcorp_client_testing.private_key
filename = "${path.module}/ourcorp_client_testing_private_key.pem"
}
# real clients would probably have a longer cert and so forth, and ideally they provide their own CSR
# curl -sv --cert ourcorp_client_testing_cert.pem --key ourcorp_client_testing_private_key.pem https://login2.staging.ourcorp.com/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment