A few words to begin with#
In this article, I’ve chosen to share with you the infrastructure code I use to configure my services through Cloudflare, as this blog benefits from the network’s security and performance layer.
To spice it up a little, I’ve decided to present and use OpenTofu as the main tool for deploying the components I need.
Although the code remains largely compatible with Terraform, it’s also an opportunity to move out of sandbox mode and use a real example to tell you about the language’s new features.
Are you ready? Let’s go !
OpenTofu, the new standard for infrastructure as code!#
Have you ever heard of OpenTofu? Certainly briefly, with my article on best practices related to infrastructure as code and, more specifically, Terraform.
This Terraform fork was created in response to concerns in the open-source community following HashiCorp’s change of license. Initially called OpenTF, then OpenTofu, it is now in version 1.8.1 and is beginning to be widely adopted by editors such as Atlantis and Terragrunt.
Today, OpenTofu and Terraform remain largely compatible, although a few differences are becoming to emerge, particularly in the behavior of certain blocks, such as removed
. Don’t worry though, the documentation can guide you if you decide to take the plunge and transition away from Terraform.
Without making any generalisations, your code can easily be migrated to OpenTofu. The only major adjustment is to use the tofu
binary instead of terraform
. In addition, you’ll need to test the plan
command to make sure there are no unwanted changes.
OpenTofu’s strength lies in the fact that it is entirely guided by the community, not only in maintaining and improving the code, but also in implementing new features.
One feature that has always been in high demand is State encryption. This is now possible with OpenTofu version 1.7.0, using several configuration blocks. Note that the plan generated by the tofu plan
command can also be encrypted.
As for version 1.8.0, it tackles an old challenge, putting variables in backend
blocks or in the source
parameter of modules. In addition, it’s possible to mock the providers or overload resources to improve the reliability of your tests and cover your code more globally.
Last but not least, OpenTofu becomes compatible with .tofu
files while maintaining compatibility with .tf
files. For the same .tf
and .tofu
filename, OpenTofu will give priority to the .tofu
file, preserving your original Terraform code while leveraging OpenTofu’s latest features.
Not everything is perfect yet, especially regarding the signature with GPG keys of certain providers, but this will be old news with the help of the community.
As you can see, OpenTofu is destined to be a serious candidate for the future of infrastructure as code. It is supported by the Linux Foundation, giving it a strong argument for its prosperity and adoption in the open-source world.
Without a doubt, I think it’s the tool to use for new infrastructure-as-code projects, and then start migrating old ones! On your marks… ready? Migrate!
Finally, for more information, I recommend listening to this episode of Google’s Kubernetes Podcast. In it, Kaslin Fields interviews Ohad Maislish, who helped co-found OpenTofu, telling the story of the tool’s creation and the community’s enthusiasm for it. I really enjoyed this interview, which made me even more eager to take up the challenge with OpenTofu!
Cloudflare#
Protect the services you host#
Cloudflare is one of the world’s largest networks, offering a range of useful features such as a Content Delivery Network (CDN), anti-DDoS protection, extremely fast DNS and the possibility of hosting static content thanks to its free offer!
You can get advanced features with the Pro, Business or Enterprise packages. For more information, please consult this page.
All you need to do is create an account and assign one or more domain names to Cloudflare for administration.
For anyone interested in getting started, all the steps are thoroughly detailed here, providing a comprehensive overview of the platform’s capabilities.
Personally, I host several types of service, and it’s often essential to utilise components like the Web Application Firewall (WAF) to limit common attacks when exposing applications on the Internet.
Beyond the web portal, Cloudflare provides a comprehensive API documentation to address your needs. I’ll come back to this later…
In addition to this, and I think you’ve guessed it, an official provider is also available for Terraform and OpenTofu.
Cloudflare’s infrastructure as code#
Although Cloudflare has its own provider, there are sometimes resources that are not available. I’ve encountered this problem several times with the mTLS part, and I’ll talk about it below.
As mentioned above, Cloudflare has an extremely rich API library, so, when the resource doesn’t exist in the official provider, I turned to Mastercard’s provider restapi. Through the unique restapi_object
resource, you can configure API paths and methods to define their behavior when creating, deleting or even updating the service you want to use.
It’s really very convenient for my use case, and very easy to set up when the API-side documentation is rich in parameters, HTTP methods and codes, etc.
Without giving too much away, I also use other providers like hashicorp/local and hashicorp/tls to generate TLS certificates and chilicat/pkcs12 to create a .p12 file to access my mTLS-protected services after configuration.
Features and services#
Cloudflare’s free offering is a gold mine for hosting personal sites or tools, with the possibility of going quite far in protecting and improving the performance of what you expose.
The aim of this section is to detail all the Cloudflare features I use, which you’ll find “as code” later in the article.
Domain Name System (DNS)#
You can manage your DNS records within Cloudflare, but it doesn’t stop there! As a special feature, Cloudflare uses a proxy service to enhance website security and performance.
As a proxy, Cloudflare acts as an intermediary between the users who visit your sites and the services you expose. This filters traffic, blocks threats and, above all, hides the real IP address of the originating server, providing an extra layer of protection.
In addition to the security aspect, Cloudflare caches static content, speeding up page load times.
This proxy option can be deactivated with fine-grained control: The DNS record.
Web Application Firewall (WAF)#
If you have chosen to activate the DNS proxy option above, you will be able to take advantage of Cloudflare’s WAF. This operates at layer 7 of the OSI model, securing applications without modifying existing infrastructure or compromising performance.
This service uses predefined and customizable rule sets to identify and block malicious requests before they reach the origin server, i.e. where you host your services.
Another advantage is the ability to view the threats blocked for each defined rule in near real time.
Please note that the free version of Cloudflare restricts the number of rules to five, so you’ll need to combine several if you don’t want to reach this threshold too quickly.
Mutual TLS (mTLS)#
As I’ve already written a lot about it on this blog, especially on Traefik, mTLS is a security method that ensures mutual authentication between two parties, usually a client and a server. Unlike traditional TLS, where only the server is authenticated, mTLS requires both parties to present and verify their certificates, thus guaranteeing that each party is who they claim to be.
This concept is highly useful if you want to expose private services containing sensitive data.
Guess what? You can generate your own certificates within Cloudflare, and have them signed by the service’s Certificate Authority (CA). With a dedicated configuration, you can protect your sites by requesting a valid certificate from the client.
In this case, Cloudflare will check the validity of the client certificate and then provide access to the underlying servers.
E-mail routing#
Another feature, which is a bit more straightforward but sometimes very useful for masking your main contact e-mail, is the ability to e-mail forwarding by creating addresses with the domain name associated with Cloudflare.
Cloudflare’s built-in workers (a bit like a Lambda on AWS) can also be used to process e-mails. If you’d like to find out more, have a look at the documentation.
Pages#
Do you create static content with CMS such as Gatsby or Hugo? Well, Cloudflare Pages is the easy way to host them!
You’ll benefit from the high availability of the Cloudflare network, and very fast content deployment via its CDN.
Another key point: Cloudflare Pages can be configured with a code repository associated with GitLab or GitHub for a highly efficient as-code approach. It’s also possible to set up different environments depending on the Git branches, allowing you to test and validate before deploying everything on your main website.
The free offer limits the number of builds per month to 500, quite a large number for personal use.
The code, nothing less than the code!#
First steps#
Before you start doing anything, you’ll need to pull up the source code through the GitHub code repository below:
Deploy your Cloudflare configuration with OpenTofu!
Second step, you’ll need to create an API token to interact with Cloudflare’s provider, as well as Mastercard’s restapi.
To do this, navigate to Profile
> API Tokens
> Create Token
and add the following permissions:
- Account - Email Routing Addresses - Edit
- Account - Cloudflare Pages - Edit
- Account - Access: Mutual TLS Certificates - Edit
- Account - Workers Scripts - Edit
- Account - Account Settings - Read
- Account - Access: Apps and Policies - Edit
- Zone - Email Routing Rules - Edit
- Zone - Zone WAF - Edit
- Zone - Access: Apps and Policies - Edit
- Zone - Zone Settings - Edit
- Zone - Zone - Read
- Zone - SSL and Certificates - Edit
- Zone - DNS - Edit
To keep things simple, I’ve chosen to use a local backend, but you can opt for a different configuration if you prefer. For a personal project, this may make sense, especially as it’s fully encrypted.
Speaking of encryption, and this is a great new feature of OpenTofu, you’ll find the configuration in the infra/encryption.tf
file:
terraform {
encryption {
key_provider "pbkdf2" "encryption_key" {
passphrase = var.encryption_passphrase
}
method "aes_gcm" "encryption_method" {
keys = key_provider.pbkdf2.encryption_key
}
state {
method = method.aes_gcm.encryption_method
enforced = true
}
plan {
method = method.aes_gcm.encryption_method
enforced = true
}
}
}
In simple terms, the PBKDF2 provider is based on a passphrase that I’ve variabilized using a new feature included in version 1.8. The state
and plan
blocks use the encryption method defined above.
Last step, you’ll need to set several environment variables:
export CLOUDFLARE_API_TOKEN= # Your Cloudflare API token
export TF_VAR_cloudflare_api_token=$CLOUDFLARE_API_TOKEN # Cloudflare API token for the restapi provider
export TF_VAR_encryption_passphrase= # Your passphrase for state and plan encryption
You can place these in an .envrc
file within the code repository if you use direnv.
The configuration#
All the code is in the infra
folder and the configuration files are in infra/configurations
. You can take the example of the example-org.tfvars
file, duplicate it and use your domain name as the file name for better readability.
The file infra/outputs.tf
, will give you the DNSSEC configuration associated with your domain name and the IP addresses of Cloudflare servers if you wish to authorize only these from your origin servers.
Zone#
The first step is to configure the name of your zone:
zone_name = "example.org"
This will retrieve the zone’s ID and enable the creation of various resources linked to it.
Settings#
The infra/settings.tf
file contains the configuration of your zone, configured through the zone_settings
variable:
zone_settings = {
always_online = "on"
always_use_https = "on"
automatic_https_rewrites = "on"
brotli = "on"
early_hints = "on" # Improve page load speeds
email_obfuscation = "on"
fonts = "on"
http3 = "off" # Disabled to avoid mTLS issues
ip_geolocation = "on"
min_tls_version = "1.3"
ssl = "full"
rocket_loader = "on"
tls_1_3 = "zrt"
websockets = "on"
zero_rtt = "on"
}
Feel free to modify these values. Beware of the “http3” option, it must be deactivated for mTLS to work properly. This is a known limitation.
DNS#
For the DNS section, the dns_records
variable lets you add your records as objects. Please note that the proxied
parameter must be enabled if you wish to configure WAF rules for this record, as mentioned above.
dns_records = [
##
# A records
##
{ name = "server", value = "x.x.x.x", type = "A", proxied = true },
##
# CNAME records
##
{ name = "website", value = "server.example.org", type = "CNAME", proxied = true },
{ name = "prometheus", value = "server.example.org", type = "CNAME", proxied = true },
]
mTLS#
We continue with mTLS, which is also fully configurable.
To take advantage of mTLS and generate your own certificates, you need to configure a set of values such as organization, city, country, etc. These values will be injected into the Certificate Signing Request (CSR) before the certificate is signed by Cloudflare’s CA.
mtls_certificate_configuration = {
common_name = "example.org"
organization = "My organization"
locality = "My city"
postal_code = "00000"
province = ""
country = "US"
}
Furthermore, it’s generally a best practice to have a personal certificate for each user, so you can revoke it if needed. This is why the mtls_certificate_users
variable allows you to generate a separate certificate per input.
mtls_certificate_users = [
"axinorm",
]
You can then configure the domain names or subdomains that are subject to mTLS with the mtls_hostnames
variable:
mtls_hostnames = [
{ domain_name = "server.example.org", create_mtls_waf_rule = true },
]
The create_mtls_waf_rule
option lets you define a rule on the WAF side checking the validity of the certificate and ensuring its not revoked before granting access to target services.
Moreover, mtls_client_certificate_forwarding
enables a parameter that forwards two headers (Cf-Client-Cert-Der-Base64
and Cf-Client-Cert-Sha256
) to Cloudflare protected servers in the event that you wish to re-check the presence or validity of the certificate(s) within your services.
Once certificates have been generated, they can be found in the mtls-certificates
folder. You can then configure your web browsers or applications.
WAF#
For the WAF part, I have configured several types of rules:
- The first, for mTLS, is built automatically with the
mtls_hostnames
variable and thecreate_mtls_waf_rule
option set totrue
.
mtls_rule_waf_expression = <<EOT
not (
cf.tls_client_auth.cert_verified and
not cf.tls_client_auth.cert_revoked and
cf.tls_client_auth.cert_subject_dn contains "O=${var.mtls_certificate_configuration.organization}" and
cf.tls_client_auth.cert_subject_dn contains "L=${var.mtls_certificate_configuration.locality}" and
cf.tls_client_auth.cert_subject_dn contains "ST=${var.mtls_certificate_configuration.province}" and
cf.tls_client_auth.cert_subject_dn contains "C=${var.mtls_certificate_configuration.country}"
) and (
http.host in {"${join("\" \"", local.mtls_waf_rule_hostnames)}"}
)
EOT
In Cloudflare terms, for the list of protected domain names or subdomains, the certificate must be valid and must not be revoked. Additionally, various details are verified to ensure the data embedded in the certificate is accurate.
- The second rule, configured with the
waf_ip_whitelist_rule
variable, authorizes only certain defined IPs to access a list of domains.
geolocation_rule_waf_expression = <<EOT
(
not ip.geoip.country in {"${join("\" \"", var.waf_geolocation_whitelist_rule.restricted_countries)}"}
) and (
http.host in {"${join("\" \"", var.waf_geolocation_whitelist_rule.domain_names)}"}
)
EOT
- Third and last predefined rule, the ability to limit certain domains by country with the
waf_geolocation_whitelist_rule
variable. This limits the exposure of your services to a configurable list of countries.
ip_whitelist_rule_waf_expression = <<EOT
(
not ip.src in {${join(" ", var.waf_ip_whitelist_rule.restricted_ips)}}
) and (
http.host in {"${join("\" \"", var.waf_ip_whitelist_rule.domain_names)}"}
)
EOT
- We end with the
waf_custom_rules
variable. Here you can add your own custom rules to suit your needs.
Email routing#
For e-mail transfers, you can define the email_routing_rules
variable. Please note that target e-mail addresses must be verified to confirm that mail can be redirected to them.
email_routing_rules = [
{
name = "pro"
enabled = true
matcher = {
type = "literal"
field = "to"
value = "[email protected]"
}
action = {
type = "forward"
value = [
"[email protected]",
]
}
},
]
If at least one entry is present in this variable, the cloudflare_email_routing_settings
resource will automatically configure the requested DNS records.
Pages#
Last but not least, Cloudflare Pages allows you to publish static content online:
pages = [
{
name = "blog"
production_branch = "main"
custom_domains = [
{
domain = "example.org"
branch = "main"
},
{
domain = "dev.example.org"
branch = "dev"
},
]
source = {
type = "github"
config = {
owner = "my_user"
repo_name = "blog"
production_branch = "main"
pr_comments_enabled = true
deployments_enabled = true
production_deployment_enabled = true
preview_deployment_setting = "custom"
preview_branch_includes = ["dev"]
preview_branch_excludes = ["main"]
}
}
}
]
Within this example, several things:
production_branch
is used to define the main branch of your website;- The list of
custom_domains
allows you to link custom domain names to Cloudflare Pages, avoiding the use of .pages.dev URLs. It’s possible to have several domains per branch, but also to define domains for branches other than the main one. For example, if a branch of your Git repository is calleddev
, then dev.example.org will redirect to the contents of the branch;- Each entry in the
custom_domains
list will be added to and validated on the DNS record side, so that Cloudflare can redirect traffic from it. These operations are performed using thecloudflare_record.pages
andrestapi_object.pages_dns_validation
resources in theinfra/pages.tf
file.
- Each entry in the
- The configuration of GitHub or GitLab is set up in the
config
block. You will need to grant Cloudflare permission to access your code repository before you can deploy this configuration.
If you need to build the static part of your site dynamically, a build_config
block can also be defined. I recommend checking the infra/variables.tf
file for the parameters to be defined.
Restapi, more than just a provider#
Let’s finish off with MasterCard’s restapi
provider, amazing stuff I must say. It allows you to directly configure the APIs you want to use within OpenTofu or Terraform, so you don’t need an official provider if you have fairly detailed documentation on the APIs of the resources you want to create.
Example here with the mTLS inside the infra/mtls.tf
file, the official provider does not currently have a resource for creating client certificates and signing them with the Cloudflare CA. For this reason, I have configured the resource restapi_object
following the API documentation:
resource "restapi_object" "certificate" {
for_each = var.mtls_certificate_users
path = "/zones/${data.cloudflare_zone.this.id}/client_certificates"
data = jsonencode({
csr = tls_cert_request.this[each.key].cert_request_pem
validity_days = 3650
})
id_attribute = "result/id"
force_new = [
sha256(jsonencode({
csr = tls_cert_request.this[each.key].cert_request_pem
}))
]
}
While resource configuration may seem tedious, the lifecycle of resources within OpenTofu or Terraform is fully configurable. It is possible to define creation, update and deletion behaviours using the create_method
, create_path
, update_method
, update_path
fields, and so on.
I honestly recommend using it if you ever come across the same need as mine.
Deployment#
To make things quicker and easier for you, I’ve added a little script called tofutil.sh
which takes two parameters:
-a
: the action to be performed: plan, apply or destroy ;-c
: the configuration file without its .tfvars extension, for example-c example-org
.
The advantage of using it is it formats your code and validates it before any action is performed.
However, you can use the usual tofu
commands:
# Init backend and download providers
tofu init -backend-config=path= # To be configured with your backend path
# Validate your configuration
tofu validate -var-file= # To be configured with your configuration file
# Plan or Apply your configuration
tofu plan -var-file= # To be configured with your configuration file
tofu apply -var-file= # To be configured with your configuration file
Now it’s your turn!
A final word#
There’s nothing like an as-code configuration to configure Cloudflare and get to grips with OpenTofu at the same time!
Although it’s still fairly young, OpenTofu has a bright future ahead of it and is very close to the aspirations of the community, so it’s an ideal candidate in terms of as-code infrastructure tools, and importantly all for gradually replacing Terraform.
Finally, this article is the result of a challenge I set myself just before the summer, so I hope you find these few lines of code useful if, like me, you have a domain name and a few services to protect before exposing them on the Internet. :-)