The site is self-hosted on a NAS at home, behind nginx, on a dynamic-DNS hostname. That means the origin IP is the house's IP and it is published in DNS for anyone who looks. This is a spike to find out what Cloudflare's free plan changes about that, and to be honest about what it does not.
The thing to be clear about first
"Spoofing" covers two different problems and Cloudflare's free tier treats them very differently.
Someone standing up a copy of the storefront to phish customers. Nothing on the free plan stops this. Cloudflare's brand-protection and phishing-takedown tooling is a paid, upper-tier product. A spike that sets out to solve this with the free plan will come back empty, so it should not be the measure of success.
Someone reaching the NAS directly, or impersonating the origin. This the free plan does help with, and it is the part worth the work.
What to actually establish
Origin hiding. With DNS proxied through Cloudflare, does the home IP stop being discoverable from public DNS? What still leaks it — mail headers from Brevo, the Gitea host on the same address, an old DNS record, certificate transparency logs?
Whether the origin can still be reached directly once proxied, and what it would take to refuse anything that did not come through Cloudflare. Authenticated Origin Pull is on the free plan; confirm whether nginx here can be made to require it.
TLS. Universal SSL is free. Establish how it interacts with whatever certificate nginx serves today, and whether Full (strict) is reachable without buying anything.
Bot Fight Mode and the free WAF rules. What they actually block, and whether they would interfere with anything real: the PayPal webhook, the upload-link submission endpoint, Brevo, or the Gitea Actions runner.
Rate limiting. The free plan's allowance is small. Worth knowing whether it adds anything on top of the application's own limiters, given #227 already put a ceiling on the intake surface and express-rate-limit covers the public routes.
What to watch for
Uploads. The free plan caps request body size at 100 MB, which is far above the 8 MB per image this app accepts, so it should be a non-issue — confirm rather than assume.
The QA stack. Whether QA goes through Cloudflare too, or stays direct. If it stays direct it keeps leaking the same IP, which would undo most of the benefit.
Anything that resolves the hostname internally. Proxying changes what the NAS itself sees when it looks up its own name.
Out of scope
Buying anything. This is specifically about the free tier; if the answer turns out to be "the useful part is paid", that is a finding worth writing down, not a reason to expand the spike.
Done when
There is a short document under docs/ops/ saying what the free plan gives us, what it costs in configuration, what it would break, and a recommendation to adopt or not — with the origin-hiding question answered concretely rather than in principle.
The site is self-hosted on a NAS at home, behind nginx, on a dynamic-DNS hostname. That means the origin IP is the house's IP and it is published in DNS for anyone who looks. This is a spike to find out what Cloudflare's free plan changes about that, and to be honest about what it does not.
## The thing to be clear about first
"Spoofing" covers two different problems and Cloudflare's free tier treats them very differently.
**Someone standing up a copy of the storefront to phish customers.** Nothing on the free plan stops this. Cloudflare's brand-protection and phishing-takedown tooling is a paid, upper-tier product. A spike that sets out to solve this with the free plan will come back empty, so it should not be the measure of success.
**Someone reaching the NAS directly, or impersonating the origin.** This the free plan does help with, and it is the part worth the work.
## What to actually establish
- **Origin hiding.** With DNS proxied through Cloudflare, does the home IP stop being discoverable from public DNS? What still leaks it — mail headers from Brevo, the Gitea host on the same address, an old DNS record, certificate transparency logs?
- **Whether the origin can still be reached directly** once proxied, and what it would take to refuse anything that did not come through Cloudflare. Authenticated Origin Pull is on the free plan; confirm whether nginx here can be made to require it.
- **TLS.** Universal SSL is free. Establish how it interacts with whatever certificate nginx serves today, and whether Full (strict) is reachable without buying anything.
- **Bot Fight Mode and the free WAF rules.** What they actually block, and whether they would interfere with anything real: the PayPal webhook, the upload-link submission endpoint, Brevo, or the Gitea Actions runner.
- **Rate limiting.** The free plan's allowance is small. Worth knowing whether it adds anything on top of the application's own limiters, given #227 already put a ceiling on the intake surface and `express-rate-limit` covers the public routes.
## What to watch for
- **Uploads.** The free plan caps request body size at 100 MB, which is far above the 8 MB per image this app accepts, so it should be a non-issue — confirm rather than assume.
- **The QA stack.** Whether QA goes through Cloudflare too, or stays direct. If it stays direct it keeps leaking the same IP, which would undo most of the benefit.
- **Anything that resolves the hostname internally.** Proxying changes what the NAS itself sees when it looks up its own name.
## Out of scope
Buying anything. This is specifically about the free tier; if the answer turns out to be "the useful part is paid", that is a finding worth writing down, not a reason to expand the spike.
## Done when
There is a short document under `docs/ops/` saying what the free plan gives us, what it costs in configuration, what it would break, and a recommendation to adopt or not — with the origin-hiding question answered concretely rather than in principle.
Spike done. Findings are in docs/ops/cloudflare-free-tier.md on branch feature/285-cloudflare-free-tier, commit 588e653. Recommendation: do not adopt.
The answer is narrower than either outcome this issue anticipated, and the reason is not the one it expected.
The blocker: we cannot put this hostname on the plan at all
Cloudflare's free plan supports exactly one onboarding path — full setup, which requires delegating the zone's authoritative nameservers at its registrar. Our zone is synology.me and belongs to Synology; we hold a DDNS label inside it and cannot delegate it.
Setup
Plan required
Full setup (NS delegation)
Free
Partial (CNAME) setup
Business or Enterprise
Subdomain NS delegation as its own zone
Enterprise
So the honest answer to "what does the free plan change about our origin IP" is nothing, because the hostname cannot go on the free plan. This issue asked for exactly this kind of result to be recorded rather than treated as a reason to widen the spike, so that is how the document frames it.
The prerequisite, if origin hiding is ever wanted, is a domain we control — with its own migration consequences for every PUBLIC_URL, the authentik forward-auth config, the Gitea host, and every verification and upload link already sent. That belongs in its own issue.
The remaining questions are answered in the document anyway, on the assumption a domain gets bought later, so that decision is made with the consequences known rather than discovered afterwards.
Two findings worth pulling out of the document
1. trust proxy breaks silently, and not only for Cloudflare. Cloudflare appends to X-Forwarded-For rather than replacing it. A second proxy in front of NPM makes app.set('trust proxy', 1) (app.ts:34) resolve req.ip to a Cloudflare edge address rather than the client. Four limiters key on req.ip — passwordResetRequestLimiter, clientErrorLimiter, intakeViewLimiter, intakeSubmitLimiter — and would go on looking healthy while no longer distinguishing callers, so one abusive client spends everybody's allowance. verificationResendLimiter is unaffected; it keys on customer id. The comment at rateLimit.ts:62-64 asserts the current behaviour as fact and has to change in the same commit. This is latent for any second proxy, not just Cloudflare.
2. Bot Fight Mode cannot be scoped on any plan. It does not run on the Ruleset Engine, so Skip, Bypass and Allow have no effect on it; scoped exceptions require paid Super Bot Fight Mode. Cloudflare documents that it may challenge API traffic, and a challenged POST /webhooks/paypal is a capture notification we never receive. It is all-or-nothing per zone, so the safe free-plan configuration is to leave it off — which removes one of the two features that made the free tier interesting.
Confirmed rather than assumed
Uploads are a non-issue. Free-plan body cap is 100 MB; MAX_IMAGE_BYTES is 8,000,000 across at most 6 images. One caveat: WAF Managed Rules inspect only the first 1 MB of a body on the free plan, so a managed rule cannot meaningfully inspect an image upload.
Free rate limiting adds nothing here. One rule, Path and Verified Bot only, IP counting, and both periods capped at 10 seconds. Our own limiters run 15-minute and 1-hour windows and key on email and customer id as well as address.
Authenticated Origin Pull is on the free plan — the one feature with real teeth for origin hiding. Needs ssl_client_certificate against Cloudflare's origin-pull-ca.pem plus ssl_verify_client on;, and Full SSL mode.
Origin hiding, concretely
Proxying stops the address appearing in a dig of the proxied record. It still leaks through: gitea.bermudalamb.synology.me on the same address (the Actions runner needs it), MX/TXT records which can never be proxied, possibly the first Received header of a Brevo-relayed message, certificate transparency logs which are public and permanent, and the QA stack if it stays direct. Treat proxying as reducing casual discovery, not as hiding.
Not established
Whether Nginx Proxy Manager's Advanced config can express ssl_verify_client on; in a way that survives regeneration.
Whether the NAS's own lookups of the hostname change once proxied — flagged in this issue, not testable without a live zone.
What a real Brevo-relayed message's Received headers actually contain.
Cloudflare behaviour was checked against their documentation on 2026-09-05, not against a live account, because one cannot be set up for this hostname. Every claim about this repository was checked against the code.
Closing per the "done when" — the document exists with a recommendation. Note the branch still needs pushing and merging for the document to land on main.
Spike done. Findings are in `docs/ops/cloudflare-free-tier.md` on branch `feature/285-cloudflare-free-tier`, commit `588e653`. **Recommendation: do not adopt.**
The answer is narrower than either outcome this issue anticipated, and the reason is not the one it expected.
## The blocker: we cannot put this hostname on the plan at all
Cloudflare's free plan supports exactly one onboarding path — **full setup**, which requires delegating the *zone's* authoritative nameservers at its registrar. Our zone is `synology.me` and belongs to Synology; we hold a DDNS label inside it and cannot delegate it.
| Setup | Plan required |
| --- | --- |
| Full setup (NS delegation) | Free |
| Partial (CNAME) setup | **Business or Enterprise** |
| Subdomain NS delegation as its own zone | **Enterprise** |
So the honest answer to "what does the free plan change about our origin IP" is **nothing**, because the hostname cannot go on the free plan. This issue asked for exactly this kind of result to be recorded rather than treated as a reason to widen the spike, so that is how the document frames it.
The prerequisite, if origin hiding is ever wanted, is **a domain we control** — with its own migration consequences for every `PUBLIC_URL`, the authentik forward-auth config, the Gitea host, and every verification and upload link already sent. That belongs in its own issue.
The remaining questions are answered in the document anyway, on the assumption a domain gets bought later, so that decision is made with the consequences known rather than discovered afterwards.
## Two findings worth pulling out of the document
**1. `trust proxy` breaks silently, and not only for Cloudflare.** Cloudflare *appends* to `X-Forwarded-For` rather than replacing it. A second proxy in front of NPM makes `app.set('trust proxy', 1)` (`app.ts:34`) resolve `req.ip` to a Cloudflare edge address rather than the client. Four limiters key on `req.ip` — `passwordResetRequestLimiter`, `clientErrorLimiter`, `intakeViewLimiter`, `intakeSubmitLimiter` — and would go on looking healthy while no longer distinguishing callers, so one abusive client spends everybody's allowance. `verificationResendLimiter` is unaffected; it keys on customer id. The comment at `rateLimit.ts:62-64` asserts the current behaviour as fact and has to change in the same commit. **This is latent for any second proxy, not just Cloudflare.**
**2. Bot Fight Mode cannot be scoped on any plan.** It does not run on the Ruleset Engine, so `Skip`, `Bypass` and `Allow` have no effect on it; scoped exceptions require paid Super Bot Fight Mode. Cloudflare documents that it may challenge API traffic, and a challenged `POST /webhooks/paypal` is a capture notification we never receive. It is all-or-nothing per zone, so the safe free-plan configuration is to leave it off — which removes one of the two features that made the free tier interesting.
## Confirmed rather than assumed
- **Uploads are a non-issue.** Free-plan body cap is 100 MB; `MAX_IMAGE_BYTES` is 8,000,000 across at most 6 images. One caveat: WAF Managed Rules inspect only the first **1 MB** of a body on the free plan, so a managed rule cannot meaningfully inspect an image upload.
- **Free rate limiting adds nothing here.** One rule, Path and Verified Bot only, IP counting, and both periods capped at **10 seconds**. Our own limiters run 15-minute and 1-hour windows and key on email and customer id as well as address.
- **Authenticated Origin Pull *is* on the free plan** — the one feature with real teeth for origin hiding. Needs `ssl_client_certificate` against Cloudflare's `origin-pull-ca.pem` plus `ssl_verify_client on;`, and Full SSL mode.
## Origin hiding, concretely
Proxying stops the address appearing in a `dig` of the proxied record. It still leaks through: `gitea.bermudalamb.synology.me` on the same address (the Actions runner needs it), `MX`/`TXT` records which can never be proxied, possibly the first `Received` header of a Brevo-relayed message, certificate transparency logs which are public and permanent, and the QA stack if it stays direct. Treat proxying as *reducing casual discovery*, not as hiding.
## Not established
- Whether Nginx Proxy Manager's Advanced config can express `ssl_verify_client on;` in a way that survives regeneration.
- Whether the NAS's own lookups of the hostname change once proxied — flagged in this issue, not testable without a live zone.
- What a real Brevo-relayed message's `Received` headers actually contain.
Cloudflare behaviour was checked against their documentation on 2026-09-05, not against a live account, because one cannot be set up for this hostname. Every claim about this repository was checked against the code.
Closing per the "done when" — the document exists with a recommendation. Note the branch still needs pushing and merging for the document to land on `main`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The site is self-hosted on a NAS at home, behind nginx, on a dynamic-DNS hostname. That means the origin IP is the house's IP and it is published in DNS for anyone who looks. This is a spike to find out what Cloudflare's free plan changes about that, and to be honest about what it does not.
The thing to be clear about first
"Spoofing" covers two different problems and Cloudflare's free tier treats them very differently.
Someone standing up a copy of the storefront to phish customers. Nothing on the free plan stops this. Cloudflare's brand-protection and phishing-takedown tooling is a paid, upper-tier product. A spike that sets out to solve this with the free plan will come back empty, so it should not be the measure of success.
Someone reaching the NAS directly, or impersonating the origin. This the free plan does help with, and it is the part worth the work.
What to actually establish
express-rate-limitcovers the public routes.What to watch for
Out of scope
Buying anything. This is specifically about the free tier; if the answer turns out to be "the useful part is paid", that is a finding worth writing down, not a reason to expand the spike.
Done when
There is a short document under
docs/ops/saying what the free plan gives us, what it costs in configuration, what it would break, and a recommendation to adopt or not — with the origin-hiding question answered concretely rather than in principle.Spike done. Findings are in
docs/ops/cloudflare-free-tier.mdon branchfeature/285-cloudflare-free-tier, commit588e653. Recommendation: do not adopt.The answer is narrower than either outcome this issue anticipated, and the reason is not the one it expected.
The blocker: we cannot put this hostname on the plan at all
Cloudflare's free plan supports exactly one onboarding path — full setup, which requires delegating the zone's authoritative nameservers at its registrar. Our zone is
synology.meand belongs to Synology; we hold a DDNS label inside it and cannot delegate it.So the honest answer to "what does the free plan change about our origin IP" is nothing, because the hostname cannot go on the free plan. This issue asked for exactly this kind of result to be recorded rather than treated as a reason to widen the spike, so that is how the document frames it.
The prerequisite, if origin hiding is ever wanted, is a domain we control — with its own migration consequences for every
PUBLIC_URL, the authentik forward-auth config, the Gitea host, and every verification and upload link already sent. That belongs in its own issue.The remaining questions are answered in the document anyway, on the assumption a domain gets bought later, so that decision is made with the consequences known rather than discovered afterwards.
Two findings worth pulling out of the document
1.
trust proxybreaks silently, and not only for Cloudflare. Cloudflare appends toX-Forwarded-Forrather than replacing it. A second proxy in front of NPM makesapp.set('trust proxy', 1)(app.ts:34) resolvereq.ipto a Cloudflare edge address rather than the client. Four limiters key onreq.ip—passwordResetRequestLimiter,clientErrorLimiter,intakeViewLimiter,intakeSubmitLimiter— and would go on looking healthy while no longer distinguishing callers, so one abusive client spends everybody's allowance.verificationResendLimiteris unaffected; it keys on customer id. The comment atrateLimit.ts:62-64asserts the current behaviour as fact and has to change in the same commit. This is latent for any second proxy, not just Cloudflare.2. Bot Fight Mode cannot be scoped on any plan. It does not run on the Ruleset Engine, so
Skip,BypassandAllowhave no effect on it; scoped exceptions require paid Super Bot Fight Mode. Cloudflare documents that it may challenge API traffic, and a challengedPOST /webhooks/paypalis a capture notification we never receive. It is all-or-nothing per zone, so the safe free-plan configuration is to leave it off — which removes one of the two features that made the free tier interesting.Confirmed rather than assumed
MAX_IMAGE_BYTESis 8,000,000 across at most 6 images. One caveat: WAF Managed Rules inspect only the first 1 MB of a body on the free plan, so a managed rule cannot meaningfully inspect an image upload.ssl_client_certificateagainst Cloudflare'sorigin-pull-ca.pemplusssl_verify_client on;, and Full SSL mode.Origin hiding, concretely
Proxying stops the address appearing in a
digof the proxied record. It still leaks through:gitea.bermudalamb.synology.meon the same address (the Actions runner needs it),MX/TXTrecords which can never be proxied, possibly the firstReceivedheader of a Brevo-relayed message, certificate transparency logs which are public and permanent, and the QA stack if it stays direct. Treat proxying as reducing casual discovery, not as hiding.Not established
ssl_verify_client on;in a way that survives regeneration.Receivedheaders actually contain.Cloudflare behaviour was checked against their documentation on 2026-09-05, not against a live account, because one cannot be set up for this hostname. Every claim about this repository was checked against the code.
Closing per the "done when" — the document exists with a recommendation. Note the branch still needs pushing and merging for the document to land on
main.