Skip to main content

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​

ScenarioRight 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​

  1. dash.cloudflare.com → select the zone → SSL/TLS → Origin Server → Create Certificate
  2. Settings:
    • Private key type: RSA (2048)
    • Hostnames: default (domain.com + *.domain.com) — covers all first-level subdomains
    • Validity: 15 years
    • Format: PEM
  3. 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

Verify before shipping

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
  • -f force-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"
Don't confuse certs in the store

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​

  1. IIS Manager → Site → Bindings
  2. Edit/create the https binding (443):
    • Host name: the FQDN (e.g. mms.richmonddairies.net)
    • Certificate: CloudFlare Origin Certificate
    • ✅ Require Server Name Indication
  3. 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:

  1. IIS Manager → site → IP Address and Domain Restrictions
  2. Edit Feature Settings → set Access for unspecified clients to Deny
  3. 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 coversFriendly 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.

note

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_client to 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​

SymptomCause / 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 storecertutil PFX handling quirk → add intermediate manually (above)
526 error from CloudflareOrigin not serving a valid chain → check binding, cert dates, intermediate
525 errorSSL handshake failure entirely — origin not listening on 443, or TLS mismatch
Origin serves a different cert than expectedAnother 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 403IP Restrictions missing/misconfigured on that site