Skip to content

Instantly share code, notes, and snippets.

@DmitrySkibitsky
Last active August 1, 2026 13:26
Show Gist options
  • Select an option

  • Save DmitrySkibitsky/5623fc698b39a28dc089bac2bf88d5fa to your computer and use it in GitHub Desktop.

Select an option

Save DmitrySkibitsky/5623fc698b39a28dc089bac2bf88d5fa to your computer and use it in GitHub Desktop.
Cloudflare WARP (Zero Trust) Setup on NixOS

Cloudflare WARP (Zero Trust) Setup on NixOS

Cloudflare WARP Zero Trust client for accessing internal resources at devteam.local.

How It Works

The cloudflare-warp NixOS module installs warp-svc (daemon) and warp-cli (control tool). There is no GUI — everything is done via warp-cli.

First-Time Setup

1. Apply NixOS Configuration

nh-switch

The cloudflare-warp.nix module enables warp-svc and starts it automatically at boot.

2. Create MDM Config (organization name)

This tells the daemon which Zero Trust organization to enroll into.

sudo mkdir -p /var/lib/cloudflare-warp
sudo tee /var/lib/cloudflare-warp/mdm.xml <<'EOF'
<dict>
    <key>organization</key>
    <string>devteam</string>
</dict>
EOF

3. Restart the Daemon

sudo systemctl restart cloudflare-warp
warp-cli mdm refresh

4. Enroll (opens browser for authentication)

warp-cli registration new devteam

A browser tab opens at https://devteam.cloudflareaccess.com/warp.

  • Log in with your @devteam.local email
  • Enter the one-time PIN sent to your inbox
  • The page shows "Success!"

5. Complete Auth Callback

The browser will show a dialog "Open Cloudflare One CLI?" — click Open Cloudflare One CLI.

If the dialog doesn't work (no registered handler visible), copy the link from the "Open Cloudflare WARP" button (right-click → Copy Link) and run:

xdg-open "com.cloudflare.warp://devteam.cloudflareaccess.com/auth?token=..."

Terminal should print Success.

6. Connect

warp-cli connect
warp-cli status   # → Status update: Connected

Troubleshooting

"Old registration is still around"

sudo systemctl stop cloudflare-warp
sudo rm /var/lib/cloudflare-warp/reg.json
sudo systemctl start cloudflare-warp
warp-cli registration new devteam

"Operation not authorized in this context"

Do not use warp-cli registration delete — it is blocked in Zero Trust mode. Remove reg.json manually as shown above.

Check daemon status / logs

systemctl status cloudflare-warp
journalctl -u cloudflare-warp -f

Daily Usage

warp-cli connect      # connect
warp-cli disconnect   # disconnect
warp-cli status       # show connection status
warp-cli tunnel stats # bytes sent/received, latency

The daemon starts automatically at boot but does not auto-connect. Run warp-cli connect after each reboot, or when you need access to internal resources.


Verify Registration

warp-cli registration show

Expected output includes Organization: devteam and Account type: Team.


Telemetry Blocking

warp-api-redirect.service blocks telemetry by redirecting the WARP API endpoint to 127.0.0.1 — a local address where nothing listens. All telemetry calls fail with a TLS error and no data leaves the machine.

The redirect is applied only after WARP reaches Connected state, so the WireGuard tunnel is fully up before the API is blocked. If WARP has not connected within 60 s, the service exits with an error and systemd retries it every 30 s — this handles both auto-connect and manual warp-cli connect.

ExecStop resets the endpoint on daemon restart, letting WARP reconnect cleanly before the block is re-applied.

Configuration (nixos/modules/cloudflare-warp.nix)

