# Tutorial: MCP honeypot (attacker canary) for beginners

**Audience:** Students and newcomers who already completed the
[first honeypot lab](beginner-student-lab.md) (or equivalent) and want to run
CyberHalluciNet’s **attacker-facing MCP decoy**: a fake Model Context Protocol
server that looks like “platform tools” to AI agents and scanners.

**Time:** about 35-50 minutes  
**Outcome:** You start the MCP canary on loopback, probe it with `curl`, read
auth / engagement telemetry, and (optionally) arm the existing CTI outbox so
high-signal MCP source IPs can reach VirusTotal / OTX via `ctiworker`.

!!! warning "Research only: loopback"
    Keep `--listen-mcp` on `127.0.0.1`. Do **not** publish MCP to the Internet
    for this tutorial. The canary is a **loud** deception signal (easy to
    fingerprint once enabled). See [safety model](../explanation/safety-model.md)
    and [MCP planes](../mcp/planes.md).

!!! danger "Not the operator MCP"
    This tutorial is **`mcpdecoy`** (attacker deception). It is **not**
    `honeypot-mcp` (Ops / management plane). Mixing those planes is a security
    bug: ports must stay separate (`--mcp-forbid-port`, default **3080**).

## What you will learn

1. What an MCP honeypot is (and is not) in CyberHalluciNet
2. How to enable the canary on the **full** sensor (in-process, recommended)
3. How to exercise open vs API-key vs “leaked key” modes
4. How to monitor events (`mcp.probe_observed`, traps, engagement labels)
5. Why scanner noise must not look like an “autonomous campaign”
6. How to arm CTI and send high-signal MCP IPs to VirusTotal / OTX

## Prerequisites

- Repository checked out
- Go toolchain: `cd engine && go build ./cmd/sensor` (and `./cmd/ctiworker` for Step 9)
- `curl` and `jq` (optional but helpful)
- You do **not** need: Ollama, Live ack, or MicroVM
- **Step 9 only:** a VirusTotal API key (and optionally OTX). Keys stay on the
  **ops / worker** side, never bake them into a public decoy image

Use the **full** `sensor` binary: **`sensor-lite` has no MCP decoy**.

## Background (3 minutes)

| Plane | Audience | Binary / package |
|-------|----------|------------------|
| Operator management | You / Ops agents | `honeypot-mcp` |
| Attacker deception | Scanners / AI tool clients | `mcpdecoy` (this tutorial) |
| Edge canary | Cheap bait (future) | Blueprint only |

The decoy:

- speaks HTTP JSON-RPC on `/` and `/mcp` (partial Streamable HTTP),
- lists four static **honeytools** (wiki / secrets / lake / deploy fiction),
- classifies args → HMAC telemetry → optional vault (no shell / DB / LLM),
- labels engagement: `scanner_noise` → `mcp_recon` → `tool_trajectory`.

GreyNoise-style auth profiles:

| Mode | Purpose |
|------|---------|
| `none` | Open canary: background scanning |
| `api_key` | Require `X-API-Key`: credential probing |
| `leaked_key` | Same + bait files that “leak” a synthetic key |

Full reference: [MCP decoy](../reference/mcp-decoy.md).

## Step 1: Build the sensor

From the repository root:

```bash
cd engine
go build -o ../bin/sensor ./cmd/sensor
mkdir -p /tmp/chn-intel-mcp /tmp/chn-spool-mcp
```

## Step 2: Start with MCP open (scanner baseline)

In a dedicated terminal (paths assume repository root):

```bash
./bin/sensor \
  --profile=research \
  --interaction=contained \
  --mode=static \
  --listen-status=127.0.0.1:8080 \
  --listen-ssh=127.0.0.1:12222 \
  --listen-http=127.0.0.1:18081 \
  --listen-mcp=127.0.0.1:3090 \
  --mcp-auth-mode=none \
  --agent-detection=observe \
  --containment-config=config/containment/research.json \
  --intel-dir=/tmp/chn-intel-mcp \
  --telemetry-spool=/tmp/chn-spool-mcp \
  --sensor-id=student-mcp-01
```

Leave it running. You should see a log line like
`mcpdecoy in-process listening on 127.0.0.1:3090 auth=none`.

!!! tip "In-process vs subprocess"
    Default is **in-process** so MCP events reach the sensor telemetry pipeline.
    Only use `--mcpdecoy-subprocess` when you need hard process isolation: that
    mode does **not** feed Observer → OTLP/spool for MCP events.

## Step 3: Confirm status

```bash
curl -sS http://127.0.0.1:8080/healthz
# or richer status if your build exposes it:
curl -sS http://127.0.0.1:8080/status | head
```

## Step 4: Probe like a scanner (GET)

```bash
curl -sS http://127.0.0.1:3090/ | jq .
```

Expect neutral JSON (`status: ok`): **no** product/canary branding.

This should emit **`mcp.probe_observed`** with engagement **`scanner_noise`**.

## Step 5: Talk MCP JSON-RPC

List tools:

```bash
curl -sS http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .
```

Ping and empty resource stubs:

```bash
curl -sS http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"ping"}' | jq .

curl -sS http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | jq .
```

Call a honeytool (benign query):

```bash
curl -sS http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"search_corporate_wiki","arguments":{"query":"onboarding guide"}}}' | jq .
```

Expect a MCP-shaped **`result.content[]`** (text), not a bare handler object.
Engagement should move toward **`tool_trajectory`**. With `--agent-detection=observe`,
scanner-only sessions score as **`internet_scanner_noise`**, not an autonomous campaign.

## Step 6: Auth profile: API key

Stop the sensor (Ctrl+C), restart with:

```bash
./bin/sensor \
  --profile=research \
  --interaction=contained \
  --mode=static \
  --listen-status=127.0.0.1:8080 \
  --listen-ssh=127.0.0.1:12222 \
  --listen-http=127.0.0.1:18081 \
  --listen-mcp=127.0.0.1:3090 \
  --mcp-auth-mode=api_key \
  --mcp-api-key=chn_syn_mcp_labkey \
  --agent-detection=observe \
  --containment-config=config/containment/research.json \
  --intel-dir=/tmp/chn-intel-mcp \
  --telemetry-spool=/tmp/chn-spool-mcp \
  --sensor-id=student-mcp-01
```

Without a key (credential probe):

```bash
curl -sS -o /tmp/mcp-unauth.json -w '%{http_code}\n' http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# expect 401 and mcp.auth_probe in telemetry
```

With the key:

```bash
curl -sS http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: chn_syn_mcp_labkey' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length'
```

## Step 7: Leaked-key bait (optional)

```bash
# same flags as Step 6 but:
#   --mcp-auth-mode=leaked_key --mcp-api-key=chn_syn_mcp_labkey

curl -sS http://127.0.0.1:3090/.env.example
curl -sS http://127.0.0.1:3090/readme.md
```

Bait pages intentionally expose the synthetic key so you can watch whether a
client “follows” it (GreyNoise-style leaked developer instance).

## Step 8: Monitor telemetry

Spool (JSONL / events under your `--telemetry-spool`):

```bash
# adjust glob to your spool layout
find /tmp/chn-spool-mcp -type f 2>/dev/null | head
grep -h 'mcp\.' /tmp/chn-spool-mcp/* 2>/dev/null | tail -n 20
```

Useful event types:

| Event | Meaning |
|-------|---------|
| `mcp.probe_observed` | GET / HTTP probe |
| `mcp.auth_probe` / `mcp.auth_ok` | API-key profile |
| `mcp.trap_observed` / `mcp.tool_invoked` | Honeytool path |
| `mcp.classify_timeout` | Classify budget exceeded |
| `agent.assessment` | Advisory score (needs `--agent-detection`) |
| `agent.campaign_assessment` | Cross-session Strong Links (in-process) |

Attribute **`honeypot.mcp.engagement`**: `scanner_noise` | `mcp_recon` |
`auth_probe` | `tool_trajectory`.

Semantic conventions: [telemetry](../telemetry/semantic-conventions.md).

## Step 9: Send high-signal MCP IPs to VirusTotal / OTX (optional)

MCP does **not** upload tool arguments or vault blobs to VT. When CTI is armed,
the in-process Observer enqueues **reportable public source IPs** into the same
durable outbox used by SSH/SMB. A separate **`ctiworker`** process submits them
to VirusTotal (and optionally OTX).

| Stage | Who | What |
|-------|-----|------|
| Capture | `sensor` + in-process MCP | `IOCIP` job under the CTI outbox |
| Send | `ctiworker` | VT IP comment / OTX indicator (ops plane) |

Full knobs: [How-to: CTI export VT/OTX](../how-to/cti-export-vt-otx.md).

### 9a: What gets enqueued (and what does not)

| Signal | Enqueued? |
|--------|-----------|
| `mcp.auth_probe` / `mcp.auth_ok` | Yes (if peer is a public IP) |
| `get_cluster_secrets` / malicious trap | Yes (if peer is a public IP) |
| GET `/` scanner probe | No |
| Benign `search_corporate_wiki` | No |
| Source `127.0.0.1` / `10.x` / other RFC1918 | **No** (same filter as SSH auth) |

!!! warning "Loopback lab vs real VT"
    Steps 1-8 use `127.0.0.1`, so **no CTI job is written** for those curls.
    That is intentional OPSEC. To actually create an outbox job you need a
    **public client IP** (remote host, research VPS, or the
    [EC2 research tutorial](remote-ec2-ctutorial-basic/README.md)). You can still
    practice arming the worker on this laptop (9b-9d).

### 9b: Build the worker and outbox dirs

```bash
cd engine
go build -o ../bin/ctiworker ./cmd/ctiworker
mkdir -p /tmp/chn-intel-mcp/cti-outbox /tmp/chn-intel-mcp/cti-logs
```

### 9c: Restart the sensor with CTI capture armed

Stop the previous sensor, then start with CTI enabled. Keep MCP **in-process**
(do **not** pass `--mcpdecoy-subprocess`).

```bash
export CYBERHALLUCINET_CTI_ENABLED=1
export CYBERHALLUCINET_CTI_OUTBOX=/tmp/chn-intel-mcp/cti-outbox
export CYBERHALLUCINET_CTI_LOG_DIR=/tmp/chn-intel-mcp/cti-logs
# Lab only: skip the product 6-24h delay so the worker can send soon:
export CYBERHALLUCINET_CTI_IMMEDIATE=1

./bin/sensor \
  --profile=research \
  --interaction=contained \
  --mode=static \
  --listen-status=127.0.0.1:8080 \
  --listen-ssh=127.0.0.1:12222 \
  --listen-http=127.0.0.1:18081 \
  --listen-mcp=127.0.0.1:3090 \
  --mcp-auth-mode=api_key \
  --mcp-api-key=chn_syn_mcp_labkey \
  --agent-detection=observe \
  --containment-config=config/containment/research.json \
  --intel-dir=/tmp/chn-intel-mcp \
  --telemetry-spool=/tmp/chn-spool-mcp \
  --sensor-id=student-mcp-01
```

You should see logs like `cti mcp IP enqueue armed (in-process Observer)`.

Trigger a high-signal event (still loopback: for telemetry only):

```bash
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3090/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 401 → mcp.auth_probe in spool; outbox stays empty from 127.0.0.1
```

From a **public** client against your research host’s MCP port (example):

```bash
curl -sS -o /dev/null -w '%{http_code}\n' "http://${DECOY_PUBLIC_IP}:3090/mcp" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Then inspect the outbox:

```bash
ls -la /tmp/chn-intel-mcp/cti-outbox/jobs/ 2>/dev/null || \
  ls -la "$CYBERHALLUCINET_CTI_OUTBOX"/jobs/
