IT. SECURITY. OPEN SOURCE.

Category: Terraform

Terraform Cloudflare

Terraforming Cloudflare

In my previous post, I walked you through configuring AWS Route53 with DNSSEC and Terraform. Cloudflare, a SaaS security provider, provides a number of different services including web application firewall (WAF), DNS, load balancing, and zero trust. By utilizing Terraform with Cloudflare, you can automate a number of infrastructure services just like those offered at AWS.

What is Cloudflare

Cloudflare is a major security services provider. Known for their DNS and WAF services, they provide additional security services such as DDoS mitigation, zero trust, caching, and CDN. Cloudflare is also a domain registrar allowing customers to buy or transfer their domains to the service. Another benefit of using Cloudflare is their free TLS certificates. By utilizing their services, you can leverage their TLS certificates to ensure privacy and security of your site.

They provide these services by proxying web traffic. When configuring the service, you provide your FQDN and the origin’s IP address. Once configured, Cloudflare will advertise their IP address instead of the origin’s IP. This forces web traffic to run through their datacenters to scrub any malicious traffic before heading to your website.

Terraforming DNS Records

The Terraform syntax used for Route53 records are a little different than those used with Cloudflare but easy to use. First we create our zone record:

data "cloudflare_zone" "example_domain" {
  name = "example.com"
  account_id = "xxxxxxx"
}

Next we create the A record. The syntax below configures the origin IP address of 1.1.1.1 to point to example.com. To force the origin’s IP address to be proxied, set the value to true.

resource "cloudflare_record" "example_a_record" {
  zone_id = data.cloudflare_zone.example_domain.id
  name = "example.com"
  type = "A"
  ttl = "1"
  value = "1.1.1.1"
  proxied = true
}

Now create a CNAME record which points www to example.com

resource "cloudflare_record" "example_www_cname_record" {
  zone_id = data.cloudflare_zone.example_domain.id
  name = "www"
  type = "CNAME"
  value = "example.com"
  ttl = "1"
  proxied = true
}

To ensure the security of your website, you should have both the A and CNAME records proxied. This will prevent those trying to find your origin IP address. Further examples for TXT and MX records can be found in my GitLab repo.

Enhancing Security Through Terraform

Cloudflare also allows you to create firewall rules and configure security enhancements through Terraform. First, lets create the file for the security enhancements.

resource "cloudflare_zone_settings_override" "example_security" {
    zone_id = data.cloudflare_zone.example_domain.id
    settings {
        brotli = "on"
        ssl = "full"
        waf = "on"
        automatic_https_rewrites = "on"
        min_tls_version = "1.2"
        hotlink_protection = "on"
        http2 = "on"
        http3 = "on"
        mirage = "on"
        webp = "on"
        security_header {
            enabled = true
            nosniff = true
        }
    }
}

This syntax ensures that TLS v1.2 encryption is being used between the user and Cloudflare and then from Cloudflare to our origin server. It also provides protections for your images and prevents those who wish to hotlink images from your site to theirs.

Cloudflare Firewall Rules

Lastly, we need to configure firewall rules to enhance the security of our website. There are a number of ways to enhance the security of a WordPress website, those being

  • XML RPC attacks
  • Spamming your site with comments
  • Geographic IP blocks

To put these mitigations in place, we first need to create the firewall filters. To do so, create the following:

resource "cloudflare_filter" "example_geoip_filter" {
    zone_id = data.cloudflare_zone.example_domain.id
    description = "GeoIP Blocks"
    expression = "(ip.geoip.country eq \"RU\")"
}

resource "cloudflare_filter" "example_xmlrpc_filter" {
    zone_id = data.cloudflare_zone.example_domain.id
    description = "Block XML RPC"
    expression = "(http.request.uri.path contains \"/xmlrpc.php\")"
}

resource "cloudflare_filter" "example_direct_comments_filter" {
    zone_id = data.cloudflare_zone.example_domain.id
    description = "Block Direct Requests To WP Comments"
    expression = "(http.request.uri.path eq \"/wp-comments-post.php\" and http.request.method eq \"POST\" and http.referer ne \"example.com\")"
}

Next create the firewall rules

resource "cloudflare_firewall_rule" "example_geoip_rule" {
    zone_id = data.cloudflare_zone.example_domain.id
    description = "GeoIP Blocks"
    filter_id = cloudflare_filter.example_geoip_filter.id
    action = "block"
    priority = "1"
}

resource "cloudflare_firewall_rule" "example_xmlrpc_rule" {
    zone_id = data.cloudflare_zone.example_domain.id
    description = "Block XMLRPC"
    filter_id = cloudflare_filter.example_xmlrpc_filter.id
    action = "block"
    priority = "2"
}

resource "cloudflare_firewall_rule" "example_comments_rule" {
    zone_id = data.cloudflare_zone.example_domain.id
    description = "Block Direct Requests To WP Comments"
    filter_id = cloudflare_filter.example_direct_comments_filter.id
    action = "block"
    priority = "3"
}

