I recently set up a Cloudflare Tunnel and wanted to find a way to protect that resource. I primary use an Apple Watch app to communicate with this service.

Some form of auth without needing to do some complicated or cumbersome process on the Apple watch was my primary goal. In talking with a friend, I discovered a very easy way to set up client certificates with Cloudflare as the means of authentication. Furthermore, the certificate can be embedded directly in the watch app for maximum convenience.

Cloudflare Setup

As a prerequsite, ensure that your service’s domain is being proxied through Cloudflare.

Next, generate a client certificate on the Cloudflare dashboard (SSL > Client Certificates) . From here, we can download the certificate and the secret. Make sure to enable mTLS if needed, as outlined in these docs.

After generating a certificate, navigate to the Web Application Firewall (WAF) portal under Security > WAF. You’ll want to add a new rule that blocks when not cf.tls_client_auth.cert_verified for your subdomain.

( in {""} and not cf.tls_client_auth.cert_verified)

If all is setup correctly, navigating to your service will be blocked. To access the service, you’ll now need to offer the client certificate to Cloudflare.


App/Device Setup

Embedding into an Apple Watch app

Using the downloaded certificate and key from Cloudflare, I generated a combined p12 file secured by a password (keep this for later!) using the openssl command below.

openssl pkcs12 -export -out cert.p12 -in cloudflare.pem -inkey cloudflare.key

Then, taking inspiration from Cloudflare’s ‘Client certificates Configure your Mobile app or IoT Device’ and a great Swift example project by @MarcoEidinger, I embedded the certificate into this Apple Watch app. The interesting bits are:

Reading in the certificate name (bundled into the application) and the password. These are exposed as an extension to Bundle (src).

extension Bundle {
    var userCertificate: UserCertificate? {
        guard let filePath = Bundle.main.path(forResource: VARIABLE_INFO_FILE_PATH, ofType: "plist"),
              let plist = NSDictionary(contentsOfFile: filePath),
              let certName = plist.object(forKey: CERTIFICATE_NAME) as? String,
              let certPassword = plist.object(forKey: CERTIFICATE_PASSWORD) as? String

        else {
            fatalError("Missing client certificate password in '\(VARIABLE_INFO_FILE_PATH)'")
        guard let path = Bundle.main.path(forResource: certName, ofType: "p12"),
              let p12Data = try? Data(contentsOf: URL(fileURLWithPath: path))
        else {
            fatalError("Missing client certificate.")
        return (p12Data, certPassword)

When sending an HTTPS request, a client certificate delegate is conditionally added depending on the request being sent (src).

    let session = URLSession(configuration: .default, delegate: certMode == CertMode.ClientCert ? URLSessionClientCertificateHandling() : nil, delegateQueue: nil)
    let task = session.dataTask(with: request) {(data, response, error) in
        if let httpResponse = response as? HTTPURLResponse {
            if queryType == QueryType.StatusCode {
                res.wrappedValue = String(httpResponse.statusCode)
            if httpResponse.statusCode > 299 {
                res.wrappedValue = "ERR (\(httpResponse.statusCode))"
            let stringResponse = String(data: data!, encoding: String.Encoding.utf8)
            res.wrappedValue = stringResponse ?? "ERR"
        } else {
            res.wrappedValue = "ERR"


Browser setup

Manually installing the certifcate is as easy as importing into an application’s certificate store. After importing in Firefox and navigating to a WAF-protected webpage, you’ll be asked once if you’d like to identify yourself with that certificate. If you do so, the Cloudflare WAF will grant you access to the resource.