Cloudflare WARP Zero Trust client for accessing internal resources at devteam.local.
The cloudflare-warp NixOS module installs warp-svc (daemon) and warp-cli (control tool).
There is no GUI — everything is done via warp-cli.
nh-switchThe cloudflare-warp.nix module enables warp-svc and starts it automatically at boot.
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>
EOFsudo systemctl restart cloudflare-warp
warp-cli mdm refreshwarp-cli registration new devteamA browser tab opens at https://devteam.cloudflareaccess.com/warp.
- Log in with your
@devteam.localemail - Enter the one-time PIN sent to your inbox
- The page shows "Success!"
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.
warp-cli connect
warp-cli status # → Status update: Connectedsudo systemctl stop cloudflare-warp
sudo rm /var/lib/cloudflare-warp/reg.json
sudo systemctl start cloudflare-warp
warp-cli registration new devteamDo not use warp-cli registration delete — it is blocked in Zero Trust mode.
Remove reg.json manually as shown above.
systemctl status cloudflare-warp
journalctl -u cloudflare-warp -fwarp-cli connect # connect
warp-cli disconnect # disconnect
warp-cli status # show connection status
warp-cli tunnel stats # bytes sent/received, latencyThe 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.
warp-cli registration showExpected output includes Organization: devteam and Account type: Team.
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.
{ 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";
};
};
}| 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 |
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-connectingjournalctl -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
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.
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.
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.
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