---
title: 'Honeypots and Sinkholes'
source: 'https://academia.sh/en/courses/network-security/honeypots-and-sinkholes'
course: 'Network Security'
language: en
updated: '2026-09-26T03:02:54+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures the signal decoy items produce as interaction count and false-positive rate'
    - 'Distinguishes, within the same event set, the classes a decoy-based arrangement covers from the ones it never sees'
    - 'Evaluates the sinkhole prefix by prefix and counts the legitimate connections cut per bad connection dropped'
---

# Honeypots and Sinkholes

Four decoy items that legitimate work never visits are run over the same period: how low the interaction false-positive rate stays next to signature-based detection, how many bad event classes never appear under this arrangement, and how many legitimate connections the sinkhole cuts per connection it drops.

All of the previous lesson's noise came from one place: signatures looked at the same space where both
legitimate work and bad events passed. A backup transfer and a data exfiltration crossed the same volume
threshold; a patch rollout and a new privileged process produced the same symptom. The way to lower the
false-positive rate is not to write a narrower signature — it is to look at a place legitimate work never
visits.

A decoy item is exactly this: an endpoint, a record, or a credential that no legitimate flow should touch
during the period. When it is touched, the signal carries no ambiguity, because there is no alternative to
ask "was this perhaps legitimate." These items are deployed on one's own network and observation stays
within one's own boundary; placing a trap on someone else's network or collecting data from it, without
written authorization and a defined scope, is both illegal and unprofessional.

- **PD24.** The four decoy items, the interaction counts, and the six prefix classes belong to a fictional
  regional measurement network. No real deployment is being made; what is measured is the model built with
  `node`.
- **PD25.** The signature-based counts being compared were read from the previous lesson's two-sensor run;
  it is the same period and the same event set.
- **PD26.** The first-signal day was hand-written. In reality this day depends on where the decoy item is
  placed and the order of discovery; it carries an order, not a count.
- **PD27.** Prefix classes are referred to by label; no real address block was written. The split between
  bad and legitimate connections was marked by hand.
- **PD28.** The sinkhole was modeled as a full cut: every connection to and from the prefix is dropped.
  Partial cutting and rate limiting were not modeled.

## Decoy Items and Coverage

The model holds four decoy items. Two of them are endpoints: a management interface with no business in
the management zone, and a data endpoint that no application calls in the data zone. The third is a
record — a row that no legitimate query returns; its appearance in an outbound flow is a signal by itself.
The fourth is a credential used nowhere.

```js
// decoy.mjs — decoy items, sources of interaction, and the prefix set; a model, not a real deployment
// [item, zone it sits in, bad interactions, legit interactions, source of legit interaction, first signal day]
export const DECOY = [
  ['honeypot-management', 'management', 412, 6, 'monthly inventory scan', 2],
  ['honeypot-data', 'data', 54, 1, 'backup verification script', 9],
  ['honey-record', 'data', 3, 0, '-', 17],
  ['honey-credential', 'management', 118, 0, '-', 2],
];
// Which of the previous lesson's seven bad event classes touches which decoy item
export const TOUCHED = {
  'heavy attempts against the management gate': ['honeypot-management', 'honey-credential'],
  'outbound to an unknown server name': [],
  'new privileged process on host': [],
  'small outbound at regular intervals': ['honey-record'],
  'local file permission change': [],
  'known payload inside encrypted flow': [],
  'scan-like sequence in the inside zone': ['honeypot-management', 'honeypot-data'],
};
// Prefix classes that can be placed in the sinkhole: [label, bad connections, legit connections, note]
export const PREFIX = [
  ['prefix-A', 8400, 0, ''], ['prefix-B', 2100, 0, ''],
  ['prefix-C', 640, 12, 'vendor in the same block'], ['prefix-D', 190, 0, ''],
  ['prefix-E', 40, 5100, "field endpoints' cellular egress in the same block"], ['prefix-F', 12000, 0, ''],
];
export const SINKHOLE = {
  none: [], clean: ['prefix-A', 'prefix-B', 'prefix-D', 'prefix-F'],
  'clean+C': ['prefix-A', 'prefix-B', 'prefix-C', 'prefix-D', 'prefix-F'],
  all: PREFIX.map((o) => o[0]),
};
export const sum = (l, i) => l.reduce((a, o) => a + o[i], 0);
export const rate = (k, m) => (k + m === 0 ? '-' : `${(m * 100 / (k + m)).toFixed(1)}%`);
// The previous lesson's signature-based counts (source: same period, both sensors combined)
export const SIGNATURE_COUNT = { alert: 17497, bad: 12283, legit: 5214, missed: 2880, class: 6 };
```

The model's second part takes the previous lesson's seven bad event classes and writes down which decoy
item each one touches. An empty list means that class never appears under this arrangement; the coverage
measure comes from here. The third part belongs to the sinkhole and works on a separate logic — what is
measured there is not a signal but a dropped connection.

```js
// run1.mjs — the decoy items' false-positive rate and the event classes left outside their coverage
import { DECOY, TOUCHED, SIGNATURE_COUNT, sum, rate } from './decoy.mjs';

console.log(`${'decoy item'.padEnd(19)} ${'zone'.padEnd(11)} ${'bad interactions'.padStart(17)} ${'legit'.padStart(6)} ` +
  `${'false positive'.padStart(15)} ${'first signal'.padStart(13)}  source of legitimate interaction`);
for (const a of DECOY)
  console.log(`${a[0].padEnd(19)} ${a[1].padEnd(11)} ${String(a[2]).padStart(17)} ${String(a[3]).padStart(6)} ` +
    `${rate(a[2], a[3]).padStart(15)} ${`day ${a[5]}`.padStart(13)}  ${a[4]}`);
const [k, m] = [sum(DECOY, 2), sum(DECOY, 3)];
console.log(`\n${'measure'.padEnd(28)} ${'signature-based'.padStart(15)} ${'decoy-based'.padStart(15)}`);
for (const [name, i, a] of [
  ['period alerts', SIGNATURE_COUNT.alert, k + m],
  ['false positives', SIGNATURE_COUNT.legit, m],
  ['false-positive rate', rate(SIGNATURE_COUNT.bad, SIGNATURE_COUNT.legit), rate(k, m)],
  ['covered bad event classes', `${SIGNATURE_COUNT.class}/7`, `${Object.values(TOUCHED).filter((d) => d.length).length}/7`],
]) console.log(`${name.padEnd(28)} ${String(i).padStart(15)} ${String(a).padStart(15)}`);
console.log('\nevent class -> decoy item touched:');
for (const [s, d] of Object.entries(TOUCHED)) console.log(`  ${s.padEnd(43)} ${d.join(', ') || '- (never seen)'}`);
```

```
decoy item          zone         bad interactions  legit  false positive  first signal  source of legitimate interaction
honeypot-management management                412      6            1.4%         day 2  monthly inventory scan
honeypot-data       data                       54      1            1.8%         day 9  backup verification script
honey-record        data                        3      0            0.0%        day 17  -
honey-credential    management                118      0            0.0%         day 2  -

measure                      signature-based     decoy-based
period alerts                          17497             594
false positives                         5214               7
false-positive rate                    29.8%            1.2%
covered bad event classes                6/7             3/7

event class -> decoy item touched:
  heavy attempts against the management gate  honeypot-management, honey-credential
  outbound to an unknown server name          - (never seen)
  new privileged process on host              - (never seen)
  small outbound at regular intervals         honey-record
  local file permission change                - (never seen)
  known payload inside encrypted flow         - (never seen)
  scan-like sequence in the inside zone       honeypot-management, honeypot-data
```

The second table is the core of this lesson. Over the same period, the signature-based arrangement
produced seventeen thousand alerts, the decoy-based arrangement five hundred ninety-four. False positives
fell from five thousand two hundred fourteen to seven; the rate fell from thirty percent to one percent.
There is a reason for this, and it is not that the signatures were poorly written. A signature looks for a
pattern in a space that legitimate traffic also crosses; a decoy item is placed where legitimate traffic
never crosses at all. The false-positive rate comes out low because **the denominator is empty by
definition**.

The source of the remaining seven false positives confirms this: both come from the organization's own
scripts. The monthly inventory scan walks every address, and the backup verification script polls every
endpoint. Since both are known by name, they can be written into an exception list and the false-positive
count brought to zero; the cost is keeping that list maintained as an item of its own. As the exception
list grows, the decoy item's signal gets diluted.

