// blog

Self-Hosting an LLM Without It Phoning Home: A Verified Telemetry Hardening Guide

deployment

You moved inference on-premise for control. The uncomfortable part is that “self-hosted” describes where the weights run, not what the surrounding stack does with your network. vLLM — the production inference engine — pings its own stats server every ten minutes by default. Open WebUI ships a PostHog analytics dependency. LiteLLM, the proxy most people put in front of everything else, has its own outbound habits that almost nobody documents.

This is an operational walkthrough of hardening a real stack, verified the only way that counts: by observing outbound connections, not by trusting documentation. Everything below was run against our own rig unless explicitly marked otherwise.

Tested on: Ubuntu-based LXCs on Proxmox, llama.cpp (llama-server) serving a Qwen3 27B GGUF, LiteLLM main-stable Docker image with Postgres 16, AdGuard Home as LAN DNS forwarding over DoT to 1.1.1.1. Verified 2026-08-14 through 2026-08-23.

The stack we audited

One methodological note before anything else: the DNS name llama-x99.lan on our network resolves inconsistently — AdGuard’s local records pointed it at 192.168.1.241, which is actually a reverse-proxy host. The live inference endpoint answered at 192.168.1.243:8080. If your lab has grown by accretion, assume your DNS records lie too, and audit by IP.

Step 1: Catch it in the act before you change anything

Do this first, on the untouched stack, or you’ll never know what you fixed. Three observation points, cheapest first:

Live sockets — on each stack host:

ss -tupn | grep ESTAB | grep -v 127.0.0.1

This shows current connections only. It’s a snapshot; a once-per-10-minutes ping will usually evade it. We ran it on the LiteLLM host during a quiet period and saw nothing but SSH — which tells you the proxy wasn’t holding persistent connections, not that it never calls out.

DNS query logs — the workhorse. If AdGuard Home (or Pi-hole, or a dnsmasq with log-queries) is your LAN resolver, every outbound attempt by every container and service leaves a line, even ones your live socket check missed. Our workhorse query:

grep '"IP":"<host-ip>"' /var/lib/AdGuardHome/data/querylog.json \
  | grep -Eo '"T":"[^"]+","QH":"[^"]+"' | tail -50

Frequency analysis — sort and count. This is where the story got interesting:

grep '"IP":"192.168.1.243"' querylog.json | grep -Eo '"QH":"[^"]+"' \
  | sort | uniq -c | sort -rn | head

What we actually found on our own stack

The inference box: 5,331 queries to huggingface.co

The llama.cpp host’s DNS log over our nine-day window contained 5,331 queries for huggingface.co, arriving in steady A/AAAA pairs roughly every 43 seconds — a metronome, not a user action. The same log showed api.comfy.org (356 hits — a ComfyUI Manager install on the same box fetching its node registry) and routine pypi.org / files.pythonhosted.org traffic from package management.

We could not SSH into that host to pin the exact process (no credentials from our admin box — noted honestly), but the cadence matches what D-Central’s August 2026 telemetry registry documents for llama.cpp: the server itself has no analytics and no update check, but a model loaded via -hf (a Hugging Face repo id) revalidates against huggingface.co rather than trusting the local cache, unless you pass --offline. The fix is either:

llama-server -m /models/qwen3-27b-q4.gguf ...   # local file path: zero outbound
# or, if you loaded via -hf:
llama-server --hf-repo <repo> --offline ...

A ~43-second revalidation loop is more aggressive than the startup-only check the source code describes, so treat our observation as a data point from one machine, not a claim about llama.cpp in general. The honest summary: llama.cpp ships no telemetry, but its HF integration keeps the door open unless you close it explicitly.

LiteLLM: the proxy phones home too

This is the layer D-Central’s 14-tool registry doesn’t cover, and the one most stacks sit directly behind. Our AdGuard logs for the LiteLLM LXC (192.168.1.249) over the audit window showed:

DestinationHitsWhat it is
checkpoint.prisma.io12Prisma ORM telemetry — LiteLLM uses Prisma for its Postgres layer, and Prisma reports home
raw.githubusercontent.com14Config/schema fetches
docs.litellm.ai7Documentation-linked callbacks

(Counts from querylog.json, 2026-08-14 → 2026-08-23; timestamps in the log are UTC.)

checkpoint.prisma.io is the one worth knowing about: it’s not LiteLLM’s own analytics, it’s a dependency’s, which is exactly the pattern that catches people out — the same way Open WebUI’s PostHog traffic actually originates in its bundled ChromaDB. We did not find a documented opt-out for Prisma’s telemetry within LiteLLM’s configuration surface; the practical mitigation is the network layer (Step 4). If someone has verified an env var that silences it, that claim is unverified by us — check before relying on it.

What we didn’t find (also worth recording)

The general lesson from our own logs: most of what phones home isn’t the thing you think you installed. It’s a dependency (Prisma, ChromaDB), a convenience layer (HF revalidation, ComfyUI’s node registry), or a co-located tool on the same box.

Step 2: Set the global opt-out everywhere

Before per-tool flags, export the industry-standard signal in every service’s environment (systemd units, Docker environment:, container ENV):

DO_NOT_TRACK=1

vLLM, Open WebUI, and a growing list of Python tools honor it. It is not universal — LiteLLM’s Prisma layer doesn’t — which is why the next step exists.

