# Cloudflare Free Tier — Proposal for Geo/Country Blocking

**To:** Caleb
**From:** Andrew
**Date:** May 25, 2026
**Re:** Replacement for checklist item #6 ("Geo/country blocking via Cloudflare")

---

## Summary

We need geo-blocking in place before the IP whitelist comes off. The original plan was Cloudflare; it briefly shifted to "use the old D7 modules" (`ip_ban` + `ip2country`), but those modules aren't ported into D11 and aren't loaded in the current codebase — not in `composer.json`, not in `web/modules/contrib/`, not enabled in `config/sync/core.extension.yml`. Porting them is more work than Cloudflare and gives weaker protection (PHP-level blocking still consumes an InMotion worker per blocked request).

This proposes going back to **Cloudflare's free tier**. The catch: it requires a few small Drupal and Apache config changes to keep the IP whitelist, reCAPTCHA, flood control, and dblog working correctly. Details below.

---

## Pros and Cons (Free Tier)

**Pros**

- Free.
- Country blocking is a single WAF rule in the Cloudflare UI — no module, no MaxMind license.
- Blocks at the edge. Abuse traffic never reaches InMotion's PHP workers, DB, or bandwidth.
- Free DDoS protection and basic bot mitigation included.
- Hides the InMotion origin IP from the public internet.
- Cloudflare's DNS is fast and free.
- Easy to roll back — change nameservers back and Cloudflare is out of the picture.

**Cons**

- Nameservers move from wherever they live now (likely the registrar or InMotion) to Cloudflare. DNS management happens in Cloudflare's dashboard going forward.
- Cloudflare terminates SSL at their edge and re-encrypts to the origin. They can technically see traffic in the clear inside their network. Acceptable for our data; flagging for completeness.
- If Cloudflare itself has an outage, ccsoccer.com is down. Historically rare (a couple of hours a year).
- Free tier caps custom firewall rules at five. Plenty for this use case.
- VPN users in the US can occasionally trip rules; users outside the US can trivially bypass a country block via VPN. This stops mass automated abuse, not a determined individual attacker.

## Ownership

The domain registration stays where it is. Cloudflare doesn't own ccsoccer.com, can't transfer it, can't hold it hostage. What changes is the *nameserver* field at the registrar — it points at Cloudflare's nameservers instead of where it points now. If we ever want off Cloudflare, we change the nameservers back and wait for DNS to propagate. No vendor lock-in.

## The .htaccess Whitelist — Coexistence

With Cloudflare in front, every request that reaches InMotion comes from a Cloudflare IP. The real client IP is in the `CF-Connecting-IP` header (and `X-Forwarded-For`). The current `.htaccess` whitelist matches against Apache's `REMOTE_ADDR`, so without intervention it sees only Cloudflare IPs — and either lets everyone through or blocks everyone, depending on whether Cloudflare's ranges happen to be on the list.

Two options for keeping it working through the soft-launch period:

