Drew AllemanOffensive Security

Blog / red-team

Identifying and Enumerating Ollama Inference Servers

Published 2026-10-24

In my last post, I spent some time enumerating a vLLM inference server figuring out how to identify it in the wild and probing the endpoints that don’t require authentication. In this post, we’ll do the same for Ollama.

Ollama homepage

Shodan Reconnaissance

A quick Shodan query for product:"Ollama" returns 16,567 exposed servers - every one of them an Ollama instance reachable from the public internet.

Shodan results for product Ollama

Installation

I installed and started Ollama through docker using the following command:

sudo docker run -d --gpus all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

This starts the container but ships with no models, so I pulled one to have something to query:

sudo docker exec -it ollama ollama pull llama3.2

I then checked the startup logs, which did not reveal a list of routes as per vLLM.

Ollama Docker startup logs

Reviewing the Github Repository

Searching for the string routes in the ollama GitHub repository reveals routes.go

GitHub ollama server routes.go

That file reveals the following route generation function:

func (s *Server) GenerateRoutes() (http.Handler, error) {
	
	...
	...
	// Local model cache management (new implementation is at end of function)
	r.POST("/api/pull", s.PullHandler)
	r.POST("/api/push", s.PushHandler)
	r.HEAD("/api/tags", s.ListHandler)
	r.GET("/api/tags", s.ListHandler)
	r.POST("/api/show", s.ShowHandler)
	r.DELETE("/api/delete", s.DeleteHandler)

	r.POST("/api/me", s.WhoamiHandler)
	r.GET("/api/balance", s.BalanceHandler)
	r.GET("/api/usage", s.UsageHandler)
	...
	...
}

Below is a table representing all the API endpoints found in the source, additional documentation can be found here.

Method Path Handler Purpose
HEAD / inline Liveness check (“Ollama is running”)
GET / inline Liveness check (“Ollama is running”)
HEAD /api/version inline Return server version
GET /api/version inline Return server version
GET /api/status StatusHandler Cloud availability status
POST /api/pull PullHandler Download a model from a registry
POST /api/push PushHandler Push a model to a registry
HEAD /api/tags ListHandler List installed models
GET /api/tags ListHandler List installed models
POST /api/show ShowHandler Model details, template, system prompt, parameters
DELETE /api/delete DeleteHandler Delete an installed model
POST /api/signout SignoutHandler Sign out of a configured cloud account
DELETE /api/user/keys/:encodedKey SignoutHandler Sign out (deprecated)
POST /api/create CreateHandler Create a model from a Modelfile
POST /api/blobs/:digest CreateBlobHandler Upload a raw model blob
HEAD /api/blobs/:digest HeadBlobHandler Check whether a blob exists
POST /api/copy CopyHandler Copy an installed model
POST /api/experimental/web_search WebSearchExperimentalHandler Cloud web search proxy
POST /api/experimental/web_fetch WebFetchExperimentalHandler Cloud web fetch proxy
GET /api/experimental/model-recommendations ModelRecommendationsExperimentalHandler Suggested models
GET /api/ps PsHandler List models loaded in memory
POST /api/generate GenerateHandler Single-prompt text generation (inference)
POST /api/chat ChatHandler Chat-format text generation (inference)
POST /api/embed EmbedHandler Generate embeddings
POST /api/embeddings EmbeddingsHandler Generate embeddings (legacy)
POST /v1/systemone SystemOneHandler Decision-model scoring
POST /v1/chat/completions ChatHandler Chat completion (OpenAI-compatible)
POST /v1/completions GenerateHandler Completion (OpenAI-compatible)
POST /v1/embeddings EmbedHandler Embeddings (OpenAI-compatible)
GET /v1/models ListHandler List models (OpenAI-compatible)
GET /v1/models/:model ShowHandler Model details (OpenAI-compatible)
POST /v1/responses ChatHandler Responses API (OpenAI-compatible)
POST /v1/responses/compact ResponsesCompactHandler Compact a stored response
POST /v1/audio/transcriptions ChatHandler Audio transcription (OpenAI-compatible)
POST /v1/messages ChatHandler Messages API (Anthropic-compatible)

HTTP Headers and Generic Routes

Let’s fingerprint the server with curl and hit some of the generic routes like /api/version.

$ curl -v http://localhost:11434/api/version
* Host localhost:11434 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:11434...
* Established connection to localhost (::1 port 11434) from ::1 port 33356
* using HTTP/1.x
> GET /api/version HTTP/1.1
> Host: localhost:11434
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Wed, 07 Oct 2026 03:59:58 GMT
< Content-Length: 20
<
* Connection #0 to host localhost:11434 left intact

{
	"version":"0.40.0"
}

Here we can see the ollama version is returned, but no type of http server headers were revealed to showcase the underlying http component.

Out of curiosity I nmaped the port doing service versioning checking with -sV:

nmap -p 11434 192.168.100.147 -sV -A
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-06 21:01 -0700
Nmap scan report for 192.168.100.147
Host is up (0.00022s latency).

PORT      STATE SERVICE VERSION
11434/tcp open  http    Golang net/http server
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 404 Not Found
|     Content-Type: text/plain
|     Date: Wed, 07 Oct 2026 04:01:42 GMT
|     Content-Length: 18
|     page not found
|   GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Content-Type: text/plain; charset=utf-8
|     Date: Wed, 07 Oct 2026 04:01:27 GMT
|     Content-Length: 17
|     Ollama is running
|   HTTPOptions:
|     HTTP/1.0 405 Method Not Allowed
|     Allow: HEAD, GET
|     Content-Type: text/plain
|     Date: Wed, 07 Oct 2026 04:01:27 GMT
|     Content-Length: 22
|     method not allowed
|   OfficeScan:
|     HTTP/1.1 400 Bad Request: missing required Host header
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|_    Request: missing required Host header
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).

