Posted on

Recently, I have been exploring two questions: how far Vibe Coding can take a person’s idea, and what attacks publicly exposed LLM services are facing.

So I put those two questions into the same project.

I defined the requirements, security boundaries, and acceptance criteria, then worked with Codex to build an original LLM API honeypot from scratch and deploy it on the Internet. From architecture design and interface emulation to event classification and report generation, the project’s code was developed step by step around this experiment.

It has five decoy profiles, each simulating a common public API shape for Ollama, OpenAI-compatible/vLLM, TGI/llama.cpp, LM Studio, and LiteLLM. The exposed ports and interfaces look familiar, but there is no real model behind them. It does not execute prompts submitted by visitors, call tools, or download files from addresses supplied in requests.

It does one thing only: observe what publicly exposed LLM APIs encounter.

From August 3 to August 22, the honeypot recorded 3,815 requests from 1,564 distinct IPs. Most of the traffic was simply automated scanning, but path traversal, oversized request bodies, and potential SSRF probes also appeared in the mix.

Twenty days were enough to make one reality clear: as soon as a port is open to the public Internet, someone will soon come to determine what it is, what it can do, and whether it offers an entry point for further exploitation.

First, a look at the honeypot

LLM Honeypot is an original, protocol-layer, non-executing honeypot that I developed with Codex. It is also a complete project from my recent Vibe Coding experiments.

Traditional honeypots often disguise themselves as SSH, databases, or web applications. This system chose another category of Internet asset that is growing rapidly: large language model APIs.

Codex and I designed different fixed responses for five common API shapes. When a scanner visits the root path, model list, chat interface, or framework-specific endpoint, it sees a convincing-looking service. Every request passes through size limits and sensitive-information redaction before being written to the event database and deterministically classified by a rule engine.

The names here represent the observation groups used by the honeypot. vLLM is an open-source inference engine that provides an OpenAI-compatible interface; it does not belong to OpenAI. TGI and llama.cpp are also two independent software projects. Because some of their exposed paths and scanning fingerprints overlap heavily, the current version places TGI and llama.cpp in the same decoy profile. A port is only the entry point for this deployment and cannot, by itself, identify the backend software.

There is one important security boundary here: the honeypot simulates the protocol only. It does not load models, execute attacker input, access URLs submitted by the other party, or call any tools.

It is deployed on a host and network isolated from the production environment. Since the honeypot itself is intended to encounter unknown attack traffic, Docker isolation alone is far from sufficient; host egress, cloud networking, management ports, and data storage all require separate controls.

LLM Honeypot 架构:五个公网诱饵传感器将有效请求写入内部事件库,经规则引擎和报告工作器形成安全报告;DeepSeek 只接触经过脱敏和边界控制的报告快照
LLM Honeypot 架构:五个公网诱饵传感器将有效请求写入内部事件库,经规则引擎和报告工作器形成安全报告;DeepSeek 只接触经过脱敏和边界控制的报告快照

What were most people looking for in 3,815 requests?

Of all events, 3,509 were service-enumeration events, accounting for 92%.

Scanners visited the root path, /v1/models, /v1/chat/completions, health checks, and framework-specific interfaces in sequence. The order was usually systematic: first determine whether a service is running on the port, then confirm whether it resembles Ollama, vLLM, or another framework, and finally look for an entry point that can generate content or manage models.

The five decoy profiles did not receive requests evenly:

  • TGI / llama.cpp merged profile: 2,052, or 53.8%;
  • OpenAI-compatible / vLLM: 892, or 23.4%;
  • LiteLLM: 374, or 9.8%;
  • LM Studio: 305, or 8.0%;
  • Ollama: 192, or 5.0%.
五个诱饵 profile 的请求分布:TGI 或 llama.cpp 合并 profile 占 53.8%,OpenAI-compatible 或 vLLM 占 23.4%
五个诱饵 profile 的请求分布:TGI 或 llama.cpp 合并 profile 占 53.8%,OpenAI-compatible 或 vLLM 占 23.4%

The TGI / llama.cpp merged profile and the OpenAI-compatible interface attracted the most traffic. This may reflect how widespread these interfaces are on the Internet, or it may be related to the default dictionaries used by scanners. Based on the honeypot data alone, I cannot determine which explanation dominates, so I did not pursue attribution.

The source IPs also showed the typical characteristics of broad scanning. Of the 1,564 IPs, 1,008 appeared only once, accounting for 64.5%. They looked more like automated probes sweeping across the entire Internet.

There were also 556 sources that appeared more than once; 151 sources remained active on at least 7 days, and 32 sources spanned at least 14 days. Persistent rescanning is common amid the background noise as well.

From model lists to inference calls

A single visit to a model list usually tells us only that “someone took a look.” What is more interesting is what the scanner did afterward.

During the reporting window, 34 sources sent 229 requests to simulated inference endpoints for chat, text generation, and related functions. These requests triggered fixed simulated responses only; no model was running behind them.

