The Problem with App-Side Authentication
When I build internal tools for myself, I don't want to spend time wiring up a custom login system. The app itself should stay lean; I'd rather handle authentication at the edge. But "just require a login" isn't specific enough for my current project—a small web form that appends expense entries to my budget spreadsheet. I want only my iPhone to reach it.
Cloudflare Access already lets you protect a subdomain without touching your server code. To restrict access to a single physical device, though, you need a way for that device to prove its identity. The standard approach is mutual TLS (mTLS): the server presents a certificate, and the client presents one back. Combined with Cloudflare's open source PKI toolkit cfssl, I can turn my phone into a cryptographic key for the app in about 45 minutes.
This scales well beyond a personal budget tool. An organization could use the same certificate-issuing workflow to secure fleets of IoT sensors or corporate laptops without custom per-device configuration.
Setting Up the Root of Trust
Cloudflare Access is a bouncer that checks credentials at the door of each protected resource. You can integrate it with identity providers like G Suite, AzureAD, or Okta, but that only answers who is asking. To answer what is asking—my specific iPhone—I need certificates.
Before any client certificate exists, there must be a Root CA that can issue it. The Cloudflare Access dashboard trusts this root, not individual client certs. Think of the root as a state DMV: bouncers don't need a list of every driver's license, they just know to validate against the state's authority.
Create a directory structure to keep things organized, then define the CA's identity and usage rules in two JSON config files. The signing config specifies the profiles that will be available—in this case, a profile for client certificates. With those files in place, generate the root materials:
$ cfssl genkey -initca ca-csr.json | cfssljson -bare ca
This command produces three artifacts: a private key, a certificate signing request, and the certificate itself. The private key must be protected carefully. In production, most organizations would create an intermediate certificate to sign client certs, keeping the root offline and only exposing it to issue new intermediates. For a personal test project, using the root directly is acceptable.
Upload the ca.pem file to the Cloudflare Access dashboard under "Mutual TLS Root Certificates". Associate it with the FQDN you're protecting—here, money.samrhea.com.
Enforcing the Policy
With the root uploaded, build an Access policy for the protected subdomain. In the policy editor, select "Non Identity" as the decision type. Under Include, choose "Valid Certificate". This tells Access to allow any request that presents a client certificate signed by the root you uploaded.
You could refine this further using Common Names in the certificate, but a valid-cert check is sufficient for a single-device prototype. Save the policy; the bouncer is now stationed and knows what ID to look for.
Issuing and Converting the Client Certificate
The Access policy is only half the setup. Now I need an actual client certificate for my iPhone. In a separate client folder, create a client-csr.json with details for the new device certificate. Then use cfssl to generate it against the root:
$ cfssl gencert -ca=../root/ca.pem -ca-key=../root/ca-key.pem -config=../root/ca-config.json -profile=client client-csr.json | cfssljson -bare iphone-client
This yields four files: the original CSR config, a private key (iphone-client-key.pem), a signing request (iphone-client.csr), and the client certificate (iphone-client.pem).
Before deployment, verify the certificate works with the Access policy using cURL. The request should succeed without server-side changes.
An iPhone won't consume raw PEM files directly. It expects a PKCS #12 container—a single file that bundles the certificate and private key. Use OpenSSL to convert:
$ openssl pkcs12 -export -out sam-iphone.p12 -inkey iphone-client-key.pem -in iphone-client.pem -certfile ../root/ca.pem
The command prompts for an "Export Password". Pick one you can remember; you'll need it when installing the profile on the phone.
Installing the Profile and Testing
Getting the .p12 file onto the device is straightforward. In enterprise environments, administrators would push certificates via mobile device management (MDM); for this personal project, AirDrop works fine
When you receive the file on your iPhone, you'll be prompted to install it as a device profile. Enter your passcode and the export password from the OpenSSL step. The certificate then appears under Profiles in Settings.
Navigating to money.samrhea.com from the phone will prompt you to confirm using the installed profile for this connection. That prompt may occur on subsequent visits as well, since browsers handle client certificate negotiation inconsistently, but it should be the only interaction needed before the page loads.
Once that final prompt is accepted, my iPhone is the only device that can reach the budget app. Any other client—laptop, tablet, another phone—gets turned away at the door, because they don't carry a certificate signed by the root that Access now trusts.
Extending the model
Right now, my prototype personal finance app is locked to my iPhone, which makes sign-in via Cloudflare Access from that device a simple flow. But Access policies don’t have to stay that rigid. I can, for example, add a rule that permits logins through Google as a fallback when I’m on another device. Or I could layer the security and build a policy that demands both a certificate and an SSO login before granting entry.
The client certificate flow opens up more than authentication, though. Cloudflare Access exposes the certificate details I generated earlier in this tutorial to Cloudflare Workers, so I can start acting on that information. That means building routing rules or triggering events based on attributes tied to the cert itself.



