I recently set myself up with a few of Cloudflare’s products to deepen my familiarity with their platform.
My exercise involved setting up a domain on Cloudflare, securing a basic website behind it using Full-Strict TLS, and implementing features like Cloudflare Tunnel, WAF rules, and rate limiting.
I then blocked direct access to the origin server and exposing an API protected with Cloudflare’s API Shield as well as that, I implemented schema validation.
The goal was to simulate a realistic customer onboarding flow while testing hands-on capabilities across the Cloudflare stack.
Domain used: gregjorg.com
The domain I wanted to use was already registered with Gandi.net (my Registrar)
My Zone File has been fully hosted in Cloudflare for over two years+
AND
Cloudflare was already set as the authoritative name server
If you are reading this blog and have not done these steps they are simple.
You do not need to transfer your domain or zone file to Cloudflare to use their products.
What you actually need is:
- Add your domain to Cloudflare in the dashboard (you’ll get a zone).
- Change your nameservers at your registrar to the ones Cloudflare provides. This delegates DNS to Cloudflare.
- Once propagation completes you can add A/AAAA/CNAME records for your app/service. (we we discuss more below)
The following articles may be useful to you if you get stuck: Cloudflare Docs Transfer your domain to Cloudflare
If you do not already have a domain you want to use: Cloudflare Docs Setup
These are the steps I took.
Step 1:
Confirmed that Cloudflare name servers (examplename.ns.cloudflare.com, anothername.ns.cloudflare.com) are active and authoritative for my domain.
Step 2:
DigitalOcean (Nginx)
Created a DigitalOcean Droplet (or similar) running Ubuntu 20.04 (or similar) I decided on Ubuntu and DigitalOcean.
You just need a basic website running on your origin server of your choice. Use your favourite 3rd party hosting platform and place it behind Cloudflare’s DNS 🙂
- I then Installed Nginx as the web server.
- I created an A record for test.gregjorg.com pointing to the server.
- I enabled Cloudflare Proxy (the orange cloud) for test.gregjorg.com.
It can be found in DNS > Records > Edit
- I set Cloudflare SSL/TLS mode to Full (Strict)
( TLS > Overview > Configure)
I then Installed a Cloudflare Origin Certificate on the server using SSH
Here, I ran into a major issue where where Nginx appeared to be serving the wrong SSL certificate. This is the correct behaviour, but I thought the certificate would have been created by Cloudflare, not Google, which it was reporting to be.
Please skip to Step 3 if you do not want to see my trouble shooting…
Symptoms:
Running curl -I https://test.gregjorg.com returned
jorgie@gregs-MacBook-Pro Downloads % curl -I https://test.gregjorg.com
HTTP/2 200
date: Mon, 03 Feb 2025 22:25:37 GMT
content-type: text/html; charset=utf-8
accept-ranges: bytes
alt-svc: h3=":443"; ma=86400
last-modified: Sun, 02 Feb 2025 19:37:53 GMT
vary: Accept-Encoding
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=MWrXATZ6tsmLzuRTJlB3FY1imWdHpcct1H5E6sFIPZtdp6R3D1GfFkHayWAZEgZRQpd%2FjbiyJeLwhUjhL6i%2BhlnUoe0aC18LAcXrN7IH6yWPbWRHq1ZwyfqlajzXMMORqxrkWw%3D%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: 90c5d680281e0691-LHR
server-timing: cfL4;desc="?proto=TCP&rtt=835&min_rtt=786&rtt_var=257&sent=5&recv=7&lost=0&retrans=0&sent_bytes=2891&recv_bytes=545&delivery_rate=3540342&cwnd=246&unsent_bytes=0&cid=24ad2c215429bac8&ts=503&x=0"
which showed Cloudflare was handling my certificate but when going to test.gregjorg.com in a browser I was presented with a Google Trust Services certificate, instead of a Cloudflare certificate.
Running:
openssl s_client -connect test.gregjorg.com:443 -servername test.gregjorg.com | openssl x509 -noout -issuer
also showed Google Trust Services.
jorgie@gregs-MacBook-Pro Downloads % openssl s_client -connect test.gregjorg.com:443 -servername test.gregjorg.com | openssl x509 -noout -issuer
depth=2 C = US, O = Google Trust Services LLC, CN = GTS Root R4
verify return:1
depth=1 C = US, O = Google Trust Services, CN = WE1
verify return:1
depth=0 CN = gregjorg.com
verify return:1
issuer= /C=US/O=Google Trust Services/CN=WE1
I assumed Cloudflare was not properly terminating SSL and was fetching SSL from the origin instead
Which didn’t make sense as this was a new virtual machine and there was no preconfigured certificates
It may have been possible that the IP given to me by Digital Ocean was previously used by another service and my certificate issue was really a caching issue but this also wasn’t the case:
My troubleshooting steps:
Checked Cloudflare Dashboard → SSL/TLS → Edge Certificates
The certificate correctly covered *.gregjorg.com meaning it should be used for test.
I disabled and re-enabled Universal SSL to force Cloudflare to regenerate the certificate
I purged Cloudflare Cache to remove any old SSL configurations
The certificate continued to say it was from Google Trust Services, not Cloudflare so I decided to switch to a brand new web server.
I decided to create a new application, and create the origin certificates again, and uploaded them to my new origin server.
I initially came to the same conclusion as before, and was reading through guides and looking at cloudflare support’s form for assistance when I physically clicked on the edge certificate drop down in TLS/SSL in my zone.
This was when I realised that the issue was not my server. But I had assumed these certificates would be from Cloudflare itself.
Cloudflare is not a publicly trusted root Certificate Authority (CA).
Step 3:
Installed Cloudflare Tunnel on the DigitalOcean Droplet.
Configured cloudflared to run as a service.
sudo apt install cloudflared
cloudflared tunnel login
- Authenticated cloudflared with Cloudflare (cloudflared tunnel login)
- Created a named Cloudflare Tunnel
- Mapped tunnel.gregjorg.com to the tunnel using a CNAME (cloudflared tunnel route dns)
- Ensured the Cloudflare DNS settings reflect this change
- Deleted any A records that exposed tunnel.gregjorg.com directly
- Restarted cloudflared and ensured it runs on boot
This is a fairly complicated step, please refer to Cloudflare Docs Create a tunnel (dashboard) for assistance with setting up the tunnel.
Step 4:
Initially, I intended to use Cloudflare’s dashboard to configure the WAF rules. However, I decided that using the Cloudflare API would:
Provide better automation and control.
Allow me to script future updates instead of making manual changes.
To make API calls, I needed authentication.
I decided to use an API Token (Bearer Authentication) since it is more secure and scoped.
To make changes to the WAF, I needed the Zone ID for gregjorg.com.
I found a Zone ID in Cloudflare’s WAF section of the dashboard, but I wanted to verify it via API.
I attempted:
curl -X GET "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer MY_API_TOKEN" \
-H "Content-Type: application/json"
but recieved an error.
The error suggested that my API Token was either incorrect or lacked necessary permissions.
Since my API Token wasn’t working, I switched to the Global API Key, which requires:
• X-Auth-Key → The admin API key.
• X-Auth-Email → My Cloudflare account email.
I now had confirmation that the zone was the correct Zone ID.
Now that I had the correct Zone ID, I switched back to my API Token.
The POST was still unsuccessful.
After some research I realised I had granted WAF permissions at the Account Level, but WAF rules need to be applied per Zone!
This was the data from the call I made to create the rule:
--data '{
"rules": [
{
"description": "Block Non-NA and Non-European Traffic",
"expression": "(not ip.geoip.country in {\\"US\\" \\"CA\\" \\"MX\\" \\"GB\\" \\"NO\\" \\"CH\\" \\"DE\\" \\"FR\\" \\"ES\\" \\"IT\\" \\"NL\\" \\"BE\\" \\"PT\\" \\"SE\\" \\"DK\\" \\"AT\\" \\"FI\\" \\"IE\\" \\"PL\\" \\"CZ\\" \\"HU\\"})",
"action": "block"
}
]
}'
For Rate Limiting;
I initially tried to apply rate limiting only to test.gregjorg.com using the expression:
(http.host eq "test.gregjorg.com")
Error: “Not entitled: The use of field http.host is not allowed, a higher Advanced Rate Limiting plan is required.”
Instead of using http.host I used:
(starts_with(http.request.uri.path, "/"))
which, I believe, applied the rate limiting rule to the entire subdomain.
Step 5:
Initially, I deployed the API at api.test.gregjorg.com, but it was not covered by Cloudflare’s Edge Certificate due to nested subdomains not being covered by *.gregjorg.com.
Fix: Moved the API to api.gregjorg.com, which was covered by the existing wildcard certificate.
Configured my server to serve an API at /health.
Tested API:
curl -I https://api.gregjorg.com/health
I initially only defined the response schema, so Cloudflare was not blocking invalid API requests.
Fix: Added requestBody validation to enforce expected input format.
Re-uploaded schema in Cloudflare API Shield.
- Configured api.gregjorg.com as the API subdomain
- Configured the server to serve the API at /health
- Ensured api.gregjorg.com is properly routed via Cloudflare (proxied)
- Uploaded a correct OpenAPI 3.0 schema in Cloudflare API Shield
- Configured Schema Validation to enforce request format
- Tested valid API requests, which returned a correct response
- Tested invalid API requests, which were blocked
This concluded the tasks I wanted to achieve, even though I ran into issue but I love issues!
Thanks for reading, if you have any questions or didn’t understand anything… I realise this guide may have been hard to follow if you have no experience with hosting or DNS.
If you have any questions feel free to reach out via my website or email me directly. Thanks!