Bring your own CA

I am planning to enable MTLS for a particular zone of mine where clients are issued certs from a 3rd party in order to access our application. First steps is configuring Bring your own CA…

Does anyone any any experience of performing these actions, I have read every article and the combination of methods to deploy does not simplify it. i.e. lack of GUI, little Terraform integration and the majority of configuring MTLS end to end requiring the API.

The process in my head:

  1. Deploy CA cert via Terraform (terraform plan run ok)
  2. Enable client certificate forwarding via API (API code written + tested although it doesn’t return any results due the above not being completed (I believe)
  3. Enable hostname association via API or GUI (API code written and tested)
    1. Would this plan sound logical?
    1. Would uploading my root CA cert impact or change any other running services?

What is the issue you’re encountering

Questions around bring your own CA + MTLS

Just to confirm, you are on an Enterprise account?

I can try this in a few minutes and see if I have any problems.

1 Like

Yes Ent account.

I just went through the process and everything worked as expected.

  1. Created my own CA with OpenSSL

  2. 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"
    }
}
  1. 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"]}}
  1. 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": []
}
  1. Create mTLS WAF rule
  2. 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.

2 Likes

Your response is fantastic thankyou and exactly what I wanted to see, some real world detail and evidence of someone else configuring.

Very much appreciate you taking the time to assist.

Cheers,

1 Like

Would it be possible to add an extract from your serverlogs so I can see the cert headers. I know from the documentation the names but it would be good to confirm they are infact the same header names.

tail /var/log/apache2/forensic.log