The syntax for the rule set is straightforward. The “filter_id” is used to identify the filter rule created above. Action, what you would like to do with the filter which in this instance is “block.” “Priority” is where in the rule set this filter should be applied to. For instance, these rules go in order, “1”, “2”, “3.” This means the first rule is the GeoIP block, then XMLRPC, and then the comments filter. If you do not place the “priority” syntax in the stanza then Terraform will put the filters in whatever order it chooses.

Wrapping It Up

There are plenty of other Cloudflare configurations that can be scripted for Terraform. For the full list, check out the Cloudflare Provider Terraform documentation.

Configuring DNSSEC With Terraform and AWS Route 53

Why Enable DNSSEC?

The Domain Name Service (or DNS) has been apart of the internet since the 1980’s by combinging names to IP addresses together. Though there have been a few blog posts about DNS, we never discussed how to provide authentication to the responses you receive. Providing DNS services without authentication could lead someone to a rogue or spoofed internet site. This is where DNSSEC comes in.

A common attack vector against DNS servers is known as DNS Cache Poisoning. This happens when an attacker is allowed to send updates to zone files which do not belong to them. For instance, if I were allowed to send updates to a DNS server that belongs to Google, I could then point destination traffic to a host in my possession. I would then be able to set up sites which look identical to those of Google and steal your username and password, or capture what you look up online.

What DNSSEC Is Not

The activation of DNSSEC on your domain provides authenticity of the zone records, it does not provide confidentiality. DNSSEC does not encrypt your DNS traffic to and from the resolver. To provide encryption you must use the DNS over HTTPS (DOH), or DNS over TLS (DOT) protocol. These protocols protect your online privacy by encrypting DNS traffic to the resolver. This prevents Internet Service Providers (ISP) from sniffing your traffic.

You can also combine DOH/DOT and DNSSEC which will provide privacy and authenticity of the resolver. This is a new concept and only a few DNS providers are performing this type of service.

Getting Started

First you need to check if your Top Level Domain (TLD) supports DNSSEC.

If you own a .com domain, you are able to activate DNSSEC. For other TLD’s, please check the ICANN website.

Creating the KMS Key and Policy

The following is a template which can be used to create the KMS key and JSON policy. This will create an ECDSA P256 asymmetric key with sign and verify capabilities. Remember, DNSSEC does not provide privacy so all we need is to sign and verify the domain.

resource "aws_kms_key" "domaindnssec" {
  customer_master_key_spec = "ECC_NIST_P256"
  deletion_window_in_days  = 7
  key_usage                = "SIGN_VERIFY"
  policy = jsonencode({
    Statement = [
      {
        Action = [
          "kms:DescribeKey",
          "kms:GetPublicKey",
          "kms:Sign",
        ],
        Effect = "Allow"
        Principal = {
          Service = "dnssec-route53.amazonaws.com"
        }
        Sid      = "Allow Route 53 DNSSEC Service",
        Resource = "*"
      },
      {
        Action = "kms:CreateGrant",
        Effect = "Allow"
        Principal = {
          Service = "dnssec-route53.amazonaws.com"
        }
        Sid      = "Allow Route 53 DNSSEC Service to CreateGrant",
        Resource = "*"
        Condition = {
          Bool = {
            "kms:GrantIsForAWSResource" = "true"
          }
        }
      },
      {
        Action = "kms:*"
        Effect = "Allow"
        Principal = {
          AWS = "*"
        }
        Resource = "*"
        Sid      = "IAM User Permissions"
      },
    ]
    Version = "2012-10-17"
  })
}

Next, we define the zone and tie the keys to the zone for signature and verification.

data "aws_route53_zone" "example" {
  name = "example.com"
}

resource "aws_route53_key_signing_key" "dnssecksk" {
  name = "example.com"
  hosted_zone_id = data.aws_route53_zone.example.id
  key_management_service_arn = aws_kms_key.dnssecksk.arn
}

resource "aws_route53_hosted_zone_dnssec" "example" {
  depends_on = [
    aws_route53_key_signing_key.dnssecksk
  ]
  hosted_zone_id = aws_route53_key_signing_key.dnssecksk.hosted_zone_id
}

Verify That DNSSEC Is Working

Use dig to verify that DNSSEC is working on the domain. To get started use the following command:

dig +short +dnssec example.com

Note that the resolver being used must be capable of providing DNSSEC look ups. To verify, run the dig command against a known DNSSEC service provider like Cloudflare.

$ dig +short +dnssec cloudflare.com. @1.1.1.1
104.16.133.229
104.16.132.229
A 13 2 300 20220104184526 20220102164526 34505 cloudflare.com. T+hHkJPzWpqYHlh9qkTz9/YUzdOdOlmj5WhDytndJTEqqd9v3KJDz+Qx L1iV2ZhgvSUnV/YhPC4ccIJitS2y8A==

Now commit your code and you are all set.

Powered by WordPress & Theme by Anders Norén