Module 4 — ZTNA: Publish a Private App with Access
Goal: Make an internal application reachable securely from anywhere — without a VPN and without opening any inbound firewall ports — and control exactly who can reach it.
| 👤 Who does this | App owner + Security |
| ⏱️ Time | ~60 minutes |
| 🎯 You'll finish with | A private app published at a real URL, reachable only by the people you allow |
| ✋ Before you begin | Modules 1–3 done; a private web app to test (e.g. an internal wiki, Grafana, a dev tool) running somewhere you control; a server/VM that can reach that app |
🧭 How this works: You'll run a tiny program called
cloudflarednext to your app. It makes a safe, outbound-only connection (a "Tunnel") to Cloudflare — so you never expose your app to the internet. Then Access puts a login check in front of it.
We'll go: (A) create a Tunnel → (B) connect your app → (C) add an Access application → (D) write the policy → (E) test → (F) build reusable groups.
Part A — Create a Tunnel
- 👉 Zero Trust → Networks → Tunnels.
- 👉 Click Create a tunnel.
- 👉 Choose Cloudflared → Next.
- ⌨️ Name it after the location, e.g.
datacenter-1→ Save tunnel.
📺 What you'll see: An "Install and run a connector" page with commands for each operating system and a long install token already filled in.
- 👉 Choose the tab for your server's OS (Windows / macOS / Debian / Red Hat / Docker).
- 👉 Copy the command shown — it already contains your unique token.
⚠️ Watch out: That command contains a secret token. Treat it like a password; don't paste it into chat or tickets.
Part B — Connect your app
Step B1 — Run the connector on your server
- 👉 Log in to the server/VM that can reach your app.
- 👉 Paste and run the command you copied. For example, on Debian/Ubuntu it looks like:
curl -L https://pkg.cloudflare.com/install.sh | sudo bash # if cloudflared isn't installed sudo cloudflared service install eyJhIjoiZXhhbXBsZS... # your token - 📺 Back in the dashboard, the tunnel's Connector status changes to Connected / Healthy (give it ~30 seconds).
✅ Checkpoint: The dashboard shows your connector as Healthy. Click Next.
Step B2 — Tell Cloudflare where your app lives (public hostname)
You'll now map a public URL to your internal app.
- 📺 You're on the Route tunnel / Public Hostnames step.
- 👉 Click Add a public hostname and fill in:
Field Example Meaning Subdomain wikiThe name users will type Domain yourcompany.comA domain in your Cloudflare account Type HTTPHow cloudflaredreaches your app locallyURL localhost:3000Where your app runs on that server - 👉 Click Save tunnel.
📺 What you'll see: wiki.yourcompany.com is now published and proxied to your internal app.
💡 No domain in Cloudflare yet? You'll need to add one (a zone) to use self-hosted Access apps with your own hostname. Ask your admin to add the domain, or use a domain you already manage in Cloudflare.
✅ Checkpoint: Visit https://wiki.yourcompany.com — you reach your app (right now it's open to anyone; the next part locks it down).
Part C — Add an Access application
This puts the login check in front of wiki.yourcompany.com.
- 👉 Zero Trust → Access controls → Applications.
- 👉 Click Create new application (Add an application).
- 👉 Choose Self-hosted and private.
📺 What you'll see: An application configuration page.
- ⌨️ Application name:
Internal Wiki. - 👉 Click Add public hostname and enter the same hostname you published: subdomain
wiki, domainyourcompany.com. - 👉 Set Session Duration to
24h(use a shorter value like1hfor sensitive apps). - 👉 Under authentication, select the identity provider from Module 2.
- 💡 If you only have one IdP, turn on Apply instant authentication so users skip the chooser screen.
- 💡 Turn on Authenticate with Cloudflare One Client to let already-signed-in WARP users in seamlessly.
- Don't click Create yet — first add a policy in Part D (the wizard lets you add it inline), or click Create and add the policy right after. Either works.
Part D — Write the access policy (who's allowed)
-
👉 In the application, go to the Policies section → Add a policy (or Create new policy).
-
⌨️ Policy name:
Allow — Employees on healthy devices. -
👉 Action: Allow.
-
👉 Build the rules:
Rule type Selector Operator Value Include Emails ending in — @yourcompany.comRequire Device Posture in Disk encrypted(from Module 3)(If your IdP groups tested correctly in Module 2, use Include → IdP Groups →
Engineeringinstead of the email domain for tighter control.) -
👉 Click Save the policy, then Save/Create the application.
💡 Best practice — add a default-deny net: create a second, lower-priority policy named Block — Everyone with Action = Block and Include = Everyone. Because policies are read top-down, the Allow matches your people first and everyone else hits the Block.
⚠️ Watch out: Never use the Bypass action on a sensitive app — it removes the login check entirely.
Part E — Test it (the satisfying part)
Test 1 — an allowed user
- 👉 On your pilot device (signed in as an allowed employee), open
https://wiki.yourcompany.com. - 📺 You're sent to your company login (or straight in, if instant auth + WARP session). After login, your app loads. ✅
Test 2 — a blocked user
- 👉 Open the same URL in a private/incognito window and sign in as someone not allowed (or use a personal email).
- 📺 You see the Cloudflare "You don't have access" block page. ✅
Test 3 — check the logs
- 👉 Zero Trust → Logs → Access (or Access → Logs).
- 📺 You see both attempts: one Allowed, one Blocked, each with the user's email and the policy that decided it.
✅ Checkpoint: The right people get in, the wrong people are blocked, and you can see it in the logs. You've just replaced VPN for this app. 🎉
Part F — Build reusable groups (so you don't repeat yourself)
Right now your rules are typed into one app. When you add a 2nd, 3rd, 10th app, you don't want to re-type them. Build a reusable Access Group once.
- 👉 Zero Trust → Access controls → Policies → Groups (or Reusable components → Groups).
- 👉 Click Add a group.
- ⌨️ Name:
Secure employees. - 👉 Add rules once, e.g.:
- Include → Emails ending in →
@yourcompany.com - Require → Device Posture →
Disk encrypted
- Include → Emails ending in →
- 👉 Click Save.
Now in any application's policy, you can simply select Include → Access Groups → Secure employees. Change the group once, and every app updates.
💡 Tip: Also create reusable Lists (Zero Trust → Reusable components → Lists) for things like an Offboarding email list or approved device serial numbers — then Exclude → Emails in list → Offboarding across all apps.
✅ Module 4 complete!
You now have:
- ✅ A private app published via Tunnel (no inbound ports opened)
- ✅ An Access policy allowing the right people, blocking others
- ✅ A working allow/block test, visible in logs
- ✅ A reusable group to apply to future apps
Want more app types?
- SaaS apps (Salesforce, etc.): Applications → Create new application → SaaS → integrate via SAML/OIDC.
- SSH / RDP servers: publish through the same Tunnel and use Access for Infrastructure (optionally browser-rendered, no client needed).
- Unmanaged-device fallback: keep the Allow for healthy devices, and add a Gateway → Isolate rule (Module 5) so risky devices open the app in a safe remote browser instead of being blocked.
Quick troubleshooting
| Problem | Fix |
|---|---|
| App URL shows a Cloudflare error, not your app | Tunnel connector not healthy, or the local URL/port is wrong — recheck Part B |
| Everyone is blocked, including you | Your Include rule is too narrow or a Block policy is above the Allow — fix order (Part D) |
| Group rule never matches | Groups didn't come through in Module 2's Test — fix the IdP first |
| Allowed but app keeps asking to log in | Session duration too short, or cookies blocked — raise session duration |
| "DNS record already exists" | A record for that hostname exists; delete it or pick another subdomain |
🔌 Want the full picture on connectors? This module used one Cloudflare Tunnel for a single app. To connect whole subnets, do site-to-site / device mesh, or bring an entire office online, see Module 4b — Connectors: Tunnel, Mesh & Appliance.
👉 Next: Module 4b — Connectors · or Module 5 — Gateway
Module 4b goes deep on Tunnel, Mesh, and the Cloudflare One Appliance. Or skip to Gateway to start filtering traffic.