{ pkgs, ... }:
{
  services.cloudflare-warp.enable = true;

  # Suppress journal spam from API retry errors once the endpoint is redirected.
  systemd.services.cloudflare-warp.serviceConfig = {
    LogRateLimitIntervalSec = 60;
    LogRateLimitBurst = 5;
  };

  # WARP overwrites /etc/resolv.conf with its DNS (127.0.2.2, 127.0.2.3) on connect.
  # This path unit watches for that write and appends dnsmasq (127.0.0.1) as fallback,
  # so DNS still resolves if WARP's proxy is unavailable.
  systemd.paths.warp-dns-fallback = {
    description = "Watch resolv.conf for WARP DNS override";
    wantedBy = [ "multi-user.target" ];
    pathConfig = {
      PathModified = "/etc/resolv.conf";
      Unit = "warp-dns-fallback.service";
    };
  };

  systemd.services.warp-dns-fallback = {
    description = "Append dnsmasq fallback to WARP-managed resolv.conf";
    serviceConfig = {
      Type = "oneshot";
      ExecStart = pkgs.writeShellScript "warp-dns-fallback" ''
        if grep -q "cloudflare-warp" /etc/resolv.conf \
          && ! grep -q "nameserver 127.0.0.1" /etc/resolv.conf; then
          echo "nameserver 127.0.0.1" >> /etc/resolv.conf
        fi
      '';
    };
  };

  # Block WARP telemetry by redirecting its API endpoint to localhost — but only
  # AFTER the tunnel reaches Connected state, so WireGuard is up before the API
  # is blocked.
  #
  # Type=simple so activation does not wait for the process to exit: the process
  # starts immediately and polls in the background until WARP connects. This
  # avoids blocking or failing nixos-switch when WARP is not yet connected.
  #
  # partOf ensures that when warp-svc is restarted, this service is also
  # restarted: ExecStop resets the endpoint so WARP can reconnect cleanly,
  # then ExecStart waits for Connected again before re-applying the block.
  systemd.services.warp-api-redirect =
    let
      warp-cli = "${pkgs.cloudflare-warp}/bin/warp-cli";
    in
    {
      description = "Redirect WARP API to localhost to block telemetry";
      after = [ "cloudflare-warp.service" ];
      partOf = [ "cloudflare-warp.service" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        Type = "simple";
        ExecStart = pkgs.writeShellScript "warp-api-redirect-start" ''
          until ${warp-cli} --accept-tos status 2>/dev/null | grep -q "Connected"; do
            sleep 5
          done
          ${warp-cli} --accept-tos api endpoint set 127.0.0.1
          exec sleep infinity
        '';
        ExecStop = "${warp-cli} --accept-tos api endpoint reset";
      };
    };
}

What still works after blocking

Feature Status
WireGuard / MASQUE tunnel ✅ — UDP, independent of API
warp-cli connect / disconnect / status ✅ — local socket
DNS-over-HTTPS resolver (127.0.2.2) ✅ — separate from API
DEX device_state telemetry ❌ blocked
report_aggregate_stats / structured_logs ❌ blocked
Zero Trust policy updates ❌ blocked

Key risk: WireGuard key rotation

WireGuard keys are rotated periodically (interval set by the org admin, typically 24 h–30 days). When warp-svc tries to rotate keys via the blocked API, the rotation fails. The tunnel stays up using the old keys until they expire. At that point WARP disconnects.

Recovery: restart the daemon — it will reset the endpoint, reconnect, rotate keys, and then warp-api-redirect.service will re-apply the block.

sudo systemctl restart cloudflare-warp
warp-cli connect   # if not auto-connecting

What to expect in logs

journalctl -u cloudflare-warp -f

Periodic errors like the following are expected and harmless — the rate-limit cap keeps them to at most 5 per minute:

ERROR warp_svc: API request failed: InvalidCertificate(UnknownIssuer), retrying in 34s
ERROR warp_svc: request=report_aggregate_stats failed, retrying in 68s

Telemetry details

What WARP collects

In Zero Trust / Teams mode, warp-svc sends the following data to Cloudflare's API (zero-trust-client.cloudflareclient.com) every ~60 seconds via the device_state endpoint:

  • Running processes — top apps by CPU and RAM usage (visible in the Zero Trust dashboard under DEX → Fleet Status → Top applications by CPU over time)
  • CPU and RAM utilization (total and per-app)
  • Network interface details — local IP, gateway IP, ISP public IP, Wi-Fi SSID, signal strength, upload/download throughput
  • Battery level and charging state
  • Disk usage percentage and I/O throughput

Additional periodic uploads:

Request What it sends
report_aggregate_stats Internal daemon counters and timing histograms
dex_test_results Results of synthetic HTTP tests (e.g. GET cloudflarestatus.com every 5 min)
structured_logs Structured event logs from the daemon
get_posture_rules Fetches device posture policy from Zero Trust

DEX monitoring and device posture collection are controlled by the org admin via dash.teams.cloudflare.com. Without admin access, these cannot be disabled.


DNS Fallback

Problem

When WARP connects, warp-svc overwrites /etc/resolv.conf with its own local DNS proxies:

nameserver 127.0.2.2
nameserver 127.0.2.3

These addresses are only served by the WARP daemon. If WARP is still connecting at boot or temporarily unavailable, 127.0.2.2 does not respond and DNS fails system-wide.

Solution

cloudflare-warp.nix adds a systemd path unit that watches /etc/resolv.conf. Whenever WARP writes to it, the unit appends 127.0.0.1 (dnsmasq) as a third nameserver:

nameserver 127.0.2.2
nameserver 127.0.2.3
nameserver 127.0.0.1   ← appended by warp-dns-fallback.service

The OS resolver tries nameservers in order. If 127.0.2.2 times out (WARP not ready), it falls through to 127.0.0.1 — dnsmasq, which uses 8.8.8.8 / 1.1.1.1 as upstreams.

If DNS breaks after a reboot

WARP may have left a stale resolv.conf without the fallback entry. Fix:

sudo systemctl restart warp-dns-fallback.service
# or restart WARP so the path unit triggers again:
sudo systemctl restart cloudflare-warp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment