SecurityThreat model
Threat model#
Who is the attacker, and what can they do?
On this page
What Mehen protects, from whom, how, and where each protection stops. The assets and the short list of limits are on What Mehen protects, and what it does not; the model it protects is on The model in one page.
Mechanisms in one place#
Each mechanism, and what it stops.
| Mechanism | Stops |
|---|---|
| No value returned to a device except a lease; relay and placeholder modes first | Secrets in context, transcripts, logs and files. |
Reveal is human only: a device lease needs a ceiling of exactly lease, and device scopes cannot include reveal; no route shows a stored value to anyone | A ceiling raised so a human can view a password widening what devices get; an owner or admin reading a colleague's credential. |
| One access check decides every human and machine right from live state, after the last step; a machine's reach is its scope intersected with its owner's current rights | A machine outliving a removed, suspended or demoted owner; a share that expired still being honoured; a stale cached decision. |
A credential belongs to one home: the workspace, a project or one member; out of reach is not_found, byte for byte as absent, before status, ceiling or OAuth state is read; creation under a taken name is one generic conflict, rate limited | Learning that a colleague's private credential exists, its status or its provider by naming it; a reconnect link minted for a credential the caller may not use. |
A team's or a guest's machine reaches a workspace credential only through a project environment that references it, named in the request, never implicitly; a project's own credential needs use on that project; a private credential is reached only by its owner's own ticked machine (a guest's included) and by nobody else's | A shared CI or chat machine (a confused deputy) using whatever the workspace holds; a colleague's or a team's machine using someone's private credential. |
Protected use needs the explicit protected environment that references the credential, the owner's protected right and maxEnvironment: "protected"; four-eyes needs a manager of every applicable project other than the owner; loosening protection on an environment that references a workspace credential, and any fence change, is owner or admin only | A prod credential reached through a dev context or a self-approved run; a project manager quietly unprotecting what an owner delegated. |
| Required binding, one matcher, canonical paths, ambiguous paths refused | Sending a credential anywhere it was not meant to go. |
| Restrictive rules matched on a decoded, lower-cased path; permissive ones on the exact canonical path | Dodging a deny or approve rule with %73end or SendMail. |
IP literals, loopback, *.workers.dev and the vault's own host only when bound exactly | A wildcard binding turning the relay into an open proxy. |
| Declared injection surface; the vault sets the header, replacing any copy sent; placeholders refused elsewhere and recorded | Smuggling a token into a body, query string or comment on a bound host. |
| Method-override headers and parameters refused | A GET-only rule admitting a DELETE. |
| Policies combined as deny over approve over allow; body matchers fail closed | A new rule silently opening something; GraphQL mutations passing as reads. |
Templates fail closed: reads allowed, other methods held for approval unless a narrow everyday write is allowed, the worst routes denied (Gmail drafts/send, Graph $batch and send, forward or reply of a stored message, forwarding, rules, keys, webhooks, collaborators) | Writes nobody looked at; sending mail without a real approval; persistence at the provider. |
| Approvals bind item, method, URL, content type, content encoding, forwarded-header hash and body hash; an eligible decider signs within their own rights, and grants and runs become stale when the recorded decision basis goes | Agents approving; approvals reused for other requests; the same bytes read another way; a guest approving a manager's ask; a consent surviving the decider's removal. |
| Approvals are bound to hashes of the exact request; the page shows its headers and a presentation of its body (JSON formatted, forms decoded, email decoded where recognised, a non-UTF-8 body only as a byte count) under a decision card drawn from that presentation; compressed bodies, bodies over 64 KiB and forwarded headers over 16 KiB are refused for approval | A truncated preview hiding a recipient or payload. The presentation is not a lossless raw-byte view, and a binary body cannot be inspected on the page. |
| Hidden and text-direction characters spelled as markers where they sit, with a warning; markers styled only when the vault generated them, as structured segments, never by matching marker-shaped text | fdp.exe shown as exe.pdf; an attacker typing [RLO U+202E] to earn the trusted style. |
| Policy notes read as fact only for a verified built-in template; anything an agent could have written is described from its matchers | A rule noted "reads mail" that allows sending being summarised as a read. |
| "Who asked" is the enrolled device's name, never the agent's session label; a workspace's name is data, never a selector | A session label naming an agent the owner trusts making a page say that agent asked; a label picking which workspace acts. |
| Run approvals bind the SHA-256 of the whole command line | An approved bun deploy grant spent on another command. |
mehen hands over only links on the vault's own origin, under the canonical slug of the workspace the call was made in, on the vault's ask routes; a refused link is rebuilt from the id under that slug or replaced by a warning, every ask carries the enrolment that raised it, and the QR code encodes exactly the printed text | A hostile or misconfigured vault sending a human to another site; a link under an alias or another workspace getting trusted chrome; a QR that differs from what the eye reads. |
One mehen mcp server serves only the workspaces its ceiling allows, resolved once at start; active slots are latched to the identity (origin, workspace, device) read at server startup; later admitted slots latch on first validation or activation, and a slot whose enrolment changes or vanishes is refused for the life of the process while the others keep serving; a later slot is admitted only by the start's resolution, never by a reused slug; sessions are cached by origin, workspace and device | A tool argument, a cwd or a replaced enrolment making a running server sign for a workspace it was not admitted to, or as a device it did not latch. |
| A machine is enrolled in a named workspace and activated only as asked: the CLI checks workspace id and slug, owner kind and a complete scope before writing, and on any mismatch attempts to revoke the new device, reporting the outcome; keys never cross workspaces or origins | A machine silently enrolled into the wrong workspace; one key serving two workspaces. |
| One terminal boundary: every human line is spelled out (controls, format characters, noncharacters) and only the styler's closed set of styles becomes an escape | Vault or ledger text moving the cursor, recolouring a line or hiding part of it. |
| One live enrolment link per key, under the workspace it was started in; the page shows three fingerprint groups, when and from where the machine asked and the complete scope, and the person whose machine it is types the hidden one and accepts with a passkey that signs the key's full thumbprint, their own account and that scope; nobody accepts for them, and a team's or a guest's machine needs a distinct admission that never satisfies acceptance | Approving a page that only looks right; guessing the hidden group through the page; an admin planting a machine in a colleague's name; a guest's machine admitted by the guest. |
| Widening a machine's scope needs its owner's own fresh acceptance; narrowing is compared on effective sets (empty as all); owners and admins are told of every new machine and can only narrow or revoke | A machine widened behind its owner's back; a widening smuggled as a rename. |
| The account directory stores an enrolment code hashed only, and a connect code likewise, bound to one account and workspace for a day; a connect code routes an enrolment to that account's inbox and claims it, and is never what accepts it; every refused connect code answers with the same bytes after a per-address limiter | A directory reader claiming a pending key with a passkey; a guessed connect code steering a machine into someone's inbox. |
| Pending enrolments are listed only to the people the workspace's own access check says may act on them, at read time: owners and admins of the intended workspace (an unclaimed own-machine start is a deliberate discovery surface for them, so a machine never waits unseen), the account a connect code claimed it for, and whoever may admit it; by id, never by code; a plain member or guest reaches an unclaimed machine only through the code the agent printed. A claimed machine is accepted only by the claimed account, enforced where the device is created | One workspace's members seeing another's pending machines; a colleague accepting a claimed machine from a list or through the code route; an id page showing a machine to someone the code route would not. |
| An enrolment attempt has a 60 minute lease renewable by the key that started it, in place, to a 24 hour cap, stored once in the directory; the signed terms never include the lease; five wrong typed groups reject the attempt across any number of renewals | A renewal reviving a rejected or superseded link; a lease change invalidating a guest's stored acceptance; renewals resetting the guessing budget. |
Device key, enclave sidecar, scope file, config and enrolment record reused only as regular files owned by this user, not symlinks, with no group or other bits; an owned state directory (MEHEN_HOME) and its slots are made private; an unsafe or symlinked ledger is not trusted as existing state | A planted or shared key becoming this machine's identity; a symlink pointing the key write at another file. |
| A key is scoped to the vault it was made for and refused, not replaced, for another | A changed MEHEN_VAULT_URL silently enrolling the same key elsewhere. |
MEHEN_WORKSPACES only narrows what a process may use, resolved once per process; nothing inside a process widens it, and mehen setup writes it into every MCP entry it adds, never wider than the shell it ran in. It is an accident control, not an identity boundary; a separate MEHEN_HOME is a separate device | A work agent requesting a credential into a personal workspace, or a setup run from a restricted shell writing an unrestricted server. |
A managed host's marker and version-3 pin are read under the user's home as the system's user database records it, looked up by uid, never from HOME; state-opening commands fail closed with key_insecure when that home cannot be resolved or its path is unsafe; pure help, guide and version output does not open the managed marker | HOME=/elsewhere mehen ... escaping a managed launcher's pin. |
An installed mehen trusts only the release keys compiled into it and the file hashes a signed manifest names. Automatic updates must be newer than the running version, at least the highest seen and above any rollback hold; an explicit force or rollback is still signature-checked. The updater replaces the binary with one journalled rename under a lock (Releases). | Whoever controls the download channel making installed machines run their binary; a downgrade to a release with a known hole. |
| A consuming app should draw a trusted vault card only for an ask from its own authenticated call, after checking exact origin, workspace and device context. Each app must enforce this boundary. | A lookalike link getting the vault's chrome, or an ask imported from another conversation or workspace. |
Step-up on every change that widens access, rebuilt by the server and bound to the workspace it is spent in (mehen-stepup/v2 with the workspace id as scope, - for account actions); only tightening actions while an account is cooling off or has a security change pending | Agents or stolen sessions moving a credential; a step-up begun on one workspace's page spent on another's; a recovered account widening anything before its older factors have had their say. |
| Invites by owners and admins only (an owner for an owner), with complete terms signed; memberships active only after an eligible confirmer signs the exact terms; email-bound invites redeemed only with a Google proof of that email | A member, or a stolen session, letting someone else in; a stale invite activating more than was granted. |
| Intake tags and the credential's home shown to the human and signed in the submit digest; a machine may only propose a home within its reach | An agent quietly making a new credential usable by another machine, or shared with the whole workspace. |
| Overrides are chosen in the browser only, by the member, for their own private credential or a plain value, in unprotected environments, and apply to their own ticked machines only; protecting an environment clears them | A member's private credential reaching a colleague's or a team's run; a dormant override reviving when protection is loosened. |
| Events about a private credential are minimised before hashing: no hosts, paths or fields, even for the owner; non-elevated readers get a filtered chain | Owners and admins reading what a colleague did with their own credential; a guest reading the workspace's history. |
| Account recovery is separate from workspace rights: no workspace role resets another person's factors; recovery cancellation tightens first and lifts restrictions last | A co-owner taking over a colleague's account, and with it their private credentials, through a reset. |
| Keyed secret fingerprints (HMAC under a per-workspace key from the root) | Offline guessing of passwords from stored data. |
| Device proofs with body hash, URL, 60 second skew and a nonce store | Bearer tokens on disk, replayed or reused requests. |
Protected environments: literal --env, an allowed device whose owner holds the protected right, an approval by an eligible decider | Prod by typo, inherited variable or branch name; a member without the protected right running prod from a permitted machine. |
| Protected items: an item any protected environment's variable uses reaches only approved protected runs, and in item mode needs the explicit protected context and an approval; only an owner or admin can point a protected environment at a workspace credential | Leasing a prod secret through a dev variable or a contextless mehen fetch; an editor delegating a workspace credential to a guest's or a team's machine. |
| Fences enforced by the vault on every run-mode request, and early by the local relay | The wrong database at the wrong time, including through mehen fetch --run. |
| Run maximum life: 12 hours, 2 hours for approved protected runs | A run, and its placeholders and approval, living forever by renewal. |
| The checkout's reported remote checked against the project's bound remotes | Accidental use of a project from a mismatching checkout. It does not attest the repository's contents or stop a hostile same-user process from changing or misreporting the remote. |
| Envelope encryption with AAD naming workspace, record type and id; one key per workspace | Ciphertext swapped between rows or workspaces. |
| Sealing secret fields in the browser | Plaintext in logs and tail sessions between the edge and the workspace's store. |
| Response scrubbing of raw, URL-encoded, JSON-escaped and supported base64 forms (not values shorter than six characters, not arbitrary transformations); gzip or br only from upstreams; no reason phrase; echoes flag the item for review | Echoed secrets reaching the agent in those forms, including in a compression the redactor cannot read. |
| Redirects not followed | Bouncing a credential to another host. |
| Run CA name constraints on both DNS names and IP addresses, path length 0, key in memory | A stolen run CA impersonating anything else. |
| Hash-chained audit, only appended by the application | An inconsistent edit or deletion. A storage or deployment authority could recompute or truncate the chain; a head saved outside the vault detects that. |
| Freeze and revoke, per workspace; removal of a person revokes their machines, deletes their private credentials and voids their decisions in one transaction | Further misuse through Mehen once noticed; a leaver's machines or decisions outliving them. Neither recalls a request already sent, undoes provider-side effects, erases a leased value or revokes a credential at its provider. |
| The identity gate: a workspace may require a verified Google identity on its domains for covered roles, checked at new sessions, acceptances, admissions and widenings and re-checked on rule change, promotion or unlink; Google only identifies, a passkey still authenticates and signs; Google is never a recovery path | A person who left the company, or never had a company identity, keeping or gaining a covered role on a still-valid passkey; a Google account standing in for a passkey. |
Actors#
A prompt-injected agent#
An agent that has read hostile text (a web page, an email, an issue, a dependency's README) and now follows someone else's instructions, with the full rights of the device it runs on.
Can:
- Use every credential its device's reach covers (its scope, under its owner's rights: the workspace's credentials for a member's machine, the referenced credentials of shared projects for a team's or a guest's, the owner's private credentials on a ticked machine), in every mode its device and the item's ceiling allow, against the hosts and paths the binding allows, within policy.
- Misuse a capability without seeing a secret: read mail the connection can read, delete what policy allows it to delete, and move data (not credentials) out through a write API on a bound host, for example by writing an email's contents into a GitHub issue with an allowed token.
- Ask a human for things: open intakes with misleading reasons, ask for tags, open connect links, and ask for approvals with a persuasive story.
- Pull the brake (
mehen freeze) or revoke its own device. That is denial of service, available to any device whose owner is not a guest, by design. - Fill the machine's queue with asks: the cap is 20 open asks per machine, which bounds the noise, not the persuasion.
Cannot:
- Read a secret value in relay or placeholder mode: no API returns one to a device (R1, R8).
- Send a credential to an unbound host, over an unlisted scheme or port, or through an ambiguous path (R2).
- Dodge a deny or approve rule by encoding or re-casing the path.
- Put a placeholder or secret into a body, query string or undeclared header and have it substituted (R9).
- Change a binding, ceiling, policy, tags, fence, protection, home or device scope, or move an existing credential: these need a passkey step-up (R5, R10).
- Approve anything, or reuse an approval for a different request: approvals need a passkey, an eligible decider and are bound to the request digest (R6).
- Reach a protected environment through
MEHEN_ENV, a default or a branch name, or get a protected item outside an approved protected run in the environment that references it (R4). - Do more than its owner: use a project its owner is not shared on, a credential outside the owner's homes, or anything at all once the owner is suspended or removed. Naming what is out of reach tells it nothing: absent and forbidden answer the same.
- Use a key outside its workspace, or make the running MCP server serve an identity outside its admitted ceiling or replace a latched device. A spanning server can deliberately use several admitted workspaces;
MEHEN_WORKSPACESis an accident control, not same-user process isolation. - Make a human act in the wrong workspace: every link it can hand over is under the canonical slug of the workspace the call was made in.
- Lease a value whose ceiling is
reveal. - Send mail through Gmail drafts, Graph
$batchor a stored Graph message without an approval that shows it.
Honest limits:
- Lease mode exposes the value to the process. If an item's ceiling is
lease, and the device and the run allow it, the agent can make the child print, transform or send the value. Redaction will not catch a transformed value. Lease is off by default for that reason. - Inside the binding and policy, the capability is the agent's to misuse. The vault narrows the blast radius; it does not judge intent. Templates allow reads and a few narrow everyday writes without asking, and an injected agent can misuse exactly those: read everything the credential can read, and make the allowed writes.
- A shared machine's capability belongs to everyone who can drive it. A team's machine on a chat platform or in CI acts for whoever can make it act; Mehen authenticates the machine and caps it at the team's shares, not the person behind the message, and removing that person from Mehen does not remove their access to the machine.
- Intake and approval pages rely on a human reading them. A custom destination is shown in red with the host spelled out, tags are shown, the decision card and the request are shown, hidden characters are marked, the binding code must match and Deny leads when the page warns, but a tired human can still approve the wrong thing. A body that is not text cannot be read meaningfully by a human, so approving one is approving it largely unseen.
- A protected run's approval binds the whole command line (its SHA-256), not what the command does: an approved
bun deployruns whatever the scripts at that commit say. - The session label is the agent's to choose. Pages never read it to say who asked, but it is shown in the details and in
mehen pending, spelled out, and an agent can make it misleading. mehen checkis advisory: it runs the vault's matcher on public metadata, locally. A wrong local answer cannot loosen anything, because the vault evaluates the real request on its own, but an agent that trusts it instead of the vault's answer can still be surprised.- Response content is attacker-controlled. The vault scrubs secrets out of responses; it does not make their content safe to follow.
- The vault cannot tell one process from another. Any process on the device, not only the agent, can use the device's capabilities.
A compromised machine#
Malware, or a hostile process, running as the same user as the agent on an enrolled machine.
Can:
- Use the device key as a signing oracle. A Secure Enclave key cannot be copied off the machine, but any process running as the same user can ask it to sign. So the attacker has every capability of the device, for as long as the machine is compromised and the device is not revoked.
- Read lease values from a leased child's environment or memory, and placeholders from any run's child. Placeholders are worthless outside their run, but the run's local relay is on the same machine while the run lives.
- Watch the human's screen and try to talk them into approving something.
Cannot:
- Copy a Secure Enclave key off the machine, so revoking the device ends the attacker's access to the vault.
- Read relay-mode secrets: they never reach the machine.
- Approve: approvals need a passkey assertion with user verification, normally on another device. If the human's passkey is on this same machine, the attacker still cannot produce an assertion without the human's touch or face, but can try to trick them into giving one.
- Widen the device's scope or move credentials (step-up).
Honest limits: The device key proves which machine is asking; it does not prove which process. Telling one process from another on the same machine is not built. A software device key is a 0600 file and can be copied, after which the attacker can sign from anywhere until the device is revoked. A machine or a key is compromised is the playbook.
A run's own program#
The program mehen run starts, which may be hostile (a malicious dependency) or simply careless.
Can:
- Read every value the run leased, and every placeholder.
- Send placeholders anywhere. Tunnelled traffic is never swapped: a placeholder sent to a host the run does not intercept travels as a worthless string, and the vault never sees it, so it is not recorded as a tripwire either.
- Ignore
HTTPS_PROXYor the run's CA (Go on macOS and some older Python builds ignore the CA variables), in which case its placeholders go straight to the upstream and fail.
Cannot:
- Get a placeholder swapped at an unbound host, outside the declared surface, or at a host fenced to another environment: the vault refuses and records it.
- Use the run's CA for anything but the bound hosts: its name constraints cover both DNS names and IP addresses.
Honest limits:
- Fences match host names only. A CNAME alias inside a wildcard binding is not caught: with an item bound to
*.example.comanddb.prod.example.comfenced to prod, a dev run reachingalias.example.com, a CNAME for the same server, is allowed. The vault never resolves DNS. The local relay refuses CONNECT to raw IP addresses while a run has fenced or intercepted hosts, and the vault relay refuses IP addresses unless bound exactly, but neither can see where a name points. - The local relay's refusals are not audited. A fenced host or an IP address refused by the local relay is refused on the machine; the vault never hears of it. Only requests that reach the vault are in the audit chain.
A malicious upstream#
An API the credential is bound to that misbehaves: echoes the secret back, redirects elsewhere, or returns hostile content.
Can:
- See the credential. It is the destination; that is unavoidable.
- Echo it back in some form, or put hostile instructions in a response.
- Redirect.
Cannot:
- For supported representations of values at least six characters long, get the echo past the scrubber in its raw, URL-encoded, JSON-escaped or supported base64 forms, in headers or body. A detected echo also flags the item for review.
- Hide an echo in a compression the vault does not decode: the relay asks for gzip or br only and refuses any other content coding with
upstream_failed. - Pass free text past the redactor in the reason phrase: it is dropped.
- Bounce the credential to another host: the relay calls upstream with
redirect: "manual", and any redirect must come back through the binding check. - Move the credential's destination with a token response: OAuth token and identity URLs are fixed in templates, and agents cannot set token URLs or scopes outside the allowed set.
Honest limit: Redaction cannot catch a transformed value (reversed, split, hashed, re-encoded in a way not listed, encrypted). Values shorter than 6 characters are not redacted at all. Hostile response content reaches the agent.
A stolen laptop#
A thief, or a finder, with an enrolled machine.
Can:
- Use the device key if they can log in as the user, or if the machine was taken unlocked: Secure Enclave keys use "after first unlock" protection so background agents keep working while the screen is locked.
- Read a
softwarekey file from an unencrypted disk.
Cannot:
- Find cached vault credential values on disk: per active workspace slot, the CLI stores its config, device key or enclave blob and public sidecar, scope and non-enrolment ask ledger. A pending slot holds the key and
enrolment.json, including the public JWK, requested intent and lease. There are no cached credential values or bearer sessions; a software device private key is nevertheless a sensitive signing credential. Ask links require a signed-in eligible human and the required passkey ceremony to complete. - Extract a Secure Enclave key.
- Approve anything without the human's passkey.
What to do: revoke the device from the browser (passkey step-up device.revoke, in each workspace it was enrolled in), or freeze the workspace first if unsure. See A machine or a key is compromised.
Someone who learns an enrolment code#
Enrolment codes travel in URL paths: /w/<slug>/e/<code> and the workspace's /enrolments/<code> API. A code can end up in browser history, request analytics or a pasted transcript. The inbox and the id form (/w/<slug>/e/enr_<id>) never carry it.
Mitigations: a link is served only under the workspace it was started in, so a code cannot be claimed into another workspace; only the person whose machine it is can accept it, with a step-up that signs the key's full thumbprint, their own account and the complete scope, so they accept exactly the key whose fingerprint they checked; they must type the hidden fingerprint group, which the page never carries and which the server checks with five attempts per row before the row is rejected; the code is stored hashed; the attempt lives 60 minutes, renewed in place by the machine that started it while something waits, never past 24 hours, and while nothing waits it lapses and cannot be accepted until the machine returns; a new attempt for the same key supersedes the older one, so there is one live link per key; the CLI checks the activated workspace against the one it asked for and, on a mismatch, writes no config, attempts to revoke the device and reports whether that succeeded, naming the device for a human to revoke when it did not.
Honest limits (low, deferred): while an enrolment is pending, another active member of the same workspace who learns the code could accept the machine as their own; the CLI then reports it enrolled under that owner's name, which the human at the terminal should read. The typed group does not stop this on its own: the page's gate hash is a 20-bit preimage, so whoever holds the page can recover the group offline; it is a forcing function for the honest owner to look at the terminal, and the server's five-attempt cap is what stops guessing through the page. Moving the code into the URL fragment, as invites already do, is deferred. An enrolment started with a connect code can be accepted only by that account, so this limit does not apply to it. A member who holds a superseded id learns that a newer link exists for the same machine and intent only when the inbox would have shown it to them anyway, and never the link of a differently claimed or differently intended attempt.
Someone who guesses connect codes#
A connect code is 8 characters from a 31-letter alphabet (about 40 bits), stored as a hash and matched exactly, so it cannot be searched prefix by prefix; starts with a code are limited to 20 a minute per address. A right guess claims a new enrolment for the code's owner, which lands in that owner's Needs you; it accepts nothing.
Honest limits: every refused code answers with the same bytes, but not in the same time: a well-formed code that still matches a suspended member's row costs one more workspace lookup before the same refusal, and a workspace that is unavailable at that point answers with an error instead. That distinguishes a matching suspended member's code from an unknown one; it reveals no account or workspace name. The per-address limit bounds one address; many addresses guessing many live codes raise the odds, so it is not a bound on a distributed guesser.
The operator#
Someone who can see the service's dashboard, logs and storage, but does not deploy code.
Can:
- See the account directory: accounts, workspaces and their slugs, memberships and roles, passkey public keys, hashed session tokens and recovery codes, hashed invite codes and their terms, Google identity subjects and emails, challenges, enrolments (the code hashed, the public key, the name, host, start context, status and wrong-group attempt count) and hashed OAuth state values. No secrets, and no enrolment code in the clear.
- Write the account directory, where the dashboard allows it, and so forge a session or a passkey: a directory writer, like a deployer, is a trusted takeover path, not a cryptographic one.
- See the service's logs. These never carry secret values, tokens, codes or nonces, and request logs are switched off because they would carry URLs and headers.
- See audit events (slugs, ids, hosts, field names).
- See each workspace's stored data: ciphertext, each field under its own data key, plus each secret's length, well-known prefix and keyed fingerprint.
Cannot:
- Decrypt stored secrets without a root key.
- Test password guesses against fingerprints: they are keyed with a key derived from the root key.
- Read secrets sealed in flight without the seal key.
Limit: metadata is visible: which items exist, their hosts, who used them and when, and how long each secret is.
Whoever can deploy the vault#
Anyone who can deploy the vault's code, or change its keys.
Can: change the code, and so read every secret as it is used or as it sits encrypted, because the running vault holds the root key. Whoever can deploy the vault's code can read every secret while it is used. The same goes for whoever can write the account directory, run the account recovery procedure or issue app sessions: those are the operator's powers, guarded as one.
Cannot: without writing trusted identity state, use members' passkeys to forge step-ups on their behalf against an honest deployment. Anyone who can write the account directory can substitute a passkey for an account and sign with the matching key, no code change needed, so deploy rights are not the only takeover path. Against a modified deployment, the checks are whatever the attacker's code says.
Limit: Whoever can deploy vault code, write trusted account identity state or operate account recovery can take over the vault's trust boundary. The operator must restrict those powers independently of agent access. These pages do not establish current operator-machine isolation or backup custody.
The host#
Can: in principle read everything: Cloudflare runs the code, holds the vault's keys and stores the data. A legal demand, an insider or a platform compromise falls here.
Cannot: decrypt a sealed tier at rest. A sealed tier, not built, would additionally wrap chosen items under a key from the owner's passkey; even then, a relayed credential must be plaintext in the vault while it is used. An external key management option is not built either.
Limit: for now the vault trusts its host completely.
A phishing page#
A page that imitates the vault or a provider, linked from an email, a chat or a hostile agent.
Cannot:
- Use a passkey: passkeys are bound to the relying party
app.mehen.dev, so a look-alike domain gets nothing it can replay. - Finish an OAuth connection with the attacker's account against your vault: connect links need the requester's signed-in vault session and a passkey confirmation of the connection's terms, are single use and last 15 minutes,
stateis bound to the member, and the verified account is compared with the expected one. - Pass a Google sign-in off as yours: the identity check is a purpose-bound flow with state, nonce and PKCE, verified against Google's keys and the vault's own client, and it only ever identifies; a passkey still has to answer.
- Turn an approval meant for one request into another: the passkey signs the digest the vault computed.
- Drive a state-changing browser route from another origin: every non-GET
/apirequest passes one same-origin check. - Get its link printed by
mehen, drawn as a QR code, put in--json, returned as an MCPresource_linkor stored in the ledger: every linkmehenhands over has passedaskUrlagainst the vault it is enrolled with, and the rest get a warning instead. - Get the vault's card or button in an app that enforces the card boundary: a consuming app should draw a trusted vault card only for an ask from its own authenticated call, after checking exact origin, workspace and device context. Each app must enforce this boundary; in such an app a lookalike is plain text.
Can: ask the human to type a key into a form it controls. The vault cannot stop a human pasting a secret into a page that is not the vault. The rule for humans: paste secrets only into https://app.mehen.dev/w/<slug>/i/... intake pages, and only after checking who asked, where the secret will go, who it will belong to and which tags it will carry.
Hostile text on the way to a human#
An agent, an upstream or a planted ledger file can put hidden characters, text-direction overrides, terminal escapes or marker-shaped text into a subject line, a device name, a session label, a policy note, a slug or an error message.
Cannot:
- Reorder or hide text on an approval card: every string is spelled, markers name each hidden character where it sits, and a warning leads the page. A marker typed as text is not styled, because styling comes from the segments the vault emitted, never from a regex over rendered text.
- Move the cursor, recolour a line or hide part of one at the terminal: every human line crosses
terminalText, which spells out controls, format characters and noncharacters, and only the styler's named styles become escapes. The same spelling is applied to MCP text content andresource_linknames;structuredContentand--jsonstay raw JSON with controls escaped. - Make a policy summary say what a note claims: an item's notes are never read; the three lines come from the matchers.
- Make a page say an agent the owner trusts asked: the asker line is the device name its owner accepted.
- Pick a workspace with a name: a workspace's display name is never a selector, in the CLI or in a copied prompt; only the id and the canonical slug select, and the Connect an agent page puts the canonical slug and origin, plus an optional connect code, into copied setup text, never the display name as a selector.
A co-owner, an admin or a colleague#
Someone inside the workspace with real rights, hostile or careless.
Can:
- As a co-owner, remove or demote the other owners and take the workspace: co-ownership is a trust relationship, and the vault does not arbitrate it. Owners are told of every change to an owner.
- As an owner or admin, see the workspace's credentials' metadata, every project, every machine, the whole audit chain, and narrow, revoke, suspend and remove at will.
- As a project manager, share the project further (not with a guest unless an owner or admin) and change its environments, within the fence and loosening rules.
Cannot:
- Read a stored value: nobody can.
- Reach a colleague's private credential, or see where it was sent: it is
not_foundto everyone else, an owner or admin may only purge it, and its audit events carry no hosts or paths even for its owner. - Accept a machine in a colleague's name, or widen one: acceptance and widening are the owner's own passkey over the exact key and scope.
- Approve beyond their own rights, approve their own machine's protected run where four-eyes applies, or approve anyone else's ask as a guest.
- Reset a colleague's passkeys, end their cooling-off or take their account: recovery is the account's own, and the operator's break-glass is not a workspace power.
- Reach another workspace: nothing crosses that wall, not a credential, a share, a machine's key or a step-up.
Honest limits: a workspace is a shared trust domain. Metadata (which credentials exist, their hosts, who used them and when) is visible to owners and admins by design. A guest or a colleague who holds a machine can still use it for what their rights cover.
Honest limit: what is spelled is still shown. A human reading Pay [RLO U+202E]fdp.exe[PDF U+202C] has to understand the warning; the page says to deny unless they know why the characters are there.
Out of scope today#
- Hostile code in a dependency the vault itself uses (supply chain). The vault has few dependencies, and all cryptography is WebCrypto; that is a mitigation, not a guarantee.
- Denial of service against the vault beyond the rate limits.
- Open sign-up. Accounts are created by invitation and workspaces by allow-listed accounts; hardening for strangers comes before either opens.
- Side channels in the host's runtime.
- Timing differences between absent and out-of-reach answers: the bytes are identical, the time is not promised to be.
Open#
- Which tripwires (a placeholder out of place, a fenced host, an echo) should freeze the workspace or suspend the device automatically. Today they are recorded and refused, nothing more.
- Anchoring the audit head outside the vault automatically (for example daily to object storage); today a head saved by hand is the check.
- Telling one process from another on the local relay and in
mehen mcp: not built.