Security

What's protected: the user's model runtime (Ollama, LM Studio, ...) when it's served or exposed, and their machine from what discovery finds. The models plugin never talks to a relay.

Only the model API is served

lnk model serve, and so lnk tunnel open llm or <model>, forwards only the runtime's OpenAI-compatible API.

  • Only the inference API is forwarded, with GET, POST and OPTIONS: /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/responses, /v1/models, and reading one model at /v1/models/<id>. Another method on these paths gets 405. GET / shows how to call it; any other path is a 404, so a runtime's own endpoints under /v1 (Jan's model pulls, LocalAI's file uploads, vLLM's adapters) aren't reached.
  • Paths with . or .. segments, % or \ are refused, so a visitor can't reach management endpoints such as Ollama's /api/pull, /api/create or /api/delete.
  • --model pins every request to one model and makes /v1/models list only it. The body must then be a JSON object of at most 16 MiB (413). Such bodies hold at most 64 MiB at once, counted as bytes arrive and until the copy with the model set in it is written to the runtime, so that copy counts too, and a runtime slow to answer holds none of it. A body that sends nothing, or waits for room, for 30 s gets 408, and so does one still arriving after 10 minutes.
  • When the runtime can't be reached, visitors get a fixed 502 message. The details go to stderr.
  • Nothing is logged per request: no prompts or completions.

On loopback only

  • lnk model serve listens on a port on 127.0.0.1, and answers only a request naming this machine (Host 127.0.0.1, localhost or [::1]), so a web page that points its own name here (DNS rebinding) gets a 403.
  • lnk tunnel open llm runs it and exposes that port. It stops when the lnk tunnel open that started it exits and its stdin closes, even if that command is killed.
  • Other local users can reach that port while it runs, as they can reach the runtime's own port.

Discovery

lnk model list, and serving without --upstream, send GET /v1/models to the runtimes' default ports on localhost (the list), and to the addresses added with lnk model add.

  • An added address is always on this machine: localhost, 127.0.0.1 or [::1], with no path, query or password. add refuses any other, and discovery skips one edited into ~/.config/lnk/models.toml (600).
  • ~/.config/lnk/model-facts.toml, the models you describe, is read strictly: a key Link doesn't know, a window under 1024 tokens or a negative price is refused, naming the line, and at most 1 MiB is read.
  • An agent, like the tunnel, takes an added runtime's models as it takes Ollama's, so add only a server you trust with the agent's prompts.
  • The requests go through no proxy, and read at most 1 MiB back.
  • A local app on one of those ports that happens to answer with a model list is treated as a runtime. lnk prints which address it exposes.

Installing Ollama

lnk model install, and the local model lnk agent start offers, install Ollama when it's missing.

  • It asks first, unless --yes. lnk agent start passes --yes once you picked the local model.
  • On macOS, Link downloads Ollama's app from ollama.com over HTTPS and installs it only if Gatekeeper accepts it (spctl --assess: signed by its developer and notarized by Apple).
  • On Linux, it runs Ollama's own install script from ollama.com, which asks for sudo and installs Ollama as a system service. That script is Ollama's to trust, as when you run it yourself. It's saved to a folder made new for it (as mkdtemp), only yours, so another user of the machine can't swap it before it runs.
  • Models come from Ollama's library through Ollama's own API, and lnk model remove deletes them the same way.

Exposed, it's anyone's with the URL

Anyone with the URL, or the password, can run the model on the user's hardware or, with a paid upstream, at the user's cost. Use --auth: the relay then checks the password before anything reaches the machine. The tunnel's protections are in Tunnel Security.

Gaps

--auth is one shared Basic password, while OpenAI SDKs send a bearer key. See Known limitations.