Cloudflare Access setup
Three pieces: a tunnel that publishes the host, an Access application that guards it, and a service token the app authenticates with.
1. Publish the host
Install cloudflared on the server and create a tunnel:
cloudflared tunnel login
cloudflared tunnel create my-ssh-host
cloudflared tunnel route dns my-ssh-host ssh.example.com
Point it at local SSH in the tunnel's config:
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
- service: http_status:404
Run it as a service. With this in place you can close port 22 to the internet entirely — the tunnel is outbound.
2. Create the Access application
Zero Trust dashboard → Access → Applications → Add an application → Self-hosted. Use the hostname you routed. Add at least one policy so it is not open to everyone.
3. Create a service token
Access → Service Auth → Service Tokens → Create. Copy the Client ID and Client Secret — the secret is shown once.
The token is not enough on its own. Go back to your application, add a policy with action Service Auth and a Service Token rule naming this token. A token with no matching policy is refused in exactly the same way as an invalid one, which is the most common first-run problem.
4. Add it to the app
Settings → Cloudflare Access. Enter the client ID and secret; the secret is sealed with the device Keystore. One token covers every application its policies allow, so this is configured once rather than per host.
5. Add a host
Choose Cloudflare Access and enter the application hostname (ssh.example.com).
Then set a password or key as usual. Access decides whether you may reach the host; the host still authenticates you. Pairing it with a hardware-backed key gives you both.
If it fails
- Rejected immediately
- Usually the missing Service Auth policy rather than a wrong token. Access answers an unauthorised request with a redirect to its login page rather than a 401.
- "No Access application on that hostname"
- The hostname does not match the application, or DNS has not propagated.
- Tunnel connects, SSH does not
- The ingress rule is not pointing at
ssh://localhost:22.