---
title: 'Rogue Access Points'
source: 'https://academia.sh/en/courses/network-security/rogue-access-points'
course: 'Network Security'
language: en
updated: '2026-09-26T03:02:54+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates the signals a client uses when choosing a network by source and counts how many carry proof'
    - 'Classifies the paths registered client profiles open to an evil twin as open, partial, or closed'
    - 'Measures cumulatively how many paths mutual authentication and profile cleanup close'
    - 'Calculates the cost of each defense in terms of devices touched, manual approvals, and devices left out'
---

# Rogue Access Points

Six of the eight signals a client looks at when choosing a network are seen before the connection is made, and none of them carry proof. Across eight registered profiles, four paths to the evil twin are open and three are partially open; trust-root pinning, deleting unused profiles, and turning off automatic connection bring the open path to zero, while the two partial paths close only with a generation mandate that leaves four thousand two hundred ninety-two devices out.

The previous lesson found the claim enterprise mode adds and left one row open in the table:
whether the client verifies who the other party is. The same row had a personal-mode
counterpart as well, and in the first lesson's table the network-name claim was unproven
across all four generations.

This gap is where the **evil twin** lives. Setting up a rogue access point means
running a device that announces the name the organization uses and appears with a stronger
signal. The lesson does not measure how this technique is set up, but **what the client looks
at to decide** and how many paths mutual authentication closes.

- **WS13.** A signal is data the client uses when choosing a network. It can have three
  sources: the other party's own announcement, the client's own measurement, or signed proof
  that can be tied to a trust root.
- **WS14.** A path is a registered client profile matching an evil twin. It has three
  outcomes: **open** (the client connects or presents its credentials), **partial** (the
  connection does not complete but handshake messages leak out), **closed** (the client rejects
  the other party).
- **WS15.** The evil twin announces the network's name, channel, and security
  indicator as they are, and appears with a stronger signal. It does not know the shared secret
  or the identity of the organization's authentication server.
- **WS16.** The profile list is fictional; how many devices each profile is registered on is a
  model value. The same device can carry more than one profile, so the total of
  profile-carrying devices exceeds the device count.
- **WS17.** Manual configuration per device is four minutes, and manual approval on an open
  network is twice daily; both are model constants.

## What the Client Looks At

When a device chooses a network, what it has at hand can be counted item by item.

```js
// twin.mjs — the signals a client looks at when choosing a network, and its registered profiles (model).
// Fictional regional measurement network; real network name, real location, and real hardware address are not written.
// [signal, source, can the client verify it]
export const SIGNAL = [
  ['network name', 'the other party\'s own announcement', false],
  ['signal strength', 'the client\'s own measurement', false],
  ['channel', 'the other party\'s own announcement', false],
  ['hardware address', 'unencrypted header field', false],
  ['security indicator', 'the other party\'s own announcement', false],
  ['capability announcements', 'the other party\'s own announcement', false],
  ['identity presented by the authentication server', 'signed, can be tied to a trust root', true],
  ['verification derived from the shared secret', 'a secret value both parties know', true],
];
// Registered client profiles: [profile, mode, is server identity verified, automatic
//  connection, device count carrying this profile]
export const PROFILE = [
  ['branch-network', 'enterprise', true, true, 210],
  ['field-network', 'enterprise', false, true, 183],
  ['aggregator-network', 'personal', false, true, 4100],
  ['maintenance-network', 'personal', false, true, 9],
  ['guest-network', 'open', false, true, 222],
  ['old-field-network', 'personal', false, true, 183],
  ['coworking-space', 'open', false, true, 219],
  ['conference-hall', 'open', false, true, 47],
];
// The evil twin announces the same name, the same channel, and a stronger signal. Outcome:
// 'open' = the client connects or presents its credentials, 'partial' = the connection does not
// complete but handshake messages leak out, 'closed' = the client rejects the other party.
export function outcome([name, mode, verifies], d) {
  if (d.autoOff.has(name)) return 'closed';        // does not connect without user approval
  if (mode === 'open') return 'open';
  if (mode === 'enterprise') return (verifies || d.fixed) ? 'closed' : 'open';
  return d.gen4 ? 'closed' : 'partial';            // personal mode: shared secret verifies mutually
}
```

