"The honeypot that talks back!β’"
Introducing honeyprompt
, an LLM-first deception framework made by/for web developers.
Supports all major cloud and local LLM providers. SSH, HTTP, TLS, TCP, Telnet and more. It ships as
a small container (and a single static binary) and keeps every knob in one honeyprompt.yaml
.
No plugins to compile, no database to run, easily extendable and can be deployed on low-end hardware.
A demo instance is available at 172.233.151.216
, with the unauthenticated web panel here:
http://172.233.151.216:9090. It is a public instance of honeyprompt running on a cheap Linode VPS,
with openrouter/free
as the sole LLM provider/model.
For easiest setup in 2026, we recommend Docker and OpenRouter/openrouter/free
as the LLM provider. All major cloud and local LLM providers are supported. Three files and one command stand up the full default deployment: seven LLM-backed decoys, durable event storage and the operator panel.
1. Grab the default config, compose file, and env template:
mkdir honeypot && cd honeypot
wget https://raw.githubusercontent.com/alectrocute/honeyprompt/main/honeyprompt.yaml
wget https://raw.githubusercontent.com/alectrocute/honeyprompt/main/compose.yaml
wget -O .env https://raw.githubusercontent.com/alectrocute/honeyprompt/main/.env.example
(Or clone the repo and cd
into it β same three files.)
2. Fill in .env. Two values are required:
OPENROUTER_API_KEY=sk-or-... # use a dedicated key with a spend limit
HONEYPROMPT_PANEL_PASSWORD=changeme # basic-auth password for the panel
3. Start it:
docker compose up -d
4. Poke it:
curl http://localhost/ # default nginx welcome page (no LLM)
ssh -p 2222 root@localhost # password: root β then type anything
curl http://localhost:2375/v1.54/containers/json # "exposed" Docker API
5. Watch it happen in the read-only panel at http://127.0.0.1:9090 (sign in as admin
with
your panel password). Every connection, credential, and command streams in live. If you are deployed
to a remote host, you'll need to expose the :9090
port in compose.yaml. This is not recommended for production deployments.
Pin a numbered release instead of
latest
for production deployments β setHONEYPROMPT_IMAGE
in.env
.
The honeyprompt.yaml you just downloaded is a fully annotated showcase. It ships profiles for:
A generic corporate web serverβ port 80, the widest net;/
serves the stock nginx welcome page instantly, and deeper paths fall through to the LLM for full HTML/CSS intranet pages, login forms, and admin panels built to keep the attacker clicking.MCP / agent gatewaysβ Streamable HTTP discovery, OAuth metadata, JSON-RPC tool calls, and tempting production tools.** Docker Engine API 29.5**β the unauthenticated port 2375 surface used by real cloud worms.** Kubernetes API v1.36**β namespace, workload, Secret, ConfigMap, and RBAC discovery.** Ubuntu 26.04 AI build infrastructure**β SSH, GPU workloads, Docker, kubeconfigs, CI state, and provider credentials.** Redis 8.8**β common RESP probes used for credential theft, persistence, and lateral movement.** Industrial edge / OT**β an intentionally legacy Telnet management plane, because modern defense still has to catch attacks against old infrastructure.
The versions are contemporary, but the personas intentionally look useful and slightly exposed. A good decoy should attract interaction, not advertise perfect hardening.
Important
Even if you're using LLMs, determine the most frequently used paths and add static rules for them.
This will save you massive amounts of LLM tokens and speed up responses to requests that are not
worth the cost of an LLM call. Random examples: whoami
, health checks, favicon, version probes, etc.
This minimal honeyprompt.yaml
fakes an SSH box with two static rules and no LLM:
panel:
enabled: true
address: "0.0.0.0:8080"
events:
buffer: 2000
file: /data/events.jsonl # durable attacker activity
services:
- protocol: ssh
address: "0.0.0.0:2222"
description: "Ubuntu 26.04 LTS build runner"
serverName: "gpu-runner-07"
passwordRegex: "^(root|admin|123456)$" # which passwords "work"
commands:
- regex: "^whoami$"
handler: "root"
- regex: "^(.+)$"
handler: "bash: command not found"
docker run --rm \
-p 2222:2222 -p 8080:8080 \
-v "$(pwd)/honeyprompt.yaml:/etc/honeyprompt/honeyprompt.yaml:ro" \
-v honeyprompt-data:/data \
alectrocute/honeyprompt:latest
For a persistent deployment, use the included compose.yaml. The
deployment guidecovers Docker Hub releases, required GitHub secrets, port and firewall setup, panel access over SSH, upgrades, rollback, event storage, and isolation.
A honeypot only has to do one thing well: stay convincing long enough that the attacker keeps
typing. Every command they run is intelligence β the tools they reach for, the credentials they
reuse, the CVEs they assume you haven't patched. Static honeypots break character the moment someone
runs a command the author didn't anticipate. honeyprompt hands that moment to an LLM, so the shell
answers dmesg | tail
or cat /etc/shadow
the way a real one would, and the session keeps going.
This is the part worth understanding up front, because the two are deliberately kept apart:
- Deception events: Every attacker interaction: connections, auth attempts, each command or request, the response honeyprompt sent back, which provider and model answered, and how long it took. This is your threat intel. It's held in a bounded in-memory buffer for the live panel and you can persist all of it to disk.
- Operational logs: Startup, which ports it bound, provider failures, shutdown, internal errors. This is what you read when the runtimemisbehaves. It has nothing to do with attacker activity.
You configure them separately:
events:
buffer: 2000 # recent events kept in memory for the panel
file: /data/events.jsonl # persist every event as JSON Lines
logging:
level: info # debug | info | warn | error
format: text # how it looks on the console: text (human) or json
file: /data/honeyprompt.log # optional; on disk it's always JSON
events.jsonl
is one self-contained JSON object per line β ready to tail -f
, ship to a SIEM, or
replay with jq
. The Docker commands above mount the named volume honeyprompt-data
at /data
, so events survive container replacement. Both files are appended to and flushed on a clean shutdown.
format
only affects how operational logs are rendered to the console; the operational log file, when enabled, is always structured JSON so it's easy to parse.
An optional, read-only dashboard streams deception events as they happen, breaks them down by protocol, and exports everything to JSON with one click:
panel:
enabled: true
address: "0.0.0.0:8080"
auth: # optional basic auth
username: admin
password: "${HONEYPROMPT_PANEL_PASSWORD}"
The dashboard is plain HTML, CSS, and JavaScript ( src/panel/assets) embedded into the binary. Leave
auth
undefined to disable authentication.Each provider is its own module with its own timeouts, retries, rate limits, and headers. Keys come from the environment. Out of the box:
| Provider | type |
Notes |
|---|---|---|
| Ollama | ollama |
Local models; defaults to localhost:11434 |
| llama.cpp | llamacpp |
Local server OpenAI endpoint |
| OpenAI | openai |
OPENAI_API_KEY |
| Azure OpenAI | azure |
needs azure.deployment + azure.apiVersion |
| OpenRouter | openrouter |
OPENROUTER_API_KEY |
| Anthropic | anthropic |
ANTHROPIC_API_KEY |
| Google Gemini | google |
GEMINI_API_KEY |
| Anything OpenAI-shaped | openai-compatible |
point baseUrl at your gateway |
Configure providers, pick a pool.strategy
(round-robin
, weighted
, random
, or failover
), and honeyprompt spreads traffic across them. If the chosen provider times out or returns a retryable error, honeyprompt transparently falls over to the next one β a dead backend never takes the honeypot offline. Non-retryable errors (a bad API key, say) stop the cascade so you find out instead of silently draining quota.
Services use the global pool unless they name their own provider subset:
llm:
enabled: true
providers: [local-ollama] # one name: force this service to this provider
List several names to keep load balancing and failover, but only within that subset:
llm:
enabled: true
providers: [openai-primary, openrouter-backup]
When several services should share the same provider group β or a subset needs its own strategy instead of the global one β define a named pool. A pool has a name, a strategy, and an ordered provider list, and a service references it by name anywhere it would name a provider:
pools:
- name: cheap-first
strategy: failover # try the local model first, fall back to the paid API
order: [local-ollama, openrouter]
- name: spread
strategy: round-robin
order: [openrouter, openai]
services:
- protocol: ssh
llm:
enabled: true
providers: [cheap-first] # a pool name, in place of a provider
- protocol: http
llm:
enabled: true
providers: [spread]
A pool name must be the only entry in providers
β mixing a pool with individual providers in one list isn't allowed, since it would be ambiguous which strategy wins. Pool names live in the same namespace as provider names and can't collide with them.
When "match a regex" or "ask the model" isn't enough, hooks let you splice your own TypeScript into the request and response path. A hook can rewrite the prompt before it reaches the model, or rewrite the reply before it reaches the attacker.
import { registerHook } from "./src/engine/hooks.ts";
registerHook({
name: "fake-latency-notice",
transformResponse(response, ctx) {
if (ctx.protocol === "ssh" && /rm -rf/.test(ctx.input)) {
return "rm: cannot remove '/': Operation not permitted\n";
}
return response;
},
});
Reference it by name from any service's hooks:
list. A built-in redact-secrets
hook ships enabled in the example config so the model can never echo a real credential back out.
Prometheus metrics are served at /metrics
on the panel (unauthenticated, so scrapers just work):
honeyprompt_events_total{protocol="ssh"} 412
honeyprompt_llm_requests_total{provider="openai",protocol="ssh"} 118
honeyprompt_auth_attempts_total{protocol="ssh"} 87
honeyprompt_engine_errors_total{protocol="http"} 0
Contributing or want a native binary? You'll need Deno 2.x β the only dependency.
deno task check # type-check
deno task lint
deno task fmt
deno task test # unit + integration tests
deno task start -- --config honeyprompt.yaml # run locally
deno task dev -- --config honeyprompt.yaml # run with file watching
deno task compile # -> ./dist/honeyprompt (self-contained binary)
deno compile
bakes the runtime, panel assets, and all into one executable with no dependencies. Prebuilt binaries for Linux, macOS, and Windows are attached to every tagged release.
CI runs formatting, lint, type-check, tests, config validation, a cross-platform compile
, and a
Docker build on every push. Tagging vX.Y.Z
cuts release binaries and publishes the provenance- and SBOM-attested multi-arch image to alectrocute/honeyprompt.
honeyprompt run [--config <path>] start every configured service (default)
honeyprompt validate [--config <path>] parse and validate config, then exit β great for CI
honeyprompt version
honeyprompt help
--config
defaults to ./honeyprompt.yaml
, or $HONEYPROMPT_CONFIG
if set (the container sets it
to /etc/honeyprompt/honeyprompt.yaml
).
This is a tool for luring and studying attackers on infrastructure you own or are authorized to test. Exposing decoy services still means exposing services; run it on isolated hosts, keep it patched, and don't point it at anything you can't afford to have probed. Deception is not a substitute for actually securing the real thing.
If you want to contribute to this project and use an AI agent or heavily lean on generative code, that's totally fineβbut you WILL be personally quizzed on every single line of code you offer and if you don't demonstrate immediate, AI-free understanding, your ENTIRE contribution will be rejected and scrapped.
MIT.