Open-WebUI#

Open-WebUI runs on ereshkigal as a Podman OCI container managed by the tsunaminoai.openWebui NixOS module (modules/nixos/containers/open-webui/default.nix). It is the LLM chat front-end for the local stack and doubles as a RAG knowledge base over the Paperless-NGX document library. All inference goes through the local Ollama failover proxy on :11434 (work-laptop-m4 primary, mokou backup) — see Ollama for the servers behind it.

  • Web UI: chat.inaba.network — the user-facing path, behind Cloudflare Access (Entra group sg-voile-personal)
  • Web UI (tailnet fallback): https://ereshkigal.<tailscaleDomain>:3001 (admin break-glass, slated for removal once the CF path is verified — #177)
  • Module option: tsunaminoai.openWebui.enable = true;

Architecture#

The container is ghcr.io/open-webui/open-webui:main, run on the openwebui_default Podman network with the alias open-webui. The host port (default 3000) maps to container port 8080, published on loopback only whenever trustedEmailHeader is set (see below). Persistent state — including the embedded ChromaDB vector store — lives in the named Podman volume openwebui_data (/app/backend/data).

Chat and RAG embeddings both go to the endpoint named by the ollamaHost option, which sets OLLAMA_BASE_URL to http://<ollamaHost>:11434 — on ereshkigal that is the host’s own failover proxy. RAG uses the ollama embedding engine backed by nomic-embed-text; web search is disabled (ENABLE_RAG_WEB_SEARCH = "false") so retrieval stays local. The container is started with --add-host=host.containers.internal:host-gateway so the host’s FQDN (and the proxy upstreams behind it, over Tailscale) resolve correctly.

WEBUI_AUTH = "true", so login is required and the first user to register becomes the admin.

Authentication (Cloudflare Access trusted header)#

trustedEmailHeader = "X-Remote-User" sets Open-WebUI’s WEBUI_AUTH_TRUSTED_EMAIL_HEADER: the app trusts the email in that header as the logged-in user and auto-provisions the account. That is a complete authentication bypass for anyone who can set the header, so the module makes the header unforgeable rather than merely unlikely:

  1. the chat Cloudflare origin vhost overwrites X-Remote-User with $http_cf_access_authenticated_user_email (validated at the CF edge);
  2. the tailnet vhost on port + 1 clears it (proxy_set_header X-Remote-User "";), so LAN/tailnet clients cannot inject an identity;
  3. the container publish becomes 127.0.0.1:<port> and port is dropped from the firewall, so there is no unproxied route to the app at all.

Removing any one of the three re-opens the bypass — change them together.

Note

When tsunaminoai.pki.acme.enable is true, the module also stands up an nginx vhost on port + 1 (3001) that reverse-proxies the web UI over TLS using the host’s step-ca ACME cert (useACMEHost = ereshkigal.<tailscaleDomain>). WEBUI_URL is set to that HTTPS origin unless externalUrl overrides it (ereshkigal points it at https://chat.inaba.network) so absolute links and CSRF/CORS checks behave behind the proxy. The firewall opens port + 1 when ACME is active; port itself is only opened when trustedEmailHeader is unset.

Enabling the module#

# hosts/x86_64-nixos/ereshkigal/default.nix
tsunaminoai.openWebui = {
  enable = true;
  # The host's own FQDN = the local Ollama failover proxy on :11434
  # (work-laptop-m4 primary → mokou backup).
  ollamaHost = "ereshkigal.${config.tsunaminoai.nix.tailscaleDomain}";
  port = 3000;
  paperlessPort = 8011;
  externalUrl = "https://chat.inaba.network";
  trustedEmailHeader = "X-Remote-User";
};

Module options#

Option Default Description
enable false Enable Open-WebUI
ollamaHost "localhost" Hostname/IP of the Ollama server (chat + embeddings); ereshkigal points it at its own failover-proxy FQDN
port 3000 Host port for Open-WebUI (HTTP); HTTPS is port + 1
paperlessPort 8011 Port Paperless-NGX listens on (used by the sync timer)
externalUrl null Public URL behind Cloudflare Access; sets WEBUI_URL. Null = the tailnet vhost URL
trustedEmailHeader null Header holding the pre-authenticated email (WEBUI_AUTH_TRUSTED_EMAIL_HEADER). Forces the loopback publish + closed firewall port
trustedNameHeader null Optional display-name header (WEBUI_AUTH_TRUSTED_NAME_HEADER)

Environment variables#

Variable Value Purpose
OLLAMA_BASE_URL http://<ollamaHost>:11434 Chat inference endpoint
WEBUI_AUTH "true" Require login; first user becomes admin
WEBUI_URL externalUrl or https://<host>:3001 Public URL for link generation / CSRF
WEBUI_AUTH_TRUSTED_EMAIL_HEADER trustedEmailHeader Trusted-header SSO — only set when the option is
RAG_EMBEDDING_ENGINE "ollama" RAG embedding backend
RAG_EMBEDDING_MODEL "nomic-embed-text" Embedding model behind ollamaHost
OLLAMA_BASE_URL_FOR_EMBEDDINGS http://<ollamaHost>:11434 Embedding endpoint
ENABLE_RAG_WEB_SEARCH "false" Keep RAG local — no outbound web search
VECTOR_DB "chroma" Embedded ChromaDB in openwebui_data

Paperless → Open-WebUI RAG sync#

A nightly systemd timer, paperless-openwebui-sync (OnCalendar = 02:00, Persistent so it catches up after downtime), pulls already-OCR’d Paperless-NGX documents and upserts them into an Open-WebUI Knowledge collection via the Knowledge API. The one-shot service reads two sops secrets at start (ExecStartPre writes them to a runtime EnvironmentFile):

  • paperless/api-token — Paperless admin → Profile → API Token
  • openwebui/api-key — Open-WebUI → Settings → API Keys

Both must be created after first boot. The sync talks to Paperless at http://127.0.0.1:<paperlessPort> and Open-WebUI at http://127.0.0.1:<port>.

Dashboard#

When tsunaminoai.homer.enable is set, the module registers a Homer tile (“Open-WebUI”, under the Media group, ai tag) pointing at the HTTPS origin when ACME is active, otherwise the plain HTTP URL.