```js
// run1.mjs — what does the client look at to accept a network's identity
import { SIGNAL, PROFILE } from './twin.mjs';

const E = [52, 40, 12];
const row = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](E[i])).join('');
console.log(row(['signal', 'source', 'is proof']));
for (const [name, source, k] of SIGNAL) console.log(row([name, source, k ? 'yes' : 'no']));
const proof = SIGNAL.filter(([, , k]) => k).length;
console.log(`\nsignal seen before connecting ${SIGNAL.length - proof}, with proof 0`);
console.log(`signal carrying proof ${proof}; both appear only once the connection begins`);
const A = [22, 12, 22, 14, 10];
const row2 = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](A[i])).join('');
console.log('\n' + row2(['profile', 'mode', 'server verified', 'automatic', 'device']));
for (const [name, mode, d, o, n] of PROFILE)
  console.log(row2([name, mode, d ? 'yes' : 'no', o ? 'yes' : 'no', n]));
console.log(`\nprofile ${PROFILE.length}, device carrying a profile ` +
  `${PROFILE.reduce((s, p) => s + p[4], 0)}`);
```

```
signal                                                                                source    is proof
network name                                              the other party's own announcement          no
signal strength                                                 the client's own measurement          no
channel                                                   the other party's own announcement          no
hardware address                                                    unencrypted header field          no
security indicator                                        the other party's own announcement          no
capability announcements                                  the other party's own announcement          no
identity presented by the authentication server          signed, can be tied to a trust root         yes
verification derived from the shared secret                 a secret value both parties know         yes

signal seen before connecting 6, with proof 0
signal carrying proof 2; both appear only once the connection begins

profile                       mode       server verified     automatic    device
branch-network          enterprise                   yes           yes       210
field-network           enterprise                    no           yes       183
aggregator-network        personal                    no           yes      4100
maintenance-network       personal                    no           yes         9
guest-network                 open                    no           yes       222
old-field-network         personal                    no           yes       183
coworking-space               open                    no           yes       219
conference-hall               open                    no           yes        47

profile 8, device carrying a profile 5173
```

The second column of the first table is the whole point of this lesson. Of the six signals
the client sees **before** connecting, four are the other party's own announcement about
itself, one is the client's own measurement, and one is an unencrypted header field. None of
them carry proof.

Signal strength occupies a special place, because it is the only "own measurement" row, and
that does not make it trustworthy. The client generally chooses **the strongest signal** among
candidates announcing the same name. This is not a flaw but a design decision — it is required
so that a moving device can hand off between access points. But as a selection criterion,
signal strength is a value the other party can raise with a single hardware setting; being its
own measurement does not make it proof.

There are two signals that carry proof, and both appear only **after** the connection is made.
Verification derived from the shared secret comes into play in personal mode, and signed
identity in enterprise mode. There is no proof available at the moment of selection; the
selection is always made with unproven signals, and proof is only requested after the client
starts talking with the other party.

The second table shows this gap's counterpart inside the organization. There are eight
registered profiles, and **automatic connection is on for all of them**. Two are not the
organization's networks: the coworking-space and conference-hall profiles are open networks
employees connected to outside, and they have stayed in the device's profile list. One more is
an organization network that is no longer in use.

The figure of five thousand one hundred seventy-three in the last line is not a device count
but a **profile-carrying device** count: a branch laptop keeps the branch network, the guest
network, and two external networks registered at the same time. The unit of measure is
therefore not the device but the profile. How many paths a device is open to is determined not
by how many networks it is connected to, but by how many networks it has ever connected to; the
profile list is the one item that keeps growing until it is deleted.

## The Path Mutual Authentication Closes

Each of the eight profiles is a path. The run below stacks defenses on top of each other and
prints the count of open, partial, and closed paths at every step.

