HydraIssues

No supported way to remove a head iPad peer; RemoveHeadIPad exists but is exposed nowhere
open bug Project: hydraguard Reporter: cederik 7 Aug 2026 21:06

Description

There is no supported way to remove a head iPad peer. Mesh.RemoveHeadIPad exists but is wired to nothing, so removing a peer means hand-editing mesh.yaml on the hub and running hub apply. That was done twice on 2026-08-06 against production.

The gap

pkg/mesh/headipad.go:28 implements it:

func (m *Mesh) RemoveHeadIPad(id string) error {

Nothing calls it outside tests. There is no CLI command and no API route:

  • internal/cli/ has air.go, neckair.go, venue.go, each with add/list/config/remove subcommands. There is no headipad.go at all, so hydraguard headipad remove <id> does not exist even though the mesh function does.
  • pkg/api/server.go exposes POST /api/v1/headipad/provision (line 71) and nothing else for head iPads. The other provision routes (air, gateway) are the same shape: create only.

So head iPad peers can be created through supported paths and only removed by editing YAML by hand.

Why it matters

Two removals were needed on 2026-08-06, both on the live hub:

  1. A stale peer named "ipad-head" on 10.10.200.3, left over from a decommissioned head, was blocking enrollment. A new iPad self-registering under the same default name got 409 "head iPad peer already exists ... Re-send with force: true to issue a new keypair", provisioning failed non-fatally, and the head ended up with no tunnel at all.
  2. A rental iPad was returned and its peer had to be revoked so the device could not route into the mesh on its way out. Deleting the head in hydracluster does not tear down the peer (hydracluster docs/runbooks/runbook.md:254 says so explicitly), so the credential stays live until someone edits mesh.yaml.

Case 2 is the security one: without a removal path, revoking a WireGuard credential is a manual YAML edit on a production mesh host, which is exactly the operation you want tooling for.

Hand-editing is worse than it looks

mesh.yaml is re-serialized by hydraguard when it provisions a peer. Between the two removals on the same day the file changed indentation (- id: at column 0 became - id:) and new entries were appended at the end rather than in slot order. Line-based edits that worked in the morning silently matched nothing in the afternoon: grep counts returned 0 and would have been taken as "no peers" by anything scripted against them.

The safe procedure ended up being: back up and checksum, locate exact line numbers by reading the file, delete, diff to prove only the intended lines changed, run hub apply --dry-run and compare the generated PublicKey set against wg show wg0 dump to prove exactly one key drops and none are regenerated, then apply and re-check handshake counts against a baseline. That is a lot of care to expose a function that already exists.

Suggested direction

  • Add hydraguard headipad add|list|config|remove <id>, matching air.go / neckair.go / venue.go. This alone closes the gap, since RemoveHeadIPad is already implemented and tested.
  • Consider DELETE /api/v1/headipad/{id} so hydracluster can revoke a peer when a head is deleted, rather than leaving it orphaned. If added, note that hydracluster currently does not attempt any teardown, so that would be a coordinated change.
  • Whatever is added should apply to the live interface, not just mesh.yaml. Removing an entry from the file leaves the peer on wg0 until hub apply runs, and a freed address can then be reassigned to a new peer while the old one still claims it. That happened on 2026-08-06: 10.10.200.3 was free in mesh.yaml but still on wg0, so the next provision would have collided.
  • Same reasoning applies to air, neckair and gateway peers, which have create-only provision routes too.