Cloudflare zero-trust tunnels via API

The idea that I need to manually create a Cloudflare tunnel to use it with cloudflared module, and then store its credentials in some file never sat well with me. Why all this trouble when CF offers API tokens, and it’s actually all you need. You can literally do everything with a token. Sadly, the default cloudflared module is very limeted. So I created my own process. It’s a work in progress, obviously :slight_smile:

I’m not big on sharing half-baked code, so here’s my thought process, use it if you like.

Pre-requisites:

  1. CF account (obviously)
  2. CF super-token (see below; I store it in SOPS-nix)
  3. A domain name, registered anywhere, but with DNS servers pointing to *.ns.cloudlare.com
  4. tunnelIngressMap that looks like this:
  tunnelIngressMap = {
    "books.mydomain.zzz" = "http://calibre-web.lan:8083";
    "ha.mydomain.zzz" = "http://rpi4.lan:8123";
    "jp.mydomain.zzz" = "http://joplin.lan:8080";
    "sp.mydomain.zzz" = "http://speedtest.lan";
    "tb.mydomain.zzz" = "http://rpi4.lan:8443";
    "whoami.mydomain.zzz" = "http://whoami.lan";
  };

Super-token permissions:

  • AccountCloudflare TunnelEdit
  • Scope: IncludeYour Account
  • ZoneZone SettingsRead
  • ZoneZoneRead
  • ZoneZoneEdit
  • ZoneDNSRead
  • ZoneDNSEdit

That’s it. But without tunnel credentials, how do you opreate one? Turns out, you don’t need credentials: using the API you can get a JWT and run the tunnel using it. Nothing extra to store in SOPS-nix.

The whole thing runs in a non-privileged LXC container, so attack surface is ~zero.


SCRIPT LOGIC: provision-tunnel-and-dns

This script is the declarative heart of the server’s cloud infrastructure. It is designed to be fully idempotent and self-healing, meaning it can be run on every boot to verify and, if necessary, repair the required Cloudflare configuration from a zero-knowledge state. It ensures that the Cloudflare Zone, DNS records, and Tunnel are all correctly configured before the main cloudflared daemon starts.

The script executes the following steps in order:

STEP 1: Zone Verification and Creation

  1. API Call: The script first makes a GET request to the Cloudflare API to
    search for a Zone with the specified name (e.g., mydomain.zzz).

  2. Conditional Logic:

  • If the Zone exists: The script extracts the Zone ID and Account ID
    from the successful response and proceeds to the next step.
  • If the Zone does NOT exist: This triggers a multi-stage
    creation process to prevent errors and provide clear feedback.
    a. Public DNS Check: Before attempting to create the zone via the API,
    it uses the dog command to perform a public DNS lookup for the
    domain’s NS (nameserver) records. (dog --json is a nice alternative to dig)
    b. Nameserver Validation: It checks if the returned nameservers belong
    to Cloudflare. This is a critical pre-flight check.
    c. Error Handling: If the nameservers are not pointing to Cloudflare,
    the script halts with a fatal error. It prints a user-friendly
    message explaining that the domain must be delegated at the registrar
    before the automation can work. This prevents confusing API errors.
    d. Zone Creation: If the nameservers are correct, the script proceeds.
    It discovers the Account ID from the API and then makes a POST
    request to create the zone under that account.

STEP 2: Wildcard A Record Provisioning

  1. Why: In addition to CF tunnels I run some services without proxying (Jellyfin, XTLS/Xray, etc). This A-record is later maintained by cloudflare-dyndns. For this to work properly I also get my own certificate from LetsEncrypt via lego/acme, in addition to CF’s cert that CF uses for its proxied services.
  2. IP Discovery: The script determines the server’s current public IPv4 address.
  3. Check for Existence: It queries the API for a wildcard A record for the
    zone (e.g., *.mydomain.zzz).
  4. Create if Missing: If no such record exists, it creates one, pointing to the
    server’s public IP. This record is explicitly set to “DNS Only” (not proxied).

STEP 3: Tunnel Provisioning

  1. Search for Active Tunnel: The script queries the API for all tunnels associated
    with the account. It then filters this list to find a tunnel that both
    matches the desired TUNNEL_NAME and is not deleted (i.e., its deleted_at
    field is null).
  2. Create if Missing: If no active tunnel with the correct name is found, the
    script makes a POST request to create a new one. It includes robust error
    checking to ensure the creation was successful.
  3. Store ID: The script stores the ID of either the found or newly created
    tunnel in the TUNNEL_ID variable for later steps. Notably, all credentials data for the tunnel is immediately discarded.

STEP 4: CNAME Record Idempotency

The script loops through every hostname defined in the tunnelIngressMap.

  1. Check Record: For each hostname, it queries the API for an existing CNAME record.
  2. Extract Details: It extracts both the ID and the content (the target it points to) of the found record.
  3. Validate Content: It compares the content of the existing record against the correct target for the currently active tunnel (e.g.,
    <current-tunnel-id>.cfargotunnel.com).
  4. Repair if Incorrect: If the record does not exist, or if it exists but points to the wrong tunnel ID (a stale record), the script takes corrective action:
    a. Delete: If an incorrect record exists, it is first deleted using its ID.
    b. Create: A new, correct CNAME record is created, pointing to the
    correct tunnel target. This record is set to “Proxied”.
  5. Do Nothing if Correct: If the record exists and is already correct, the
    script reports this and moves on.

STEP 5: Fetch Ephemeral JWT

  1. With the guaranteed-correct Tunnel ID, the script makes a final API call to fetch a short-lived, ephemeral JSON Web Token (JWT). This token is used by the cloudflared daemon to authenticate its connection to the Cloudflare network.

STEP 6: Save JWT for the Service

  1. Write to File: The script writes the fetched JWT to a temporary file at
    /run/cloudflared/ephemeral-token.jwt.
  2. Format for systemd: It writes the token in the TUNNEL_TOKEN=<jwt> format, which is required by systemd’s EnvironmentFile directive.
  3. Set Permissions: It securely sets the ownership and permissions on the file so that only the cloudflared user can read it.

1 Like