Self-Hosting an LLM Without It Phoning Home: A Verified Telemetry Hardening Guide
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
- Inference: llama.cpp
llama-serveron an OpenAI-compatible endpoint at:8080, serving a Qwen3 27B model - Proxy/router: LiteLLM in Docker (
ghcr.io/berriai/litellm:main-stable) on LXC192.168.1.249, routing between the local box and remote APIs - DNS: AdGuard Home on
192.168.1.251, the LAN’s recursive resolver, with full query logging enabled - Firewall: UFW on the hosts that have it (more on why that wasn’t enough later)
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:
| Destination | Hits | What it is |
|---|---|---|
checkpoint.prisma.io | 12 | Prisma ORM telemetry — LiteLLM uses Prisma for its Postgres layer, and Prisma reports home |
raw.githubusercontent.com | 14 | Config/schema fetches |
docs.litellm.ai | 7 | Documentation-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)
- No
stats.vllm.ai— we don’t run vLLM on this stack. The well-documented default (first-party usage stats on engine start, then every 10 minutes, killable withVLLM_NO_USAGE_STATS=1or the standardDO_NOT_TRACK=1) is cited from D-Central’s source-verified registry and vLLM’s own docs, not personally observed by us. - No Ollama traffic — not installed here. Per the registry (source-graded), headless
ollama servehas no analytics and no updater; only the desktop tray app pingsollama.comhourly.registry.ollama.aitraffic is user-initiated model pulls only. - No PostHog endpoints anywhere in our logs — we don’t run Open WebUI or AnythingLLM on this network. For reference, the registry’s kill switches: Open WebUI’s telemetry is
ANONYMIZED_TELEMETRY=false(already pre-set in the official Docker image) plusDO_NOT_TRACK=trueandSCARF_NO_ANALYTICS=true; AnythingLLM’s PostHog is on by default and needsDISABLE_TELEMETRY="true"— exact string match — with issue reports suggesting the flag alone may not kill every connection, so firewallapp.posthog.comas well.
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):
| Tool | Env / setting |
|---|---|
| vLLM | VLLM_NO_USAGE_STATS=1 (or DO_NOT_TRACK=1, or touch ~/.config/vllm/do_not_track) |
| llama.cpp | local -m path, or --offline with -hf models |
| Open WebUI | ANONYMIZED_TELEMETRY=false, DO_NOT_TRACK=true, SCARF_NO_ANALYTICS=true |
| AnythingLLM | DISABLE_TELEMETRY="true" + in-app Privacy toggle |
| Ollama (desktop) | run headless ollama serve, or block ollama.com |
| HF-based tooling generally | HF_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
| Component | Default behavior | Opt-out | Status on our rig |
|---|---|---|---|
| llama.cpp (llama-server) | No telemetry; -hf models revalidate against huggingface.co | local -m path or --offline | Observed: 5,331 huggingface.co queries from the inference host over 9 days, ~43s cadence; process-level attribution pending host access |
| LiteLLM | Outbound via its Prisma layer (checkpoint.prisma.io), GitHub fetches | No verified in-app toggle | Observed: 12 / 14 / 7 hits to prisma-checkpoint / raw.githubusercontent / docs.litellm.ai; mitigation via DNS block |
| vLLM | stats.vllm.ai on start + every 10 min | VLLM_NO_USAGE_STATS=1 / DO_NOT_TRACK=1 | Not installed here — cited from source-verified registry, unverified locally |
| Ollama | Headless server: silent; desktop tray: hourly ollama.com update check | run headless / block domain | Not installed here — cited |
| Open WebUI | PostHog via bundled ChromaDB; Scarf install analytics | ANONYMIZED_TELEMETRY=false etc. | Not installed here — cited |
| AnythingLLM | PostHog on by default | DISABLE_TELEMETRY="true" + firewall app.posthog.com | Not installed here — cited |
| AdGuard DNS | Full query log of every LAN client | — | Observed: primary evidence source for this entire audit |
| UFW | Not enforcing egress on our hosts | default deny outgoing policy | Observed: 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