Hardware Attestation for Agentic AI: A YubiKey Reference Architecture for Hermes Agent

~18 minute read

Hardware Attestation for Agentic AI: A YubiKey Reference Architecture for Hermes Agent

A physical boundary between what a human controls and what an agent executes — designed for a self-hosted AI agent handling real credentials and real money.

Our advice

Start with Paper Zero — find yourself first.

Before the frameworks land the way they’re meant to, see which of the five financial personas is running your business today — Which Financial Persona Is Running Your Business? is the recognition on-ramp: find yourself first, then read on. From there, The Two Perspectives names the disciplines — knowledge governance and operational data integration — that determine whether AI produces operating intelligence or expensive theater. The papers below build from that diagnosis to the lab result that tests it.

Reading order

  1. ★ Which Financial Persona Is Running Your Business? — find yourself first, then read on. ~13 minutes.
  2. The Two Perspectives — the AI-readiness diagnostic. ~16 minutes.
  3. Tax Ready Bookkeeping + The AI Stack — the bookkeeping-specific application. ~29 minutes.
  4. The CFO Operating System — the Stage-4 advisory layer; what clean books are for. ~15 minutes.
  5. ProjectBits Thought-OS™ — the full methodology umbrella. ~9 minutes.
  6. AI Debt: The Tax on Small Business — the cost of deploying AI without naming the decisions first. ~22 minutes.
  7. The Five Questions Test — the lab result: why clean books beat AI infrastructure. ~22 minutes.
  8. The Hill-Climbing Machine — the ecosystem view: what Satya Nadella got right, and the SMB foundation he skipped. ~20 minutes.
  9. The Third Perspective — People, Preparation & Readiness; the human discipline behind the harness, for change-management professionals. ~30 minutes.
  10. The Managed Initiative — the governance capstone: run an AI initiative the way product teams run products, translated for the $5M–$25M owner. ~30 minutes.
  11. Signal Clarity. Owner Amplification. — the owner’s time is fixed; the return on it is not. The governing layer that amplifies the owner’s judgment, proven on the practice’s own pipeline. ~28 minutes.

Don Lovett, Fractional CFO & Managing Principal · ProjectBits Consulting · July 2026


Bottom line up front: A self-hosted AI agent that touches financial credentials needs a boundary money can’t cross without a human present. Container isolation and local inference do not provide it — the risk lives in what the agent is permitted to reach, not where the model runs. This piece is the reference architecture for that boundary: a hardware key (YubiKey) that gates every human-tier action, scoped credentials for the agent, and an append-only event log that records who reached what, under which approval. It is a design to stand up, not a report of a running system.

A nine-minute explainer walking through both halves of this architecture — the physical boundary and the work that waits on a human.


A Gap You May Not Have Run Into Yet

Hermes Agent, released by Nous Research in February 2026, ships with serious container-level security: read-only root filesystem, dropped Linux capabilities, process ID (PID) limits, zero telemetry, all data stored locally in ~/.hermes/. For a self-hosted open-source AI agent, that is a strong baseline.

What you may not have encountered yet — depending on what you are using Hermes to automate — is the absence of a hardware attestation layer: a physical boundary between what a human operator can do and what the agent can do autonomously.

For general automation tasks that gap rarely surfaces. For the use case this piece works through — designing a system where Hermes Agent automates utility portal logins, bill downloads, and payments, with real credentials and real money in scope — it becomes something you need to have thought about before you build, not after.

This is a reference architecture and build guide. It describes a design you would stand up, the boundaries that design enforces, and the sequence you would follow to enforce them. It is not a report that this pipeline is currently running against a client’s live financial accounts.


The Use Case That Forces the Issue

Utility portals — electric, gas, telecom, cell carriers — have increasingly moved to logins protected by multi-factor authentication (MFA). Automating bill download and payment against these portals means an agent would need to:

  1. Retrieve stored credentials
  2. Submit username and password to the portal
  3. Handle MFA — a time-based one-time password (TOTP) code, a short message service one-time password (SMS OTP), or a push approval
  4. Navigate to the bill, download the PDF
  5. Optionally execute payment
  6. Write the transaction to QuickBooks Online (QBO) with the PDF attached as supporting documentation

