Physical Security & Access Control
Definition
Section titled “Definition”Physical security and access control is the discipline of governing who and what occupies space: the entry layers (perimeter, building, floor, room, rack, port), the credential systems (badges — WI9’s RFID/NFC class, PINs, biometrics, mobile credentials), the detection layer (door alarms, tailgating sensors, CCTV analytics, environmental monitoring), and the process layer (visitor management, delivery handling, clean desk, decommissioning) where most real failures live. The doctrine that separates modern physical security from guard-and-gate: physical is an identity problem — every door is an authorization decision, every badge is a credential with a lifecycle, and every tailgated entry is a session hijack that bypassed authentication — so the same disciplines this handbook applies to digital identity (least privilege, lifecycle, ceremony, audit) apply to the door. The craft’s center: physical controls fail at the culture and process layer (the door held open for a stranger, the visitor badge never returned, the decommissioned laptop in the closet) more than at the technology layer, and the physical program’s metrics are therefore process metrics verified by tests.
Why It Matters
Section titled “Why It Matters”Every digital attack in this handbook ends at a machine in a room, and physical access is the credential that skips every network control at once: the adversary inside the office is on the corporate VLAN (or the conference-room guest network with a packet capture), can touch the unattended workstation (a USB drop, a rubber-ducky class device — H5’s join), can photograph the whiteboard with the architecture diagram, can reach the network closet where the patch panel and the uplink live (BT13’s segmentation depends on those closets being locked). The hardware attacks (H1-H12) mostly require physical proximity at least once — the JTAG header needs the device on the bench, the BadUSB needs a port. And the data-center-class incidents (the insurance claim, the regulator’s question) begin with a physical log: who entered, when, and why did no control notice?
The failure modes are cultural and procedural. Tailgating culture: the held door as workplace politeness — the most reliable physical bypass in existence, defeating million-dollar badge systems with a smile. Visitor drift: the visitor badge that never expires, the escort requirement waived by habit, the contractor badge active three months after the contract. The unowned closet: the network room shared with building maintenance, the cabinet with a default key — BT13’s segmentation architecture ends at a door with a padlock from 2011. Decommissioning gaps: the laptop in the drawer, the badge in the desk, the departing employee’s access persisting (BT15’s leavers process, physical edition) — the physical estate accumulates ghosts like the CMDB does. And detection without response: the tailgating alarm that fires to nobody, the CCTV nobody reviews — detection physical-style needs the same closed loop as BT9’s alerts.
Detailed Explanation
Section titled “Detailed Explanation” LAYERED PHYSICAL MODEL (each layer assumes the outer failed) PERIMETER fence, parking, loading dock, signage v BUILDING lobby (visitor mgmt, guard), badge readers, turnstiles, after-hours controls v FLOOR/ZONE badge tiers, sensitive-area separation v ROOM network closets, IDF/MDF, comms cabinets, data center (mantrap, anti-passback) v RACK/PORT locked racks, port locks, cable discipline v DESK clean desk, screen lock, USB ports (H5 join)
CREDENTIAL LIFECYCLE (BT15's doctrine, physical edition) issue (vouch + purpose) -> use (anti-passback) -> review (quarterly) -> revoke (leaver SLA) -> destroyLayered access follows the same defense-in-depth logic as segmentation (BT13): each layer assumes the outer one failed, so the network closet’s lock assumes the floor badge was cloned, and the rack lock assumes the closet was entered. The high-value mappings: data centers get anti-passback (a badge cannot enter twice without exiting — the tailgating counter at the protocol level) and mantraps; network closets get per-building keyed isolation with alarm contacts; the executive floor and the mailroom get zone separation (the mailroom is a delivery attack surface — H5’s poisoned-peripheral class arrives by courier).
The credential lifecycle is BT15 transplanted: issuance requires vouching and purpose (who requested, for which zone, until when — a badge is a scoped credential, not an identity), use is monitored (anti-passback, first-badged-in anomalies), review is quarterly (active badges vs HR roster — the ghost-badge sweep, the physical edition of BT15’s exemption register), revocation follows the leaver SLA (badge dead before the exit interview ends), and destruction is logged (the returned badge is destroyed, not recycled into someone’s drawer). Visitor management runs the same cycle compressed: pre-registration, escort requirements, expiry-by-default badges, and return-verification at exit.
Detection closes loops or it decorates: door-held-open alarms route to a response (a guard or a process, within minutes), tailgating sensors (stereo-vision or millimeter-wave [verify current sensor classes]) feed the same metrics as the tests (S4’s interface verdicts), CCTV analytics (loitering, after-hours movement) join the SIEM (BT3’s pipeline — physical events are security events), and environmental monitoring (water, heat, power — the data center’s real daily threats) alerts before the hardware dies. The response-side integration matters most: the physical alarm that creates a BT1 case with a camera bookmark is the closed loop; the alarm that emails a distribution list is decoration.
Deep dive: the service badge — the permanent credential nobody reviews
Service badges (vendor maintenance, cleaning crews, elevator technicians, the compressor company) are the physical estate’s service accounts: long-lived, broadly-scoped, issued once, and reviewed never — the physical edition of the service-account sprawl BT15 documents. The failure pattern is uniform: the badge issued for a two-week project persists for years, the vendor’s staff turn over while the badge stays with the company, and the register’s purpose field says “vendor” — which describes nothing. The discipline: every service badge carries purpose text naming the system and site (“HVAC — BMS access, bldg 2”), an expiry date synced to the contract (the badge cannot outlive the invoice), a named vendor contact whose employment still matters, and a quarterly reconciliation against the vendor list — badgeaudit’s purpose-length check and the vendor-list join are that discipline automated. The compressor badge that outlived its contract by eleven months (see What caught me) entered the data center every second Tuesday because the register said it could — the mantrap, biometrics, and cameras all worked exactly as designed, on a credential nobody had reviewed since the Obama administration. Service badges are tier-0 physical credentials; they get tier-0 review.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Layered zone model | Defense-in-depth per layer | One lock everywhere | BT13 analogy |
| Badge lifecycle mgmt | Scoped, reviewed, revoked | Ghost badges accumulate | Quarterly sweep |
| Anti-passback | Enter-exit pairing enforced | Reader bypass tricks | Data-center class |
| Visitor management | Pre-reg, escort, expiry badges | Escort waiver by habit | The politeness risk |
| Tailgating tests | Door culture measured (S4 join) | Once-a-year theater | Quarterly, varied |
| CCTV analytics + SIEM | Physical events as security events | Cameras nobody reviews | BT3 join |
| Environmental monitoring | Water/heat/power before failure | Server room = closet | The daily threat |
| Clean desk policy | Screen lock, document discipline | Policy without checks | Desk audits |
| Decommissioning process | Devices/badges/data returned | The drawer laptop | BT15 leaver SLA |
| Delivery/mailroom controls | Courier identity, scan-in | Poisoned-peripheral class | H5 adjacency |
Full Working Code
Section titled “Full Working Code”badgeaudit — the physical credential lifecycle auditor: reads the badge register (CSV: badge_id, holder_id, holder_type, zone_access, issued, last_used, status, purpose) and the HR roster export (CSV: person_id, active, department), and produces the ghost-badge report (badges active with no active person), the stale-badge list (issued/unused beyond policy), the over-privileged sweep (zone access vs declared purpose), and the visitor-badge residue (visitor badges past expiry still active). The physical edition of BT15’s mfaudit.
#!/usr/bin/env python3"""badgeaudit — physical badge lifecycle audit.
Inputs: badges.csv: badge_id,holder_id,holder_type,zone_access,issued,last_used, status,purpose (holder_type: employee|contractor|visitor|service; status: active|suspended|returned|lost) roster.csv: person_id,active,department (HR truth; person_id empty for visitor rows => use holder_id field 'VISITOR')Policy: employees/contractors require active roster entry; visitor badges expire after 1 day; any badge unused > 90d is stale; service badges require purpose text naming the system/vendor."""import argparseimport csvimport sysfrom datetime import date, datetimefrom pathlib import Path
REQ_B = ["badge_id", "holder_id", "holder_type", "zone_access", "issued", "last_used", "status", "purpose"]REQ_R = ["person_id", "active", "department"]VISITOR_TTL_DAYS = 1STALE_DAYS = 90
def parse_d(raw): for fmt in ("%Y-%m-%d", "%Y/%m/%d"): try: return datetime.strptime(raw.strip(), fmt).date() except ValueError: continue sys.exit(f"fail-loud: unparseable date {raw!r} — fix the register")
def main(): ap = argparse.ArgumentParser(description="badge lifecycle audit") ap.add_argument("badges", type=Path) ap.add_argument("--roster", type=Path, required=True) ap.add_argument("--today", default=date.today().isoformat()) ap.add_argument("--out", type=Path, default=Path("badgeaudit_out")) args = ap.parse_args()
for path, req, label in ((args.badges, REQ_B, "badges"), (args.roster, REQ_R, "roster")): if not path.exists(): sys.exit(f"fail-loud: {label} not found: {path}") with args.badges.open(encoding="utf-8", errors="replace") as fh: badges = list(csv.DictReader(fh)) with args.roster.open(encoding="utf-8", errors="replace") as fh: roster = {r["person_id"].strip(): r for r in csv.DictReader(fh)} if not badges: sys.exit("fail-loud: empty badge register — an empty register is " "not a clean estate, it's a broken export")
today = parse_d(args.today) ghosts, stale, overreach, visitor_residue = [], [], [], [] active_count = 0 for b in badges: bid = b["badge_id"].strip() if b["status"].strip().lower() != "active": continue active_count += 1 htype = b["holder_type"].strip().lower() holder = b["holder_id"].strip() purpose = b["purpose"].strip()
if htype in ("employee", "contractor"): person = roster.get(holder) if person is None: ghosts.append({"badge_id": bid, "holder": holder, "reason": "no roster entry"}) elif person["active"].strip().lower() != "yes": ghosts.append({"badge_id": bid, "holder": holder, "reason": "person inactive in HR"}) elif htype == "visitor": issued = parse_d(b["issued"]) if (today - issued).days > VISITOR_TTL_DAYS: visitor_residue.append({"badge_id": bid, "age_days": (today - issued).days, "holder": holder}) elif htype == "service": if len(purpose) < 8 or not any( c.isalnum() for c in purpose): overreach.append({"badge_id": bid, "holder": holder, "reason": "service badge without " "meaningful purpose"})
last_used_raw = b["last_used"].strip() if last_used_raw: lu = parse_d(last_used_raw) if (today - lu).days > STALE_DAYS: stale.append({"badge_id": bid, "holder": holder, "days_idle": (today - lu).days})
args.out.mkdir(parents=True, exist_ok=True) with (args.out / "findings.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.writer(fh) w.writerow(["class", "badge_id", "detail"]) for g in ghosts: w.writerow(["GHOST", g["badge_id"], f"{g['holder']}: {g['reason']}"]) for v in visitor_residue: w.writerow(["VISITOR-RESIDUE", v["badge_id"], f"visitor badge {v['age_days']}d old (TTL 1d): " f"{v['holder']}"]) for o in overreach: w.writerow(["SERVICE-NO-PURPOSE", o["badge_id"], o["reason"]]) for s in stale: w.writerow(["STALE", s["badge_id"], f"idle {s['days_idle']}d (policy {STALE_DAYS}d)"])
print(f"badges={len(badges)} active={active_count} " f"roster={len(roster)}") print(f"GHOST (active, holder gone): {len(ghosts)}") for g in ghosts[:6]: print(f" {g['badge_id']} {g['holder']} {g['reason']}") print(f"VISITOR-RESIDUE (past 1-day TTL): {len(visitor_residue)}") print(f"SERVICE-NO-PURPOSE: {len(overreach)}") print(f"STALE (>{STALE_DAYS}d idle): {len(stale)}") print(f"findings: {args.out / 'findings.csv'}") print("next: revoke ghosts before the exit interview ends (BT15 SLA); " "visitor TTL is 1 day — residue means reception process decay") if ghosts or visitor_residue: sys.exit(1)
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The roster join (lines 51-66) is the ghost detector’s core: a badge active for an employee/contractor holder either matches an active HR entry or it’s a ghost — two ghost reasons distinguished deliberately (no roster entry at all vs person-inactive-in-HR), because the fixes differ (data error vs leaver-process miss). The visitor TTL (lines 67-71) encodes the compressed lifecycle: a visitor badge is a day-scope credential; anything older is reception process decay, counted with its age so the report shows how deep the decay runs. The service-badge purpose check (lines 72-78) requires meaningful purpose text (≥8 chars with alphanumeric content — “vendor” fails, “HVAC vendor — BMS access, bldg 2” passes): the service badge is the permanently-scoped credential that outlives memory of why it exists.
The stale sweep (lines 80-86) applies the 90-day idleness rule to all active badges — the badge nobody swipes is either a leaver’s ghost the HR join missed (contractors paid through an agency, rostered under the agency) or a real badge whose holder stopped coming (a recovery conversation, not a revocation). Exit discipline (lines 108-110): ghosts and visitor residue fail the audit — both are lifecycle failures, not data quirks — while stale and no-purpose items report without blocking (they’re review queues). The output joins BT15’s revocation SLA directly: the ghost list is the leaver-process miss list, reverse-indexed.
How It Reaches The Target
Section titled “How It Reaches The Target”The quarterly physical audit runs badgeaudit against the access-control export and HR truth; the findings route: ghosts to the leaver-process owner (each one is a missed revocation — the BT15 SLA missed physically), visitor residue to reception (the process decay conversation, with ages as evidence), service badges without purpose to the facilities owner. The same week, S4’s physical tests run the behavioral layer: three tailgating attempts across three doors (the reception-challenge verdicts from the S4 join), one visitor-escort test (does the escort stay?), one network-closet check (is the door closed, alarmed, and does the lock actually lock?).
The incident that ties the layers together: a courier delivers a “replacement network appliance” to the mailroom (H5’s poisoned-peripheral class); the delivery-control process (scan-in, recipient verification, no direct-to-rack) holds it at the desk; the recipient verifies with the vendor out-of-band and finds no such order — the delivery attack dies at the mailroom’s process, exactly where BT14’s payment-change control died… no, held. Post-incident, the badge audit’s ghost list shrinks quarter over quarter (the leaver SLA fixed), the tailgating verdicts improve after the door-culture debrief (the S4 join), and the network closets get their alarm contacts wired into BT3’s pipeline — physical events landing in the SIEM beside the digital ones, which is the integration that lets BT1’s triage see the whole picture: the badge anomaly and the impossible-travel sign-in, correlated.
Exploitation Chain
Section titled “Exploitation Chain”- Audit — badgeaudit: ghosts, visitor residue, stale, over-privileged service badges
- Test — S4’s physical layer: tailgating, escort compliance, closet checks, clean-desk sweeps
- Find — lifecycle failures (ghosts), culture failures (doors), process failures (mailroom, decommissioning)
- Fix — revocation SLA enforcement, reception process, door-culture debrief, closet alarms to BT3
- Integrate — physical events into the SIEM: badge anomalies beside digital anomalies
- Re-test — quarterly: the estate’s physical truth measured, not assumed
- Correlate — the badge anomaly + impossible-travel sign-in: BT1 sees both planes
- Report — BT20 metrics: ghost count trending zero, tailgating verdicts, closet alarm coverage
Building Your Own Tool
Section titled “Building Your Own Tool”The door system produces events; doorwatch makes them investigative: reads the access-event export (CSV: ts, badge_id, door_id, result — granted/denied), applies the lifecycle rules (first-entry-per-day anomalies, denied-then-granted sequences on the same badge, after-hours access by role class), and emits the anomaly register — the physical edition of BT15’s sign-in risk analytics.
Design decisions
Section titled “Design decisions”Role-class context over raw events: after-hours means nothing without knowing the door’s normal population (the night-shift data center crew vs the 9-to-5 floor) — the door register carries door_id, zone, and normal_hours, and the analytics run per-door context. Denied-then-granted is the physical brute-force tell: several denials followed by a grant on the same badge/door within minutes (a badge copied? a code shared? a guard overriden?) — its own anomaly class. First-entry-of-day per badge catches the impossible commute (badged into the Berlin office and the Singapore lobby forty minutes apart — the physical impossible-travel). Fail-loud on event/export schema drift; the register must never silently skip rows.
Full source
Section titled “Full source”#!/usr/bin/env python3"""doorwatch — access-event anomaly analytics for physical doors.
Inputs: events.csv: ts,badge_id,door_id,result (granted|denied) doors.csv: door_id,zone,normal_start,normal_end (24h format)Output: anomaly register (after-hours, denied-then-granted, impossible- commute) per badge, with evidence lines."""import argparseimport csvimport sysfrom collections import defaultdictfrom datetime import datetime, timedeltafrom pathlib import Path
REQ_E = ["ts", "badge_id", "door_id", "result"]REQ_D = ["door_id", "zone", "normal_start", "normal_end"]COMMUTE_MIN = 90 # minutes between entries = impossible commuteDENIED_STREAK = 3
def main(): ap = argparse.ArgumentParser(description="door event analytics") ap.add_argument("events", type=Path) ap.add_argument("--doors", type=Path, required=True) ap.add_argument("--out", type=Path, default=Path("doorwatch_out")) args = ap.parse_args()
for path, req, label in ((args.events, REQ_E, "events"), (args.doors, REQ_D, "doors")): if not path.exists(): sys.exit(f"fail-loud: {label} not found: {path}") with args.events.open(encoding="utf-8", errors="replace") as fh: events = list(csv.DictReader(fh)) with args.doors.open(encoding="utf-8", errors="replace") as fh: doors = {r["door_id"].strip(): r for r in csv.DictReader(fh)} if not events: sys.exit("fail-loud: zero events — controller export broken")
parsed = [] for n, e in enumerate(events, 2): if any(not e.get(k, "").strip() for k in REQ_E): sys.exit(f"fail-loud: events line {n}: missing fields") try: ts = datetime.fromisoformat(e["ts"].replace("Z", "+00:00")) except ValueError: sys.exit(f"fail-loud: events line {n}: bad ts {e['ts']!r}") parsed.append({"ts": ts, "badge": e["badge_id"].strip(), "door": e["door_id"].strip(), "result": e["result"].strip().lower()}) parsed.sort(key=lambda e: e["ts"])
anomalies = []
# after-hours by door context by_badge = defaultdict(list) for e in parsed: d = doors.get(e["door"]) if d is None: sys.exit(f"fail-loud: door {e['door']!r} not in door register") try: ns = datetime.strptime(d["normal_start"], "%H:%M").time() ne = datetime.strptime(d["normal_end"], "%H:%M").time() except ValueError: sys.exit(f"fail-loud: door {e['door']}: bad normal hours") t = e["ts"].time() after = not (ns <= t <= ne) if ns <= ne else not (ns <= t or t <= ne) if after and e["result"] == "granted": anomalies.append({"kind": "AFTER-HOURS", "badge": e["badge"], "ts": e["ts"], "detail": f"{e['door']} ({d['zone']}) granted at " f"{t.strftime('%H:%M')} (normal {d['normal_start']}" f"-{d['normal_end']})"}) by_badge[e["badge"]].append(e)
# denied-then-granted streaks per (badge, door) streaks = defaultdict(list) for e in parsed: streaks[(e["badge"], e["door"])].append(e) for (badge, door), evs in streaks.items(): run = 0 for e in evs: if e["result"] == "denied": run += 1 else: if run >= DENIED_STREAK: anomalies.append({ "kind": "DENIED-THEN-GRANTED", "badge": badge, "ts": e["ts"], "detail": f"{run} denials then grant on {door}"}) run = 0
# impossible commute: same badge, two buildings, < 90 min grants = [e for e in parsed if e["result"] == "granted"] for i in range(1, len(grants)): a, b = grants[i - 1], grants[i] if a["badge"] == b["badge"] and a["door"] != b["door"]: za, zb = doors.get(a["door"], {}).get("zone", "?"), \ doors.get(b["door"], {}).get("zone", "?") if za != zb and (b["ts"] - a["ts"]) <= timedelta( minutes=COMMUTE_MIN): anomalies.append({ "kind": "IMPOSSIBLE-COMMUTE", "badge": a["badge"], "ts": b["ts"], "detail": f"{za} -> {zb} in " f"{(b['ts'] - a['ts']).seconds // 60} min"})
args.out.mkdir(parents=True, exist_ok=True) with (args.out / "anomalies.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=["kind", "badge", "ts", "detail"]) w.writeheader() w.writerows(anomalies)
kinds = defaultdict(int) for a in anomalies: kinds[a["kind"]] += 1 print(f"events={len(parsed)} anomalies={len(anomalies)} " f"by kind: {dict(kinds)}") for a in anomalies[:10]: print(f" [{a['kind']:<20}] {a['badge']:<10} {a['ts']:%m-%d %H:%M} " f"{a['detail'][:50]}") print(f"register: {args.out / 'anomalies.csv'}") print("next: join badge IDs with badgeaudit ghosts (anomalies on ghosts " "are the loudest signal)")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The per-door context (lines 46-62) is the analytics’ spine: after-hours is computed against each door’s normal window (the overnight-shift guard’s 03:00 data-center entry is normal; the 03:00 executive-floor entry is the anomaly), with the wrap-around window case handled (22:00-06:00 spans midnight, lines 55-57). The denied-then-granted detector (lines 65-77) tracks per-(badge, door) streaks — three denials followed by a grant is the copied-badge, shared-code, or guard-override signature, and the threshold is a deliberate constant (fewer denials is normal human error). The impossible commute (lines 80-92) pairs consecutive grants by the same badge across different zones within 90 minutes — the physical clone of BT15’s impossible-travel, catching badge duplication and tailgating-in-reverse (one badge, two people).
The output’s final instruction (line 106) names the highest-value join: anomalies on badges that badgeaudit flags as ghosts are the loudest signal in the physical estate — a credential that shouldn’t exist, moving. That join is the physical estate’s version of BT15’s session-revocation doctrine: the ghost badge gets killed the moment it moves.
Compilation and test procedure
Section titled “Compilation and test procedure”audit the badge register against HR truth
Section titled “audit the badge register against HR truth”$ python3 badgeaudit.py badges.csv —roster hr_export.csv badges=412 active=398 roster=380 GHOST (active, holder gone): 7 B-0042 E-1103 person inactive in HR VISITOR-RESIDUE (past 1-day TTL): 12 SERVICE-NO-PURPOSE: 2 STALE (>90d idle): 15 findings: badgeaudit_out/findings.csv
weekly door-event analytics
Section titled “weekly door-event analytics”$ python3 doorwatch.py events.csv —doors doors.csv events=81240 anomalies=23 by kind: {‘AFTER-HOURS’: 14, ‘DENIED-THEN-GRANTED’: 6, ‘IMPOSSIBLE-COMMUTE’: 3} [AFTER-HOURS ] B-0042 10-12 02:14 MDF closet (bldg-2) granted at 02:14 [IMPOSSIBLE-COMMUTE ] B-0042 10-12 02:41 bldg-2 -> bldg-5 in 27 min register: doorwatch_out/anomalies.csv
the ghost badge B-0042 moving at 2 a.m. across buildings —
Section titled “the ghost badge B-0042 moving at 2 a.m. across buildings —”badgeaudit’s ghost + doorwatch’s movement = the loudest physical finding
Section titled “badgeaudit’s ghost + doorwatch’s movement = the loudest physical finding”Detection considerations
Section titled “Detection considerations”Physical telemetry joins the SIEM (BT3) as first-class security events: badge anomalies, door-held-open alarms, CCTV analytics events, and environmental alerts flow through the same pipeline as digital events — the correlation that matters (badge anomaly + impossible-travel sign-in for the same person’s account) only exists if both planes land in one place. The badgeaudit/doorwatch joins feed BT20’s physical metrics: ghost count (trending zero), tailgating verdicts (S4’s interface metric), closet alarm coverage (BT13’s closets), and visitor TTL compliance. One privacy note: badge analytics are people analytics — retention and access of movement data belong in S10’s discipline, and the program’s charter should say so before the first request arrives.
Improvements to add
Section titled “Improvements to add”- Join doorwatch anomalies with BT15’s sign-in risk events per person (physical+digital impossible-travel).
- Add visitor-badge to doorwatch event context (visitors get different anomaly thresholds).
- Add environmental telemetry (temp/humidity) to the same register with threshold alerts.
- Auto-create BT1 cases for ghost-badge movement (the physical pager).
- Emit the quarterly physical-evidence pack (audits + tests + fixes) for the BT20 scorecard.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Ghost badge movement | doorwatch × badgeaudit | per event | Loudest signal |
| Impossible commute | doorwatch | per event | Badge duplication |
| Denied-then-granted | doorwatch | per event | Copied/shared creds |
| After-hours anomalies | doorwatch | per event | Per-door context |
| Tailgating verdicts | S4 tests | quarterly | Door culture |
| Visitor TTL compliance | badgeaudit | quarterly | Reception decay |
| Door-held-open alarms | Door controllers | per event | Response SLA |
| Closet alarm coverage | BT3 pipeline | per closet | Segmentation ends here |
| Environmental alerts | Facility sensors | per event | Daily real threats |
| Clean-desk audit rate | Desk sweeps | quarterly | H5 exposure |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries evade physical controls with the same professionalism as digital ones. Credential cloning: the badge copied in a brush-by (WI9’s RFID class — the high-frequency cards with weak crypto clone in seconds) — the counters are encrypted credentials (DESFire-class [verify current recommendations]), anti-passback, and the anomaly analytics that catch the clone’s behavior (two locations, one badge). Tailgating with pretext: the arms-full courier, the confused visitor, the delivery driver — social engineering wearing a hard hat (S4’s discipline applies verbatim); anti-passback and turnstiles are the technical counter, door culture is the human one. After-hours patience: the adversary who badges in legitimately at 2 a.m. for weeks, escalating access gradually (the physical privilege creep — each visit slightly deeper) — the doorwatch per-door context and first-entry analytics catch the pattern that any single event doesn’t show. Visitor-channel abuse: the pre-registered “contractor” whose escort steps away, the badge borrowed between legitimate visitors — visitor TTL, escort accountability (the escort’s badge is co-logged), and lobby CCTV analytics counter it. The mailroom: poisoned peripherals and supply-chain hardware arrive by courier (H5’s class) — delivery process (scan-in, verification, no direct-to-rack) is the control that keeps the attack surface at the desk, not the rack. Closet ecology: the network closet that building maintenance opens with a master key, the cabinet on the default combination — BT13’s segmentation ends at hardware; the closet audit (locks, alarms, access logs) is the check that keeps the layering honest.
⚠️ What caught me: our data center had textbook controls — mantrap, anti-passback, biometrics, cameras — and I’d have signed any audit saying so. Then a routine doorwatch run showed a service badge entering the mantrap every second Tuesday at 6 a.m., escorted by nobody, logged as granted because anti-passback only counts pairing, not authorization. The badge belonged to the compressor maintenance vendor; the contract had ended eleven months earlier; nobody had ever reconciled the service-badge register against the vendor list. A nine-million-euro room, defended to the teeth, with a standing invitation in its own access register. Physical security fails in the register, not at the reader — and the fix cost one audit command and one awkward phone call to a vendor that no longer had any business inside our mantrap.
Lab Practice
Section titled “Lab Practice”Run the physical audit loop on your lab’s door/badge exports (or synthesize a month of realistic events). Audit: run badgeaudit against a register containing deliberate ghosts and visitor residue; confirm the exit code fails and the classes separate. Analyze: generate door events including an after-hours grant, a denied-then-granted streak, and an impossible commute; run doorwatch — confirm each class fires with its evidence line. Correlate: join the two outputs — find the ghost badge that moved, the loudest physical finding. Test the culture: with teammates, run one tailgating attempt at your lab’s (real) door and log the interface verdict through S4’s seops. Fix: revoke one ghost, set one visitor TTL alarm, and wire one door-alarm event into your BT3 pipeline. Debrief: which finding would your current physical program have caught — and which one did it take this module’s tooling to see?
Interview Questions
Section titled “Interview Questions”- Technical: Why is physical access an identity problem, and how do BT15’s lifecycle doctrines (issue, review, revoke) transplant to badges?
- Technical: Explain
doorwatch’s three anomaly classes and why after-hours analysis requires per-door context rather than global time windows. - Scenario: The badge audit finds seven ghost badges; one of them moved through two buildings last night. Walk the first hour: revocation, review, and the investigation it opens.
- Scenario: A courier arrives with a “replacement firewall” nobody ordered, addressed to the network team. Walk the mailroom decision and the process that should have held it.
- Conceptual: Why does “detection without response” make physical alarms decoration, and what does the closed loop look like concretely?
- Deep-dive: Design physical security for a two-building HQ with a data center: layered zones, credential lifecycle, visitor process, detection integration with the SIEM, and the BT20 metrics.
- Practical: You inherit a site with paper visitor logs, unmanaged service badges, and no door analytics. What do you fix in the first 30 days — and what do you explicitly defer?
Related Modules
Section titled “Related Modules”S4 (Social Engineering Operations) · BT5 (Digital Forensics & Evidence Handling) · BT13 (Network Segmentation) · WI9 (RFID/NFC) · H1 (Hardware Hacking Fundamentals)