The last row shows where the real cost sits. The signature-based arrangement covered six of the seven bad
classes, the decoy-based arrangement three. Four classes never appear, and all of them are of the same
kind: events that do not require touching a decoy item. A process opened on a host or a file permission
that changes never touches any trap set up on the network. Decoy items do not replace a detection layer;
where that layer is at its noisiest, they give a very clean signal, and where it is quietest, they give
nothing at all.

The interaction counts in the first table also say something on their own. The load the four items carry
is very different from one another: the fake interface in the management zone sees four hundred twelve
interactions, while the record in the data zone sees three. Both give a signal with the same certainty and
require the same response. A decoy item's value is not measured by the volume it sees, because the volume
itself carries no information — one interaction and four hundred interactions reach the same conclusion.
The only thing that can be measured is **where** the item was placed: the signal says that someone has
reached the zone the item sits in.

There is one row in the middle of the intersection: small outbound traffic at regular intervals. Of the
previous lesson's seven classes, this was the only one that slipped past both sensors at once — it was
visible, but no signature matched it. The honey-record catches this class, and it catches it with only
three interactions in the period. That the signal arrives on day seventeen is also meaningful: a decoy
item is not a measure of speed, it is a measure of presence.

One cost of decoy items does not appear in the table either: keeping the item convincing. If an endpoint
disguised as a management interface can be told apart from the genuine items around it, no one touches it
and the signal never arrives. Staying indistinguishable requires that it also be updated, follow the
naming convention, and have a matching entry in inventory. Maintaining four items is a separate task from
maintaining the two hundred ten protected items, and when it is forgotten, the item silently becomes
non-functional — and because it produces no false positives, no one notices either. The silence of an
unmeasured defense is not proof that it is working.

## The Sinkhole

```js
// run2.mjs — the sinkhole: bad connections dropped, legit connections cut, and bad connections remaining
import { PREFIX, SINKHOLE, sum } from './decoy.mjs';

const [TK, TM] = [sum(PREFIX, 1), sum(PREFIX, 2)];
console.log(`six prefix classes: ${TK} bad, ${TM} legit connections\n`);
console.log(`${'prefix'.padEnd(8)} ${'bad'.padStart(6)} ${'legit'.padStart(6)} ${'legit cut if sinkholed'.padStart(29)}  note`);
for (const o of PREFIX)
  console.log(`${o[0].padEnd(8)} ${String(o[1]).padStart(6)} ${String(o[2]).padStart(6)} ${String(o[2]).padStart(29)}  ${o[3]}`.trimEnd());
console.log(`\n${'sinkhole list'.padEnd(14)} ${'pfx'.padStart(4)} ${'bad dropped'.padStart(11)} ${'bad remaining'.padStart(13)} ` +
  `${'legit cut'.padStart(13)} ${'legit cut per bad'.padStart(25)}`);
for (const [name, list] of Object.entries(SINKHOLE)) {
  const inn = PREFIX.filter((o) => list.includes(o[0]));
  const [dk, km] = [sum(inn, 1), sum(inn, 2)];
  console.log(`${name.padEnd(14)} ${String(list.length).padStart(4)} ${String(dk).padStart(11)} ${String(TK - dk).padStart(13)} ` +
    `${String(km).padStart(13)} ${(dk ? (km / dk).toFixed(4) : '-').padStart(25)}`);
}
const e = PREFIX.find((o) => o[0] === 'prefix-E'), c = PREFIX.find((o) => o[0] === 'prefix-C');
console.log(`\ncost of the last two prefixes, one by one:`);
for (const o of [c, e])
  console.log(`  adding ${o[0]}  bad dropped +${o[1]}, legit cut +${o[2]}  (${o[3]})`);
console.log(`\ntraffic that falls into the sinkhole never reaches its destination, so it produces no sensor record;`);
console.log(`${sum(PREFIX.filter((o) => SINKHOLE.clean.includes(o[0])), 1)} connections drop silently under the clean list`);
```

```
six prefix classes: 23370 bad, 5112 legit connections

prefix      bad  legit        legit cut if sinkholed  note
prefix-A   8400      0                             0
prefix-B   2100      0                             0
prefix-C    640     12                            12  vendor in the same block
prefix-D    190      0                             0
prefix-E     40   5100                          5100  field endpoints' cellular egress in the same block
prefix-F  12000      0                             0

sinkhole list   pfx bad dropped bad remaining     legit cut         legit cut per bad
none              0           0         23370             0                         -
clean             4       22690           680             0                    0.0000
clean+C           5       23330            40            12                    0.0005
all               6       23370             0          5112                    0.2187

cost of the last two prefixes, one by one:
  adding prefix-C  bad dropped +640, legit cut +12  (vendor in the same block)
  adding prefix-E  bad dropped +40, legit cut +5100  (field endpoints' cellular egress in the same block)

traffic that falls into the sinkhole never reaches its destination, so it produces no sensor record;
22690 connections drop silently under the clean list
```