```js
// run2.mjs — how many paths does mutual authentication close, how many stay open, at what cost
import { PROFILE, outcome } from './twin.mjs';

const EMPTY = new Set();
const UNUSED = new Set(['old-field-network', 'conference-hall']);
const OPEN_NETWORKS = new Set(PROFILE.filter((p) => p[1] === 'open').map((p) => p[0]));
const ALL = new Set([...UNUSED, ...OPEN_NETWORKS]);
const DEFENSE = [
  ['baseline (none)', { fixed: false, gen4: false, autoOff: EMPTY }],
  ['trust root is pinned', { fixed: true, gen4: false, autoOff: EMPTY }],
  ['unused profile is deleted', { fixed: true, gen4: false,
    autoOff: UNUSED }],
  ['automatic connection off on open networks', { fixed: true, gen4: false,
    autoOff: ALL }],
  ['simultaneous authentication generation', { fixed: true, gen4: true,
    autoOff: ALL }],
];
const E = [45, 7, 9, 9, 13, 16];
const row = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](E[i])).join('');
console.log(row(['defense (stacked)', 'open', 'partial', 'closed', 'open device',
  'partial device']));
const results = DEFENSE.map(([name, d]) => {
  const r = PROFILE.map((p) => [p, outcome(p, d)]);
  const n = (t) => r.filter(([, s]) => s === t).length;
  const a = (t) => r.filter(([, s]) => s === t).reduce((s, [p]) => s + p[4], 0);
  console.log(row([name, n('open'), n('partial'), n('closed'), a('open'), a('partial')]));
  return r;
});
const P = [22, 10, 10, 10];
const row2 = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](P[i])).join('');
console.log('\n' + row2(['profile', 'baseline', 'pinning', 'all']));
PROFILE.forEach((p, i) => console.log(row2([p[0], results[0][i][1], results[1][i][1],
  results.at(-1)[i][1]])));

// Cost: the devices each defense touches and the legitimate work it interrupts (model constants).
const MANUAL = 4, APPROVAL = 2;             // minutes per device, daily manual approvals
const enterprise = PROFILE.filter((p) => p[1] === 'enterprise').reduce((s, p) => s + p[4], 0);
const deleted = PROFILE.filter((p) => UNUSED.has(p[0])).reduce((s, p) => s + p[4], 0);
const openDevice = PROFILE.filter((p) => OPEN_NETWORKS.has(p[0])).reduce((s, p) => s + p[4], 0);
const personal = PROFILE.filter((p) => p[1] === 'personal').reduce((s, p) => s + p[4], 0);
console.log(`\ncost:`);
console.log(`  trust root pinning     ${enterprise} devices with profiles, ${enterprise * MANUAL} min`);
console.log(`  profile deletion       ${deleted} devices touched, ${deleted * MANUAL} min`);
console.log(`  automatic connection   ${openDevice} devices, ${openDevice * APPROVAL} manual approvals per day`);
console.log(`  top-generation mandate ${personal} devices left out`);
```

```
defense (stacked)                               open  partial   closed  open device  partial device
baseline (none)                                    4        3        1          671            4292
trust root is pinned                               3        3        2          488            4292
unused profile is deleted                          2        2        4          441            4109
automatic connection off on open networks          0        2        6            0            4109
simultaneous authentication generation             0        0        8            0               0

profile                 baseline   pinning       all
branch-network            closed    closed    closed
field-network               open    closed    closed
aggregator-network       partial   partial    closed
maintenance-network      partial   partial    closed
guest-network               open      open    closed
old-field-network        partial   partial    closed
coworking-space             open      open    closed
conference-hall             open      open    closed

cost:
  trust root pinning     393 devices with profiles, 1572 min
  profile deletion       230 devices touched, 920 min
  automatic connection   488 devices, 976 manual approvals per day
  top-generation mandate 4292 devices left out
```

At baseline, only **one** of the eight paths is closed: the enterprise profile with a pinned
trust root. Four paths are open, and three are partially open. This is the opposite of the
answer one would give without reading the table — most of the organization's networks are
encrypted, but encryption does not close the path to an evil twin.

