Skip to content

LiteLLM over the private Tailscale network

The operator manages the LiteLLM host. It is registered with Headscale as litellm, tagged tag:llm, with VPN address 100.64.0.4. The selected API endpoint is http://100.64.0.4:4000. WireGuard encrypts the traffic between VPN peers; a separate HTTPS listener and DNS record are not required for this setup.

Host configuration

Run LiteLLM on port 4000 and make that port reachable on the host's Tailscale interface. When publishing a Docker port, bind it to the VPN address, for example 100.64.0.4:4000:4000. Arrange startup after Tailscale has restored the address. Retain LiteLLM authentication. Do not publish the management key in client configuration or disable authentication because the transport is private.

The deployed Headscale policy permits tag:client and tag:odoo to connect to tag:llm on TCP 4000. Client-to-client access remains denied. Inference and management use the same listener, so HTTP endpoint authorization is enforced by LiteLLM keys rather than separate network ports:

Credential Purpose Destination
Headscale enrollment key Register one host in the VPN Tailscale on that host
LiteLLM management key Create and inspect client keys Control Odoo only
LiteLLM client key Call approved models within its budget That client's VM

Client keys are issued as llm_api keys. The control plane verifies their allowed_routes, explicit model allowlist, budget, and ownership metadata before publishing them through Salt. Check actual denial of management endpoints using a client key when validating a new LiteLLM deployment.

Register a new LiteLLM host

Install and start Tailscale first. Then create an enrollment in Oduflow → Private Network → Server Enrollments, issue a key, and open its connection instructions. The default key is single-use, non-ephemeral, and valid for an initial registration within 30 minutes. Save it to /run/headscale.authkey, owned by root with mode 0600, and run as root on the target host:

tailscale up \
  --login-server=https://headscale.example.com \
  --auth-key=file:/run/headscale.authkey \
  --hostname=litellm \
  --accept-dns=false
tailscale ip -4
rm /run/headscale.authkey

Remove the key file after successful registration. Preserve Tailscale's state across restarts. The Headscale coordination endpoint still uses HTTPS; the application API inside WireGuard uses HTTP. An alternative root command on the coordination server, issued only when the target host is ready, is:

umask 077
headscale preauthkeys create --tags tag:llm --expiration 30m \
  --output json > /run/litellm-enrollment.json

Transfer only the key value through a private file and remove the temporary JSON afterward. Use either Odoo or the CLI for one enrollment, not both.

Control Odoo configuration

Configure the non-secret values in Oduflow → Configuration → Settings → LiteLLM:

Management URL: http://100.64.0.4:4000
Client URL: http://100.64.0.4:4000/v1
SOCKS Proxy: socks5h://oduflow-1-svc-oduflow-vpn:1055
Models: ["gpt-5.6-sol"]
Default Model: gpt-5.6-sol
Reasoning Effort: medium
API Mode: responses

Use actual configured model aliases. The management credential remains in the ODUFLOW_LITELLM_MANAGEMENT_KEY environment secret. The management URL has no /v1 suffix because its adapter calls /key/generate and /key/info. The client base URL includes /v1 and is passed unchanged to OpenCode. The selected gpt-5.6-sol alias uses the Responses API and medium reasoning. Other deployments may explicitly select chat_completions for a compatible model. Model choice and reasoning settings are frozen with the encrypted provisioning configuration and included in Salt pillar only after the client key has been verified.

The management virtual key must belong to a service user with the proxy_admin role. Keep its allowed routes restricted to management operations. The management_routes allowlist alone does not grant that role: a key without an owning user can read some metadata while /key/generate still returns 401. Changing a management key's owner in LiteLLM preserves the key fingerprint; do not replace an in-flight candidate or clear its durable dispatch receipt merely to retry an ambiguous creation.

Control Odoo needs the explicit SOCKS proxy because its VPN gateway uses userspace networking. Client VMs and their Paseo/OpenCode processes use their host's ordinary Tailscale routes. They need no additional VPN identity per agent.

Client URL validation permits HTTP only for literal IPv4 addresses inside 100.64.0.0/10. Public addresses, arbitrary HTTP hostnames and credentials in URLs remain invalid. HTTPS remains supported for other deployments.

Apply environment changes through Megaflow update_environment. Issue a client key, apply updated Salt configuration, then verify inference and a coding-agent session before marking the complete client workflow successful.

References: Headscale registration, Tailscale connections, LiteLLM virtual keys.

Recover an unresolved candidate after management configuration changes

An Oduflow Admin can enqueue action_recover_llm_key() on a prepared client. This explicit recovery applies only to an unpublished candidate with an existing unresolved dispatch receipt. It retains the original encrypted configuration and receipt, keeps exactly the same candidate key, and creates a separate recovery receipt. Changing the API endpoint is refused. No provider request runs inside the HTTP action.

The queued job first looks up the candidate by its SHA256 hash. If absent, it checks /openapi.json for the reviewed LiteLLM 1.96.2 implementation before allowing one create request. In that version, the caller's token hash is the primary key and creation upserts with update={}. This prevents a second virtual key row for the same candidate; it does not promise that ancillary event hooks run only once. An alias conflict or lost POST response is reconciled with an exact-key GET. Re-running recovery after a claimed request performs reconciliation only and never issues a second POST.

The returned key must match the complete ownership metadata, alias, budget, model allowlist, inference-only routes and expiration policy before the new configuration becomes canonical or its key enters Salt pillar. An existing key with an old model policy fails closed; recovery does not silently edit its policy or reset spend. The original dispatch remains unchanged, including when the new receipt succeeds. A different provider version requires a fresh implementation review rather than weakening this gate.

Implementation evidence: LiteLLM 1.96.2 token upsert, caller key and alias handling, token primary key.