{"record":{"id":"b2c4ea3537a2c45f","repo":"Graphify-Labs/graphify","slug":"ollama-base-url-points-at-a-link-local-metadata-ad","errorCode":null,"errorMessage":"OLLAMA_BASE_URL points at a link-local/metadata address ({host!r}); refusing to send the corpus there. Set it to a real Ollama host.","messagePattern":"OLLAMA_BASE_URL points at a link-local/metadata address \\((.+?)\\); refusing to send the corpus there\\. Set it to a real Ollama host\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"graphify/llm.py","lineNumber":2810,"sourceCode":"        parsed = urlparse(url)\n    except Exception:\n        if warn:\n            print(\n                f\"[graphify] WARNING: OLLAMA_BASE_URL={url!r} is not a parseable URL.\",\n                file=sys.stderr,\n            )\n        return\n    if parsed.scheme not in (\"http\", \"https\"):\n        if warn:\n            print(\n                f\"[graphify] WARNING: OLLAMA_BASE_URL has unexpected scheme {parsed.scheme!r}; \"\n                \"expected http or https.\",\n                file=sys.stderr,\n            )\n        return\n    host = (parsed.hostname or \"\").lower()\n    if _ollama_host_is_link_local_or_metadata(host):\n        raise ValueError(\n            f\"OLLAMA_BASE_URL points at a link-local/metadata address ({host!r}); refusing to \"\n            \"send the corpus there. Set it to a real Ollama host.\"\n        )\n    is_loopback = host in (\"localhost\", \"127.0.0.1\", \"::1\") or host.startswith(\"127.\")\n    if warn and not is_loopback:\n        scheme_note = \" (UNENCRYPTED)\" if parsed.scheme == \"http\" else \"\"\n        print(\n            f\"[graphify] WARNING: OLLAMA_BASE_URL points to non-loopback host {host!r}{scheme_note}. \"\n            \"Your full corpus will be sent to that endpoint. \"\n            \"Set OLLAMA_BASE_URL=http://localhost:11434/v1 to keep extraction local.\",\n            file=sys.stderr,\n        )\n\n\ndef detect_backend() -> str | None:\n    \"\"\"Return the name of whichever backend has an API key set, or None.\n\n    Priority: gemini → kimi → claude → openai → deepseek → azure → bedrock → ollama (last, opt-in).","sourceCodeStart":2792,"sourceCodeEnd":2828,"githubUrl":"https://github.com/Graphify-Labs/graphify/blob/7fe58b0b0f3873be9a21c30106b8b8527c353aa6/graphify/llm.py#L2792-L2828","documentation":"ValueError raised as an SSRF guard when OLLAMA_BASE_URL's host resolves to a link-local or cloud-metadata address (checked by _ollama_host_is_link_local_or_metadata, e.g. 169.254.x.x). graphify refuses to send the user's full corpus to such an endpoint. This is deliberately a hard stop, unlike the non-loopback warning printed right after.","triggerScenarios":"Validating OLLAMA_BASE_URL (with warn enabled) where the parsed hostname matches link-local (169.254.0.0/16, fe80::/10) or metadata addresses like 169.254.169.254 (llm.py:2806-2810). A common accident is a URL template substituting to http://169.254.169.254:11434/v1 or a misconfigured zero-config address.","commonSituations":"Config templating that injects the wrong IP; copying cloud-init metadata examples into OLLAMA_BASE_URL; on some networks mDNS/link-local resolution of 'ollama.local' landing in 169.254.x.x; confusion between the metadata IP and a real tailnet IP.","solutions":["Set OLLAMA_BASE_URL to the real Ollama host, e.g. export OLLAMA_BASE_URL='http://<real-ip>:11434/v1'.","If you meant a local model: export OLLAMA_BASE_URL='http://localhost:11434/v1' - loopback is allowed.","If the hostname legitimately resolves link-local (rare IPv6 fe80 setups), switch to the host's routable address instead."],"exampleFix":"# before\nexport OLLAMA_BASE_URL=\"http://169.254.169.254:11434/v1\"   # metadata IP\n$ graphify extract --backend ollama   # ValueError: refused\n\n# after\nexport OLLAMA_BASE_URL=\"http://192.168.1.20:11434/v1\"   # real Ollama host","handlingStrategy":"validation","validationCode":"import ipaddress, socket\nfrom urllib.parse import urlparse\n\nurl = \"http://169.254.169.254:11434/v1\"  # your OLLAMA_BASE_URL\nhost = (urlparse(url).hostname or \"\").lower()\ntry:\n    for info in socket.getaddrinfo(host, None):\n        ip = ipaddress.ip_address(info[4][0])\n        if ip.is_link_local:\n            raise SystemExit(f\"{host} resolves link-local ({ip}); fix OLLAMA_BASE_URL\")\nexcept socket.gaierror:\n    pass","typeGuard":null,"tryCatchPattern":"try:\n    validate_ollama_url(warn=True)  # or the graphify entrypoint that reads OLLAMA_BASE_URL\nexcept ValueError as exc:\n    if \"link-local/metadata\" in str(exc):\n        raise SystemExit(\"OLLAMA_BASE_URL targets a metadata/link-local IP - refusing\") from exc\n    raise","preventionTips":["Never hardcode 169.254.169.254 in any URL but a deliberate metadata request.","Prefer hostnames with stable routable DNS over raw IPs in base-URL config.","Treat this guard as intentional - do not 'fix' it by disabling validation; fix the URL."],"tags":["security","ssrf","ollama","configuration","environment"],"backgroundTag":null,"analyzedSha":"7fe58b0b0f3873be9a21c30106b8b8527c353aa6","analyzedAt":"2026-08-14T19:23:21.323Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}