Skip to main content

Fleet Dual-Channel Email Verification

Protocol for agent-to-agent action authorization across the Tailscale fleet. An emailed action request between agents requires two independent channels to agree before it is honored: the email itself, and an HMAC-signed challenge file deposited over SSH.

Deployed: VPS (vmi3537362) ↔ tactical-grace — Sep 2026.


Why​

Email alone is spoofable. SSH alone has no instruction context. The protocol binds the two:

  • An attacker who can spoof email cannot produce the SSH-deposited HMAC (they'd need shell access on the issuing node).
  • An attacker with SSH access but no email never learns the instruction context or a valid nonce.
  • Both channels must agree — and the validation file is single-use and expires in 24 hours.
Channel 1 (email): instruction + "VERIFY:<action_id>:<nonce>" line
Channel 2 (SSH): HMAC-SHA256(nonce|ts|action_id|details) deposited
on the recipient's ~/.hermes/verify/<action_id>.json

The shared HMAC secret lives only on the two machines' disks (~/.hermes/fleet_verify_secret, chmod 600). It never crosses the network in email.


Roles​

Each node plays both roles depending on direction:

RoleRuns onScriptWhat it does
IssuerThe node requesting the actiongrace_issue.py (on tactical-grace)Generates nonce, signs HMAC, SSH-deposits the challenge on the recipient
ValidatorThe node receiving the requestfleet_verify.py (on VPS), grace_verify.py (on tactical-grace)Reads the deposited file locally, validates the email's nonce against the HMAC

Example flow — Grace asks the VPS to do something:

  1. Grace: python3 ~/.hermes/verify_docs/grace_issue.py issue <action_id> "details"
  2. Script SSH-deposits ~/.hermes/verify/<action_id>.json on the VPS
  3. Grace emails sleeper.service@pagemind.org (CC angus.halliday@gmail.com) with the VERIFY:<action_id>:<nonce> line
  4. VPS Hermes runs fleet_verify.py verify <action_id> <nonce> — acts only on ok: true

The reverse direction mirrors it: the VPS deposits on Grace's disk, Grace validates locally.


Setup (per node pair)​

  1. Shared secret: identical content at ~/.hermes/fleet_verify_secret on both machines, chmod 600. Generate once: python3 -c "import secrets; print(secrets.token_urlsafe(48))" and deploy out-of-band (never by email).
  2. Verify directory: ~/.hermes/verify/ on both machines, chmod 700.
  3. Scripts: issuer + validator for each direction, chmod +x.
  4. SSH paths: each issuer script names its target (VPS_DEST) and key (VPS_KEY). Host keys must be in known_hosts (use ssh-keyscan on first contact).
  5. All verification emails CC angus.halliday@gmail.com for audit.

Issuer usage (on the node requesting the action)​

python3 ~/.hermes/verify_docs/grace_issue.py issue <action_id> ["details"]

Returns:

{
"ok": true,
"action_id": "test-grace-to-vps-2",
"nonce": "ba1fe928...",
"email_body_line": "VERIFY:test-grace-to-vps-2:ba1fe928...",
"expires_hours": 24
}

Then email the recipient with the VERIFY:... line in the body. Convention: action_ids are <from>-to-<to>-<n> (e.g. test-grace-to-vps-2).

Validator usage (on the node receiving the request)​

python3 ~/.hermes/scripts/fleet_verify.py verify <action_id> <nonce>

Returns {"ok": true, ...} or {"ok": false, "reason": "..."}. Possible failures:

ReasonMeaning
no validation file depositedEmail arrived but SSH deposit never landed — do not act
challenge expiredFile was older than 24h (burned)
HMAC mismatchFile tampered, or nodes hold different secrets
nonce in email does not matchEmail nonce ≠ deposited challenge

Properties & Guarantees​

  • Single-use: the validator deletes the challenge file on successful validation. Replays fail.
  • Expiry: challenges older than 24h are rejected and burned.
  • Non-repudiation within the fleet: the HMAC key provably came from a node holding the shared secret.
  • Direction-symmetric: both nodes hold the same secret, so either can issue. If you want asymmetric authority, give the secret only to nodes allowed to issue.

Known Pitfalls (learned in deployment)​

  1. Silent SSH deposit failures happen. Grace's first two issue runs generated nonces but the deposit never landed (non-interactive SSH environment quirk). The protocol correctly rejected the emailed VERIFY lines — that's the system working. Re-issuing through an interactive SSH session landed first try. Always run hermes cron history-style verification or check the recipient's verify dir if an action seems ignored.
  2. hermes config set with dotted keys mangles nesting. For model_overrides and any nested YAML, edit the file directly instead of using dotted one-liners.
  3. Hermes' 64K context gate applies to any local LM Studio model used for agent jobs — needs BOTH a model_overrides.custom.<model>.context_window entry (≥ 65536) AND the model loaded in LM Studio with context ≥ 8192. See LM Studio cron routing companion notes.
  4. Host key verification on first SSH contact between fleet nodes: ssh-keyscan -t ed25519 <ip> >> ~/.ssh/known_hosts before scripted runs.

Continuous Monitoring (live since Sep 7)​

Both nodes run an hourly SSH-deposit probe — the protocol's control loop:

  • VPS: ~/.hermes/scripts/gateway_ssh_probe.sh → cron 5 * * * * (job b3a8f0bb67e2), deposits to Grace
  • tactical-grace: ~/.hermes/verify_docs/gateway_ssh_probe.sh → Grace's hourly cron, deposits to VPS

Design: each probe deposits one real timestamped challenge (cron-probe-<ts>.json). Prints ssh-probe:ok on success, ssh-probe:FAIL:<stderr> on failure. Cron is no-agent + deliver local: silent on success, error email on failure. This converts "passed N times" into "continuously watched, alarm on any failure."

Unvalidated probe files accumulate (they only burn if validated) — prune cron-probe-*.json older than 48h from both verify dirs periodically.


Fleet Deployment Status​

PairDirectionStatus
tactical-grace → VPSdeposit + validate✅ live (test-grace-to-vps-1/2, gateway-deposit-probe)
VPS → tactical-gracedeposit + validate✅ live (test-vps-to-grace-1, periodic-check)

New fleet nodes: deploy the secret, both scripts, verify dir, and SSH paths — then run a two-direction test cycle before trusting emailed actions.


Live Operations Notes (from production deployment)​

  • Validation is CC-audited: every verification email between fleet agents CCs angus.halliday@gmail.com — Gus holds the full audit trail.
  • Watch-item (resolved Sep 7): one silent SSH deposit failure occurred during initial testing (Grace's first gateway-executed run). Grace falsified the "persistent gateway sandbox restriction" hypothesis with a scrubbed-env test (env -i) — 7 consecutive successes across every environment shape. Verdict: one-time first-run event (likely known_hosts). Both nodes now run continuous hourly probes (see below) so any recurrence triggers an alarm with captured stderr. The known fix if it recurs: propagate exit-code/stderr from the ssh subprocess in the issuer script. Do not pre-emptively reconfigure for a failure you cannot reproduce.
  • Check the verify dir before concluding: a validator returning "no validation file deposited" means exactly that — the email arrived but the SSH half never did. Cross-check the issuer's filesystem before assuming spoofing.
  • Validation files burn on read: even a failed HMAC check that reads the file leaves it intact, but any successful validation (or expiry) deletes it. If you need to re-validate the same challenge, the issuer must re-issue.