Skip to content

Teleport behind Cloudflare Tunnel + Access + Duo

This is how the Tenantry maintainer runs Teleport. Nothing is opened on the firewall. People reach the Teleport web UI through Cloudflare Access with Duo. This page explains the settings and the one hard problem: tsh and agents are not browsers.

flowchart LR
  B[Browser] -->|Duo MFA| CFA[Cloudflare Access]
  CFA --> CF[Cloudflare edge]
  CF -->|outbound tunnel| CD[cloudflared]
  CD -->|https://teleport:443| T[Teleport proxy]
  TSH[tsh and client proxies] -.->|"blocked by Access: needs a separate path"| CF
  TSH ==>|"private network path (WireGuard / NetBird)"| T

What works through the tunnel

  • The Teleport web UI and application access (browser sessions), behind Cloudflare Access.
  • tsh, agents and tbot work through a layer-7 proxy too. Since Teleport 15.1 the clients use native WebSocket upgrades for TLS routing behind load balancers and reverse proxies (TLS routing architecture).
  • What does not work: Cloudflare Access cannot be passed by these clients, because they cannot complete an interactive Access login. Access blocks them before they reach Teleport. You need one of the three options below.

Teleport still enforces its own authentication and MFA on every connection. Access is an extra layer, not a replacement.

Cloudflare tunnel settings

In Zero Trust, Networks, Tunnels, your tunnel, Public hostname:

Public hostname Service Additional application settings
teleport.acme.ch https://teleport:443 TLS: Match SNI to Host on
*.teleport.acme.ch (only for application access) https://teleport:443 TLS: Match SNI to Host on
tenantry.acme.ch (only if Tenantry rides the same tunnel) http://tenantry:8443 none
  • Teleport routes by SNI, so Cloudflare must present the public name to the origin: Match SNI to Host.
  • Teleport keeps its own certificate (ACME DNS-01 with the wildcard, or your files), verified by cloudflared. Only a self-signed lab cluster needs No TLS Verify.
  • The Teleport master bundle with Cloudflare Tunnel switched on writes the cloudflared service and lists these hostnames in its README. See A new Teleport cluster.

The equivalent locally-managed ingress rules, for reference:

ingress:
  - hostname: teleport.acme.ch
    service: https://teleport:443
    originRequest:
      matchSNItoHost: true
  - service: http_status:404

Applications need three more things

Application access gives every application its own name (asu-steward.teleport.acme.ch). For that name to open in a browser, three things must exist. Each one fails differently, so check them in this order.

# What If it is missing
1 DNS: a proxied wildcard record *.teleport.acme.ch, type Tunnel, pointing at your tunnel. The browser says "Server not found" (the name does not resolve).
2 A certificate at the Cloudflare edge that covers *.teleport.acme.ch. Universal SSL covers acme.ch and *.acme.ch only: a name with two labels under the zone is not covered. Order an Advanced Certificate for *.teleport.acme.ch (TXT validation is automatic when Cloudflare hosts the zone), or switch Total TLS on. Both need Advanced Certificate Manager (paid). The browser shows a certificate error on the application name, while teleport.acme.ch itself works.
3 A route in the tunnel for *.teleport.acme.ch, with the settings below. Cloudflare answers 502 or 404 for the application name.

Route settings for the wildcard (they differ from the teleport.acme.ch rule):

  • service: https://teleport:443 and Match SNI to Host on (matchSNItoHost: true), so Teleport receives the application name.
  • Do not copy httpHostHeader or originServerName from the teleport.acme.ch rule if it has them. They force one fixed name, so every application lands on the proxy home page instead of its own app.
  • noTLSVerify: true only if the origin certificate is not trusted by cloudflared (self-signed lab). Keep it off otherwise.

The dashboard may refuse the wildcard

The Public hostname form can answer Invalid subdomain format for *.teleport (a wildcard with two labels). Add the rule through the API instead. A tunnel managed from the dashboard has no config.yml on the server (/etc/cloudflared/config.yml missing is normal): cloudflared runs with a token and fetches its rules from Cloudflare, so there is no file to edit.

Create an API token limited to Account / Cloudflare Tunnel / Edit (My Profile, API Tokens, Custom token). The account id is the first path segment of the dashboard URL (dash.cloudflare.com/<ACCOUNT_ID>/...), the tunnel id is on the tunnel page. Then:

