-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix: bind Ollama to localhost with authenticated reverse proxy #679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f06796f
96f248c
a205395
068cde3
d18684f
034fb44
df8a678
3ca045f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ const path = require("path"); | |||||||||||||||||||||||||
| const { spawn, spawnSync } = require("child_process"); | ||||||||||||||||||||||||||
| const { ROOT, SCRIPTS, run, runCapture, shellQuote } = require("./runner"); | ||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||
| HOST_GATEWAY_URL, | ||||||||||||||||||||||||||
| getDefaultOllamaModel, | ||||||||||||||||||||||||||
| getBootstrapOllamaModelOptions, | ||||||||||||||||||||||||||
| getLocalProviderBaseUrl, | ||||||||||||||||||||||||||
|
|
@@ -1133,6 +1134,36 @@ function sleep(seconds) { | |||||||||||||||||||||||||
| require("child_process").spawnSync("sleep", [String(seconds)]); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ── Ollama auth proxy ───────────────────────────────────────────── | ||||||||||||||||||||||||||
| // Ollama has no built-in auth and must not listen on 0.0.0.0 (PSIRT | ||||||||||||||||||||||||||
| // bug 6002780). We bind Ollama to 127.0.0.1 and front it with a | ||||||||||||||||||||||||||
| // token-authenticated proxy on 0.0.0.0:11435 so the OpenShell gateway | ||||||||||||||||||||||||||
| // (running in a container) can still reach it. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let ollamaProxyToken = null; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function startOllamaAuthProxy() { | ||||||||||||||||||||||||||
| // Kill any stale proxy from a previous onboard run so the new token takes effect | ||||||||||||||||||||||||||
| run('lsof -ti :11435 | xargs kill 2>/dev/null || true', { ignoreError: true }); | ||||||||||||||||||||||||||
| const crypto = require("crypto"); | ||||||||||||||||||||||||||
| ollamaProxyToken = crypto.randomBytes(24).toString("hex"); | ||||||||||||||||||||||||||
| run( | ||||||||||||||||||||||||||
| `OLLAMA_PROXY_TOKEN=${shellQuote(ollamaProxyToken)} ` + | ||||||||||||||||||||||||||
| `node "${SCRIPTS}/ollama-auth-proxy.js" > /dev/null 2>&1 &`, | ||||||||||||||||||||||||||
| { ignoreError: true }, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| sleep(1); | ||||||||||||||||||||||||||
| // Verify proxy is actually listening before proceeding | ||||||||||||||||||||||||||
| const probe = runCapture("curl -sf --connect-timeout 2 http://127.0.0.1:11435/api/tags 2>/dev/null", { ignoreError: true }); | ||||||||||||||||||||||||||
| if (!probe) { | ||||||||||||||||||||||||||
| console.error(" Warning: Ollama auth proxy did not start on :11435"); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function getOllamaProxyToken() { | ||||||||||||||||||||||||||
| return ollamaProxyToken; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async function ensureNamedCredential(envName, label, helpUrl = null) { | ||||||||||||||||||||||||||
| let key = getCredential(envName); | ||||||||||||||||||||||||||
| if (key) { | ||||||||||||||||||||||||||
|
|
@@ -1871,11 +1902,12 @@ async function setupNim(gpu) { | |||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| } else if (selected.key === "ollama") { | ||||||||||||||||||||||||||
| if (!ollamaRunning) { | ||||||||||||||||||||||||||
| console.log(" Starting Ollama..."); | ||||||||||||||||||||||||||
| run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); | ||||||||||||||||||||||||||
| console.log(" Starting Ollama (localhost only)..."); | ||||||||||||||||||||||||||
| run("OLLAMA_HOST=127.0.0.1:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); | ||||||||||||||||||||||||||
| sleep(2); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| console.log(" ✓ Using Ollama on localhost:11434"); | ||||||||||||||||||||||||||
| startOllamaAuthProxy(); | ||||||||||||||||||||||||||
| console.log(" ✓ Using Ollama on localhost:11434 (proxy on :11435)"); | ||||||||||||||||||||||||||
| provider = "ollama-local"; | ||||||||||||||||||||||||||
| credentialEnv = "OPENAI_API_KEY"; | ||||||||||||||||||||||||||
| endpointUrl = getLocalProviderBaseUrl(provider); | ||||||||||||||||||||||||||
|
|
@@ -1912,10 +1944,11 @@ async function setupNim(gpu) { | |||||||||||||||||||||||||
| } else if (selected.key === "install-ollama") { | ||||||||||||||||||||||||||
| console.log(" Installing Ollama via Homebrew..."); | ||||||||||||||||||||||||||
| run("brew install ollama", { ignoreError: true }); | ||||||||||||||||||||||||||
| console.log(" Starting Ollama..."); | ||||||||||||||||||||||||||
| run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); | ||||||||||||||||||||||||||
| sleep(2); | ||||||||||||||||||||||||||
| console.log(" ✓ Using Ollama on localhost:11434"); | ||||||||||||||||||||||||||
| console.log(" Starting Ollama (localhost only)..."); | ||||||||||||||||||||||||||
| run("OLLAMA_HOST=127.0.0.1:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); | ||||||||||||||||||||||||||
| sleep(2); | ||||||||||||||||||||||||||
| startOllamaAuthProxy(); | ||||||||||||||||||||||||||
| console.log(" ✓ Using Ollama on localhost:11434 (proxy on :11435)"); | ||||||||||||||||||||||||||
| provider = "ollama-local"; | ||||||||||||||||||||||||||
| credentialEnv = "OPENAI_API_KEY"; | ||||||||||||||||||||||||||
| endpointUrl = getLocalProviderBaseUrl(provider); | ||||||||||||||||||||||||||
|
|
@@ -2029,9 +2062,12 @@ async function setupInference(sandboxName, model, provider, endpointUrl = null, | |||||||||||||||||||||||||
| console.error(" On macOS, local inference also depends on OpenShell host routing support."); | ||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const baseUrl = getLocalProviderBaseUrl(provider); | ||||||||||||||||||||||||||
| upsertProvider("ollama-local", "openai", "OPENAI_API_KEY", baseUrl, { | ||||||||||||||||||||||||||
| OPENAI_API_KEY: "ollama", | ||||||||||||||||||||||||||
| // Use the auth proxy URL (port 11435) instead of direct Ollama (11434). | ||||||||||||||||||||||||||
| // The proxy validates a per-instance Bearer token before forwarding. | ||||||||||||||||||||||||||
| const proxyToken = getOllamaProxyToken() || "ollama"; | ||||||||||||||||||||||||||
| const proxyBaseUrl = `${HOST_GATEWAY_URL}:11435/v1`; | ||||||||||||||||||||||||||
| upsertProvider("ollama-local", "openai", "OPENAI_API_KEY", proxyBaseUrl, { | ||||||||||||||||||||||||||
| OPENAI_API_KEY: proxyToken, | ||||||||||||||||||||||||||
|
Comment on lines
+2067
to
+2070
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fallback token "ollama" will cause 401s if proxy is running with a different token.
Consider either:
Option 2: Fail explicitly- const proxyToken = getOllamaProxyToken() || "ollama";
+ const proxyToken = getOllamaProxyToken();
+ if (!proxyToken) {
+ console.error(" Ollama proxy token is not set. Run the full onboard flow to initialize the proxy.");
+ process.exit(1);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| runOpenshell(["inference", "set", "--no-verify", "--provider", "ollama-local", "--model", model]); | ||||||||||||||||||||||||||
| console.log(` Priming Ollama model: ${model}`); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| #!/usr/bin/env node | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /** | ||
| * Authenticated reverse proxy for Ollama. | ||
| * | ||
| * Ollama has no built-in authentication. This proxy sits in front of it, | ||
| * validating a Bearer token before forwarding requests. Ollama binds to | ||
| * 127.0.0.1 (localhost only) while the proxy listens on 0.0.0.0 so the | ||
| * OpenShell gateway (running in a container) can reach it. | ||
| * | ||
| * Env: | ||
| * OLLAMA_PROXY_TOKEN — required, the Bearer token to validate | ||
| * OLLAMA_PROXY_PORT — listen port (default: 11435) | ||
| * OLLAMA_BACKEND_PORT — Ollama port on localhost (default: 11434) | ||
| */ | ||
|
|
||
| const crypto = require("crypto"); | ||
| const http = require("http"); | ||
|
|
||
| const TOKEN = process.env.OLLAMA_PROXY_TOKEN; | ||
| if (!TOKEN) { | ||
| console.error("OLLAMA_PROXY_TOKEN required"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const LISTEN_PORT = parseInt(process.env.OLLAMA_PROXY_PORT || "11435", 10); | ||
| const BACKEND_PORT = parseInt(process.env.OLLAMA_BACKEND_PORT || "11434", 10); | ||
|
|
||
| const server = http.createServer((clientReq, clientRes) => { | ||
| const auth = clientReq.headers.authorization; | ||
| // Allow unauthenticated health checks (model list only, not inference) | ||
| const isHealthCheck = clientReq.method === "GET" && clientReq.url === "/api/tags"; | ||
| const expected = `Bearer ${TOKEN}`; | ||
| const tokenMatch = auth && auth.length === expected.length && | ||
| crypto.timingSafeEqual(Buffer.from(auth), Buffer.from(expected)); | ||
| if (!isHealthCheck && !tokenMatch) { | ||
| clientRes.writeHead(401, { "Content-Type": "text/plain" }); | ||
| clientRes.end("Unauthorized"); | ||
| return; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Strip the auth header before forwarding to Ollama | ||
| const headers = { ...clientReq.headers }; | ||
| delete headers.authorization; | ||
| delete headers.host; | ||
|
|
||
| const proxyReq = http.request( | ||
| { | ||
| hostname: "127.0.0.1", | ||
| port: BACKEND_PORT, | ||
| path: clientReq.url, | ||
| method: clientReq.method, | ||
| headers, | ||
| }, | ||
| (proxyRes) => { | ||
| clientRes.writeHead(proxyRes.statusCode, proxyRes.headers); | ||
| proxyRes.pipe(clientRes); | ||
| }, | ||
| ); | ||
|
|
||
| proxyReq.on("error", (err) => { | ||
| clientRes.writeHead(502, { "Content-Type": "text/plain" }); | ||
| clientRes.end(`Ollama backend error: ${err.message}`); | ||
| }); | ||
|
|
||
| clientReq.pipe(proxyReq); | ||
| }); | ||
|
|
||
| server.listen(LISTEN_PORT, "0.0.0.0", () => { | ||
| console.log(` Ollama auth proxy listening on 0.0.0.0:${LISTEN_PORT} → 127.0.0.1:${BACKEND_PORT}`); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't rotate the proxy token unless the new proxy actually owns
:11435.This helper always generates a new token and backgrounds a new proxy, but it never checks whether
:11435is already occupied or whether the child bound successfully. Since Step 1 only cleans up the OpenShell gateway, rerunning onboarding with Ollama can leave the old proxy serving the old token whileollama-localgets updated to the new one, which breaks later requests from the sandbox.Suggested hardening
function startOllamaAuthProxy() { const crypto = require("crypto"); - ollamaProxyToken = crypto.randomBytes(24).toString("hex"); + if (runCapture("curl -sf http://127.0.0.1:11435/api/tags 2>/dev/null", { ignoreError: true })) { + console.error(" Ollama auth proxy is already running on port 11435. Stop it before rerunning onboard."); + process.exit(1); + } + const token = crypto.randomBytes(24).toString("hex"); run( - `OLLAMA_PROXY_TOKEN=${shellQuote(ollamaProxyToken)} ` + + `OLLAMA_PROXY_TOKEN=${shellQuote(token)} ` + `node "${SCRIPTS}/ollama-auth-proxy.js" > /dev/null 2>&1 &`, { ignoreError: true }, ); sleep(1); + if (!runCapture("curl -sf http://127.0.0.1:11435/api/tags 2>/dev/null", { ignoreError: true })) { + console.error(" Ollama auth proxy failed to start on port 11435."); + process.exit(1); + } + ollamaProxyToken = token; }🤖 Prompt for AI Agents