diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 000000000..e61ff928d --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,15 @@ + +**/.terraform/* +*.tfstate +*.tfstate.* +crash.log +crash.*.log +*.tfvars +*.tfvars.json +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraform.tfstate.lock.info +.terraformrc +terraform.rc \ No newline at end of file diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl new file mode 100644 index 000000000..7d31fccf4 --- /dev/null +++ b/infra/.terraform.lock.hcl @@ -0,0 +1,47 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/fastly/fastly" { + version = "5.13.0" + constraints = "5.13.0" + hashes = [ + "h1:op/7hntTRkfZFIZ5xLNtLb7eBY155ywQIVSy56XCmBE=", + "zh:04f7405ee22a8ace546b90cc3a08d81f1a49dae8b1050500398d4b0244dcbc86", + "zh:0e0c48aca34a1fc7ed7382c8e85b5da770f63f3c9aa79bc2c3c55ed570f9d0ab", + "zh:302d2b9872ab8ffee2082291cc2cfec487633e22c7970b2c9d22268d6b5f7624", + "zh:346ea021dbe2c7128cddc2c9e01a95242b8bceeda20d9d1b00ae09ee90e3962b", + "zh:41fbe18f63154a6a1a46e1b1cc909bfe90f5bba7f5cfab0a80d15be7eceec4c3", + "zh:524c2a54282a92d0d7633bfd511427f6d9aa6b6a52b7d9f71cf5206dedab381e", + "zh:721fe08bfb1b85f8946aeba3bdb7e0de3d74fce94c8657d0086b153f58558d89", + "zh:9c627b3170a5505c73455e6c2a99d2ce4187e225130e12aececdc808357f8b66", + "zh:a61a62cec9612358b08ef1895277a37d4d4ec134972991fa414255ef95683dba", + "zh:bde1a51553c15d333140c2b77481ee668c4af8de93a968d869c02a736db460c4", + "zh:c2683862bd0e9633d3800503a71b3aab51ec8e3aac3f6ef6b71831efe81a2afd", + "zh:dff5ad3766432550974d2f0c24535c572fee5eeb0dce7befeaa97cb6ca3d8443", + "zh:ec3c56fc43344a07b0eef5158df6dd50e68bdcee1b03299bb2acd502d11582d5", + "zh:ec8d899cafd925d3492f00c6523c90599aebc43c1373ad4bd6c55f12d2376230", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.64.0" + constraints = "~> 5.0" + hashes = [ + "h1:YH4I78rsS9t+YoGMPNzrM53aWi0Rb9Nud16iusrSXMg=", + "zh:1d361f8062c68c9d5ac14b0aa8390709542129b8a9b258e61bbbabc706078b44", + "zh:39dcbf53e3896bdd77071384c8fad4a5862c222c73f3bcf356aca488101f22fd", + "zh:3fad63505f0c5b6f01cc9a6ef02b2226983b79424126a9caf6eb724f654299f4", + "zh:53a8b90d00829cc27e3171a13a8ff1404ee0ea018e73f31d3f916d246cc39613", + "zh:5734c25ef5a04b40f3c1ac5f817f11e42ee3328f74dbc141c0e64afbb0acc834", + "zh:66ea14dbd87f291ce4a877123363933d3ca4022f209f885807a6689c22c24e80", + "zh:68e79654ad0894a3d93134c3377748ace3058d5fad5ec09d1e9a8f8f9b8a47ea", + "zh:7b74259d0ceef0c49cea6bcd171df997b6bad141085bbadded15b440faeb0eee", + "zh:988ebfb5d115dc57070b5abf2e4200ad49cde535f27fd2ba5e34cf9ab336a57f", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a0a2d4efe2835f0101a0a5024e044a3f28c00e10a8d87fce89c707ef6db75cea", + "zh:aecb3e4b9121771dee9cac7975bf5d0657b5f3e8b57788c455beaeb0f3c48d93", + "zh:d2d3393170b8ef761d3146f39f6788c4a3e876e6c5d4cedca4870c2680688ae6", + "zh:daba5a005c1baa4a5eefbfb86d43ccf880eb5b42e8136f0d932f55886d72bda0", + "zh:de16a6ff3baacdaf9609a0a89aa1913fc19cccaf5ee0fc1c49c5a075baa47c02", + ] +} diff --git a/infra/Makefile b/infra/Makefile new file mode 100644 index 000000000..12cd13558 --- /dev/null +++ b/infra/Makefile @@ -0,0 +1,15 @@ + +.PHONY: fmt +fmt: + @tf fmt ../**/*.tf + @tf fmt ../**/*.tfvars + @tf fmt ../**/*.tftest.hcl + +.PHONY: check +check: + @tf validate + +.PHONY: yolo +yolo: + @echo "Wise, you are not..." + @tf apply -auto-approve diff --git a/infra/cdn/README.md b/infra/cdn/README.md new file mode 100644 index 000000000..f300fdfef --- /dev/null +++ b/infra/cdn/README.md @@ -0,0 +1,3 @@ +# Fastly CDN Config + +This module sets up the Fastly CDN stuff diff --git a/infra/cdn/certs/psf.io.pem b/infra/cdn/certs/psf.io.pem new file mode 100644 index 000000000..7952bb36b --- /dev/null +++ b/infra/cdn/certs/psf.io.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIUYH38nEb2KLRgscKhjcNpBLRUz+UwDQYJKoZIhvcNAQEL +BQAwgbAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZPcmVnb24xEjAQBgNVBAcMCUJl +YXZlcnRvbjEjMCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRpb24xHDAa +BgNVBAsME0luZnJhc3RydWN0dXJlIFRlYW0xDzANBgNVBAMMBlBTRl9DQTEoMCYG +CSqGSIb3DQEJARYZaW5mcmFzdHJ1Y3R1cmVAcHl0aG9uLm9yZzAeFw0yNDAyMTIx +NzU0MDZaFw0yOTAyMTAxNzU0MDZaMIGwMQswCQYDVQQGEwJVUzEPMA0GA1UECAwG +T3JlZ29uMRIwEAYDVQQHDAlCZWF2ZXJ0b24xIzAhBgNVBAoMGlB5dGhvbiBTb2Z0 +d2FyZSBGb3VuZGF0aW9uMRwwGgYDVQQLDBNJbmZyYXN0cnVjdHVyZSBUZWFtMQ8w +DQYDVQQDDAZQU0ZfQ0ExKDAmBgkqhkiG9w0BCQEWGWluZnJhc3RydWN0dXJlQHB5 +dGhvbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXAZagv2UK +AEnnnnrK/WWcZIKo/l+HTgL01XhReu9CDNs3f3ESlRT3Y4Hbla/pYRu9VM8tMGYS +xG5FGJQ2JPVnKCb3mIEC7wy9+VOaQIp3l8+o0lDQhsOZs78ZA8XQpNLD5OURsUHJ +re1U6WOTryMJwxpO+DzSBU+oSwfdn2k0BAJqSeIU45hHXeHO24z7GePuk3I1wb+E +vfhtdIF/tHvF1I6h7ntmHUeUWYrTKXKB9meMAFwEC1ZNoN1z05X68cSeK8dAsxYh +ghmQnUZ4hHH8pLlhYW/QBTol0nutwgHPyC9FIJnZzX50xAMRx3TKP1IbIehWBwF2 +CYJq6pRBZ1mfAgMBAAGjUzBRMB0GA1UdDgQWBBQrAQVRNWd6eVr6ZGn8vshzgS09 +qDAfBgNVHSMEGDAWgBQrAQVRNWd6eVr6ZGn8vshzgS09qDAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBmtyljZ1q2manMvIMEtXtc9lq3gwxIP4Pq +ic5hKuEHDSy5iN0vZRhoqfgPzXMy61zCrvLmvxv8nN2B4Us44KQRzWwDvi8SavfQ +LxRZ4KLe5Bg7MNfIKM/ZqYqHIt1FtVFYR7UyEILN/yDCyQC+8n6s8RLmT5OtZHPL +0YAyHgdao4qCICkZShbCukq81ULvkq7i6QvHWZrVGAIc/1nN71QNEUMr9KtlTKO3 +TeSd+l13+CDGwMXUpglDiFL329TmG5pKr/zoTCGDmRvEfRPtICwY3FgqGDpmIwhw +dXq0JPGHrFODeVrchUMSGqXhAZ+k/9YdJlGLbv3WJmD1GwFTs3Wf +-----END CERTIFICATE----- \ No newline at end of file diff --git a/infra/cdn/main.tf b/infra/cdn/main.tf new file mode 100644 index 000000000..3a2a4f5b8 --- /dev/null +++ b/infra/cdn/main.tf @@ -0,0 +1,346 @@ +resource "fastly_service_vcl" "python_org" { + name = var.name + default_ttl = var.default_ttl + http3 = false + stale_if_error = false + stale_if_error_ttl = 43200 + activate = true + + domain { + name = var.domain + } + + dynamic "domain" { + for_each = var.extra_domains + content { + name = domain.value + } + } + + backend { + name = "cabotage" + address = var.backend_address + port = 443 + shield = "iad-va-us" + auto_loadbalance = false + use_ssl = true + ssl_check_cert = true + ssl_cert_hostname = var.backend_address + ssl_sni_hostname = var.backend_address + weight = 100 + max_conn = 200 + connect_timeout = 1000 + first_byte_timeout = 30000 + between_bytes_timeout = 10000 + override_host = "www.python.org" + } + + backend { + name = "loadbalancer" + address = "lb.nyc1.psf.io" + shield = "iad-va-us" + healthcheck = "HAProxy Status" + auto_loadbalance = false + use_ssl = true + ssl_check_cert = true + ssl_cert_hostname = "lb.psf.io" + ssl_sni_hostname = "lb.psf.io" + ssl_ca_cert = file("${path.module}/cdn/certs/psf.io.pem") + weight = 100 + max_conn = 200 + connect_timeout = 1000 + first_byte_timeout = 15000 + between_bytes_timeout = 10000 + override_host = var.domain == "test.python.org" ? "www.python.org" : null + } + + acl { + name = "Generated_by_IP_block_list" + force_destroy = false + } + + cache_setting { + action = "pass" + cache_condition = "Force Pass No-Cache No-Store" + name = "Pass No-Cache No-Store" + stale_ttl = 0 + ttl = 0 + } + + condition { + name = "Force Pass No-Cache No-Store" + priority = 10 + statement = "beresp.http.Cache-Control ~ \"(no-cache|no-store)\"" + type = "CACHE" + } + condition { + name = "Generated by IP block list" + priority = 0 + statement = "client.ip ~ Generated_by_IP_block_list" + type = "REQUEST" + } + condition { + name = "HSTS w/ subdomains" + priority = 10 + statement = "req.http.host == \"${var.domain}\"" + type = "RESPONSE" + } + condition { + name = "HSTS w/o subdomain" + priority = 10 + statement = "req.http.host == \"${var.domain}\"" + type = "RESPONSE" + } + condition { + name = "Homepage" + priority = 10 + statement = "req.url.path ~ \"^/$\"" + type = "REQUEST" + } + condition { + name = "Is Download" + priority = 10 + statement = "req.url ~ \"^/ftp/\"" + type = "REQUEST" + } + condition { + name = "Is Not Download" + priority = 5 + statement = "req.url !~ \"^/ftp/\"" + type = "REQUEST" + } + condition { + name = "Uncacheable URLs" + priority = 10 + statement = "req.url ~ \"^/(api|admin)/\"" + type = "REQUEST" + } + condition { + name = "apex redirect" + priority = 10 + statement = "req.http.Host == \"python.org\"" + type = "RESPONSE" + } + condition { + name = "apex" + priority = 1 + statement = "req.http.host == \"python.org\"" + type = "REQUEST" + } + + gzip { + name = "Default rules" + content_types = [ + "application/javascript", + "text/css", + "application/javascript", + "text/javascript", + "application/json", + "application/vnd.ms-fontobject", + "application/x-font-opentype", + "application/x-font-truetype", + "application/x-font-ttf", + "application/xml", + "font/eot", + "font/opentype", + "font/otf", + "image/svg+xml", + "image/vnd.microsoft.icon", + "text/plain", + "text/xml", + ] + } + + header { + action = "delete" + destination = "http.Cookie" + name = "Remove cookies" + priority = 10 + request_condition = "Is Download" + type = "request" + } + header { + action = "set" + destination = "backend" + name = "Is Download Director" + priority = 10 + request_condition = "Is Download" + source = "F_loadbalancer" + type = "request" + } + header { + action = "set" + destination = "backend" + name = "Is Not Download Backend" + priority = 10 + request_condition = "Is Not Download" + source = "F_cabotage" + type = "request" + } + header { + action = "set" + destination = "http.Fastly-Token" + name = "Fastly Token" + priority = 10 + source = "\"${var.fastly_header_token}\"" + type = "request" + } + header { + action = "set" + destination = "http.Location" + name = "www redirect" + priority = 10 + response_condition = "apex redirect" + source = "\"https://www.\" + req.http.host + req.url" + type = "response" + } + header { + action = "set" + destination = "http.Strict-Transport-Security" + name = "HSTS w/ subdomains" + priority = 10 + response_condition = "HSTS w/ subdomains" + source = "\"max-age=63072000; includeSubDomains; preload\"" + type = "response" + } + header { + action = "set" + destination = "http.Strict-Transport-Security" + name = "HSTS w/o subdomains" + priority = 10 + response_condition = "HSTS w/o subdomain" + source = "\"max-age=315360000; preload\"" + type = "response" + } + header { + action = "set" + destination = "url" + name = "Chop off query string" + priority = 10 + request_condition = "Is Download" + source = "regsub(req.url, \"\\?.*$\", \"\")" + type = "request" + } + header { + action = "set" + destination = "url" + name = "Strip Query Strings" + priority = 10 + request_condition = "Homepage" + source = "req.url.path" + type = "request" + } + + healthcheck { + check_interval = 15000 + expected_response = 200 + host = var.domain + http_version = "1.1" + initial = 4 + method = "HEAD" + name = "HAProxy Status" + path = "/_haproxy_status" + threshold = 3 + timeout = 5000 + window = 5 + } + + logging_datadog { + name = "ratelimit-debug" + token = var.datadog_key + region = "US" + } + + logging_s3 { + name = "psf-fastly-logs" + bucket_name = "psf-fastly-logs-eu-west-1" + domain = "s3-eu-west-1.amazonaws.com" + path = "/${replace(var.domain, ".", "-")}/%Y/%m/%d/" + period = 3600 + gzip_level = 9 + format = "%%h \"%%{now}V\" %%l \"%%{req.request}V %%{req.url}V\" %%{req.proto}V %%>s %%{resp.http.Content-Length}V %%{resp.http.age}V \"%%{resp.http.x-cache}V\" \"%%{resp.http.x-cache-hits}V\" \"%%{req.http.content-type}V\" \"%%{req.http.accept-language}V\" \"%%{cstr_escape(req.http.user-agent)}V\"" + timestamp_format = "%Y-%m-%dT%H:%M:%S.000" + redundancy = "standard" + format_version = 2 + message_type = "classic" + s3_access_key = var.fastly_s3_logging["access_key"] + s3_secret_key = var.fastly_s3_logging["secret_key"] + } + + logging_syslog { + name = "syslog" + address = "cdn-logs.nyc1.psf.io" + port = 514 + format = "%%h \"%%{now}V\" %%l \"%%{req.request}V %%{req.url}V\" %%{req.proto}V %%>s %%{resp.http.Content-Length}V %%{resp.http.age}V \"%%{resp.http.x-cache}V\" \"%%{resp.http.x-cache-hits}V\" \"%%{req.http.content-type}V\" \"%%{req.http.accept-language}V\" \"%%{cstr_escape(req.http.user-agent)}V\"" + } + + product_enablement { + bot_management = true + brotli_compression = false + domain_inspector = true + image_optimizer = false + origin_inspector = true + websockets = false + } + + rate_limiter { + action = "log_only" + client_key = "client.ip" + feature_revision = 1 + http_methods = "GET,PUT,TRACE,POST,HEAD,DELETE,PATCH,OPTIONS" + logger_type = "datadog" + name = "${var.domain} backends" + penalty_box_duration = 2 + rps_limit = 10 + window_size = 10 + + response { + content = <<-EOT + + + Too Many Requests + + +