Of these, 215 inference requests occurred within 30 minutes after the same source completed root-path, health-check, or model-list enumeration, accounting for 93.9%. The sequence is clear: first confirm that the service is alive, then read the model list, and then try inference one model at a time.

The honeypot advertised three fake model names. Among the subsequent 229 simulated inference requests:

  • qwen2.5:14b-instruct-q4_K_M: 92 requests, or 40.2%;
  • deepseek-r1:32b: 73 requests, or 31.9%;
  • llama3.3:70b-instruct: 60 requests, or 26.2%;
  • 3 requests omitted the model, and 1 other request asked for qwen2.5:7b, which was outside the list.
229次模拟推理请求的模型选择:绝大多数请求直接使用蜜罐模型列表中公布的三个虚假模型
229次模拟推理请求的模型选择:绝大多数请求直接使用蜜罐模型列表中公布的三个虚假模型

The three main models together accounted for 98.3%. This does not mean that attackers inherently prefer Qwen, DeepSeek, or Llama, because those three names were exactly the list the honeypot had just shown them. A more reasonable explanation is that the scanner read the data returned by the service and automatically filled the discovered model name into the next request.

In other words, the model list is not an inconsequential display interface. It is providing downstream automation tools with a target list they can consume directly.

What did they send to the “models”?

Including compatibility probes for /v1/responses, there were 268 inference attempts in total. The message content fell roughly into three groups.

The first group consisted of liveness and compatibility tests: 160 requests, or 59.7%. The content was usually short, such as Reply with OK., hi, hello, and Reply with the single word: verified, as well as the 29-character say pong. [IGNORE: PR0B3]. These messages seek predictable results that are easy for machines to judge: as long as the response format is correct and the expected word appears, the scanner can confirm that the interface works.

The second group looked more like security-boundary testing. Over approximately 14 hours, one source sent 50 requests, rotating five categories of high-risk questions across three fake models: explosives, drug manufacturing, cryptocurrency money laundering, violent harm, and explicit pornography. Each topic appeared 10 times; Qwen received 20 requests, while DeepSeek and Llama each received 15.

The pattern was highly regular: the same questions, the same parameters, rotating models, and repeated execution. It looked more like an automated model-safety evaluation or a survey of jailbreak capability; the data cannot establish that the other party intended to commit an illegal act in the real world. The risk still exists for real services: an open interface may be used free of charge by a third party to test safety guardrails in bulk, consume compute, and search for models with weaker refusal strategies.

The third group contained 58 other requests. One streaming request included a fairly complete system message asking the model to determine whether a user message should enter Agent Mode; the user’s question was “What model are you?” Immediately afterward, the same source called Llama, DeepSeek, and Qwen separately, sending Chinese messages such as “What model are you?” and “Hello.” This already looked very much like a client or frontend connecting to the discovered service as though it were a real model provider.

Where did they go after inference?

Of the 34 sources that called an inference interface, 21 continued sending inference requests within 30 minutes of their first call; 4 revisited the model list; 2 turned to model-management paths; and 13 generated no further events during those 30 minutes.

The most active inference source behaved especially mechanically. It first tested each of the three models 13 times on /v1/chat/completions, then switched to the newer /v1/responses and tested each model 13 times again, for a total of 78 requests. The honeypot did not implement /v1/responses; all 39 requests returned 404, yet the other party still completed the full polling sequence.

This looked more like a compatibility scanner running continuously for the long term: it was not satisfied with “can the interface answer?” It also had to determine whether the service supported Chat Completions or the Responses API, which models were available, and whether the streaming format matched expectations.

Another source did only one thing: it sent hi 7 times to each of the three models through /api/generate, 21 times in total. It did not explore complex capabilities; it simply repeated validation of the minimum generation path.

These behaviors show that enumeration of public LLM services is beginning to move from “identify the port” toward “consume the model list, poll models, switch API versions, and validate response formats.” Compared with traditional port scanning, it is closer to an automated product acceptance test.

Putting an OAST address into the model-pull interface

On August 19, a source submitted a remote model address to the /api/pull endpoint of the Ollama-disguised service. The domain pointed to an OAST service, and the request also included insecure: true.

OAST is commonly used to verify whether a target will actively connect to a server controlled by the tester. Placed in a model-pull interface, this request looked like an SSRF or data-exfiltration capability probe: the attacker wanted to know whether the server would initiate a connection to the supplied address.

The honeypot did not access this URL. The log shows that someone performed the test, but it cannot show that an outbound connection actually occurred.

If a real system allows users to specify model-download addresses, the risk expands quickly. An attacker could use this to access internal services, cloud metadata addresses, and loopback interfaces, or trick the server into downloading a controlled file.

Safer designs include allowing only verified model repositories; rejecting private, loopback, link-local, and reserved addresses; preventing DNS rebinding; ensuring that the download component holds no sensitive credentials; and placing model downloads in an egress-restricted isolated network.

Traditional Web attacks also land on LLM ports

In addition to probes centered on model downloads and inference interfaces, this honeypot also received path-traversal and oversized-request-body tests. These are not unique to LLMs; they are closer to the traditional Web attacks that public Internet services face every day.

Including them in the report is still valuable, because real LLM services typically sit behind a CDN, reverse proxy, API gateway, and application framework. Attackers can check these common components directly without first confirming which model is running in the backend.

Reading /etc/passwd and .env

The system recorded 24 path-traversal and sensitive-file probes, with a rule severity of 7 and confidence of 0.96.

Twenty of them attempted to read /etc/passwd through multiple layers of ../; the other 4 targeted .env. These requests appeared across the TGI/llama.cpp, OpenAI-compatible/vLLM, and LiteLLM decoy profiles.

This shows that visitors were not betting on a specific, already confirmed framework vulnerability. They looked more like they were checking in bulk for oversights in reverse proxies, URL decoding, path normalization, and static-file mapping.

Four sources performed this kind of probe repeatedly, three of them across multiple dates. Every request ultimately received a 404; the logs contained no file-content disclosure and showed no deeper activity after a successful attempt.

Therefore, these records can prove only that attack attempts occurred; they cannot prove that the system was compromised.

For a real LLM gateway, a request may pass through multiple rounds of decoding and rewriting among the CDN, Nginx, application framework, and upstream service. A path that looks safe at the entry point may no longer be safe by the time it reaches the final layer.

Sending 10 MiB request bodies in succession

On August 16, the same source sent two POST requests of approximately 10 MiB to the TGI/llama.cpp merged profile in less than one second; the two request bodies differed by only 1 byte.

Such a neat boundary value looks like a request-body-size test, and it may also be a probe before a denial-of-service attempt. Both requests were rejected with 413, showing that the limit was effective.

But “returning 413” is not enough.

What really needs to be confirmed is at which layer the request was rejected. If the reverse proxy first fully receives and buffers the 10 MiB content, or even passes it to a parser, and the application then returns 413, network, TLS, memory, and connection resources are still consumed. Ideally, the request should be discarded as early as possible at the outermost layer, and repeated 413 responses from a single source within a short period should trigger an alert.

事件分类图:服务枚举占 92%,路径穿越与超大请求体属于传统 Web 攻击,模型拉取 SSRF 和推理接口探测更贴近 LLM 服务能力
事件分类图:服务枚举占 92%,路径穿越与超大请求体属于传统 Web 攻击,模型拉取 SSRF 和推理接口探测更贴近 LLM 服务能力

If you also deploy an LLM API on the public Internet

The data from these 20 days offers several specific recommendations.

First, avoid exposing model interfaces directly to the entire Internet whenever possible. Consolidate access through a VPN, zero-trust access, fixed sources, or an authenticated API gateway, and set request-rate, concurrency, and token quotas for both IPs and API keys.

Second, inventory every capability that can make the server connect to an external address. Model downloads, plugin installation, tool calls, and webhooks all require a unified egress policy; a single string check in application code is not enough.

Third, limit request bodies and connection rates at the CDN, WAF, or outermost reverse proxy. A 413 returned by the application is only the result; also monitor request-body-size distributions, connection duration, the number of rejections before parsing, and concurrent occupancy by each interface.

Fourth, retain enough troubleshooting information for path handling. The original path, a hash of the normalized path, the number of decoding layers, the upstream routing result, and the request-id can all help locate double-decoding or rewrite problems. Do not store tokens or complete sensitive parameters in logs.

Fifth, pull high-severity events out of the weekly report in advance. Only 27 events in this run had a severity of 5 or higher, accounting for 0.7% of the total, yet they contained the most valuable attack signals in the window. They should trigger immediate aggregated notifications by source, service type, and attack technique as soon as they appear.

After 20 days, I see “opening a port” differently

At 116 to 321 requests a day, the volume is hardly astonishing. What really stayed with me is how naturally this traffic arrived.

There was no publicity, no domain name, and no real model capability. The five public ports still attracted 1,564 sources. Some merely passed by; some read the model list and called each model in turn; some switched between Chat Completions and the Responses API; and some tested model safety boundaries in bulk.

Public LLM APIs have entered the routine field of view of automated Internet scanning. Deployers are dealing with more than prompt injection. Model-list exposure, inference-quota abuse, interface-compatibility probing, bulk safety-guardrail testing, the model supply chain, and outbound network control have all appeared in real traffic.

This honeypot will continue running. The current data covers only 19 days and 20 hours, which is not enough to represent a complete calendar month. In the next phase, I want to keep observing whether these sources form more stable scanning cycles, and I will also add immediate summaries of high-severity events and more complete activity correlation.

There is always someone knocking on a door on the Internet.

Knowing which door they knocked on and where they went next gives defenders a chance to act one step earlier.

Leave a Reply