Steps 1 through 6 are all automatable. Step 3 is where MFA handling determines whether you need a phone in the loop or not.

In this design, the credential store is HashiCorp Vault. Browser automation runs via Playwright. Orchestration runs through Hermes Agent on a dedicated Proxmox virtual machine (VM). The Model Context Protocol (MCP) surface at mcp.projectbitz.com — a Cloudflare-gated REST service — is where a shared tool pattern like the one below would live so that multiple agents could call the same governed implementation.

The design question that drives the whole architecture is: what stops a compromised agent session from reading Vault credentials directly, without any human involvement?

The answer this architecture proposes is a physical key.


What Happens When Agents Touch Credentials Without Governance

The risk is not abstract and it does not require a malicious actor. It requires only an agent operating outside a defined boundary — which is the default state of most agentic deployments.

Consider what Hermes Agent has access to in a typical self-hosted deployment:

  • A credential store or environment variables containing portal usernames and passwords
  • A browser automation capability that can navigate, click, and submit forms
  • An execution environment with network access to financial portals
  • Persistent memory in ~/.hermes/ that accumulates context across sessions

None of those capabilities are dangerous individually. Combined without a governance layer, they describe an agent that can authenticate to financial accounts, execute transactions, and remember what it did — with no requirement for human presence at any step.

The failure modes are not hypothetical:

Scope creep without audit trail. An agent tasked with downloading a bill navigates further than intended — checking account balances, clicking through to payment history, or following a portal link to a connected account. Without an append-only event log, you may not know what the agent accessed during a session.

Credential exposure in logs. Agent frameworks log extensively for debugging. Without explicit controls, credentials retrieved from environment variables or a credential store can appear in plaintext in session logs, memory files, or error traces. ~/.hermes/ persists across sessions by design — whatever lands there stays there.

Compromised session escalation. If a Hermes session is compromised — through a malicious tool call, a prompt injection via portal content, or a supply chain issue in a dependency — an attacker inheriting that session inherits everything the agent can reach. If the agent can reach your credential store without a scoped access boundary, the attacker can too.

No distinction between human and agent access. When credentials live in a shared location accessible to both human operators and agents without differentiation, a credential audit cannot tell you whether a human or an agent read a secret, when, or why. For a practice holding client financial credentials, that ambiguity is a liability.

The pattern underlying all of these is the same: the agent and the human operator share an access surface with no physical boundary between them. The design in this piece addresses exactly that.


Running Local Does Not Close This Exposure

This point is worth stating plainly because it runs counter to a common assumption in the self-hosted AI community. Local inference — whether via Ollama, vLLM, or any other on-premise LLM runtime — means your prompts and completions do not leave your network. That is a meaningful privacy protection and a legitimate reason to self-host.

It does not protect you from what happens on the outbound side of an agent action.

When an agent authenticates to a utility portal, executes a payment, or reads from a credential store, those actions travel outbound to external systems regardless of where the model inference ran. The model being local controls data flow into the LLM. It does not control what the agent does with the result. An ungoverned agent running against a local Llama 3 model has exactly the same credential exposure surface as one running against a cloud API — because the risk lives in the action layer, not the inference layer.

There is a second dimension that compounds this. Once confidential information exits your infrastructure — whether through a compromised session, an unscoped credential read, or an agent action that traveled further than intended — the speed at which it propagates is no longer under your control. Credentials posted to a breach aggregator, account data harvested through an authenticated session, or financial information exposed via an agent log are not contained by your network perimeter. They move at internet speed, across systems you have no visibility into, and the window between exposure and consequence is measured in minutes, not days. The local model that processed the original request is irrelevant at that point. What matters is what the agent was permitted to reach, and whether any governance layer was in place to limit and log that reach before the session ran.

Local inference is a privacy control. It is not a substitute for access governance.

There is no undo button for a breach.


How the Browser Automation Connects

In this architecture, the browser automation runs via the Chrome DevTools Protocol (CDP) on the utility VM. Playwright connects to a CDP endpoint to control a Chromium instance programmatically, giving the agent a fully controllable browser without requiring manual interaction.

The browser can run in either of two modes depending on the portal:

Headless — no visible browser window, lower resource overhead, appropriate for most utility portals that do not actively detect automation. This would be the default operating mode for unattended overnight runs.

