Securing Open WebUI and Ollama deployments
How-to
The usual self-hosted chat stack is Ollama for local models and Open WebUI in
front of it. Ollama needs no credential. Open WebUI collects them: an
OPENAI_API_KEY for the models Ollama cannot run, a web search key, image
generation keys, tool server credentials, and WEBUI_SECRET_KEY, which signs
every session token the UI issues. Most of them end up in a .env beside the
compose file, and anything set later through the admin panel is written to
Open WebUI's database, on the same volume as every chat.
This post covers three steps: get the keys out of the compose file, make Open WebUI hold placeholders for paid APIs, and stop the admin panel from undoing the second step.
Step 1: one credential in the compose file
Import the .env, mint a token, delete the file:
seekrit secrets import .env --app chat --env production
seekrit token create --app chat --env production --name open-webui
rm .env
The Open WebUI image has no entrypoint of its own and starts with
bash start.sh, so seekrit-run slots in front of that with an image that adds
the binary:
FROM seekritdev/run:latest AS seekrit
FROM ghcr.io/open-webui/open-webui:main
COPY --from=seekrit /seekrit-run /usr/local/bin/seekrit-run
ENTRYPOINT ["seekrit-run", "--"]
CMD ["bash", "start.sh"]
services:
ollama:
image: ollama/ollama
volumes: [ollama:/root/.ollama]
open-webui:
build: .
ports: ["3000:8080"]
environment:
SEEKRIT_TOKEN: ${SEEKRIT_TOKEN}
OLLAMA_BASE_URL: http://ollama:11434
volumes: [open-webui:/app/backend/data]
volumes:
ollama:
open-webui:
At start the container resolves its environment, decrypts it locally, and
launches start.sh with WEBUI_SECRET_KEY, OPENAI_API_KEY, and the rest
present as environment variables. docker inspect shows one token, bound to
one environment, revocable from the dashboard. If you would rather not build an
image, seekrit run -- docker compose up -d supplies the ${VAR} values from
the child environment instead, at the cost of the values appearing in
docker compose config. The Compose post
compares the two.
WEBUI_SECRET_KEY deserves a fixed value here rather than the random one Open
WebUI generates on first start. A generated one lives in a file on the data
volume, and losing it logs every user out. In seekrit it is a secret like any
other, rotatable on purpose rather than by accident.
Step 2: placeholders for paid APIs
Open WebUI reaches OpenAI-compatible providers through two variables:
OPENAI_API_BASE_URL and OPENAI_API_KEY. Point the first at
seekrit-proxy and make the second a placeholder.
Neither is a secret any more, so both can live in the compose file:
open-webui:
environment:
SEEKRIT_TOKEN: ${SEEKRIT_TOKEN}
OLLAMA_BASE_URL: http://ollama:11434
OPENAI_API_BASE_URL: http://seekrit-proxy:8080/openai/v1
OPENAI_API_KEY: "{{seekrit:OPENAI_API_KEY}}"
seekrit-proxy:
image: seekritdev/proxy
environment:
SEEKRIT_TOKEN: ${SEEKRIT_PROXY_TOKEN}
volumes: ["./seekrit-proxy.toml:/seekrit-proxy.toml"]
command: ["--listen", "0.0.0.0:8080"]
# seekrit-proxy.toml
[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["GET", "POST"]
paths = ["/v1/models", "/v1/chat/completions", "/v1/embeddings"]
Open WebUI sends Authorization: Bearer {{seekrit:OPENAI_API_KEY}} to the
proxy. The proxy swaps in the real key and forwards to OpenAI, for the two
methods and three paths listed and nothing else. The Open WebUI container now
holds no provider key at all, in its environment or its database. The same
shape covers any provider Open WebUI reaches over an OpenAI-compatible API,
which is most of them: OPENAI_API_BASE_URLS and OPENAI_API_KEYS take
semicolon-separated lists, so several providers can each get a proxy route.
Once the provider key is out of the container, the token in SEEKRIT_TOKEN
should be bound to an environment that does not contain it either. Two tokens,
two environments: the Open WebUI one holds WEBUI_SECRET_KEY and the database
URL; the proxy's holds the API keys. A compromise of the chat container then
yields a signing key and a placeholder, not a provider bill.
Step 3: stop the admin panel from bypassing the proxy
Open WebUI persists settings changed in the admin panel to its database and,
by default, prefers those over environment variables on subsequent starts.
That is convenient and it is also how the base URL gets pointed back at
api.openai.com with a real key pasted in, six months from now, by an admin
who did not know the proxy existed. Set:
ENABLE_PERSISTENT_CONFIG: "false"
With persistent config off, environment variables are authoritative on every start and the connection settings in the UI cannot override them. The trade is that every setting you want has to be in the compose file, which for a deployment you manage is the right place for it anyway.
Ollama
Ollama has no authentication. If OLLAMA_HOST is bound to anything other than
loopback or a private compose network, anyone who can reach the port can run
inference on your hardware and pull models onto your disk. In the compose file
above, Ollama publishes no ports and Open WebUI reaches it by service name.
Keep it that way. If you need Ollama reachable from other machines, put it
behind something that authenticates, not on 0.0.0.0:11434.
Recap
| Before | After |
|---|---|
Provider keys in .env and in docker compose config | One token in the compose file, bound to one environment |
WEBUI_SECRET_KEY generated into a file on the data volume | A managed secret, rotatable on purpose |
OPENAI_API_KEY in the Open WebUI container | A placeholder; the key lives in the proxy sidecar |
| Admin can paste a key and a direct URL into the UI | Environment is authoritative |
Ollama on 0.0.0.0 | Ollama on the compose network only |
The agent proxy guide has the full route syntax, and the Compose post has the options for getting the token itself into a container without an environment variable.