Skip to content
academia.sh

Lesson 13 / 20

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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.

// 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

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close