A sinkhole is an item that works in the reverse direction: a decoy item is placed to produce a signal, a
sinkhole to never see the traffic at all. The four clean prefixes drop twenty-two thousand six hundred
ninety bad connections and cut not a single legitimate connection. The remaining six hundred eighty bad
connections come from two prefixes, and both are mixed in with legitimate traffic.

The last column gives the price of this mixing. Adding the fifth prefix cuts five legitimate connections
per ten thousand bad connections — practically free. Adding the sixth pushes that same rate to one in
five: dropping forty bad connections costs five thousand one hundred legitimate connections. Both moves
add one row each to the list, and the rows look alike; the only thing that tells them apart is who stands
behind the prefix. The length of a block list is therefore not a measure; the measure is the legitimate
connections cut per bad connection dropped.

The list also has a time dimension that does not appear in the table. A prefix enters the list because it
carried bad traffic during the period; in a later period the party behind it may change, and the row stays
where it is. The reason for adding a row is recorded, but the reason for removing one never arises
anywhere — because traffic that falls into the sinkhole produces no record at all, so it is equally
invisible that the prefix now carries legitimate traffic. In this respect a sinkhole has the same habit as
a firewall rule set: it grows in one direction only.

The last two lines give the sinkhole's own blind spot. Traffic that falls into the sinkhole never reaches
its destination, so it produces no sensor record at all: twenty-two thousand six hundred ninety
connections vanish silently. The previous lesson's alert queue gets lighter by exactly that much, but the
same move also makes invisible what that traffic was looking for and what it was trying. Because a
sinkhole and a honeypot can be deployed together in the same period, the two items complement each other:
one drops noise without ever producing it, the other turns a single interaction into a signal at the exact
place where there is no noise at all.

This lesson's two items say one more thing together. A decoy item is a defense with narrow coverage and a
clean signal; a sinkhole is a defense with broad coverage and no signal at all. The previous lesson's
signature-based arrangement sits between the two: broad coverage, a noisy signal. When all three run
together over the same period, their measurement columns do not add up, because each one sees the same
event at a different place. The only thing that can be measured is each one's own column — the classes
covered, the false positives produced, and the legitimate work cut — and when a defense is said to improve
only one of these three, it means no one has asked where the other two went.

## Summary

- Over the same period, the signature-based arrangement produces 17,497 alerts and a 29.8% false-positive
  rate; the decoy-based arrangement produces 594 alerts and 1.2%. The rate is low because legitimate work
  never visits a decoy item, by definition.
- Both sources of the remaining 7 false positives are the organization's own scripts. They can be zeroed
  out with an exception list; the cost is keeping that list maintained, and it dilutes the signal as it
  grows.
- Coverage is the cost: the signature-based arrangement covers 6 of 7 bad classes, the decoy-based
  arrangement covers 3. The 4 classes that never appear are of the same kind — events that require
  touching no trap at all.
- The honey-record catches, with 3 interactions, the one class that both sensors in the previous lesson
  missed. The signal arrives on day 17: a decoy item is a measure of presence, not of speed.
- In the sinkhole, 4 clean prefixes drop 22,690 bad connections at zero legitimate cost. The fifth prefix
  costs 0.0005 legitimate connections per bad connection, the sixth costs 0.2187. The sinkhole's own cost
  is blindness: the 22,690 connections it drops produce no sensor record at all.

## Next Step

Every defense counted so far either split traffic into zones, routed it through one point, recorded it, or
dropped it. Their shared assumption was that the connecting party stood **outside** the network. A tunnel
removes that assumption: a remote endpoint, once verified, receives an address inside the network and
becomes a node in the zone graph. The next lesson's question is not whether the tunnel gets established,
but how many items become reachable once it is — how many items under a full tunnel, how many stay inside
and how many outside under a split tunnel, and how many connections a single verification is enough to
count as proven.
