Skip to content

Cloudflare Zero Trust

The cloudflare role manages Cloudflare Tunnels (connectors) and Cloudflare Access applications. It uses the Cloudflare API directly – no cloudflared login required on the control node.

How it works

  1. Connector (tunnel): A named cloudflared tunnel is created in your Cloudflare account and installed as a systemd service on the target host.
  2. Published hostnames: Each route in published_hostnames is added to the tunnel's ingress config and gets a CNAME DNS record pointing to the tunnel.
  3. Access applications: Each route also gets a corresponding Cloudflare Access application that enforces authentication policies before allowing traffic through.

Required secrets

These must be in an encrypted vault file:

vault_cloudflare_credentials_api_token: "..."   # Cloudflare API token
vault_cloudflare_credentials_account_id: "..."  # Cloudflare account ID

Minimal host configuration

The simplest way to configure a web host is via cloudflare.zero_trust in the host's main.yml:

# inventories/prod/host_vars/bkdk-web-01/main.yml

cloudflare:
  zero_trust:
    slug_web: bkdk-e01          # becomes the tunnel name and SSH hostname prefix

    application:
      public_hostnames:
        - subdomain: cms
          domain: barberklingen.dk
          path: /wp/wp-graphql    # allow public access to this path
        - subdomain: cms
          domain: barberklingen.dk
          path: wc-api/*

      private_hostnames:
        - subdomain: cms
          domain: barberklingen.dk
          path: '*'               # require authentication for everything else

The role automatically: - Creates a tunnel named bkdk-e01 - Adds an SSH ingress rule pointing to localhost:22 - Creates CNAME DNS records for all published hostnames - Creates Access applications with appropriate policies

Access policies

Three pre-defined policy UUIDs are available in roles/cloudflare/defaults/main.yml:

Variable Cloudflare policy name Who it allows
cloudflare_policy_allow_emails ALLOW-GROUP-IT-ORG Subscribed team email addresses
cloudflare_policy_allow_bitbucket_ci ALLOW-GROUP-BITBUCKET-CI Bitbucket CI service token
cloudflare_policy_allow_all BYPASS-GROUP-PUBLIC-WEB Everyone (no authentication)

Always use the variable, never hardcode the policy name or UUID. Combine policies only when necessary – default to the most restrictive option.

To assign a specific policy to a route:

cloudflare:
  zero_trust:
    connector:
      application_routes:
        - subdomain: metrics
          domain: example.com
          service: http
          url: localhost:9090
          policies:
            - "{{ cloudflare_policy_allow_emails }}"

Advanced connector and application configuration

For full control, use the lower-level variables directly.

Connectors define what the server exposes to Cloudflare (the tunnel ingress). Applications define who may access those exposed services (the Access policy layer).

cloudflare_connectors:
  - name: mysite-e01
    published_hostnames:
      - subdomain: mysite-e01
        domain: subscribed.build
        service: ssh
        url: localhost:22
      - subdomain: cms
        domain: mysite.dk
        service: http
        url: localhost:80

cloudflare_applications:
  - name: mysite-e01 - SSH
    attr:
      type: self_hosted
      hostnames:
        - subdomain: mysite-e01
          domain: subscribed.build
      policies:
        - "{{ cloudflare_policy_allow_emails }}"
        - "{{ cloudflare_policy_allow_bitbucket_ci }}"

Adding connectors or applications without replacing the base

Use the _extra variants – they are merged additively with the base lists:

cloudflare_connectors_extra:
  - name: mysite-e01-extra
    published_hostnames:
      - subdomain: metrics
        domain: subscribed.build
        service: http
        url: localhost:9090

cloudflare_applications_extra:
  - name: mysite-e01 - Metrics
    attr:
      policy:
        - "{{ cloudflare_policy_allow_emails }}"
      hostnames:
        - subdomain: metrics
          domain: subscribed.build
          path: ""

Extending existing public/private applications

To add hostnames to the built-in private or public application without creating a new application:

# Protected by Access policy (admin routes, internal paths)
cloudflare_private_hostnames:
  - subdomain: cms
    domain: mysite.dk
    path: '*'

# Intentionally open to the public (no authentication)
cloudflare_public_hostnames:
  - subdomain: cms
    domain: mysite.dk
    path: /wp/wp-graphql

Keep the existing default entries in these lists – they are part of the base setup.

Zero Trust lockdown mode

When zt_lockdown_enabled: true, the server becomes fully tunnel-only. Three things happen:

  • SSH is bound to 127.0.0.1 only (sshd no longer listens on the network)
  • UFW removes all allow rules for port 22 and adds an explicit deny
  • Access shifts to Cloudflare: connectors publish localhost:22 as a TCP service; Access applications control who may open a session

The model is service-level exposure, not server-level: the host itself is not broadly reachable – only specific routes defined in cloudflare_connectors are exposed, and only to principals allowed by cloudflare_applications.

Enabling lockdown

See the New server guide for the full workflow. The lockdown.yml playbook verifies that cloudflared is healthy before closing the firewall.

SSH access after lockdown

# Direct SSH via cloudflared
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" pt@bkdk-e01.subscribed.build

# Or configure ~/.ssh/config
Host *.subscribed.build
  User pt
  IdentityFile ~/.ssh/id_ed25519
  ProxyCommand cloudflared access ssh --hostname %h

Ansible access after lockdown

Set these host variables:

# inventories/prod/host_vars/bkdk-web-01/main.yml
ansible_host: bkdk-e01.subscribed.build
ansible_ssh_common_args: '-o ProxyCommand="cloudflared access ssh --hostname %h"'

Install cloudflared without creating a tunnel

Set cloudflared_install_only: true to install the cloudflared binary and service without configuring a tunnel. Useful during the bootstrap phase.

Key defaults

Variable Default Description
cloudflared_remote_config true Use Cloudflare-managed config (not local YAML)
cloudflared_create_tunnel true Create tunnel if it doesn't exist
cloudflared_dns_proxied true DNS records are proxied through Cloudflare
cloudflare_application_session_duration 24h Access session duration
cloudflared_no_log false Suppress sensitive output in task logs