export CF_TOKEN='...'   # type it in your own shell, never in a chat or a ticket
URL="https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/cfd_tunnel/<TUNNEL_ID>/configurations"
cfg=$(curl -s -H "Authorization: Bearer $CF_TOKEN" "$URL" | jq '.result.config
  | .ingress |= (.[:-1] + [{"hostname":"*.teleport.acme.ch","service":"https://teleport:443","originRequest":{"matchSNItoHost":true}}] + .[-1:])')
echo "$cfg" | jq .ingress      # review: your existing rules, the new one, then the catch-all last
curl -s -X PUT -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" --data "{\"config\":$cfg}" "$URL" | jq .success

The new rule goes before the final catch-all (http_status:404); cloudflared picks it up without a restart. Delete the token afterwards. To undo, remove the rule from the tunnel's public hostnames.

To test before DNS caches expire, resolve the name yourself: curl -sS -D - --resolve asu-steward.teleport.acme.ch:443:<a Cloudflare IP> https://asu-steward.teleport.acme.ch/. A 302 to /web/launch/... is the proxy asking you to sign in: the path works.

Cloudflare Access with Duo

Create a self-hosted Access application for teleport.acme.ch (and *.teleport.acme.ch if you publish applications). Policy: Allow for your team, session 8 hours or less.

  • Duo as the Access identity provider. Duo Single Sign-On can be added to Cloudflare Access as a SAML identity provider, so the Access login is your directory plus Duo MFA. Follow Cloudflare's and Duo's current guides for the exact fields.
  • You can instead keep your usual identity provider in Access and let it require Duo.
  • Create the Access application before the public hostname.
  • Duo protects the browser layer. Teleport adds its own MFA (WebAuthn or OTP) for logins and, with require_session_mfa, for sensitive sessions.

Letting tsh and agents through

Choose one. They can be combined.

Option A: Access Bypass for known source IPs

Add a second Access policy of action Bypass (IP ranges selector) listing your office and the egress IPs of the client sites where agents run.

  • Simple, nothing to install on the clients.
  • It is a minimal but real risk. Egress addresses may be shared or behind carrier-grade NAT, so someone else on the same address also gets past Access. The list needs maintenance when a site changes ISP.
  • Teleport still demands a valid identity, certificate and MFA. A stranger who gets past Access has reached the same login screen as anyone on the Internet would on a bare Teleport.

Good for a handful of stable sites. It gets tiring beyond that.

Option B: a private network path (WireGuard or NetBird)

tsh and agents reach Teleport over a private network and never touch Cloudflare. In the master bundle, set Private address for tsh and agents to the address of the VPN interface: port 443 is then published on that address only, and the tunnel stays the public way in. (All-in-one Steward bundles: the bind address in the advanced options.)

Clients must resolve teleport.acme.ch to that private address (internal DNS, /etc/hosts or the VPN's DNS), so the name, the SNI and the certificate all still match.

WireGuard (plain) NetBird
What it is Kernel VPN. You manage keys and [Peer] blocks by hand Management layer on top of WireGuard: central server, SSO, ACLs, UI
Documentation Very good, but low level: the quick start and man pages cover the tunnel, not a fleet Task oriented: how it works, install, ACLs, SSO guides
Adding a peer Edit the config on both ends Install the client, enrol with SSO or a setup key
Access control Firewall rules you write Groups and access policies in the UI, enforced on each client
Users and SSO None Your identity provider (OIDC)
NAT traversal You need a reachable UDP endpoint (a hub) Peers connect out; direct when possible, relay fallback
Self-hosting Nothing to host Management, Signal and Relay can be self-hosted, or use the cloud
Failure mode Silent: a wrong key just drops packets A UI shows peers and their state

Recommendation. For an MSP with many client sites, use NetBird. SSO enrolment, groups and ACLs (for example "client-acme gateways can reach only the Teleport proxy on 443"), a management UI that shows which site is online, and outbound-only connectivity from the client side make a fleet manageable. Use plain WireGuard for a few fixed sites, where one config file per site is fine and the extra moving parts of NetBird are not worth it. Both give you the same property: the Teleport port is never on the Internet.

Verify before you commit

Product documentation and features change. Read the current NetBird and WireGuard documentation before you pick one, and test one real client site first.

Option C: a direct TCP endpoint with a firewall allow-list

Publish a second public name that points straight at the server's port 443, outside the tunnel, and allow only known source IPs on the firewall.

  • Teleport must know that name (public_addr), and its certificate must cover it.
  • You get the simplicity of option A without Cloudflare in the path, but an open port exists. Any mistake in the allow-list exposes the proxy, and the same shared-IP caveat applies.
  • Prefer B when you can.

Which one?

Situation Choose
A few stable sites with fixed addresses, the lowest effort A (Bypass for IPs)
Many client sites, changing addresses, you want ACLs and visibility B with NetBird
A few fixed sites and you like simple things B with plain WireGuard
You cannot install anything on the client side and addresses are stable C

Tenantry in front of this

Tenantry on the same server uses --teleport-local, which reaches the auth service (<container>:3025) on the Docker network. It never goes through the tunnel, the WAF or Access.

For Tenantry itself, either add it as a route of the same tunnel (with its own Access application and the Bypass application for /api/callback and /api/i, see Exposure: never expose raw), or publish it as a Teleport application.