I just went through the process and everything worked as expected.
-
Created my own CA with OpenSSL
-
Uploaded CA via API (done via python because I’m too stupid to escape JSON myself):
#!/usr/bin/python3
import json
from urllib.request import Request, urlopen
import urllib
api_key = "XXXXXXXXXXXXXXXXXXXXX"
email = "XXXXXXXXXXXXXXXXXX"
accid = "XXXXXXXXXXXXXXXXXXXXXXX"
user_agent = "XXXXXXXXXXXXXXXXXX"
data = {
"ca": True,
"certificates": "-----BEGIN CERTIFICATE-----\nMIIFlTCCA32gAwIBAgIUcjgnsNZ0P7ycE/x9N08qQy1kmZkwDQYJKoZIhvcNAQEL\nBQAwWjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAwwKbGF1ZGlhbi5kZTAe\nFw0yNTA2MjQxMDQxMjBaFw0zNTA2MjIxMDQxMjBaMFoxCzAJBgNVBAYTAkFVMRMw\nEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0\neSBMdGQxEzARBgNVBAMMCmxhdWRpYW4uZGUwggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQDDz8GNww+ZiI97IzyaWO6WM4HpQzjJWU2apY5YQQd3upNMyvSi\n+frD/SUA2oHgX2LgmX0O6Hk8FwsF5RZCkuYVCfQme0NXLfYofKIFPoTLhin7febd\nJrsJowqaMIYz2yPOkJfxwc+pn7tFMEzRodBJLfx1XNijJhZ3IDYWy2RIv5IxcnWt\nAifbLE/E3KrJsuF1DKV2w+sLfh1I4nsqdfMswYaDTV+QuyFpRPgiyHFr+9FaaZuj\nMM8yQbj4HVZcXaSrkbtXH2K9BAzuLxyCByiuq+Q+uo7oWf8QXg5rdlL2K6DI+zcW\nu4Z0wpA2FyucQ41uU0Lk78p/DnpNbPbLC95lSBs/32QxG5F4S4BLzRFpKFLYkYyP\n9wrjmYlDI3B5g3LXkBsrRigfO70OvJAq50n3jmVsK+0eZNy1IvhcsKSBi6c6L+zK\nD0m3wNJHBt8EIub8Ws3YV/pOcQQnzBY2qkqoonENybualNORQwVF1/b6r2cqZTDh\nBb6Ycusyiyj7PUkHCxMVUoJZWoFiXtMcMh54/iKlgPbU8s7lYpKQfxPsbevR6+MA\nku6NmSHvPXHdaeNWqA/jsZiSSQTNKgdh8Et8lRHsrhN4apBSbZoO7vwEmwv9VMdr\n+gYHmu3g4Jb9wzrcly1JGcbN1AFh890eN8P7CVsvNI21PeQuJ3x12AY7BwIDAQAB\no1MwUTAdBgNVHQ4EFgQUTCyW3BqZscG3iY+23NP+Rj+FzXswHwYDVR0jBBgwFoAU\nTCyW3BqZscG3iY+23NP+Rj+FzXswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B\nAQsFAAOCAgEAThk6mbwxojII7gAFcMjUmUOTvR8MJE6/asEd0q0wED1useYxIrLT\neZe2sAbUNN0TsbgX5yRQkXcTXtnI9CfrKZnPtc4F77ceqxZdmE9Qcg0OHD8jzbb/\nBI6mfPZRtF7ekt5By6s2T9uDaOv1XfoU3nbr98z9mk8iCs0qZYUICQfyfNVkWYy5\nkNVKiVUeoO5axFIFk32HuiC0KG4ty26SLrWqaIRnCSy4t6995K+lhetX+UkU/vFb\nXBsFKMtcx2Ja4AkxBR5cJIMLiwzKDROUnHFyvPzqM8vRhwlGqKPUzD9CtWjwg/ii\nFL68mLvEejAKz0MWmBKiHI6/sFvKVnQ4KLL5zwVMVzkyxQyxZE+k/L2s+cOibZKy\n9Uzbn6LtZy8w3ojXz6V/en+GdbarTi/3J9+/pv6V4FnwR2BP5Kz1hKKfJPM4R3zX\nfwhDhH+f4fhoKJFilmt3RB17At48U7//uyjEFbd5dylhvVr0I7JjEH7gNiyBcGjf\nrbNq6L9w4rdXy03QnqbQIiKCw4q1pH0AK4/B39Q4BzZh6HnHRl3G2+CUeTri0zf5\naWmf1LgEJ9SpvG1KG0BBZDI159WkW0CWkw5q1FNZb5yOryNo0pCdAerCEq6HNfBn\n6/haZSzTyzrZzvWsu2aQ53KpaUDnCoT/36yBiYTjGdqLmJ66QanQsUE=\n-----END CERTIFICATE-----\n",
"name": "own_ca_test"
}
request = Request(f"https://api.cloudflare.com/client/v4/accounts/{accid}/mtls_certificates",
headers = {"X-Auth-Key": f"{api_key}",
"X-Auth-Email": f"{email}",
"Content-Type": "application/json",
"User-Agent": f"{user_agent}"},
data = json.dumps(data).encode("utf-8"),
method = "POST")
try:
response = json.loads(urlopen(request).read())
except urllib.error.HTTPError as e:
response = json.loads(e.fp.read())
print(json.dumps(response, indent=4))
{
"success": true,
"errors": [],
"messages": [],
"result": {
"id": "b34f8ca9-40ef-4ad0-a065-d775aa8a73a4",
"name": "own_ca_test",
"issuer": "CN=laudian.de,O=Internet Widgits Pty Ltd,ST=Some-State,C=AU",
"signature": "SHA256WithRSA",
"serial_number": "652077247158941727661858633190390673640104696217",
"certificates": "-----BEGIN CERTIFICATE-----\nMIIFlTCCA32gAwIBAgIUcjgnsNZ0P7ycE/x9N08qQy1kmZkwDQYJKoZIhvcNAQEL\nBQAwWjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAwwKbGF1ZGlhbi5kZTAe\nFw0yNTA2MjQxMDQxMjBaFw0zNTA2MjIxMDQxMjBaMFoxCzAJBgNVBAYTAkFVMRMw\nEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0\neSBMdGQxEzARBgNVBAMMCmxhdWRpYW4uZGUwggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQDDz8GNww+ZiI97IzyaWO6WM4HpQzjJWU2apY5YQQd3upNMyvSi\n+frD/SUA2oHgX2LgmX0O6Hk8FwsF5RZCkuYVCfQme0NXLfYofKIFPoTLhin7febd\nJrsJowqaMIYz2yPOkJfxwc+pn7tFMEzRodBJLfx1XNijJhZ3IDYWy2RIv5IxcnWt\nAifbLE/E3KrJsuF1DKV2w+sLfh1I4nsqdfMswYaDTV+QuyFpRPgiyHFr+9FaaZuj\nMM8yQbj4HVZcXaSrkbtXH2K9BAzuLxyCByiuq+Q+uo7oWf8QXg5rdlL2K6DI+zcW\nu4Z0wpA2FyucQ41uU0Lk78p/DnpNbPbLC95lSBs/32QxG5F4S4BLzRFpKFLYkYyP\n9wrjmYlDI3B5g3LXkBsrRigfO70OvJAq50n3jmVsK+0eZNy1IvhcsKSBi6c6L+zK\nD0m3wNJHBt8EIub8Ws3YV/pOcQQnzBY2qkqoonENybualNORQwVF1/b6r2cqZTDh\nBb6Ycusyiyj7PUkHCxMVUoJZWoFiXtMcMh54/iKlgPbU8s7lYpKQfxPsbevR6+MA\nku6NmSHvPXHdaeNWqA/jsZiSSQTNKgdh8Et8lRHsrhN4apBSbZoO7vwEmwv9VMdr\n+gYHmu3g4Jb9wzrcly1JGcbN1AFh890eN8P7CVsvNI21PeQuJ3x12AY7BwIDAQAB\no1MwUTAdBgNVHQ4EFgQUTCyW3BqZscG3iY+23NP+Rj+FzXswHwYDVR0jBBgwFoAU\nTCyW3BqZscG3iY+23NP+Rj+FzXswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B\nAQsFAAOCAgEAThk6mbwxojII7gAFcMjUmUOTvR8MJE6/asEd0q0wED1useYxIrLT\neZe2sAbUNN0TsbgX5yRQkXcTXtnI9CfrKZnPtc4F77ceqxZdmE9Qcg0OHD8jzbb/\nBI6mfPZRtF7ekt5By6s2T9uDaOv1XfoU3nbr98z9mk8iCs0qZYUICQfyfNVkWYy5\nkNVKiVUeoO5axFIFk32HuiC0KG4ty26SLrWqaIRnCSy4t6995K+lhetX+UkU/vFb\nXBsFKMtcx2Ja4AkxBR5cJIMLiwzKDROUnHFyvPzqM8vRhwlGqKPUzD9CtWjwg/ii\nFL68mLvEejAKz0MWmBKiHI6/sFvKVnQ4KLL5zwVMVzkyxQyxZE+k/L2s+cOibZKy\n9Uzbn6LtZy8w3ojXz6V/en+GdbarTi/3J9+/pv6V4FnwR2BP5Kz1hKKfJPM4R3zX\nfwhDhH+f4fhoKJFilmt3RB17At48U7//uyjEFbd5dylhvVr0I7JjEH7gNiyBcGjf\nrbNq6L9w4rdXy03QnqbQIiKCw4q1pH0AK4/B39Q4BzZh6HnHRl3G2+CUeTri0zf5\naWmf1LgEJ9SpvG1KG0BBZDI159WkW0CWkw5q1FNZb5yOryNo0pCdAerCEq6HNfBn\n6/haZSzTyzrZzvWsu2aQ53KpaUDnCoT/36yBiYTjGdqLmJ66QanQsUE=\n-----END CERTIFICATE-----\n",
"ca": true,
"uploaded_on": "2025-06-24T11:44:37.909841Z",
"updated_at": "2025-06-24T11:44:37.909841Z",
"expires_on": "2035-06-22T10:41:20Z",
"type": "custom"
}
}
- Used the ID to associate the hostname:
curl https://api.cloudflare.com/client/v4/zones/XXXXXXXXXXXXXXX/certificate_authorities/hostname_associations \
-X PUT \
-H 'Content-Type: application/json' \
-H "X-Auth-Email: XXXXXXXXXXXXXXXX" \
-H "X-Auth-Key: XXXXXXXXXXXXXXXX" \
-d '{"hostnames":["laudian.de"], "mtls_certificate_id":"b34f8ca9-40ef-4ad0-a065-d775aa8a73a4"}'
{"success":true,"errors":[],"messages":[],"result":{"hostnames":["laudian.de"]}}
- Enable mtls forwarding:
curl "https://api.cloudflare.com/client/v4/zones/XXXXXXXXXXXXXXXXXXXXXXXXX/access/certificates/settings" \
--request PUT \
-H 'Content-Type: application/json' \
-H "X-Auth-Email: XXXXXXXXXXXXXXXXXXXXXXX" \
-H "X-Auth-Key: XXXXXXXXXXXXXXXXXXXX" \
--json '{
"settings": [
{
"hostname": "laudian.de",
"china_network": false,
"client_certificate_forwarding": true
}
]
}'
{
"result": [
{
"hostname": "laudian.de",
"china_network": false,
"client_certificate_forwarding": true
}
],
"success": true,
"errors": [],
"messages": []
}
- Create mTLS WAF rule
- Try:
curl -sI https://laudian.de
HTTP/2 403
curl -sI https://laudian.de --cert client.cert.pem --key client.key.pem
HTTP/2 200
And I can also see the cert in my server logs, so the forwarding also works.