Ollama 403 Forbidden: The OLLAMA_ORIGINS CORS Fix
Want to go deeper than this article?
Free account unlocks the first chapter of all 25 courses — RAG, agents, MCP, voice AI, MLOps, real GitHub repos.
Ollama’s running. Here’s what to build with it. Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.
A 403 from Ollama is not a connection problem and not an auth problem — it is Ollama's CORS middleware rejecting the Origin header your browser attached, before the request ever reaches the API handler. curl works because curl sends no Origin at all. The fix is to add your app's exact origin to OLLAMA_ORIGINS and restart the server — for example OLLAMA_ORIGINS=chrome-extension://* for a browser extension, or OLLAMA_ORIGINS=http://192.168.1.50:3000 for a web UI on another machine. Two things trip people up. First, on Linux export OLLAMA_ORIGINS=... does nothing, because the daemon runs under systemd and never reads your shell. Second, there is a second middleware that also returns a bare 403 based on the Host header, which is what breaks reverse proxies and tunnels — no Origin involved, and no amount of OLLAMA_ORIGINS will fix it.
The reason this gets filed as a bug rather than found as a setting is that nothing in the response says "CORS". You get a naked 403 with no body. The browser console says the fetch failed. The same URL pasted into curl returns JSON. Nothing about that sequence points at an environment variable.
Everything below was read from the Ollama source on main on 23 August 2026 — envconfig/config.go for the origin list and server/routes.go for the middleware — and from the official FAQ. Bug reports are linked by number so you can check whether yours has moved since.
Why Does curl Work but the Browser Get 403
Because curl does not send an Origin header and a browser always does. That single difference is the whole diagnosis, and you can prove it in one command without touching any config:
curl -i http://localhost:11434/api/tags
curl -i -H "Origin: http://example.com" http://localhost:11434/api/tags
The first returns 200 OK and your model list. The second returns 403 Forbidden with an empty body. Same host, same port, same path, same running server.
That is not a quirk of our setup — it is the exact reproduction in ollama/ollama issue #6021, "API returns 403 Forbidden when Origin http header is set" (opened 28 July 2024, still open at the time of writing). The reporter's failing case was a plain POST to /v1/chat/completions with -H "Origin: abc", triggered in the wild by chatGPTBox. Note what that tells you: the rejection is not limited to preflight OPTIONS requests. Any request carrying a disallowed Origin is aborted with 403, including the real POST.
So the mental model to carry into the rest of this page:
- No
Originheader → the CORS middleware has nothing to check → request proceeds. This is curl, Pythonrequests, a Go client, your terminal. Originheader present and on the allow-list → request proceeds, and the response carriesAccess-Control-Allow-Origin.Originheader present and not on the allow-list → 403, empty body, no explanation.
Every browser-based client is in the third bucket by default unless its origin happens to be localhost. Web apps, extensions, Obsidian plugins, Electron and Tauri shells, VS Code webviews — all of them attach an origin, and only some of those origins are pre-approved.
Reading articles is good. Building is better.
Free account = 20+ free chapters across 25 courses, with a per-chapter AI tutor. No card. Cancel anytime if you ever upgrade.
Which 403 Do You Have, Origin or Host
Ollama has two independent middlewares that both abort with a bare 403, and they have completely different fixes. From server/routes.go on main, they are registered in this order:
r.Use(
cors.New(corsConfig),
allowedHostsMiddleware(s.addr),
)
The first is CORS and looks at Origin. The second is a DNS-rebinding guard and looks at Host. Telling them apart takes two curls:
| Test | Response | What it means |
|---|---|---|
curl -i http://localhost:11434/api/tags | 200 | Baseline. Server is healthy. |
Same, plus -H "Origin: http://example.com" | 403 | Origin rejection. OLLAMA_ORIGINS is your fix. |
Same, plus -H "Host: ollama.example.com" | 403 | Host rejection. OLLAMA_ORIGINS will not help. |
| Both headers removed, still 403 | — | Not this page. See the note below. |
If the third row is the one that fires, skip ahead to the reverse-proxy section — that is a different bug with a different fix, and it is the one that catches people running Ollama behind nginx, Caddy, ngrok, zrok or Cloudflare Tunnel.
One more 403 that is not this page at all: requests to Ollama's hosted cloud models can return 403 for account and plan reasons rather than CORS ones — #16773 asks how to tell in advance whether a model will 403 on a free plan, and #15990 and #15707 are cloud-auth 403s from Claude Code and GitHub Copilot. If the model name in your request ends in :cloud, you are in that family, not this one.
What Origins Does Ollama Already Allow
Seventeen origins are hard-coded and always present, whether or not you set anything. This matters because half the confusion on the tracker comes from people not knowing what is already allowed. Here is the function that builds the list, verbatim from envconfig/config.go on main:
func AllowedOrigins() (origins []string) {
if s := Var("OLLAMA_ORIGINS"); s != "" {
origins = strings.Split(s, ",")
}
for _, origin := range []string{"localhost", "127.0.0.1", "0.0.0.0"} {
origins = append(origins,
fmt.Sprintf("http://%s", origin),
fmt.Sprintf("https://%s", origin),
fmt.Sprintf("http://%s", net.JoinHostPort(origin, "*")),
fmt.Sprintf("https://%s", net.JoinHostPort(origin, "*")),
)
}
origins = append(origins,
"app://*",
"file://*",
"tauri://*",
"vscode-webview://*",
"vscode-file://*",
)
return origins
}
Count it: three hosts × four scheme/port combinations = 12, plus 5 fixed scheme wildcards = 17 default entries.
| Default entry | Covers |
|---|---|
http://localhost, https://localhost | The bare hostname on the default port |
http://localhost:*, https://localhost:* | localhost on any port — this is why a Vite dev server on 5173 just works |
http://127.0.0.1, https://127.0.0.1, and the :* pair | The same, written as an IP. Browsers treat these as different origins from localhost |
http://0.0.0.0, https://0.0.0.0, and the :* pair | Rarely useful as a browser origin, present for symmetry |
app://* | Electron apps that serve from a custom app:// scheme |
file://* | A local HTML file opened directly from disk |
tauri://* | Tauri shells on macOS and Linux |
vscode-webview://*, vscode-file://* | VS Code webview panels |
Read that table for what is missing, because that absence is the whole reason you are here:
chrome-extension://,moz-extension://,safari-web-extension://— no browser extension origin is allowed by default. This is the single most common 403.- Any LAN address.
http://192.168.1.50:3000is notlocalhost. A web UI on a second machine is rejected. - Any real hostname.
http://nas.local:8080,https://chat.yourdomain.com— rejected. - Tauri on Windows, which does not use the
tauri://scheme. Issue #10507 ("Fully support Tauri in OLLAMA_ORIGINS (Specifically missing Windows support)", open) explains that Windows Tauri apps present ashttp(s)://tauri.localhostbecause of WebView constraints, so thetauri://*default misses them entirely.
The list has also grown over time. The output pasted into issue #6389 back in 2024 ends at tauri://* with no VS Code entries. Do not trust any published list, including this one, over your own server's startup log — the last section shows you how to read it.
Does OLLAMA_ORIGINS Replace the Defaults or Add to Them
It adds. Your values are prepended, then all 17 defaults are appended on top, unconditionally. Look again at the function above: the if block seeds origins from the environment variable, and every append after it runs regardless. There is no branch that skips the defaults.
That is exactly what issue #6389, "OLLAMA_ORIGINS environment variables appends instead of sets" (opened 16 August 2024, still open), documents. The reporter set OLLAMA_ORIGINS=*://localhost,*://127.0.0.1 expecting to narrow the list, and got their two entries followed by the full default set:
OLLAMA_ORIGINS:[*://localhost *://127.0.0.1 http://localhost https://localhost
http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1
http://127.0.0.1:* https://127.0.0.1:* http://0.0.0.0 https://0.0.0.0
http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://*]
A closed request, #8118, asked for the update policy to be changed to override. It was not.
The practical consequences, both directions:
- Adding an origin always works. You never need to re-list
http://localhost:*to keep it — it is already there. Just add what is missing. - Removing an origin is impossible through this variable. You cannot use
OLLAMA_ORIGINSto stopfile://*pages or localhost from reaching your server. If you need that, you need a reverse proxy in front, which is the subject of our guide to securing an Ollama server.
One more detail from the same function: Var() is strings.Trim(strings.TrimSpace(os.Getenv(key)), "\"'"), so surrounding single or double quotes are stripped for you. Quoting the value in a systemd unit or a shell is safe.
Reading articles is good. Building is better.
Free account = 20+ free chapters across 25 courses, with a per-chapter AI tutor. No card. Cancel anytime if you ever upgrade.
What Origin Is Your Client Actually Sending
Stop guessing and read it off the request. In Chrome or Firefox, open DevTools → Network, trigger the failing call, click it, and read the Origin request header. Whatever string is there is exactly what has to appear in OLLAMA_ORIGINS — character for character, scheme included, no trailing slash.
For clients where DevTools is not available, this table covers the common shapes:
| Client | Origin it sends | OLLAMA_ORIGINS value that works |
|---|---|---|
| Chrome / Edge extension | chrome-extension://<32-char id> | chrome-extension://* |
| Firefox extension | moz-extension://<uuid> | moz-extension://* |
| Safari web extension | safari-web-extension://<uuid> | safari-web-extension://* |
| Local dev server | http://localhost:5173 | Already allowed — do nothing |
| Web UI on another machine | http://192.168.1.50:3000 | http://192.168.1.50:3000 |
| Web UI behind a domain | https://chat.yourdomain.com | https://chat.yourdomain.com |
| Tauri app, macOS/Linux | tauri://localhost | Already allowed via tauri://* |
| Tauri app, Windows | http://tauri.localhost | http://tauri.localhost (issue #10507) |
| Electron app with a custom scheme | app://. | Already allowed via app://* |
A local .html file | file:// or null | file://* is allowed; a null origin is not |
The extension line is the one the official FAQ addresses directly, and its recommended value is a comma-separated set of all three:
OLLAMA_ORIGINS=chrome-extension://*,moz-extension://*,safari-web-extension://* ollama serve
That form — variable in front of the command — only applies to a server you start yourself in that terminal. It is useless if a service manager or a desktop app is already running the daemon, which is the next section's problem.
A note on the null origin. Sandboxed iframes, some data: URLs and certain redirect chains send the literal string null as the origin. There is no wildcard that matches it and adding null to a scheme-checked allow-list is not viable, so if your client sends null, change how the client is loaded rather than fighting the server.
How Do You Set OLLAMA_ORIGINS on Each Platform
Setting the variable in your shell and setting it for the running daemon are different operations on every platform. This is where most "I set it and it still 403s" reports come from.
Linux with systemd. Do not export it. The daemon runs as the ollama system user under systemd and never sees your shell environment. Use a drop-in:
sudo systemctl edit ollama
Add under [Service]:
[Service]
Environment="OLLAMA_ORIGINS=chrome-extension://*,http://192.168.1.50:3000"
Then sudo systemctl daemon-reload && sudo systemctl restart ollama. If any part of that is unfamiliar — where the unit file lives, why the override is a separate file, how to confirm the daemon actually received it — that is a whole mechanism in itself and we wrote it up separately: Ollama systemd service: env vars that don't work.
macOS. The menu-bar app is the server, and it does not inherit your .zshrc. The FAQ's answer is launchctl setenv, then restart the application:
launchctl setenv OLLAMA_ORIGINS "chrome-extension://*"
There is a trap here worth knowing about, recorded in issue #14511 ("Fix: bind: address already in use + 403 from Chrome Extension (macOS)", closed). The reporter tried the inline form instead — OLLAMA_ORIGINS="..." ollama serve — and got:
Error: listen tcp 127.0.0.1:11434: bind: address already in use
The LaunchAgent-managed daemon was already holding the port, so the new server never started and the old one, without the variable, kept answering with 403. The thread's sequence is to find it with launchctl list | grep -i ollama, remove it with launchctl remove com.ollama.ollama, confirm the port is free with lsof -i :11434, and only then start a server with the origins you want. If you see that bind error, your variable did not fail — your server did not start.
Windows. Quit Ollama from the taskbar tray first. Then open Settings, search for environment variables, choose Edit environment variables for your account, add OLLAMA_ORIGINS, click OK, and start Ollama again from the Start menu. A running process cannot pick up a variable edited after it launched.
Docker. The official image already sets OLLAMA_HOST=0.0.0.0:11434 in its final stage and does not bake in an OLLAMA_ORIGINS value, so a -e flag is all you need:
docker run -d -e OLLAMA_ORIGINS="chrome-extension://*,http://192.168.1.50:3000" -p 11434:11434 -v ollama:/root/.ollama --name ollama ollama/ollama
docker run -e overrides an image ENV of the same name, so if you think a baked-in value is winning — the complaint in issue #9359, "The variable 'ollama_origins' in the Docker image cannot be overwritten" (closed) — check the startup log before concluding that. What you are almost certainly looking at is the 17 appended defaults sitting after your value, not a variable you cannot change. If you run Ollama and Open WebUI as separate containers, our Ollama + Open WebUI Docker setup covers the network wiring that makes the origin question mostly disappear.
Why Does Ollama Refuse to Start After You Set It
Because a malformed origin is a fatal startup panic, not a warning. If Ollama stops responding entirely after you edit the variable, do not assume the edit had no effect — assume it had too much.
The CORS layer validates every entry at startup. An origin that contains no * must begin with a recognised scheme, and if it does not, the server dies with this message, quoted from PR #14813:
bad origin: origins must contain '*' or include http://,https://,chrome-extension://,safari-extension://,moz-extension://,ms-browser-extension://
Three ways to trigger it, all of them things people do:
- A trailing comma.
OLLAMA_ORIGINS="http://example.com,"splits into two entries, the second of which is an empty string. That is exactly what PR #14813 (opened 13 March 2026, open at the time of writing) exists to fix — it filters empty entries so that append-style patterns likeOLLAMA_ORIGINS="http://example.com,$OLLAMA_ORIGINS"stop crashing. Until it merges, keep your list free of trailing and doubled commas. - A bare hostname.
OLLAMA_ORIGINS=example.comhas no scheme and no wildcard. Writehttp://example.comorhttps://example.com. - Spaces after commas.
OLLAMA_ORIGINS="http://a.com, http://b.com"produces a second entry that begins with a space. Split is on the comma only; nothing trims the pieces. No spaces.
There is a second, separate panic in the same layer — only one * is allowed — for a single entry containing more than one wildcard. http://*.example.com:* is two, and will not load.
The tell for all of these is that curl http://localhost:11434 stops answering entirely rather than returning 403. On Linux the message will be in journalctl -u ollama; on macOS it is in ~/.ollama/logs/server.log; in Docker it is in docker logs.
Is OLLAMA_ORIGINS Safe to Set to a Wildcard
OLLAMA_ORIGINS=* will make your 403 go away and it is the wrong fix on any machine you care about. It is worth being precise about why, because the usual "that's insecure" hand-wave does not explain the actual attack.
Ollama has no authentication of its own. The only thing standing between a random web page you happen to visit and your local Ollama instance is the browser's same-origin policy — and OLLAMA_ORIGINS=* is you instructing Ollama to waive it for everyone. After that, any site open in any tab can script requests to http://localhost:11434: enumerate your models via /api/tags, run inference on your hardware, pull multi-gigabyte models onto your disk, or delete the ones you have. None of that needs a network exposure or an open port to the outside world. The tab is the vector.
What to do instead, in order of preference:
- List the exact origins.
OLLAMA_ORIGINS=chrome-extension://abcdefghijklmnopqrstuvwxyz123456is more work than*exactly once, at setup. - Scheme-wildcard rather than total-wildcard.
chrome-extension://*is a large improvement on*— it still trusts every installed extension, but not every website. - Put a reverse proxy in front if the client is anything other than your own machine, and do authentication there. Our securing Ollama guide has working nginx configs for bearer tokens, basic auth and SSO, and it is the right page once other people are involved.
The same caution scales with OLLAMA_HOST. If you have set both OLLAMA_HOST=0.0.0.0 and OLLAMA_ORIGINS=*, you have an unauthenticated inference server reachable from your whole network with the browser's last safety check switched off.
Why Does a Reverse Proxy or Tunnel Get 403
This 403 has nothing to do with CORS and OLLAMA_ORIGINS cannot fix it. It is a DNS-rebinding guard that inspects the Host header, and it is the reason a setup that works perfectly on localhost breaks the moment you put nginx, Caddy, ngrok, zrok or Cloudflare Tunnel in front of it.
Here is the middleware, verbatim from server/routes.go on main:
func allowedHost(host string) bool {
host = strings.ToLower(host)
if host == "" || host == "localhost" {
return true
}
if hostname, err := os.Hostname(); err == nil && host == strings.ToLower(hostname) {
return true
}
tlds := []string{
"localhost",
"local",
"internal",
}
// check if the host is a local TLD
for _, tld := range tlds {
if strings.HasSuffix(host, "."+tld) {
return true
}
}
return false
}
And the wrapper that calls it aborts with c.AbortWithStatus(http.StatusForbidden) when it returns false. Two structural details from that wrapper decide whether you are affected at all:
- If Ollama is bound to a non-loopback address, the whole check is skipped. The middleware returns early when the listen address is not loopback. So a server on
0.0.0.0:11434never performs the host check — only the default127.0.0.1binding does. - If the
Hostheader parses as an IP, it is allowed when that IP is loopback, private, unspecified or one of the machine's own addresses.Host: 192.168.1.50:11434passes.Host: chat.yourdomain.comdoes not.
Putting those together gives you the accepted set for a default loopback-bound server: an empty Host, localhost, the machine's own hostname, anything ending in .localhost, .local or .internal, and any loopback/private/local IP literal. Everything else is 403, with or without an Origin header.
That is the mechanism behind issue #3269, "Error 403 with zrok and other reverse proxies" (closed), where zrok share public localhost:11434 returned 403 through the tunnel while direct localhost requests were fine, and behind #4262, "403 using zrok". A tunnel forwards its own public hostname in Host; Ollama sees a name it does not recognise and refuses.
Two fixes, pick one:
location / {
proxy_pass http://127.0.0.1:11434;
proxy_set_header Host localhost;
proxy_read_timeout 600s;
proxy_buffering off;
}
Rewriting Host to localhost at the proxy is the surgical option and keeps the guard doing its job for everything else. The alternative is to bind Ollama to 0.0.0.0 or a specific LAN address, which disables the host check entirely — do that only behind a firewall or a proxy that authenticates, and read the exposure warning in our connection refused on port 11434 walkthrough first, since that page covers what changing the bind address does to everything else.
Tunnels have their own Host-rewrite settings rather than a proxy config file; check your tunnel's documentation for the option that presents localhost to the origin server.
How Do You Confirm the Origin List Ollama Is Using
Ollama prints its entire effective configuration at startup, including the fully resolved origin list. This is the check that ends every argument about whether a variable took effect, and almost nobody knows it exists. From server/routes.go:
slog.Info("server config", "env", envconfig.Values())
So on Linux:
journalctl -u ollama --no-pager | grep "server config"
On macOS:
grep "server config" ~/.ollama/logs/server.log
In Docker: docker logs ollama 2>&1 | grep "server config".
Inside that line you will find OLLAMA_ORIGINS:[...] with every entry, in order — your values first, the defaults after. That output is the ground truth. If your origin is not in that bracket, the variable never reached the process, and you have an environment problem rather than a CORS problem. If it is in there and you still get 403, compare the string character by character against the Origin header in DevTools; a trailing slash or an http vs https mismatch is a different origin as far as the browser is concerned.
The complete list of Ollama's environment variables is in ollama serve --help, and our complete Ollama guide covers the ones that matter day to day.
FAQ
Why does Ollama return 403 to my Chrome extension but not to curl?
Because curl sends no Origin header and an extension always sends chrome-extension://<id>. No extension scheme is in Ollama's 17 default origins, so the CORS middleware aborts the request with 403 before it reaches the API. Set OLLAMA_ORIGINS=chrome-extension://* — or better, the specific extension ID — and restart the server.
Does OLLAMA_ORIGINS replace the default allowed origins?
No. envconfig.AllowedOrigins() seeds the list from your variable and then appends all 17 defaults unconditionally, so your entries are added rather than substituted. Issue #6389 documents this and it is still open. You can widen the allow-list with this variable; you cannot narrow it.
What is the correct OLLAMA_ORIGINS syntax?
A comma-separated list with no spaces and no trailing comma, where every entry either contains a * or starts with a recognised scheme — for example chrome-extension://*,http://192.168.1.50:3000. Surrounding quotes are stripped automatically. A malformed entry does not warn, it panics the server at startup with bad origin: origins must contain '*' or include http://,https://,....
I set OLLAMA_ORIGINS on Linux and nothing changed. Why?
Almost certainly because you exported it in a shell. The Linux daemon runs under systemd as the ollama user with its own environment and never reads your shell. Add it as an Environment= line in a systemctl edit ollama drop-in, run daemon-reload, restart, and confirm with the server config log line above.
Is it safe to set OLLAMA_ORIGINS to a wildcard?
Not on a machine you care about. Ollama has no authentication, so the browser's same-origin policy is the only thing stopping any website you have open from listing your models, running inference on your GPU or pulling models onto your disk. OLLAMA_ORIGINS=* removes that. List real origins, or scheme wildcards at worst, and put a proxy with auth in front if other people are involved.
Why do I still get 403 through nginx when the origin is allowed?
Because that is the other 403. Ollama's allowedHostsMiddleware rejects requests whose Host header is not localhost, the machine's hostname, a .localhost/.local/.internal name, or a private IP literal — and it only runs when Ollama is bound to a loopback address. Add proxy_set_header Host localhost; to your proxy block, or bind Ollama to 0.0.0.0 and secure it another way.
Do I need OLLAMA_ORIGINS for a Python or Node backend?
No. Server-side HTTP clients do not send an Origin header, so the CORS middleware never engages. If a Python script is getting 403, look at whether something in your stack is adding Origin deliberately, or whether you are hitting the Host check through a proxy. Our Ollama Python API guide covers the client-side patterns that work without any CORS configuration at all.
Sources
- ollama/ollama — envconfig/config.go (the
AllowedOrigins()function andVar()quote-trimming, read onmain, 23 August 2026) - ollama/ollama — server/routes.go (middleware order,
allowedHost(), theserver configlog line) - Ollama FAQ (per-OS configuration procedures and the browser-extension
OLLAMA_ORIGINSvalue) - ollama/ollama issues #6021, #6389, #9359, #10507, #14511, #3269, #4262, #8118, and PR #14813
Ollama’s running. Here’s what to build with it.
Go from “ollama run” to RAG apps, agents, and fine-tuned models — structured and hands-on. First chapter free.
Stop piecing Ollama together from blog posts
Ollama Mastery is 15 chapters end to end — install, model choice, Modelfiles, GPU offload, the API, and the 20 errors that actually happen. Plus 24 more courses.
Liked this? 20 full AI courses are waiting.
From fundamentals to RAG, agents, MCP servers, voice AI, and production deployment with real GitHub repos. First chapter free, every course.
Build Real AI on Your Machine
RAG, agents, NLP, vision, and MLOps - chapters across 25 courses that take you from reading about AI to building AI.
Want structured AI education?
25 courses, 519+ chapters, from $9. Understand AI, don't just use it.
Continue Your Local AI Journey
- PILLARBest Ollama Models 2026: 15 Ranked (Coding, Reasoning, Chat)
- AI on QNAP & TrueNAS: Run Ollama with GPU Passthrough
- AI on Steam Deck: Run Local LLMs with Ollama on SteamOS
- Air-Gapped AI Deployment: Install Ollama With No Internet
- Best Free AI Models to Run Locally With Ollama, No API Key
- Best Local LLMs for Tool & Function Calling (2026 Tested)
- Best Ollama Embedding Models: We Benchmarked All 6 for RAG
- Best Ollama Models for 8GB RAM 2026: 12 Tested Local Picks
- Best Ollama Models for AI Agents 2026: 9 Tested & Ranked
- Best Uncensored Local LLMs: Abliterated Ollama Models
Comments (0)
No comments yet. Be the first to share your thoughts!