Cloudflare Origin Certificate Deployment — IIS on Windows Server
Procedure for securing a Cloudflare-proxied IIS site with an Origin Certificate: generate in dashboard, convert to PFX, import via certutil, bind in IIS, and harden with IP Restrictions + Zero Trust.
Reference deployment: mms.richmonddairies.net (Server 2019, IIS 10) — Sep 2026.
When to Use an Origin Certificate
| Scenario | Right choice |
|---|---|
| Site is orange-clouded (proxied) through Cloudflare | ✅ Origin Cert — free, 15-year validity, no renewal |
| Origin can't get a public cert (internal hostname, no public DNS validation) | ✅ Origin Cert |
| Origin must be reachable directly by browsers (grey-cloud / direct IP access by users) | ❌ Need a publicly-trusted CA cert (e.g. Namecheap/SSL.com) |
| Non-web service that doesn't sit behind Cloudflare | ❌ Public CA cert |
Origin Certs are only trusted by Cloudflare's edge. Browsers hitting the origin directly will get a trust warning — which is fine (and desirable) when the origin is locked to Cloudflare IP ranges.
Step 1 — Generate in Cloudflare Dashboard
- dash.cloudflare.com → select the zone → SSL/TLS → Origin Server → Create Certificate
- Settings:
- Private key type: RSA (2048)
- Hostnames: default (
domain.com+*.domain.com) — covers all first-level subdomains - Validity: 15 years
- Format: PEM
- Create, then copy both the certificate and the private key immediately (key shown only once).
Step 2 — Convert to PFX (for Windows/IIS)
IIS imports PFX/P12. On any machine with OpenSSL (or the VPS):
# Save the two PEM blocks as origin.pem and origin.key, then:
openssl pkcs12 -export -out origin.pfx -inkey origin.key -in origin.pem
# Enter a temporary PFX password when prompted (make one up)
IMPORTANT — include the CA in the PFX. A leaf-only PFX will trigger IIS error "One or more intermediate certificates in the certificate chain are missing" (KB954755). Build the full chain instead:
# Download Cloudflare Origin CA root (public, from CF docs)
curl -o origin_ca_rsa_root.pem https://developers.cloudflare.com/ssl/static/origin_ca_rsa_root.pem
# Combine leaf + key + CA, export as full-chain PFX
cat origin.key origin.pem origin_ca_rsa_root.pem > fullchain.pem
openssl pkcs12 -export -out origin-fullchain.pfx -in fullchain.pem
Check the pair matches and dates are right:
openssl x509 -in origin.pem -noout -subject -dates -ext subjectAltName
# SANs must include your hostname (e.g. *.richmonddairies.net)
openssl x509 -in origin.pem -noout -pubkey | openssl md5
openssl pkey -in origin.key -pubout | openssl md5
# The two MD5 hashes must be identical
Step 3 — Import on Server 2019
certutil -f -p <pfx-password> -importpfx origin-fullchain.pfx
-fforce-replaces an existing cert with the same name- certutil places the leaf in Personal — but the CA intermediate often does NOT land automatically. Verify:
certutil -store CA | findstr /i "cloudflare"
If empty, add the intermediate manually:
certutil -addstore CA origin_ca_rsa_root.pem
Verify the leaf:
certutil -store My | findstr /i "cloudflare" /c:"Cert Hash" /c:"NotBefore"
A server may hold unrelated certs that match loose findstr filters (e.g. WMSvc-SHA2-<hostname> — the IIS Web Deploy self-signed cert). Identify by NotBefore date + SHA1 hash before deleting anything. Never delete WMSvc-* certs: they're system certs for Web Deploy / remote IIS management.
Step 4 — Bind in IIS
- IIS Manager → Site → Bindings
- Edit/create the https binding (443):
- Host name: the FQDN (e.g.
mms.richmonddairies.net) - Certificate: CloudFlare Origin Certificate
- ✅ Require Server Name Indication
- Host name: the FQDN (e.g.
- Save. If IIS still warns about missing intermediates after the intermediate is confirmed in the store, close/reopen the binding dialog (stale UI) or run
iisreset.
Confirm what the origin actually serves:
openssl s_client -connect <origin-ip>:443 -servername mms.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
# Expect: issuer = CloudFlare Origin SSL Certificate Authority
Step 5 — Cloudflare SSL Mode
SSL/TLS → Overview → set to Full (strict).
- Full (strict) accepts both publicly-trusted certs and Cloudflare Origin CA certs (built into the CF edge trust store).
- Flip only after the origin cert is bound — otherwise the site throws 526 errors.
- Zone-wide effect: every orange-clouded hostname must serve a valid chain at its origin.
Step 6 — Hardening (two independent layers)
A. IIS IP Restrictions (origin can only be reached via Cloudflare)
Restrict port 443 on the site to Cloudflare IP ranges:
- IIS Manager → site → IP Address and Domain Restrictions
- Edit Feature Settings → set Access for unspecified clients to Deny
- Add Allow Entry for each range from the Cloudflare IPv4 ranges list (currently ~15 IPv4 ranges, e.g.
173.245.48.0/20,103.21.244.0/22,104.16.0.0/13, …)
Result: direct-to-IP requests from anywhere except Cloudflare get 403 Forbidden before touching the app.
B. Cloudflare Zero Trust (identity at the edge)
Zero Trust → Access → Applications → protect the hostname with an access policy (email OTP / SSO / service token). Unauthenticated visitors are challenged at the edge and never reach the origin.
Defense in depth summary
Internet ──► CF Edge ──────────► Origin (IIS)
Zero Trust Access IP Restrictions (allow CF ranges only)
(identity check) + Full (strict) TLS (Origin Cert)
- Bypassing Cloudflare and hitting the IP directly → 403 from IP Restrictions
- Coming through Cloudflare without identity → challenged at the edge
- Edge→origin traffic → encrypted, Full (strict) verified
Optional upgrade — Authenticated Origin Pulls
Cloudflare presents a client certificate on every origin request; IIS requires it (mTLS). This blocks even Cloudflare-IP-spoofed traffic that doesn't carry CF's cert. Setup guide: Authenticated Origin Pulls
Step 7 — Naming certificates for multi-domain servers
Cloudflare Origin Certs have a generic CN (CloudFlare Origin Certificate), and Windows auto-uses the CN as the IIS friendly name on import — so a server hosting several TLDs ends up with multiple identical-looking entries. Fix by labeling each cert with its wildcard scope:
GUI: MMC → Certificates (Local Computer) → Personal → double-click cert → General tab → edit Friendly name.
PowerShell (recommended — precise and repeatable):
Get-ChildItem Cert:\LocalMachine\My |
Where-Object { $_.Subject -like "*CloudFlare Origin*" } |
ForEach-Object { $_.FriendlyName = "*.richmonddairies.net - CF Origin" }
Label by the SAN scope, not a single hostname — a cert covering *.domain.com serves every first-level subdomain, so name it *.domain.com - CF Origin. Suggested convention: <wildcard-scope> - CF Origin, e.g.:
| Cert covers | Friendly name |
|---|---|
*.richmonddairies.net | *.richmonddairies.net - CF Origin |
*.project4.net | *.project4.net - CF Origin |
*.thenewcamerahouse.com | *.thenewcamerahouse.com - CF Origin |
Best practice for multi-TLD boxes: generate one Origin Cert per zone in Cloudflare (each carries only that zone's hostnames — tighter scope, cleaner revocation), import with the domain-labeled friendly name, and the IIS binding dropdown becomes self-documenting.
The friendly name is Windows store metadata only — it is not part of the certificate and changing it does not invalidate the cert or the TLS handshake.
Verification Checklist
-
openssl s_clientto origin shows CloudFlare Origin Cert with complete chain - IIS binding saves with no "intermediate certificates missing" warning
- Zone SSL mode = Full (strict)
- Site loads in browser with no SSL errors (behind Zero Trust login)
- Direct-to-origin-IP request returns 403 (IIS block page, not the app)
- Edge-blocked unauthenticated request returns CF "Attention Required" page
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
| IIS binding warns "intermediate certificates missing" (KB954755) | Intermediate CA not in store → certutil -addstore CA origin_ca_rsa_root.pem |
| PFX import succeeds but CA not in Intermediate store | certutil PFX handling quirk → add intermediate manually (above) |
| 526 error from Cloudflare | Origin not serving a valid chain → check binding, cert dates, intermediate |
| 525 error | SSL handshake failure entirely — origin not listening on 443, or TLS mismatch |
| Origin serves a different cert than expected | Another site's binding is winning the SNI match, or binding didn't save — check all site bindings on that IP:port |
| Direct-IP access shows the site instead of 403 | IP Restrictions missing/misconfigured on that site |