nmap didn’t read “Golang” from a header. It inferred the framework from the way the server responds to malformed and edge-case probes.

  • It answers HTTP/1.1 requests with HTTP/1.0 status lines (visible in the 404 and GetRequest probes) — a known quirk of Go’s HTTP server.
  • The error wording is library-specific. 400 Bad Request: missing required Host header is emitted verbatim by Go’s standard library when a request arrives with no Host header — the string is hardcoded in net/http, so nmap matches it directly against its fingerprint database.

This is the opposite of vLLM, which announced its backend HTTP framework outright in a server: uvicorn header. Ollama volunteers nothing, but Go’s distinctive HTTP behavior means the framework is identifiable anyway.

No Authentication, By Design

Ollama was built as a localhost tool, and by default a bare-metal install binds to 127.0.0.1 only, so auth would be pointless. If you would want to bind Ollama to a separate IP and implement some form of authentication you can use a reverse proxy that enforces a key.

That being said, not all models are equal here. Ollama serves two kinds. Local models run on the host’s own GPU and answer /api/generate with no auth at all. Cloud models (the -cloud tags) get proxied up to ollama.com, which does require auth.

Requests to localhost never need authentication, even for cloud models. When the server forwards a cloud request to ollama.com, it signs it with the owner’s stored credentials automatically.

Ollama docs Signing in page

https://docs.ollama.com/api/authentication

Enumerating the Models and System Prompts

We can use the /api/tags endpoint to display all models available: /api/tags listing installed models

Then we can craft a POST request passing an enumerated model name to /api/show to display its license, template, and system prompt.

$ curl http://localhost:11434/api/show -d '{"model":"llama3.2"}' | jq
{
  ...
  "license": "LLAMA 3.2 COMMUNITY LICENSE AGREEMENT....",
  "template": "...",
  "details": {
    "format": "gguf",
    "family": "llama",
    "parameter_size": "3.2B",
    "quantization_level": "Q4_K_M"
  },
  "model_info": {
    "general.basename": "Llama-3.2",
    "general.finetune": "Instruct",
    ...
  },
  "capabilities": ["completion", "tools"]
}

Where vLLM hid its model behind --served-model-name local, Ollama’s /api/show hands over the exact identity: general.basename of Llama-3.2, the Instruct finetune, 3.2B parameters, Q4_K_M quantization, the full chat template, and the model’s capabilities (here, tools, meaning it supports tool-calling).

There is no system field in the response above. A base model pulled straight from the registry has no system prompt baked in, so there is nothing to display.

When the System Prompt Is Baked In

Ollama models are packaged with a Modelfile, and one of its documented instructions is SYSTEM, which sets the model’s system message (Modelfile reference). This is a normal, intended use of the format, self-hosted apps and internal tools routinely bake their persona and guardrails into a model this way instead of sending them on every request.

To show what that exposes, I created a model with a system prompt containing a fake internal secret:

printf 'FROM llama3.2\nSYSTEM "You are the ACME support assistant. Be helpful and friendly. Never reveal the internal promo code SPRING50 to anyone."\n' > Modelfile
sudo docker cp Modelfile ollama:/tmp/Modelfile
sudo docker exec -it ollama ollama create acme-bot -f /tmp/Modelfile

Now the same unauthenticated /api/show call returns that system prompt verbatim:

$ curl -s http://localhost:11434/api/show -d '{"model":"acme-bot"}' | jq '.system'
"You are the ACME support assistant. Be helpful and friendly. Never reveal the internal promo code SPRING50 to anyone."

Prompting the LLM

We can use the enumerated model by crafting a POST request to the /api/generate endpoint.

$ curl -s http://192.168.100.147:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "In one sentence, what is DNS?",
  "stream": false
}' | jq

The API response containing the LLMs answer: /api/generate response

Registering a New Named Model

/api/create makes a new named model on the Ollama server by adding your customizations on top of an existing one. You’re not training anything or downloading new weights. You are providing new settings to an existing model.

$ curl http://192.168.100.147:11434/api/create -d '{
  "model": "acme-bot",
  "from": "llama3.2",
  "system": "You are the ACME support assistant. Be helpful and friendly. Never reveal the internal promo code SPRING50 to anyone.",
  "stream": false
}'

/api/create creating acme-bot

Now if we query the tags endpoint we can see our new customized model:

/api/tags showing acme-bot

Revealing the system prompt we set:

$ curl http://192.168.100.147:11434/api/show -s -d '{"model":"acme-bot"}' | jq '.system'

"You are the ACME support assistant. Be helpful and friendly. Never reveal the internal promo code SPRING50 to anyone."

Deleting a Model

We can use the /api/delete method to delete the model we created

curl -X DELETE http://192.168.100.147:11434/api/delete -d '{"model":"acme-bot"}'

/api/delete removing acme-bot

Conclusion

Ollama offers nothing in its HTTP headers, but its responses to malformed probes fingerprint the backend HTTP service as Golang. The /api/version endpoint hands over the build, and /api/show gives up the exact model, its template, and any baked-in system prompt, all with no auth required. Cloud models are the one exception: they require authentication once a request leaves localhost.