The partial result shows exactly what personal mode does. Because the shared secret is known
by both parties, an access point that does not know the secret cannot complete the connection;
the client rejects it. But the client only realizes this after it has **started** the
handshake, and those messages have already gone out. For the three generations that lesson
one's table found no proof of offline attempts against, this means a recorded handshake exists
even if the connection is never established. Personal mode closes the path, not the
trace.

The pinning row closes one path: the enterprise profile that does not verify. The affected
device count is one hundred eighty-three, and this is the counterpart of the one row the
second lesson left open. A profile that has moved to enterprise mode but does not verify server
identity is **worse off** than personal mode: where personal mode's result is partial, here it
is open, because the client presents its identity to the other party.

Deleting unused profiles closes two more paths, and this has nothing to do with defense
technology. The old-field-network and conference-hall profiles kept the names of networks that
no longer exist in the client; the name of a nonexistent network means the network belongs to
whoever announces that name. These two rows are the **cheapest** defense in the whole table:
nine hundred twenty minutes of cleanup, no new infrastructure item, and no legitimate
connection is cut, because there is no legitimate connection left to cut. The path it closes
being cheap makes it the most frequently skipped defense — an item invisible in inventory does
not make it onto the maintenance list either.

## The Path That Drops to Zero, and the Cost Paid

The fourth row drops the open path to zero. Turning off automatic connection on open networks
affects four hundred eighty-eight devices and adds nine hundred seventy-six manual approvals
per day. What closes the path is not verification but shifting the decision to the user; this
is the row where the defense's cost is most visible, because the cost is paid every day.

The two partial paths close only on the last row, at a cost of four thousand two hundred
ninety-two devices. The first lesson's inventory table says where this number comes from:
nearly all of the devices remaining in personal mode do not support the top generation. The
last row is therefore measurable but not selectable; the two partial paths stay
open in practice.

The total cost adds up across four items: a one-time configuration of two thousand four hundred
ninety-two minutes, nine hundred seventy-six manual approvals per day, and a renewal affecting
four thousand two hundred ninety-two devices. The first three defenses are one-time and cheap;
the fourth is continuous; the fifth is a budget decision.

There is something the model does not see. The table counts the client matching a
**registered profile**; it does not count connecting to an unregistered name at all. When a
user manually selects a network, the pinned trust root still works, but when they select an
open network, no defense is active. The eight paths dropping to zero means the registered
profiles on this network drop to zero; it does not mean the paths the client could choose drop
to zero.

## Summary

- **None** of the six signals the client sees before connecting carry proof: four are the
  other party's announcement, one is its own measurement, one is an unencrypted header field.
  The two signals that do carry proof appear only once the connection is being made.
- Of the eight registered profiles, only one is closed at baseline; four are open, three are
  partially open. The encryption generation alone does not close the path to an evil
  twin.
- Personal mode does not let the connection complete, but handshake messages leak out; a
  profile that has moved to enterprise mode but does not verify server identity is worse off
  than personal mode, because it presents its identity to the other party.
- Trust-root pinning, deleting unused profiles, and turning off automatic connection on open
  networks bring the open path from 4 to 0; the cost is 2,492 minutes of configuration and 976
  manual approvals per day.
- The two partial paths close only when the top generation is made mandatory, at a cost of
  4,292 devices; it is therefore measurable but not a practical option.

## Next Step

This lesson's whole measurement rested on one assumption: that the client establishes its
connection **on its own**. Once six of the eight profiles are closed, the client stays on the
network it is already connected to and never sees the rogue candidate. But a client staying
connected also requires the messages that keep that connection alive to be verified. Data
frames are encrypted and carry integrity proof; the frames that establish, sustain, and
**terminate** the connection are a separate class. This is the next lesson's question: which
frame type in a wireless connection carries integrity proof, which does not, how many frame
types gain proof once protection is turned on, and in which defense counter the deviation shows
up.