Headed — a full visible browser window running on the utility VM. Some portals rate limit or block sessions they identify as automated, using signals like user agent strings, canvas fingerprinting, missing browser plugins, or behavioral timing patterns. A headed Chromium instance presents a full rendering environment that passes most of these checks because the browser is genuinely rendering — it is not simulating a browser, it is one. When a portal resists headless sessions, switching to headed mode resolves the issue without changing any other part of the workflow.

The CDP port would be bound to localhost on the utility VM only. It is not exposed to the network. This is a critical security constraint — an exposed CDP port gives any reachable client full browser control, including the ability to read page content, intercept form submissions, and navigate freely. Localhost binding combined with the SSH access controls on the utility VM keeps that surface contained.

In this design the YubiKey is physically attached to that same VM. When Playwright reaches an MFA screen, ykman generates the TOTP code in the same execution environment and passes it to the browser session directly — no network hop, no external service, no phone.


The Core Argument: Physical Presence as a Governance Boundary

A mature governance framework has a concept of humans above the loop — actions the agent proposes, humans approve, agents execute. The YubiKey makes that boundary physical rather than merely logical.

[Visual here — https://projectbits.com/wp-content/uploads/2026/07/projectbits-risk-lives-in-reach.png — The risk lives in the reach, not the container: a well-isolated agent still reaches bank APIs, vaults, and portals.]

Without a hardware attestation layer:

Compromised agent session
    → reads AppRole token from environment
    → calls Vault API
    → reads all utility credentials
    → executes payments without human knowledge

With a hardware attestation layer:

Compromised agent session
    → reads AppRole token from environment
    → calls Vault API
    → gets scoped read access to approved paths only
    → cannot escalate to human-tier Vault access
    → cannot modify workflow logic in n8n
    → cannot alter Open Policy Agent (OPA)/Rego policy files
    → physical key required for all of the above

The YubiKey does not stop the agent from doing its job. It stops anything — including a compromised agent — from doing things only a human should do.


Infrastructure Overview

For context, the stack this design integrates with:

ComponentRole
Proxmox clusterHypervisor, two Dell R730s
Hermes Agent VMNous Research Hermes Agent, dedicated VM
Utility VMPlaywright browser automation, ykman
HashiCorp VaultCredential store, per-tenant namespacing
n8nDeterministic workflow rail, webhook handling
Flowroute DIDDirect Inward Dial (DID) number for inbound SMS OTP codes (10DLC registered)
Twenty CRMHuman-facing approval surface
PostgresAppend-only event log (events.operational_log), MFA correlation table
MCP serverCloudflare-gated REST service where a shared tool pattern would be exposed to agents
Cloudflare Zero TrustTunnel and access policy enforcement
OPA/RegoDeny-by-default policy on every agent action

YubiKey Deployment Architecture

Two Keys, Two Roles

One YubiKey is never enough for production infrastructure. The posture this design uses:

  • Primary — USB passthrough to the utility VM via Proxmox, permanently attached, used by ykman automation
  • Secondary — enrolled identically on every account and system, stored offline in a physically secure location, never used except when primary fails

Every enrollment step below is performed on both keys simultaneously. If you enroll only the primary and the primary fails, every portal account becomes inaccessible.

What the YubiKey Protects

Human access paths — requires physical key. In the table below, FIDO2 (Fast IDentity Online 2) is the phishing-resistant hardware-credential standard the YubiKey speaks, and ZT is Cloudflare Zero Trust:

SystemMethodWhat it protects
Vault (human CLI/UI)FIDO2 or TOTPCredential read/write, policy admin
Proxmox web UITOTP slotAll VM console access
Proxmox SSHFIDO2 PAMHost-level shell access
n8n admin UIFIDO2 via Cloudflare ZTWorkflow logic modification
OPA/Rego policy filesSSH gate to policy VMDeny-by-default rules
MCP server adminFIDO2 via Cloudflare ZTTool definition and modification
TrueNAS admin UITOTP slotFile system and SMB config
Microsoft 365FIDO2 via Entra IDM365 tenant, Graph API admin

Agent access paths — no physical key, scoped credentials:

SystemMethodScope
VaultAppRole tokenRead-only, specific paths
PostgresService accountInsert to events.operational_log, MFA correlation table
Twenty CRMAPI keyTask creation, status updates
MCP server toolsPer-agent credentialCall existing tools, not define new ones

(AppRole is Vault’s machine-to-machine authentication method — a role identifier plus a secret identifier, in place of a human login.)

The boundary is explicit: humans need the key, agents do not. But agents cannot reach human-tier access without the key.


Implementation Guide

The steps below are a build guide. None of this is deployed yet — the checklist at the end of this piece is the sequence you would work through to stand it up.

Part 1: YubiKey Enrollment on Vault

Vault supports multiple auth methods simultaneously. The goal is to add FIDO2 as the human auth method while preserving AppRole for agent access.

Enable the TOTP secrets engine (for generating agent TOTP codes — separate from human auth):

vault secrets enable totp

Enable OpenID Connect (OIDC) auth for human access backed by Entra ID:

# Enable OIDC auth method pointing to your identity provider
vault auth enable oidc

# Configure with your Entra ID tenant (already has YubiKey enrolled)
vault write auth/oidc/config \
  oidc_discovery_url="https://login.microsoftonline.com/{tenant}/v2.0" \
  oidc_client_id="{client_id}" \
  oidc_client_secret="{client_secret}" \
  default_role="human-operator"

With this in place, human Vault access requires Entra ID login, which requires the YubiKey per your Conditional Access policy. AppRole remains untouched for agents.

Confirm AppRole is scoped correctly:

# Create a policy for utility agent — read only, specific paths
vault policy write utility-agent - <<EOF
path "secret/utility/*" {
  capabilities = ["read"]
}
path "auth/token/renew-self" {
  capabilities = ["update"]
}
EOF

# Bind AppRole to that policy only
vault write auth/approle/role/utility-agent \
  token_policies="utility-agent" \
  token_ttl=1h \
  token_max_ttl=4h

Enable Vault audit logging:

vault audit enable file file_path=/var/log/vault/audit.log

Once enabled, every credential read produces an immutable log entry with timestamp, accessor, and path.


Part 2: YubiKey TOTP for Portal Automation via ykman

Install ykman on the utility VM:

sudo apt install yubikey-manager
ykman --version

Configure Proxmox USB passthrough for the primary YubiKey:

# Find YubiKey USB ID on Proxmox host
lsusb | grep Yubico
# Bus 001 Device 003: ID 1050:0407 Yubico.com Yubikey 4 OTP+U2F+CCID

# Add to VM config
qm set VMID --usb0 host=1050:0407

Verify inside the utility VM:

ykman list
# YubiKey 5 NFC [OTP+FIDO+CCID] Serial: XXXXXXXX

Enroll a utility portal:

# From the QR code URI during portal enrollment
ykman oath accounts uri "otpauth://totp/PortalName:user@domain.com?secret=BASE32SECRET&issuer=PortalName"

# Or enter the base32 seed directly
ykman oath accounts add "Verizon-Business" --oath-type TOTP BASE32SECRETHERE

# Verify
ykman oath accounts code "Verizon-Business"
# Verizon-Business  847291

Enroll the secondary YubiKey identically before the enrollment window closes.

Store the slot name in Vault — not the seed:

vault kv put secret/utility/verizon \
  username="admin@projectbits.com" \
  password="[password]" \
  ykman_slot="Verizon-Business" \
  payment_method="CreditCard" \
  portal_url="https://businesslogin.verizon.com"

The seed never enters Vault. Only the slot name is stored — meaningless without the physical key.


Part 3: MCP Tool Pattern (Proposed)

The function below is the shape of a proposed MCP tool — a design pattern, not a running registered tool. The intent is that once exposed as an MCP tool, multiple agents could call the same governed implementation rather than each reimplementing credential handling.

import subprocess
from datetime import datetime, timezone

def ykman_totp_generate(vendor_name: str, vault_client, pg_conn) -> str:
    """
    Generate current TOTP code for a utility portal via YubiKey.
    Logs invocation to events.operational_log. Seed never leaves the hardware.
    """
    # Read slot name from Vault
    secret = vault_client.secrets.kv.read_secret_version(
        path=f"utility/{vendor_name}"
    )
    slot_name = secret["data"]["data"]["ykman_slot"]

    # Generate code via ykman subprocess
    try:
        result = subprocess.run(
            ["ykman", "oath", "accounts", "code", slot_name],
            capture_output=True, text=True, timeout=10
        )
        if result.returncode != 0:
            raise RuntimeError(f"ykman error: {result.stderr}")

        # Parse "Verizon-Business  847291" → "847291"
        code = result.stdout.strip().split()[-1]

    except subprocess.TimeoutExpired:
        raise RuntimeError("YubiKey did not respond within 10 seconds")

    # Log to events.operational_log — slot name only, never a credential value.
    # (events.operational_log is a process-mining event spine: one row per activity,
    #  keyed by process_type + case_id, with actor attribution.)
    with pg_conn.cursor() as cur:
        cur.execute("""
            INSERT INTO events.operational_log
                (tenant_id, process_type, case_id, activity,
                 actor_type, actor_id, timestamp, status, source_table)
            VALUES
                (%s, 'utility_payment', %s, 'totp_generated',
                 'agent', 'ykman', %s, 'OK', %s)
        """, (tenant_id, correlation_id, datetime.now(timezone.utc),
              f"ykman:{slot_name}"))
        pg_conn.commit()

    return code

Exposing ykman_totp_generate as an MCP tool rather than agent-specific code means Claude, Hermes, and any future agent would call the same implementation. Build once, all agents consume it. No agent has access to the underlying seed — only to the current code the hardware produces. The governance lives in the tool, not in the agent.


Part 4: SMS OTP via Flowroute

For portals using SMS OTP, the design has Flowroute handle inbound codes via an n8n webhook.

Postgres correlation table (an MFA-correlation table this design defines):

CREATE TABLE mfa_correlation (
    id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    correlation_id  text NOT NULL UNIQUE,
    vendor_name     text NOT NULL,
    did             text NOT NULL,
    code            text,
    status          text DEFAULT 'WAITING',
    created_at      timestamptz DEFAULT now(),
    fulfilled_at    timestamptz,
    CONSTRAINT status_check CHECK (
        status IN ('WAITING', 'FULFILLED', 'EXPIRED', 'TIMEOUT_ESCALATE')
    )
);

n8n webhook handler — Flowroute inbound SMS:

const body = $input.first().json;
const messageText = body.body;

// Extract 4-8 digit OTP code
const codeMatch = messageText.match(/\b(\d{4,8})\b/);
if (!codeMatch) {
  return [{ json: { status: 'no_code_found', message: messageText } }];
}

return [{
  json: {
    code: codeMatch[1],
    from_did: body.from,
    received_at: new Date().toISOString()
  }
}];

Playwright polling pattern:

async def handle_sms_mfa(page, vendor_name: str, pg_conn) -> str:
    correlation_id = str(uuid.uuid4())

    with pg_conn.cursor() as cur:
        cur.execute("""
            INSERT INTO mfa_correlation (correlation_id, vendor_name, did, status)
            VALUES (%s, %s, %s, 'WAITING')
        """, (correlation_id, vendor_name, 'YOUR_FLOWROUTE_DID'))
        pg_conn.commit()

    timeout_seconds = 180
    elapsed = 0

    while elapsed < timeout_seconds:
        await asyncio.sleep(5)
        elapsed += 5

        with pg_conn.cursor() as cur:
            cur.execute("""
                SELECT status, code FROM mfa_correlation
                WHERE correlation_id = %s
            """, (correlation_id,))
            row = cur.fetchone()

        if row and row[0] == 'FULFILLED':
            return row[1]

    raise TimeoutError(f"SMS OTP not received within {timeout_seconds}s")

Part 5: Break Glass Protocol

One YubiKey in production requires a documented emergency bypass. The design principle: break glass exists, using it is auditable and deliberate, and it is never easier than the normal path.

Vault break glass token:

vault policy write break-glass - <<EOF
path "secret/utility/*" {
  capabilities = ["read"]
}
path "secret/tenant/*/utility/*" {
  capabilities = ["read"]
}
path "sys/*" {
  capabilities = ["deny"]
}
EOF

vault token create \
  --policy="break-glass" \
  --ttl="8760h" \
  --display-name="break-glass-2026" \
  --no-default-policy

The token would be printed once, never stored digitally, placed in a sealed physical envelope.

Proxmox console break glass:

useradd --shell /bin/bash breakglass
passwd breakglass  # strong random password → envelope

echo "DenyUsers breakglass" >> /etc/ssh/sshd_config
systemctl reload sshd

Physical console access requires physical presence at the machine.

YubiKey enrollment registry (a registry table this design defines):

CREATE TABLE yubikey_enrollment_registry (
    id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    system_name         text NOT NULL,
    account_identifier  text NOT NULL,
    ykman_slot          text,
    primary_enrolled    boolean DEFAULT false,
    secondary_enrolled  boolean DEFAULT false,
    enrollment_date     timestamptz,
    last_verified       timestamptz,
    notes               text
);

When the primary fails, query this table to know exactly which accounts the secondary covers and which need immediate attention.

Annual renewal reminder — n8n scheduled workflow:

Schedule: First Monday of June annually
Action: Create Twenty CRM task
Title: "Break glass credentials — annual review"
Notes: "Verify Vault token expiry. Regenerate if within 60 days.
        Update sealed envelope. Confirm secondary YubiKey enrollment."

Part 6: QBO Close — Bill to Payment to Attachment

Sequence (order matters):

def post_utility_bill_to_qbo(
    vendor_name, amount, due_date, pdf_path,
    confirmation_number, qbo_client, vendor_config
):
    # 1. Create Bill
    bill = Bill()
    bill.VendorRef = {"value": vendor_config["qbo_vendor_id"]}
    bill.DueDate = due_date
    bill.Line = [{
        "Amount": amount,
        "DetailType": "AccountBasedExpenseLineDetail",
        "AccountBasedExpenseLineDetail": {
            "AccountRef": {"value": vendor_config["qbo_account_id"]}
        }
    }]
    bill.PrivateNote = f"Automated via Hermes | Confirmation: {confirmation_number}"
    bill.save(qb=qbo_client)

    # 2. Create BillPayment linked to Bill
    payment = BillPayment()
    payment.VendorRef = {"value": vendor_config["qbo_vendor_id"]}
    payment.PayType = vendor_config["payment_method"]
    payment.TotalAmt = amount
    payment.Line = [{
        "Amount": amount,
        "LinkedTxn": [{"TxnId": bill.Id, "TxnType": "Bill"}]
    }]
    payment.save(qb=qbo_client)

    # 3. Attach PDF to Bill transaction
    # POST to /v3/company/{realm_id}/upload — multipart with base64 PDF
    # Links PDF to bill.Id via Attachable API

    return {
        "bill_id": bill.Id,
        "payment_id": payment.Id,
        "confirmation_number": confirmation_number
    }

Idempotency guard — if the QBO write fails after portal payment succeeds:

INSERT INTO events.operational_log
    (tenant_id, process_type, case_id, activity,
     actor_type, actor_id, timestamp, status, variant_flags)
VALUES (
    'projectbits-internal', 'utility_payment', 'CONF-ABC123',
    'payment_executed_qbo_pending', 'agent', 'hermes', now(),
    'PENDING', 'portal-paid,qbo-unconfirmed'
);

A scheduled n8n workflow would scan for payment_executed_qbo_pending events older than 15 minutes and retry the QBO write or escalate to a Twenty CRM task.

A framing note that governs this whole section: what the event log records is that an action was taken under a defined process and that an approval existed — not a certification that the resulting books are correct. ProjectBits’ role is advisor and preparer, not attestor. The audit trail below evidences process and access; it does not vouch for the accuracy of a client’s financial results.


The Audit Trail

The blocks below are illustrative — example log entries showing the audit chain this design would produce for one utility payment workflow. They are not captured logs from a live run.

Vault audit log (illustrative example entry):

time=2026-06-21T14:32:07Z type=response
accessor=utility-agent/xxxxx
path=secret/data/utility/verizon
operation=read

Append-only event log — illustrative events.operational_log timeline this design produces:

14:32:05  workflow_initiated         vendor=verizon
14:32:07  totp_generated             ykman slot: Verizon-Business
14:32:09  portal_login_attempted     url: businesslogin.verizon.com
14:32:11  mfa_submitted              method: totp
14:32:14  portal_authenticated       session established
14:32:18  bill_downloaded            /mnt/bills/verizon_2026-06.pdf
14:32:20  human_approval_required    amount: $127.43
14:32:47  human_approval_received    Twenty CRM task approved
14:32:49  payment_executed           confirmation: CONF-ABC123
14:32:51  qbo_bill_created           bill_id: 1047
14:32:52  qbo_payment_created        payment_id: 1048
14:32:54  qbo_attachment_created     verizon_2026-06-21.pdf → bill 1047
14:32:54  workflow_completed

The value of this chain is that it records who or what reached which credential, under what approval, and when. It is an access-and-process record. It does not, and is not intended to, certify that the payment amount or the resulting bookkeeping is correct — that judgment stays with the human above the loop.


What This Enables Going Forward

FIDO2 / passkey readiness. As utility portals migrate to passkey authentication, the YubiKey 5 series handles FIDO2 natively. When a portal adds passkey support, you would enroll the key once and remove the TOTP slot. No infrastructure change required.

Multi-tenant extension. The utility_vendors table is designed to be tenant-aware. A second client’s utility accounts would get their own Vault namespace, their own AppRole, and their own ykman slots. The same MCP tools would serve all tenants; isolation is enforced at the Vault and Postgres policy layers.

The shared MCP surface. Because ykman_totp_generate is designed as an MCP tool rather than Hermes-specific code, Claude, Hermes, and any future agent would call the same implementation. The governance layer lives in the tool, not in the agent.


Closing

Hermes Agent ships with strong process isolation. What it does not ship with is a physical boundary between what humans control and what agents execute autonomously.

The YubiKey provides that boundary — not by restricting what the agent can do within its authorized scope, but by ensuring that the scope itself, the credentials it reads, the workflows that govern it, and the policies that constrain it cannot be modified without physical human presence.

For self-hosted AI agents designed to operate against real financial accounts, that boundary is not optional.

[Visual here — https://projectbits.com/wp-content/uploads/2026/07/projectbits-hardware-attestation-infographic.png — Securing AI with hardware attestation: the human and agent access paths at a glance.]


Implementation Checklist

Use this as the build sequence. Every item is unchecked because this is a design to be stood up, not a running deployment.

  • [ ] Primary YubiKey USB passthrough configured on utility VM (qm set VMID --usb0 host=1050:0407)
  • [ ] ykman installed and recognizes key (ykman list)
  • [ ] Secondary YubiKey purchased and available for simultaneous enrollment
  • [ ] Vault OIDC auth configured with Entra ID (YubiKey-enforced Conditional Access)
  • [ ] Vault AppRole for utility agent scoped to secret/utility/* read-only
  • [ ] Vault audit logging enabled (vault audit enable file)
  • [ ] Break glass token generated and in sealed physical envelope
  • [ ] Break glass annual renewal reminder in n8n scheduler
  • [ ] ykman_totp_generate MCP tool implemented and exposed
  • [ ] MFA correlation table created with indexes
  • [ ] Flowroute SMS webhook configured to n8n endpoint
  • [ ] mfa_create and mfa_poll MCP tools implemented
  • [ ] YubiKey enrollment registry table created
  • [ ] Each portal enrolled on primary key with ykman oath accounts add
  • [ ] Each portal enrolled on secondary key with same seed/QR
  • [ ] Cloudflare Zero Trust policy: n8n admin requires hardware key
  • [ ] OPA/Rego policy files on SSH-gated VM
  • [ ] utility_vendors table populated with ykman_slot per vendor
  • [ ] QBO Bill → BillPayment → Attachable workflow implemented
  • [ ] Idempotency guard for payment_executed_qbo_pending events
  • [ ] events.operational_log reviewed for one complete end-to-end workflow run
  • [ ] CDP headless vs. headed mode tested per portal
  • [ ] CDP port confirmed bound to localhost only

Don Lovett is the founder and Managing Principal of ProjectBits Consulting, a fractional CFO and financial operations practice based in Reston, VA. He works with owner-operated businesses on financial maturity, AI governance, and the CFO Operating System™ framework. This post is part of the ProjectBits position paper series.

Scroll to Top