# Expect jobs with kind "ip" and meta.service "MCP"
```

### 9d: Run `ctiworker` toward VirusTotal (ops plane)

In a **second** terminal (keys here, not on a public decoy image):

```bash
export CYBERHALLUCINET_CTI_OUTBOX=/tmp/chn-intel-mcp/cti-outbox
export CYBERHALLUCINET_CTI_LOG_DIR=/tmp/chn-intel-mcp/cti-logs
export CYBERHALLUCINET_VT_API_KEY='YOUR_VT_API_KEY'
# Optional:
# export CYBERHALLUCINET_OTX_API_KEY='YOUR_OTX_API_KEY'

./bin/ctiworker \
  --outbox="$CYBERHALLUCINET_CTI_OUTBOX" \
  --log-dir="$CYBERHALLUCINET_CTI_LOG_DIR" \
  --interval=5m
```

Worker behavior:

- Reads durable jobs from the outbox (sensor already wrote them).
- Default product delay is **6-24h**; this lab used `CYBERHALLUCINET_CTI_IMMEDIATE=1` on the sensor so `send_after` is near now.
- Submits at most once per `--interval` window (minimum 5m).
- On VT **429**, a quota halt is persisted (see the CTI how-to).

Check worker logs:

```bash
grep -E '\[cti\.(vt|otx)\]|mcp|IOCIP|kind=ip' /tmp/chn-intel-mcp/cti-logs/* 2>/dev/null | tail -n 30
```

### 9e: OPSEC reminders

- Prefer the default delay in real deployments; `CTI_IMMEDIATE=1` is for lab burn-in only.
- Keep VT/OTX keys on the **private worker**, not on Internet-facing sensors.
- MCP IP comments can fingerprint a decoy if published while the attacker is still online.
- Subprocess MCP (`--mcpdecoy-subprocess`) does **not** feed this CTI path: stay in-process.

## Production wiring checklist (honest)

| Feature | Live on full sensor? | How |
|---------|----------------------|-----|
| MCP listener | **Opt-in** | `--listen-mcp=127.0.0.1:3090` (off by default) |
| Auth profiles | **Yes (in-process)** | `--mcp-auth-mode` / `--mcp-api-key` |
| Engagement → telemetry | **Yes (in-process)** | automatic when MCP runs in-process |
| Engagement → agent score | **If** agent detection on | `--agent-detection=observe\|probe` |
| Protocol `content[]` / ping / stubs | **Yes** | whenever Handler runs |
| Subprocess MCP → sensor OTLP | **No** | use default in-process |
| `sensor-lite` | **No MCP** | use full `sensor` |
| CTI → VT/OTX (MCP IPs) | **Yes when CTI armed** | `CYBERHALLUCINET_CTI_ENABLED=1` + in-process MCP + `ctiworker` (Step 9) |

## Troubleshooting

| Symptom | Check |
|---------|--------|
| No MCP listen log | `--listen-mcp` empty? Using `sensor-lite`? |
| 401 on every POST | `--mcp-auth-mode` requires `X-API-Key` / Bearer |
| No `mcp.*` in spool | Subprocess mode? Prefer in-process |
| Everything looks “agentic” | Turn on `--agent-detection=observe` so scanner baseline can apply |
| Port refused / fail-closed | Collision with management port 3080? Exposure gate on non-loopback? |
| CTI outbox empty after curl | Peer is loopback/private? Need a public client IP (Step 9a) |
| No VT traffic | `ctiworker` running? `CYBERHALLUCINET_VT_API_KEY` set? Job still in delay window? |
| `cti mcp IP enqueue` log missing | `CYBERHALLUCINET_CTI_ENABLED` not `1`? |

## Next steps

- Reference: [MCP decoy](../reference/mcp-decoy.md) · [MCP planes](../mcp/planes.md)
- Agent detection: [beginner lab](beginner-student-lab.md) with `--agent-detection`
- CTI deep dive: [VT/OTX how-to](../how-to/cti-export-vt-otx.md)
- Remote public IP lab: [EC2 research tutorial](remote-ec2-ctutorial-basic/README.md)
- Optional vault: [forensic vault spooler](../ops/forensic-vault-spooler.md)
