Skip to content

docs(adr): read device joins from the DHCP server, not the eero cloud (#98) - #271

Merged
Gerrrt merged 2 commits into
mainfrom
gerrrt/home-assistant-eero-integration-5a6a64
Sep 4, 2026
Merged

docs(adr): read device joins from the DHCP server, not the eero cloud (#98)#271
Gerrrt merged 2 commits into
mainfrom
gerrrt/home-assistant-eero-integration-5a6a64

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #98 — with the landing site the issue chose and a different source, and
no longer blocked behind ADR-0008's mini PC.

#98 is right that device joins belong in the log pipeline rather than in a Home
Assistant notification. What it assumed rather than chose is where the events
come from, and that assumption is the whole of the dependency: Home Assistant
sits on ADR-0008's sensitive tier, on hardware nobody has bought.

What the checks found

There is no local eero API. eero is app-and-cloud managed; the units serve
no management interface on the LAN. The integration everyone means is
schmittx/home-assistant-eero
— HACS, not Home Assistant core — and read on 2026-09-04 its API base is
https://api-user.e2ro.com, its default poll interval is 120 seconds, and its
README says it cannot log in with an Amazon-linked eero account at all.

So the proposal in full: a join that happens on this wire is reported to
Amazon, held there, and fetched back over the WAN up to two minutes later — a
security signal that is unavailable exactly when the WAN is.

morpheus already knows. Measured on the firewall, 2026-09-04:

Fact Value
DHCP backend Kea, syslog program name kea-dhcp4
Lease lifetime 7200 s, so devices renew about hourly
Volume ~1,200 lines/day — 1.3% of what filterlog already ships
DHCP4_LEASE_ALLOC 4,732 lines in four days, from 57 distinct MACs
Largest gap between lines 975 s over 13 days
Forwarded today No — kea-dhcp4 is excluded from the System Events selector, so the DHCP content class has to be ticked

The eeros are bridged, so every wireless client in the house takes its lease
from Kea. The event is one checkbox from the listener that already carries
filterlog, on a path that never leaves the house.

The rules

A lease is not a join — with a two-hour lifetime, DHCP4_LEASE_ALLOC is
overwhelmingly renewal. The event is the first line from a MAC on a segment,
which LogQL expresses with no state of its own: the last 10 minutes unless
the 7 days before it.

Rule Segment Severity Measured behaviour over 13 days of dhcpd.log
UnknownDeviceOnTrustedSegment Hicks (50) warning 5 firings — one new device that stayed, one phone rotating its private MAC, two an OUI the inventory places on Skids, one a new machine on a familiar OUI
UnknownDeviceOnManagementSegment Winterfell (99) critical 0 firings
DhcpLeaseLogsStopped warning 2 h, against a worst observed gap of 975 s

Two design points worth the review:

  • mac is a parser label, never an ingest label — ADR-0003's cardinality
    constraint, the same call as src/dst in the filterlog rules beside it.
    And the address is deliberately not in the sum by: a known device
    renewing onto a different address would otherwise have no match on the
    right-hand side and would fire as a stranger.
  • No allowlist of household MACs exists anywhere. The set of known devices
    is whatever Loki has seen in seven days. A rule that enumerated them would
    have published exactly what security.md says this repository withholds.

Verified, not just parsed

check_loki_rules.sh proves the LogQL parses. The semantics were checked
separately against a throwaway Loki 3.7.7 seeded with eight days of synthetic
renewals: the committed rules fire for a brand-new MAC, for a MAC long known on
another segment, and for one absent nine days — and stay silent for the device
that renewed throughout. absent_over_time returns 1 for a stream that never
existed, which is why the ordering below is not optional.

Before this merges

Tick DHCP Events in Status → System Logs → Settings on morpheus, the
unreliable-save way ship-firewall-logs.md documents. Merged ahead of the
checkbox, DhcpLeaseLogsStopped fires truthfully and permanently — the same
ordering ADR-0018 needed for the host override.

Then expect one alert per device for the first week — about twenty on Hicks
and three on Winterfell — while the seven-day baseline fills. That is an
inventory check, not noise to silence.

Also

  • Leaves are dropped rather than deferred: 9 releases against 4,732
    allocations, and a departure is not a security event.
  • Loki rules 13 → 16; README, observability.md and security.md counts
    updated, check_docs.py agrees.
  • security.md gains the threat-model row and the three limits — leases not
    associations, segment not radio, private MAC rotation as the noise floor.

🤖 Generated with Claude Code

…#98)

#98 asks for a Home Assistant integration with the eero API so device joins
stop being accidents, and it is right about where the events belong: Alloy on
1514, Loki, a rule in security.rules.yaml. It is the source that was assumed
rather than chosen, and the assumption is what blocked the issue behind
ADR-0008's unbought mini PC.

Checked first, which changed the answer.

There is no local eero API. The integration everyone means —
schmittx/home-assistant-eero, a HACS component, not core — polls
https://api-user.e2ro.com every 120 seconds and cannot log in with an
Amazon-linked account. So a join on this wire would go to Amazon and come back
over the WAN, which means it is unavailable exactly when the WAN is.

Meanwhile morpheus already knows. The eeros are bridged — every phone and
camera takes its lease from Kea on the firewall — and the lease log is 1,200
lines a day, one pfSense checkbox from the listener that already carries
filterlog. Measured on the firewall on 2026-09-04.

A lease is not a join: valid-lifetime is 7200s, so DHCP4_LEASE_ALLOC is mostly
renewal (4,732 in four days from 57 MACs). The first line from a MAC on a
segment is the event, and LogQL expresses that with no state of its own — the
last 10 minutes `unless` the 7 days before it. `mac` is a parser label, never
an ingest one, for ADR-0003's reason; the address is deliberately not in the
`sum by`, because a device renewing onto a new address would then read as a
stranger. There is no allowlist of household MACs anywhere, which is the half
security.md's publication policy would not have allowed.

Three rules: first lease on Hicks in seven days (warning, measured at about one
every three days, and all five in the sample were worth a look), the same on
Winterfell (critical, zero in 13 days), and DhcpLeaseLogsStopped at two hours
against a measured worst gap of 975 seconds — because the first two fail
silently, and unticking the DHCP class alone is a fault FirewallLogsStopped
cannot see.

Verified against a real Loki, not just parsed: seeded with eight days of
renewals, the committed rules fire for a new MAC, for a MAC known on another
segment, and for one absent nine days, and stay silent for the device renewing
throughout. absent_over_time returns 1 for a stream that never existed, which
is why the DHCP class has to be ticked before this deploys.

Leaves are dropped rather than deferred: 9 releases against 4,732 allocations,
and a departure is not a security event.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 4, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are a few concrete doc/alert-text inconsistencies/clarity issues (including an outdated count and a broken alert description sentence) that should be corrected before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR implements ADR-0019 by adding Loki alert rules that treat “device joins” as first-seen DHCP lease activity from Kea on morpheus (instead of polling the eero cloud), and updates documentation to reflect the new detection approach and rule counts.

Changes:

  • Add a new dhcp Loki rule group with two “unknown device” alerts (VLAN 50/99) plus a “lease logs stopped” watchdog.
  • Document the operational prerequisite (enable pfSense “DHCP Events” remote logging before deploying rules) and expected cold-start behavior.
  • Update repository documentation and summaries to reflect the increased log-rule count and the new threat-model coverage.
File summaries
File Description
stacks/observability/loki/rules/security.rules.yaml Adds DHCP-based security/monitoring Loki alerts for first-seen MACs on VLAN 50/99 and missing DHCP logs.
README.md Updates top-level and layout summaries to reflect the new Loki rule count.
docs/security.md Extends threat model + documents DHCP-based join detection and its limitations.
docs/runbooks/ship-firewall-logs.md Updates runbook with DHCP Events enablement, validation queries, and rollback guidance.
docs/roadmap.md Updates #98 roadmap entry to reflect ADR-0019’s approach and removes the HA dependency.
docs/observability.md Documents pfSense log ingestion (including Kea DHCP leases) and updates total rule counts.
docs/adr/0019-read-device-joins-from-the-dhcp-server.md Adds the ADR capturing the rationale and consequences of DHCP-based join detection.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/security.md Outdated
Comment thread stacks/observability/loki/rules/security.rules.yaml Outdated
Comment thread stacks/observability/loki/rules/security.rules.yaml
…se menu

Three points from review on #271, all correct.

- security.md's threat-model row counted 48 where the control it describes —
  losing visibility of a failure, "30 days of metrics and logs" — is now all 64
  rules. check_docs.py accepts either reading, which is why it did not catch it.
- UnknownDeviceOnManagementSegment's description read "the segment ADR-0002
  says compromise of is total". It is on-call text; it now parses.
- FirewallLogsStopped has been sending people to Services → System Logs →
  Settings since it was written. System Logs are under Status, which is what
  both runbooks say. Pre-existing, one line, fixed here rather than left to
  disagree with the DHCP rule next to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Gerrrt
Gerrrt merged commit da0a071 into main Sep 4, 2026
3 checks passed
@Gerrrt
Gerrrt deleted the gerrrt/home-assistant-eero-integration-5a6a64 branch September 4, 2026 13:33
Gerrrt added a commit that referenced this pull request Sep 4, 2026
main is currently failing its own numbering guard: #98's "read device joins from
the DHCP server" and #101's "run the lab stack in a guest" both merged as
ADR-0019, twenty-three minutes apart, and check_adr_numbers reports the pair on
any tree containing both. That blocks every open pull request, not this one.

check_docs.py states the rule in the failure text — "renumber the one that
landed second" — so this is arithmetic rather than a judgement call. Landing
order on main's first-parent line, which is when a document became referenceable
rather than when it was written:

  da0a071  06:33  #271  device joins    stays ADR-0019
  c7071c0  06:40  #270  lab stack       becomes ADR-0020
  (this branch, unmerged)               becomes ADR-0021

The converge ADR yields to both, because merged work should not renumber to
accommodate an open branch. Its number has now moved twice in one day, which is
the cost of three ADRs being written against the same tip.

Also fixes two link-text/target mismatches this branch introduced earlier: the
0019->0020 pass used a blanket `ADR-0019]` -> `ADR-0020]` substitution scoped to
files mentioning converge, and roadmap.md and observability.md mention both, so
two device-joins links were relabelled while still pointing at the right file.
check_docs.py compares a filename to its own H1 and cannot see that, so both are
now checked directly: every [ADR-NNNN](...NNNN-*.md) in the tree agrees with its
target, and every ADR link resolves.

The README conflict was main reflowing the runbook list against this branch
adding `converge` to it; kept both. The adr/ count goes to 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Home Assistant integration with the eero API, so device joins are events

2 participants