Cloudflare Tunnel and Access¶
A Cloudflare Tunnel publishes the apps without opening a port on your router, and Cloudflare Access makes people sign in at Cloudflare before a request reaches them. The apps work with both, and the Access setup is done once for the whole suite.
The tunnel¶
In Cloudflare Zero Trust › Networks › Tunnels, create a tunnel, run cloudflared on a machine in your network, and add
a public hostname per app pointing at its address and port (calendar.example.com → http://192.168.1.10:8620, and so
on; see the table in Reverse proxy and HTTPS). Set PUBLIC_URL and COOKIE_DOMAIN
in each app as described there.
Access: one application for every app¶
- In Zero Trust › Access › Applications, add a self-hosted application for
*.example.com(or one per host) with a policy that lets your family in. - Copy the application's AUD tag (Overview › Application Audience tag).
- In any app, as a suite admin: Settings › Security › Sign-in at the proxy › Cloudflare Access, with your team
(
myteam.cloudflareaccess.com) and the AUD tag. Several AUD tags, comma-separated, if you made an application per host.
That setting is kept in the Shared ioSuite database, so every app picks it up. Each app checks the signed token
Cloudflare adds to every request (Cf-Access-Jwt-Assertion) against your team's keys and signs in the account with that
email, then that name. Sign out also ends the Access session. Turn on Sign-in required (Settings › Global settings ›
Apps) as well, so nobody gets in without it. Requests through Cloudflare never get one-click sign-in for passwordless
accounts.
Paths that must bypass Access¶
Phones and sync apps can't open the Access sign-in page. Add a policy with the action Bypass (Include: Everyone) for these paths, or a second Access application covering only them. They stay protected by the apps' own sign-in:
| App | Path | Used by | Still protected by |
|---|---|---|---|
| ioCalendar | /dav/*, /.well-known/caldav |
CalDAV clients and the Apple profile | user name and app password |
| ioCalendar | /ics/* |
subscription links and free/busy | the private token in the link |
| ioContacts | /dav/*, /.well-known/carddav |
CardDAV clients and the Apple profile | user name and app password |
| ioHealth | /v1/* |
the ioHealthSync iPhone app (or give it a service token) | user name and password |
| ioPix, ioNotes, ioCalendar, ioContacts | /mcp |
AI assistants, if MCP is on | an app password |
Everything else, the web apps and /api/, stays behind Access. A script calling /api/ from outside needs a Cloudflare
service token (CF-Access-Client-Id and CF-Access-Client-Secret) allowed in the policy, as well as an app password.
The ionotes Mac and iPhone apps take a service token in their settings. The ioSuite start page asks the other apps for its
tiles with the suite's service token (Settings › Global settings › Keys and tokens) when they are only reachable through
Access.
Optional: checking app passwords at the edge¶
Since /dav* bypasses Access, a small Cloudflare Worker can check the app password there and turn wrong ones away
before they reach your server. The apps keep a Workers KV namespace up to date with a SHA-256 of each app password
(never an account password). Store the three settings once under Settings › Global settings › Keys and tokens:
iosuite-shared edge --account-id … --namespace-id … --token-file token.txt
The Worker, its wrangler.toml, the rollout (monitor first, then enforce) and a WAF rate-limit rule are in
iosuite-shared's cloudflare/edge-auth.
Once it enforces, sync clients must use an app password, not the account password.