+2171715:685a98d7:0
HEAD / HTTP/2.0
Cf-Ray:954c32e1b3c9d91e-HEL
Cf-Device-Type:desktop
X-Forwarded-For:XXXXXXXXXXXXXXXXXXXXX
Cf-Connecting-Ip:XXXXXXXXXXXXXXXXXXX
Accept-Encoding:gzip
Cf-Client-Cert-Sha256:502e4f511d1a7c6477dc35e4368f3383d083df330024741edc0458265280b24b
X-Forwarded-Proto:https
Cf-Ipcountry:FI
Cf-Visitor:{"scheme"%3a"https"}
Cf-Client-Cert-Der-Base64:MIIFfDCCA2SgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRMwEQYDVQQDDApsYXVkaWFuLmRlMB4XDTI1MDYyNDEwNDUwN1oXDTI5MTIzMDEwNDUwN1owWjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAwwKbGF1ZGlhbi5kZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALP5THXkHfnHMgJIJ0tCyyutIivgWyNfITdHp5/m9wsVn5f83JObhBXxaJxDpZ9IPlFFv4A2rY76sLzy+ANkk2jMYWmc6fax5Ak5mYom8LuuejeCeYxjzrfdDBHfVvrP6SNeZddEOsFl4J7rAf4NSSNZCEUBaL1/KJZTea4nUYISUOJ1k3wrJye4IQcsJSpBqAwX1prNKA6HcOjM4v5bR/2hYesmhSMsxDMEdc1YckoDrAfVmKLBvotFhg/rzoXRKo8oaSl8QWmFK1LtOh4jftZ4Uh6ShTUKaXdmKAm7poGpY0LfiKc9gypZ7sI0L2LcBe7MlqBzXadjkfFPIE1bwIY0B6Ko3cohfXNbA9559Bmfx0aznt6y71MUNwbvr5QYP3hycTcQfDOrW23MtEhhRO0Ey0sP+V/8mlAJPWqzaX85gIIOgmIXAG8VBCM5idTYRp45loWi3QoXDNf7SsppmMnTz71v6fX/USHd4MLJl6M8zKCwFACNnFHh14QOrso3O1noJnqT/j6oDtwpdxg7W8OrFytU0jsdNW5KrCrBjGd29l8edsEnIOTx7rwlyQ33cgLCms53yfddGQkdtpggwdWHYDNakdCWWoqRkGX+St+lE0xq5uIKvwSLLOVKwyiY5+Am6RWsCMaHxFZdnuDNWJ3C1CrjO1e7FbABZKqfuOnDAgMBAAGjTTBLMAkGA1UdEwQCMAAwHQYDVR0OBBYEFDkdkzg4vbUbUjmWrFCUYo+X5dg0MB8GA1UdIwQYMBaAFEwsltwambHBt4mPttzT/kY/hc17MA0GCSqGSIb3DQEBCwUAA4ICAQCLaCqZWrDsjtVDG3uAMGn71dXdrkxBXI65Lg9TUOY+sR+0bdCWGwd+F4sx2rTR3HU+ytNmEO2lyTsAe1Uf6dF/tamJ0UXK8aJ9VH2kJB/7OoG07xuJfSxD8KRHZ/WC2slJ3RyVQcRW0OAna4pN43dEmrIWkUJFpY26SmVLrtspcks7lg0Tqe+rHX1J1PET1q/OSR1js4jEBlxwuibygRzNsaW4pLT/TuFxaWA8p5DXn4pqAW5l5BDKWvL/X0ZvTqdXN1ZoV8Rq1QinVPNzt0NQniFizhoT6m5Z1X17EwGfaMoH7mXcmJQO57imJODGAcqP168Rdqcu4MXB16WKqetlihx9TPrUDOEbq6TQOHXY22EvfojDnKHGFqvwpJGxw1yFv2dMfYlLpl8vZ4Ohq+yuJ8tdDP4lWnqpnFjW+wlqLUZ6BTFDO88Q5V5aQ3gMQmkvLbTW3GJT3obAhsajUxzPFU2TJ8uJCLLBH7xx8G95DGsVcUn3AClpovg/z8BJDlg31UIiKe8Pz1JkT4vOpfK9H5s76aUfgvo9UGxDQzFQ6Qu0gznWCg/ILTIk5Aujx8Zlb7DC9RkHY2Pu42w2CNpx8Ig6tHjhudmyKXnqbzmUhRmIEhi7lRaWvOMIWpkBdX0vXYgl96B5KM3egLzOuQGlM0nH8c5BNkAVHE8Z+xsn1w==
User-Agent:curl/8.5.0
Accept:*/*
Cdn-Loop:cloudflare; loops=1; subreqs=1
Cf-Ew-Via:15
Host:laudian.de
-2171715:685a98d7:0

Perfect thakyou.

Finally, the account where you configured MTLS, I assume it contains other zones and potentially live and production solutions. The key question was whether adding the CA had any impact on those?

Cheers,

I didn’t notice anything.

Ive managed to perform all steps fine except the one to configure certificate forwarding.
You’ll note I am performing the steps with PowerShell not CURL and performing against a test zone which is in an enterprise account.

A little stumped, it always returns a 400 bad request

Variables

$ApiUrl = “https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/certificates/settings

Header detail

$headers = @{
“Content-Type” = “application/json”
“X-Auth-Key” = “$ApiKey”
“X-Auth-Email” = “$Email”
}

Body detail

$body = @{
‘Settings’ = @(
@{
“hostname” = [string]$hosts
“china_network” = ‘false’
“client_certificate_forwarding” = ‘true’
}
)
} | convertto-json

Make the API call

$Responseget = Invoke-RestMethod -Uri $apiUrl -Method Put -Headers $headers -Body $body
#$Responseget.result

Issue resolved…as the true and fals’s are “Bool” I needed to specify $true and $false and not just true and false.

1 Like

Did you perform any additional steps outside of the above? I have enabled and everything appears set but MTLS dosent seem to be functioning.

Did you have to complete this as shown in the document?

Enable mTLS

To enable mutual Transport Layer Security (mTLS) for a host from the Cloudflare dashboard:

  1. Log in to the Cloudflare dashboard :up_right_arrow: and select your account and application.
  2. Go to SSL > Client Certificates.
  3. To enable mTLS for a host, select Edit in the Hosts section of the Client Certificates card.
  4. Enter the name of a host in your current application and press Enter.
  5. Select Save.

I haven’t done anything other than what I’ve shown above.

Was the Cloudflare “zone” you used proxied?

Of course.

Or rather, zones aren’t proxied, hostnames are. I used a proxied hostname.

Yes apologies!

I think we need further app side changes (different department!) as enabling MTLs has not changed any behaviour.

Accessing the site and it being proxied thus having a Cloudflare CA (Google) cert is throwing me off.

MTLs, the client has 3rd party providers cert and the Cloudflare BYO CA I installed is the 3rd party providers Root CA.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.