Too Many Requests

+ + + EOT + content_type = "text/html" + status = 429 + } + } + + request_setting { + action = null + bypass_busy_wait = false + force_ssl = true + max_stale_age = 86400 + name = "Default cache policy" + xff = "append" + } + request_setting { + action = "pass" + bypass_busy_wait = false + force_ssl = false + max_stale_age = 60 + name = "Force Pass" + request_condition = "Uncacheable URLs" + xff = "append" + } + + response_object { + name = "www redirect" + request_condition = "apex" + response = "Moved Permanently" + status = 301 + } + response_object { + content_type = "text/html" + name = "Generated by IP block list" + request_condition = "Generated by IP block list" + response = "Forbidden" + status = 403 + } + + force_destroy = true +} diff --git a/infra/cdn/providers.tf b/infra/cdn/providers.tf new file mode 100644 index 000000000..2346f041f --- /dev/null +++ b/infra/cdn/providers.tf @@ -0,0 +1,4 @@ +provider "fastly" { + alias = "cdn" + api_key = var.FASTLY_API_KEY +} diff --git a/infra/cdn/variables.tf b/infra/cdn/variables.tf new file mode 100644 index 000000000..da28835cd --- /dev/null +++ b/infra/cdn/variables.tf @@ -0,0 +1,39 @@ +variable "fastly_key" { + type = string + description = "API key for the Fastly VCL edge configuration." +} +variable "fastly_header_token" { + description = "Fastly header token ensure we only allow Fastly to access the service" + type = string + sensitive = true +} +variable "datadog_key" { + type = string + description = "API key for Datadog logging" + sensitive = true +} +variable "fastly_s3_logging" { + type = string + description = "S3 bucket keys for Fastly logging" + sensitive = true +} +variable "name" { + type = string + description = "The name of the Fastly service." +} +variable "domain" { + type = string + description = "The domain name of the service." +} +variable "extra_domains" { + type = list(string) + description = "Extra domains to add to the service." +} +variable "backend_address" { + type = string + description = "The hostname of the backend service." +} +variable "default_ttl" { + type = number + description = "The default TTL for the service." +} \ No newline at end of file diff --git a/infra/cdn/versions.tf b/infra/cdn/versions.tf new file mode 100644 index 000000000..da9c01f79 --- /dev/null +++ b/infra/cdn/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + fastly = { + source = "fastly/fastly" + version = "5.13.0" + } + } +} diff --git a/infra/config.tf b/infra/config.tf new file mode 100644 index 000000000..65b1a5210 --- /dev/null +++ b/infra/config.tf @@ -0,0 +1,9 @@ +# Connect us to TF Cloud for remote deploys +terraform { + cloud { + organization = "psf" + workspaces { + name = "pythondotorg-infra" + } + } +} diff --git a/infra/dns/README.md b/infra/dns/README.md new file mode 100644 index 000000000..7d98749e1 --- /dev/null +++ b/infra/dns/README.md @@ -0,0 +1,48 @@ +# DNS Module + +This Terraform module sets up AWS Route53 resources for managing DNS records, +supporting both apex domains and subdomains with Fastly CDN integration. + +## Features + +- Creates Route53 hosted zones for primary and user content (subdomain) domains +- Supports both apex domains and subdomains +- Automatically configures A/AAAA records for apex domains or CNAME for subdomains +- Integrates with Fastly CDN endpoints +- Provides a consistent set of name servers across hosted zones +- Allows addition of TXT records to the apex domain for verification purposes + +## Usage + +```hcl +module "CoolPythonWebsite" { + source = "./dns" + + primary_domain = "CoolPythonWebsite.com" + user_content_domain = "users.CoolPythonWebsite.com" + apex_txt = ["v=spf1 include:_spf.example.com ~all"] + tags = { + Environment = "Staging" + Project = "CoolPythonWebsite" + } +} +``` + +## Input Variables + +- `primary_domain`: Your main domain (e.g., "CoolPythonWebsite.com") +- `user_content_domain`: Domain for user-generated content (e.g., "users.CoolPythonWebsite.com") +- `apex_txt`: List of TXT records to add to the apex domain +- `tags`: Map of tags to apply to all created resources + +## Outputs + +- `primary_zone_id`: The Zone ID of the primary hosted zone +- `nameservers`: The set of name servers for your domains +- `user_content_zone_id`: The Zone ID of the user content hosted zone + +## Requirements + +Tested on +- Terraform 1.8.5 +- AWS provider ~> 5.0 diff --git a/infra/dns/main.tf b/infra/dns/main.tf new file mode 100644 index 000000000..9cdb28c83 --- /dev/null +++ b/infra/dns/main.tf @@ -0,0 +1,73 @@ +# This Terraform configuration sets up AWS Route53 resources +# ? TODO: It may be nice to allow a var when sourcing this module to +# force destroy/allow overwriting of existing resources + +# Retrieve the current AWS account data (based on secrets provided in .tfvars or TF Cloud) +data "aws_caller_identity" "current" {} + + + +# see if we're dealing with an apex domain or subdomain by splitting the domain name and counting the parts +locals { + apex_domain = length(split(".", var.domain)) > 2 ? false : true +} + +# Create a reusable set of nameservers +# This ensures consistent NS across multiple hosted zones +resource "aws_route53_delegation_set" "ns" {} + +resource "aws_route53_zone" "primary" { + name = var.domain + delegation_set_id = aws_route53_delegation_set.ns.id + tags = var.tags +} + +resource "aws_route53_zone" "user_content" { + name = var.user_content_domain + delegation_set_id = aws_route53_delegation_set.ns.id + tags = var.tags +} + +resource "aws_route53_record" "apex_txt" { + zone_id = aws_route53_zone.primary.zone_id + name = var.primary_domain + type = "TXT" + ttl = 3600 + records = var.apex_txt +} + +# Create the main DNS record, automatically choosing between A (for apex) and CNAME (for subdomain) +# depending on the value of `local.apex_domain` above +# This will point to our Fastly CDN endpoints +resource "aws_route53_record" "primary" { + zone_id = var.zone_id + name = var.domain + type = local.apex_domain ? "A" : "CNAME" + ttl = 86400 + records = var.fastly_endpoints[join("_", concat([var.domain_map[var.domain]], [local.apex_domain ? "A" : "CNAME"]))] +} + +# Same as above, but also create an AAAA record for IPv6 +resource "aws_route53_record" "primary-ipv6" { + count = local.apex_domain ? 1 : 0 + zone_id = var.zone_id + name = var.domain + type = "AAAA" + ttl = 86400 + records = var.fastly_endpoints[join("_", [var.domain_map[var.domain], "AAAA"])] +} + +# Output things we need to ref. elsewhere +output "primary_zone_id" { + value = aws_route53_record.primary.zone_id + description = "The Zone ID of our primary hosted zone." +} + +output "nameservers" { + value = aws_route53_delegation_set.ns.name_servers + description = "The set of name servers for our domains." +} + +output "user_content_zone_id" { + value = aws_route53_zone.user_content.zone_id +} \ No newline at end of file diff --git a/infra/dns/providers.tf b/infra/dns/providers.tf new file mode 100644 index 000000000..92caf800d --- /dev/null +++ b/infra/dns/providers.tf @@ -0,0 +1,6 @@ +provider "aws" { + alias = "dns" + region = "us-east-2" + access_key = var.aws_access_key + secret_key = var.aws_secret_key +} diff --git a/infra/dns/variables.tf b/infra/dns/variables.tf new file mode 100644 index 000000000..35133bef9 --- /dev/null +++ b/infra/dns/variables.tf @@ -0,0 +1,66 @@ +variable "aws_access_key" { + type = string + description = "Access key for the AWS account." + sensitive = true +} + +variable "aws_secret_key" { + type = string + description = "Secret access key for the AWS account." + sensitive = true +} + +variable "route53_zone_id" { + type = string + description = "The Route 53 hosted zone ID" + default = "Z3JUI7A2G39FQL" # python.org +} + +variable "route53_record_name" { + type = string + description = "The name of the CNAME record" + default = "test.python.org" +} + +variable "route53_record_ttl" { + type = number + description = "The TTL for the CNAME record" + default = 60 +} + +variable "tags" { + type = map(any) + description = "Tags to apply to all resources" +} +variable "primary_domain" { + type = string + description = "The primary domain name" +} +variable "user_content_domain" { + type = string + description = "The user content (sub)domain name" +} +variable "apex_txt" { + type = list(any) + description = "The TXT record for the apex domain" +} +variable "name" { + type = string + description = "The name of the Fastly service" +} +variable "zone_id" { + type = string + description = "The Route 53 hosted zone ID" +} +variable "domain" { + type = string + description = "The domain name of the service" +} +variable "fastly_endpoints" { + type = map(any) + description = "The Fastly endpoints" +} +variable "domain_map" { + type = map(any) + description = "The domain map" +} \ No newline at end of file diff --git a/infra/dns/versions.tf b/infra/dns/versions.tf new file mode 100644 index 000000000..1ac87da57 --- /dev/null +++ b/infra/dns/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 000000000..65acb05c3 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,73 @@ +locals { + tags = { + Application = "Python.org" + Environment = "Production" + } +} + +locals { + fastly_endpoints = { + "python.map.fastly.net_A" = ["151.101.128.223", "151.101.192.223", "151.101.0.223", "151.101.64.223"] + "python.map.fastly.net_AAAA" = ["2a04:4e42:200::223", "2a04:4e42:400::223", "2a04:4e42:600::223", "2a04:4e42::223"] + "python.map.fastly.net_CNAME" = ["dualstack.python.map.fastly.net"] + } + domain_map = { + "python.org" = "python.map.fastly.net" + "test.python.org" = "python.map.fastly.net" + } +} + +module "dns" { + # TODO: this doesn't accommodate for DNS management splits between environments + source = "./dns" + tags = local.tags + primary_domain = "python.org" + zone_id = module.dns.primary_zone_id + fastly_endpoints = local.fastly_endpoints + domain_map = local.domain_map + + aws_access_key = var.AWS_ACCESS_KEY_ID + aws_secret_key = var.AWS_SECRET_ACCESS_KEY + + # TODO: the below needs to be parameterized or fixed + apex_txt = [] + domain = "" + name = "" + user_content_domain = "" +} + +module "fastly_production" { + source = "./cdn" + + name = "Python.org" + domain = "python.org" + extra_domains = ["www.python.org"] + backend_address = "pythondotorg.ingress.us-east-2.psfhosted.computer" + default_ttl = 3600 + + datadog_key = var.DATADOG_API_KEY + fastly_key = var.FASTLY_API_KEY + fastly_header_token = var.FASTLY_HEADER_TOKEN + fastly_s3_logging = var.fastly_s3_logging +} + +module "fastly_staging" { + source = "./cdn" + + name = "test.Python.org" + domain = "test.python.org" + extra_domains = [] + backend_address = "test-pythondotorg.ingress.us-east-2.psfhosted.computer" + default_ttl = 3600 + + datadog_key = var.DATADOG_API_KEY + fastly_key = var.FASTLY_API_KEY + fastly_header_token = var.FASTLY_HEADER_TOKEN + fastly_s3_logging = var.fastly_s3_logging +} + + +# module "ngwaf" { +# source = "./ngwaf" +# +# } diff --git a/infra/ngwaf/README.md b/infra/ngwaf/README.md new file mode 100644 index 000000000..9d01f4ced --- /dev/null +++ b/infra/ngwaf/README.md @@ -0,0 +1,4 @@ +# Fastly's NGWAF + +This module sets up the Fastly Next-Gen Web Application Firewall (NGWAF) for our Fastly services +related to python.org / test.python.org. diff --git a/infra/ngwaf/main.tf b/infra/ngwaf/main.tf new file mode 100644 index 000000000..288c48309 --- /dev/null +++ b/infra/ngwaf/main.tf @@ -0,0 +1,149 @@ +# ? TODO: this needs reorgnization :P, was jsut trying to yank out NGWAF stuff from main CDN module +# the goal is to let cdn apply the core config, and then ingest the service ID from that +# and apply ngwaf stuff on top THEN set up signal sciences stuff on top of that + + +# ! TODO: refactor this to be dynamic depending on {test,prod}.python.org +resource "fastly_service_vcl" "ngwaf_service" { + name = var.service_name + activate = true + + domain { + name = var.USER_VCL_SERVICE_DOMAIN_NAME + comment = "NGWAF domain" + } + + backend { + address = var.USER_VCL_SERVICE_BACKEND_HOSTNAME + name = "ngwaf_backend" + port = 443 + use_ssl = true + ssl_cert_hostname = var.USER_VCL_SERVICE_BACKEND_HOSTNAME + ssl_sni_hostname = var.USER_VCL_SERVICE_BACKEND_HOSTNAME + override_host = var.USER_VCL_SERVICE_BACKEND_HOSTNAME + } + + # NGWAF Dynamic Snippets + dynamicsnippet { + name = "ngwaf_config_init" + type = "init" + priority = 0 + } + + dynamicsnippet { + name = "ngwaf_config_miss" + type = "miss" + priority = 9000 + } + + dynamicsnippet { + name = "ngwaf_config_pass" + type = "pass" + priority = 9000 + } + + dynamicsnippet { + name = "ngwaf_config_deliver" + type = "deliver" + priority = 9000 + } + + dictionary { + name = var.Edge_Security_dictionary + } + + product_enablement { + bot_management = true + } + + lifecycle { + ignore_changes = [product_enablement] + } +} + +output "ngwaf_service_id" { + value = fastly_service_vcl.ngwaf_service.id +} + +# Fastly Service Dictionary Items +resource "fastly_service_dictionary_items" "edge_security_dictionary_items" { + for_each = { + for d in fastly_service_vcl.ngwaf_service.dictionary : d.name => d if d.name == var.Edge_Security_dictionary + } + service_id = fastly_service_vcl.ngwaf_service.id + dictionary_id = each.value.dictionary_id + items = { + Enabled : "100" + } +} + +# Fastly Service Dynamic Snippet Contents +resource "fastly_service_dynamic_snippet_content" "ngwaf_config_init" { + for_each = { + for d in fastly_service_vcl.ngwaf_service.dynamicsnippet : d.name => d if d.name == "ngwaf_config_init" + } + service_id = fastly_service_vcl.ngwaf_service.id + snippet_id = each.value.snippet_id + content = "### Fastly managed ngwaf_config_init" + manage_snippets = false +} + +resource "fastly_service_dynamic_snippet_content" "ngwaf_config_miss" { + for_each = { + for d in fastly_service_vcl.ngwaf_service.dynamicsnippet : d.name => d if d.name == "ngwaf_config_miss" + } + service_id = fastly_service_vcl.ngwaf_service.id + snippet_id = each.value.snippet_id + content = "### Fastly managed ngwaf_config_miss" + manage_snippets = false +} + +resource "fastly_service_dynamic_snippet_content" "ngwaf_config_pass" { + for_each = { + for d in fastly_service_vcl.ngwaf_service.dynamicsnippet : d.name => d if d.name == "ngwaf_config_pass" + } + service_id = fastly_service_vcl.ngwaf_service.id + snippet_id = each.value.snippet_id + content = "### Fastly managed ngwaf_config_pass" + manage_snippets = false +} + +resource "fastly_service_dynamic_snippet_content" "ngwaf_config_deliver" { + for_each = { + for d in fastly_service_vcl.ngwaf_service.dynamicsnippet : d.name => d if d.name == "ngwaf_config_deliver" + } + service_id = fastly_service_vcl.ngwaf_service.id + snippet_id = each.value.snippet_id + content = "### Fastly managed ngwaf_config_deliver" + manage_snippets = false +} + +# NGWAF Edge Deployment +resource "sigsci_edge_deployment" "ngwaf_edge_site_service" { + site_short_name = var.NGWAF_SITE +} + +resource "sigsci_edge_deployment_service" "ngwaf_edge_service_link" { + site_short_name = var.NGWAF_SITE + fastly_sid = fastly_service_vcl.ngwaf_service.id + activate_version = true + percent_enabled = 100 + depends_on = [ + sigsci_edge_deployment.ngwaf_edge_site_service, + fastly_service_vcl.ngwaf_service, + fastly_service_dictionary_items.edge_security_dictionary_items, + fastly_service_dynamic_snippet_content.ngwaf_config_init, + fastly_service_dynamic_snippet_content.ngwaf_config_miss, + fastly_service_dynamic_snippet_content.ngwaf_config_pass, + fastly_service_dynamic_snippet_content.ngwaf_config_deliver, + ] +} + +resource "sigsci_edge_deployment_service_backend" "ngwaf_edge_service_backend_sync" { + site_short_name = var.NGWAF_SITE + fastly_sid = fastly_service_vcl.ngwaf_service.id + fastly_service_vcl_active_version = fastly_service_vcl.ngwaf_service.active_version + depends_on = [ + sigsci_edge_deployment_service.ngwaf_edge_service_link, + ] +} \ No newline at end of file diff --git a/infra/ngwaf/providers.tf b/infra/ngwaf/providers.tf new file mode 100644 index 000000000..c4a34abff --- /dev/null +++ b/infra/ngwaf/providers.tf @@ -0,0 +1,7 @@ +provider "sigsci" { + alias = "firewall" + corp = var.NGWAF_CORP + email = var.NGWAF_EMAIL + auth_token = var.NGWAF_TOKEN + fastly_api_key = var.FASTLY_API_KEY +} diff --git a/infra/ngwaf/variables.tf b/infra/ngwaf/variables.tf new file mode 100644 index 000000000..0cab70d93 --- /dev/null +++ b/infra/ngwaf/variables.tf @@ -0,0 +1,46 @@ +variable "service_name" { + type = string +} + +variable "USER_VCL_SERVICE_DOMAIN_NAME" { + type = string + description = "The domain name of the service." +} + +variable "USER_VCL_SERVICE_BACKEND_HOSTNAME" { + type = string + description = "The hostname of the backend service." +} + +variable "Edge_Security_dictionary" { + type = string + description = "The dictionary name for the Edge Security product." +} + +variable "NGWAF_CORP" { + type = string + description = "Corp name for NGWAF" + default = "python" +} + +variable "NGWAF_SITE" { + type = string + description = "Site SHORT name for NGWAF" + default = "test" +} + +variable "NGWAF_EMAIL" { + type = string + description = "Email address associated with the token for the NGWAF API." +} + +variable "NGWAF_TOKEN" { + type = string + description = "Secret token for the NGWAF API." + sensitive = true +} + +variable "FASTLY_API_KEY" { + type = string + description = "API key for the Fastly VCL edge configuration." +} diff --git a/infra/ngwaf/versions.tf b/infra/ngwaf/versions.tf new file mode 100644 index 000000000..f8c137ba6 --- /dev/null +++ b/infra/ngwaf/versions.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + fastly = { + source = "fastly/fastly" + version = "5.13.0" + } + sigsci = { + source = "signalsciences/sigsci" + version = "3.3.0" + } + } +} diff --git a/infra/out.tf b/infra/out.tf new file mode 100644 index 000000000..bc969b4ca --- /dev/null +++ b/infra/out.tf @@ -0,0 +1,16 @@ +output "testing-the_ngwaf" { + value = <