Per-tool, the ones we’d set on any stack (from the D-Central registry, source-verified on their end):

ToolEnv / setting
vLLMVLLM_NO_USAGE_STATS=1 (or DO_NOT_TRACK=1, or touch ~/.config/vllm/do_not_track)
llama.cpplocal -m path, or --offline with -hf models
Open WebUIANONYMIZED_TELEMETRY=false, DO_NOT_TRACK=true, SCARF_NO_ANALYTICS=true
AnythingLLMDISABLE_TELEMETRY="true" + in-app Privacy toggle
Ollama (desktop)run headless ollama serve, or block ollama.com
HF-based tooling generallyHF_HUB_OFFLINE=1, TRANSFORMERS_OFFLINE=1 once models are cached

Step 3: Verify the opt-out actually worked

Docs say; logs prove. Re-run the DNS frequency analysis after applying the flags and confirm the destination’s query count goes flat. For vLLM specifically, there’s a nice local check: it writes what it would have sent to ~/.config/vllm/usage_stats.json — read it and see exactly what you’re not transmitting (GPU count/type, OS, version, model architecture; no prompts).

For our stack, the post-hardening verification is the AdGuard log itself: after moving the llama.cpp box to a local-model-path invocation and DNS-blocking the Prisma checkpoint (below), new entries for huggingface.co from .243 should drop to user-initiated model downloads only. We’re still inside the observation window on that change — treat the “verified silent” column below accordingly.

Step 4: Network-level enforcement — the backstop that catches what you missed

Per-tool flags rot. A minor version bump can reintroduce a ping, and a dependency you didn’t know about (see: Prisma) never had a flag. Two enforcement layers, both cheap:

DNS-level blocking (AdGuard). Add custom filtering rules for the endpoints you never want resolved:

||stats.vllm.ai^
||app.posthog.com^
||eu-assets.i.posthog.com^
||checkpoint.prisma.io^
||api.mixpanel.com^

This is our current state on 192.168.1.251: the rules above are the ones we’re adding as a result of this audit (the box shipped with an empty user_rules list — worth checking yours). DNS blocking is coarse — it won’t stop a hard-coded-IP caller — but it silently kills the overwhelming majority of analytics endpoints, and your query log then becomes a tripwire: any new hit on a blocked domain means something tried.

Egress firewall (UFW). The stricter layer. Our audit found UFW effectively unused on the stack hosts — default-allow everywhere. The hardened baseline for a server that should only ever talk to the LAN and a short allowlist:

ufw default deny outgoing
ufw allow out to 192.168.1.0/24
ufw allow out to <your-DoT-resolver-or-allowlist-IPs> port 443 proto tcp
ufw allow in from 192.168.1.0/24 to any port 8080 proto tcp
ufw enable

Deny-outbound is the only control that catches the unknown future call. The cost is maintenance: every legitimate new dependency (a model mirror, a package repo) needs an explicit allow, and you’ll find them by reading UFW’s logs for a week. For a single-purpose inference box, that trade is usually worth it. Note that DNS blocking and egress filtering are complementary, not redundant: DNS rules are per-domain and easy to audit; firewall rules are per-host and catch IP-literal calls DNS blocking misses.

Verification summary

ComponentDefault behaviorOpt-outStatus on our rig
llama.cpp (llama-server)No telemetry; -hf models revalidate against huggingface.colocal -m path or --offlineObserved: 5,331 huggingface.co queries from the inference host over 9 days, ~43s cadence; process-level attribution pending host access
LiteLLMOutbound via its Prisma layer (checkpoint.prisma.io), GitHub fetchesNo verified in-app toggleObserved: 12 / 14 / 7 hits to prisma-checkpoint / raw.githubusercontent / docs.litellm.ai; mitigation via DNS block
vLLMstats.vllm.ai on start + every 10 minVLLM_NO_USAGE_STATS=1 / DO_NOT_TRACK=1Not installed here — cited from source-verified registry, unverified locally
OllamaHeadless server: silent; desktop tray: hourly ollama.com update checkrun headless / block domainNot installed here — cited
Open WebUIPostHog via bundled ChromaDB; Scarf install analyticsANONYMIZED_TELEMETRY=false etc.Not installed here — cited
AnythingLLMPostHog on by defaultDISABLE_TELEMETRY="true" + firewall app.posthog.comNot installed here — cited
AdGuard DNSFull query log of every LAN clientObserved: primary evidence source for this entire audit
UFWNot enforcing egress on our hostsdefault deny outgoing policyObserved: absent/empty; remediation specified above

Prior art: D-Central’s Local-AI Telemetry & Air-Gap Audit (last reviewed July 18, 2026) covers 14 tools with source-code-graded evidence and is the best existing registry. What it doesn’t give you is the end-to-end view — a real multi-layer stack, the dependencies that leak through the gaps (Prisma under LiteLLM isn’t in anyone’s list), and the network backstop. That’s the part you have to observe on your own hardware. Start with the DNS logs; they’ll tell you in ten minutes what your stack actually does when you’re not watching.

If you’re sizing the hardware side of a self-hosted stack, we’ve written separately about how much GPU you actually need and the real cost math of self-hosting vs. API calls.

Building your own AI infrastructure?

Talk to us