Security — Threat model + defense layers
Threat model
| # | Threat | Impact | Defense |
|---|---|---|---|
| T-1 | MFA bombing (approval fatigue) | User taps [Approve] without thinking | 6-digit number_match + 10-minute cool-down + in-flight suppression + Rack::Attack throttle |
| T-2 | Agent token theft | Attacker fires arbitrary approval requests | signed mode = Ed25519 key separation (the token alone is useless), instant revoke, optional IP allowlist |
| T-3 | Push not delivered (Focus mode) | TTL exceeded → work blocked | iOS time-sensitive notification + FCM HIGH priority + user-defined TTL of 30s–30min |
| T-4 | Shifting blame (user repudiation) | In a dispute, "I didn't tap that" | WebAuthn assertion + display_payload_hash seal + binding_message + device ID + IP record |
| T-5 | Audit log leak | High-value transaction data exposed | Application-layer encryption (encrypts :context_ciphertext), hash chain + DB WORM trigger, separate retention policy |
| T-6 | Agent bypasses logi | Ignores the rules and acts directly | logi cannot prevent this — we provide a post-hoc audit reconciliation cookbook |
| T-7 | Agent identity spoofing | Impersonating another agent | Asymmetric key issued at registration, used to sign requests, "verified" badge in the user's inbox |
| T-8 | Replay attack | Resending a captured signature | timestamp ±5min + single-use nonce (5-minute window) + single-use idempotency_key |
| T-9 | Display tampering | Agent's display ≠ server's record | display_payload_hash seals every field the user saw with SHA-256, verified at decision time |
| T-10 | auth_mode downgrade | Attempting to demote a signed agent to bearer_only | auth_mode is loaded only from the DB row; request parameters are ignored |
| T-11 | Host bypass | Calling the mobile API from an attacker's host | The mobile endpoint enforces an api.1pass.dev exact-match host constraint (end_user_host_constraint) |
| T-12 | Hash chain race | Concurrent writes fork the chain | chronological.lock.last + transaction (borrowed from the Authentication::AuditLogger pattern) |
Defense layers in detail
6-digit Number Matching
The same 6 digits appear on the user's phone screen and on the agent's side. After confirming they match, the user uses Face ID. This gives an attacker a 1-in-1,000,000 chance of guessing correctly.
SecureRandom.random_number(1_000_000) gives a uniform distribution — no modulo bias.
2 digits (1%) is not safe — even with the uniform distribution of random_number(100), 100 attempts will pass right away.
Ed25519 signature (signed mode)
Every request's canonical string includes the method, path, timestamp, nonce, body digest, and agent ID. If any one of them is tampered with, the signature breaks.
The key is shown only once at registration, and the server stores only the public key. If a key is leaked, the user presses rotate_credentials! in the console and everything — including in-flight requests — is invalidated.
In-flight suppression
If a pending or delivered request already exists for the same (user, agent, action_type), a new request returns the existing row as-is. No new push is sent. Even 100 POSTs in one second produce just one notification on the user's phone.
Cool-down
For 10 minutes after a rejection, a new request with the same action_type is blocked with 429. This stops infinite retries.
WORM (Write-Once-Read-Many)
agent_action_audit_logs:
- Rails
readonly?is true — an attemptedupdate!/destroy!raisesActiveRecord::ReadOnlyRecord - PostgreSQL trigger — even raw SQL
UPDATE/DELETEis blocked (the only bypass is thelogi.allow_agent_audit_purgesession flag) - Hash chain — forging even a single row breaks every row after it in the chain
User control
In the "Agents" tab of the logi mobile app, the user can:
- View the list of registered agents and each one's last-used time
- Instantly revoke an individual agent
- Report suspicious activity to the logi security team
Cautions when using bearer_only mode
| Item | Recommendation |
|---|---|
| Environment | demo / dev / local testing only |
| Token expiry | default 30 days; shorter is better |
| IP allowlist | required. A single IP or a narrow CIDR |
| Production | can be blocked with the AGENT_BEARER_ONLY_ALLOWED=false environment variable |
| Audit | every request records auth_mode, so it can be separated out after the fact |
If a single bearer_only token leaks, an attacker can immediately issue arbitrary approval requests with 100% success. Take signed mode as your default.
Reporting security issues
Email security@1pass.dev — PGP or plaintext both welcome. We reply within 24 hours.
A bounty program is coming. We'll announce it later on a separate page.