pimenta.fun lab
Tests / Challenges / mTLS / Implementation guide
Guide

Implementing mTLS end-to-end

Two independent TLS legs, each with mutual authentication: the client proves itself to Cloudflare, and Cloudflare proves itself to your origin.

Clientbrowser / app / API caller
Leg 1
client cert → Cloudflare
Cloudflareedge / API Shield
Leg 2
CF cert → origin (AOP)
Originyour Apache server
Leg 1 — Client → Cloudflare: Cloudflare requests & verifies a client certificate against a CA you control.
Leg 2 — Cloudflare → Origin (Authenticated Origin Pulls): Cloudflare presents a client certificate that your origin verifies, so the origin only accepts traffic from Cloudflare.
Contents

0. Prerequisites

Part 1 — Client → Cloudflare (client certificate auth)

Cloudflare challenges the client for a certificate during the TLS handshake and verifies it against a CA you register (API Shield → mTLS).

1

Create a CA & client certificate CLI

Generate your own CA, then issue a client cert signed by it:

# 1a. Root CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/CN=Pimenta Lab Client CA" -out ca.pem

# 1b. Client key + CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -subj "/CN=lab-client" -out client.csr

# 1c. Sign the client cert with the CA
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key \
  -CAcreateserial -days 365 -sha256 -out client.crt

# 1d. (optional) bundle for browsers
openssl pkcs12 -export -inkey client.key -in client.crt \
  -certfile ca.pem -out client.p12
2

Register the CA with Cloudflare Dashboard API

Dashboard: SSL/TLS → Client Certificates (or Security → API Shield → mTLS) → Add mTLS certificate → paste the contents of ca.pem.

Or via API (upload the CA to the mTLS certificate store):

curl -X POST \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/certificate_authorities/hostname_associations" \
  -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \
  --data '{"certificates":[""]}'
3

Associate the hostname & require mTLS Dashboard

Enable mTLS for the hostname (e.g. mtls.pimenta.fun), then enforce it with a WAF custom rule so unverified clients are blocked:

# Expression (Security -> WAF -> Custom rules)
(http.host eq "mtls.pimenta.fun" and not cf.tls_client_auth.cert_verified)

# Action: Block   (or "Skip" the rule to allow only when verified)
Fields you can use in rules: cf.tls_client_auth.cert_verified, cf.tls_client_auth.cert_presented, cf.tls_client_auth.cert_subject_dn.
4

Test the client leg CLI

# Without a cert -> blocked (403)
curl -i https://mtls.pimenta.fun/

# With the client cert -> allowed
curl -i --cert client.crt --key client.key https://mtls.pimenta.fun/

Browsers: import client.p12 into the OS/keychain; the browser prompts to send it.

Part 2 — Cloudflare → Origin (Authenticated Origin Pulls)

Authenticated Origin Pulls (AOP) makes Cloudflare present a client certificate to your origin. Your origin verifies it, so it accepts traffic only from Cloudflare.

⚡ Live AOP test page →

1

Choose a mode & enable AOP Dashboard

  • Zone-level: SSL/TLS → Origin Server → Authenticated Origin Pulls → On. Cloudflare presents its shared origin-pull certificate; your origin trusts Cloudflare's origin-pull CA.
  • Per-hostname: upload your own client cert/key via API and bind it to the hostname — stronger isolation per zone.
2

Get the CA the origin should trust CLI

For zone-level AOP, download Cloudflare's origin-pull CA and place it on the origin:

sudo mkdir -p /etc/apache2/cloudflare
sudo curl -fsSL \
  https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem \
  -o /etc/apache2/cloudflare/origin-pull-ca.pem

For per-hostname AOP, trust the CA that signed the cert you uploaded instead.

3

Require & verify the client cert on the origin (Apache)

Add to your origin's :443 vhost so Apache demands Cloudflare's certificate:

<VirtualHost *:443>
    ServerName mtls.pimenta.fun
    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/mtls.pimenta.fun/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/mtls.pimenta.fun/privkey.pem

    # --- Authenticated Origin Pulls ---
    SSLVerifyClient        require
    SSLVerifyDepth         1
    SSLCACertificateFile   /etc/apache2/cloudflare/origin-pull-ca.pem
</VirtualHost>

nginx equivalent: ssl_client_certificate + ssl_verify_client on;

4

Reload & lock down CLI

sudo apache2ctl configtest && sudo systemctl reload apache2

Optionally restrict the origin firewall to Cloudflare IP ranges so direct (non-CF) traffic can't reach it.

3. Set the SSL/TLS encryption mode

Use Full (Strict) (SSL/TLS → Overview) so Cloudflare validates the origin certificate as well. Combined with AOP you get verified TLS in both directions on Leg 2.

4. Verify end-to-end

# Leg 1: client must present a verified cert
curl -i --cert client.crt --key client.key https://mtls.pimenta.fun/

# Leg 2: hitting the origin directly (bypassing CF) should now FAIL,
# because the origin requires Cloudflare's client certificate:
curl -i --resolve mtls.pimenta.fun:443:<ORIGIN_IP> https://mtls.pimenta.fun/
#   -> expect: 400 No required SSL certificate was sent

The lab's mTLS test page also surfaces the cf-client-cert-* headers when Leg 1 is configured.

5. Troubleshooting

Go to the mTLS test page →