**Option A (recommended): move the whitelist into Cloudflare.** The country block and the IP whitelist both live as Cloudflare WAF rules. `.htaccess` goes back to stock content (still skip-worktree'd, just not whitelisting). One source of truth, simpler to reason about.

**Option B: keep the whitelist in `.htaccess`, fix it to read the real IP.** Enable `mod_remoteip` on InMotion and configure it with Cloudflare's IP ranges as trusted proxies. That rewrites `REMOTE_ADDR` to the real client IP for the rest of the request, so existing rules work unchanged.

Either is fine. A is cleaner. B is closer to the current setup.

---

## Drupal-Side Configuration (Required Either Way)

Whether we choose Option A or B, Drupal itself needs to know it's behind a reverse proxy, or it will see every visitor as the same Cloudflare IP and several things will misbehave.

**What breaks without this:**

- **Login flood control** — Drupal locks out an IP after N failed logins in a window. If every user looks like a Cloudflare IP, one user's bad-password attempts can lock out the entire user base. Our current limit would be hit in minutes.
- **reCAPTCHA** — the recaptcha module passes the user's IP to Google as part of the verification call. With everyone showing as Cloudflare, Google's risk model can't distinguish users and may start flagging the whole site as suspicious, hurting CAPTCHA scoring for legitimate users.
- **dblog / watchdog** — every log entry shows a Cloudflare IP. Useless for forensics.
- **Any custom code that reads `$_SERVER['REMOTE_ADDR']` directly** — needs a grep through `web/modules/custom/ccsoccer/` to confirm we're using `$request->getClientIp()` everywhere, which the fixes below cover.

**Settings.local.php changes (both TEST and PROD, both skip-worktree'd):**

```php
// Reverse proxy support — Cloudflare sits in front.
$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = [
  // Cloudflare's published IPv4 ranges — full list at https://www.cloudflare.com/ips-v4
  '173.245.48.0/20',
  '103.21.244.0/22',
  '103.22.200.0/22',
  '103.31.4.0/22',
  '141.101.64.0/18',
  '108.162.192.0/18',
  '190.93.240.0/20',
  '188.114.96.0/20',
  '197.234.240.0/22',
  '198.41.128.0/17',
  '162.158.0.0/15',
  '104.16.0.0/13',
  '104.24.0.0/14',
  '172.64.0.0/13',
  '131.0.72.0/22',
  // IPv6 ranges from https://www.cloudflare.com/ips-v6
  '2400:cb00::/32',
  '2606:4700::/32',
  '2803:f800::/32',
  '2405:b500::/32',
  '2405:8100::/32',
  '2a06:98c0::/29',
  '2c0f:f248::/32',
];
$settings['reverse_proxy_trusted_headers'] =
  \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_FOR
  | \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_HOST
  | \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PROTO
  | \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PORT;
```

Cloudflare sets `X-Forwarded-For` to the real client IP, so Drupal's standard reverse-proxy machinery picks it up — no need to teach Drupal about `CF-Connecting-IP` specifically.

**Apache side (recommended belt-and-suspenders):** enable `mod_remoteip` with the same Cloudflare ranges marked as trusted proxies. That rewrites `REMOTE_ADDR` at the Apache layer so anything reading `$_SERVER['REMOTE_ADDR']` directly — including `.htaccess` `Require ip` directives — gets the real client IP too. With this in place, Option B above just works.

**Refreshing the Cloudflare IP list.** It's stable for years at a time but does occasionally change. Options: a small drush command in `ccsoccer` that fetches `https://www.cloudflare.com/ips-v4` and `ips-v6` weekly and writes them to an included file, or just review by hand once a year. The list hasn't materially changed in 5+ years, so manual review is fine.

---

## Other Setup We Shouldn't Miss

**DNS migration.** Cloudflare will scan and import existing DNS records when we sign up. Verify against the current zone that MX, SPF, DKIM, and DMARC records for Google Workspace are all copied across — those are how `ccsoccer@ccsoccer.com` keeps sending and receiving. If any are missed, email breaks the moment nameservers flip.

**Orange cloud vs grey cloud.** In Cloudflare's DNS UI each record is either "proxied" (orange cloud — traffic flows through Cloudflare) or "DNS only" (grey cloud — Cloudflare just resolves the name). The bare domain and `www` should be **orange**. MX records and any `mail.ccsoccer.com` hostname must be **grey** — Cloudflare doesn't proxy SMTP, and orange-clouding mail records will break sending and receiving.

**SSL mode = Full (strict).** Cloudflare offers Off / Flexible / Full / Full (strict). Use Full (strict) so traffic from Cloudflare to InMotion is encrypted using InMotion's real cert. Flexible is insecure (traffic between Cloudflare and InMotion is unencrypted). Full without strict accepts self-signed origin certs, which negates most of the security benefit.

**TEST environment.** `test.ccsoccer.com` should go behind Cloudflare too — same config story, and it lets us validate the whole reverse-proxy chain before flipping PROD.

## Suggested Order of Operations

1. Sign up for Cloudflare free, add `ccsoccer.com`.
2. Verify the auto-imported DNS records against InMotion's zone — fix mail records to grey cloud.
3. Set SSL/TLS mode to Full (strict).
4. Add WAF rules: country = US allowed (consider US + Canada); everything else blocked. If going Option B, also add the IP whitelist as a Cloudflare allow rule and remove from `.htaccess`.
5. Update `settings.local.php` on **TEST** first with the reverse_proxy config. Push, verify dblog shows real visitor IPs and not Cloudflare IPs.
6. Enable `mod_remoteip` on Apache on TEST. Verify `$_SERVER['REMOTE_ADDR']` is the real client IP.
7. Switch TEST's nameservers (or just the `test` subdomain) over and run through full smoke test: login, password reset, reCAPTCHA, checkout, order completion email.
8. Repeat steps 5–7 on PROD.
9. Watch dblog, login behavior, reCAPTCHA scoring, and order completion for the first 24 hours.
10. Once stable: if going Option A, strip the IP whitelist from `.htaccess`.

**Rollback at any point:** change nameservers back to current. Site is off Cloudflare within DNS TTL (set Cloudflare's TTL to 5 minutes during the cutover to keep rollback fast).

## Estimated Effort

Roughly half a day to set up and validate on TEST, then about an hour to flip PROD assuming TEST is clean. Most of the time is in step 5 (verifying real client IPs propagate everywhere they need to) and waiting for DNS.

## Open Questions for Caleb

1. Does InMotion's shared hosting have `mod_remoteip` available out of the box, or do we need to file a ticket?
2. Are you OK doing this in the order above, or would you rather flip DNS first and add reverse_proxy config afterward? (I'd argue against — too easy to lock ourselves out of password reset for an hour.)
3. Any concern about Cloudflare's TOS or privacy posture I should know about?
4. Should TEST and PROD share one Cloudflare account, or do you want them separated?
