mehendocs

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.

MechanismStops
No value returned to a device except a lease; relay and placeholder modes firstSecrets 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 anyoneA 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 rightsA 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 limitedLearning 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'sA 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 onlyA 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 refusedSending a credential anywhere it was not meant to go.
Restrictive rules matched on a decoded, lower-cased path; permissive ones on the exact canonical pathDodging a deny or approve rule with %73end or SendMail.
IP literals, loopback, *.workers.dev and the vault's own host only when bound exactlyA 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 recordedSmuggling a token into a body, query string or comment on a bound host.
Method-override headers and parameters refusedA GET-only rule admitting a DELETE.
Policies combined as deny over approve over allow; body matchers fail closedA 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 goesAgents 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 approvalA 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 textfdp.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 matchersA 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 selectorA 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 lineAn 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 textA 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 deviceA 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 originsA 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 escapeVault 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 acceptanceApproving 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 revokeA 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 limiterA 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 createdOne 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 renewalsA 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 stateA 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 anotherA 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 deviceA 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 markerHOME=/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 pendingAgents 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 emailA 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 reachAn 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 themA 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 chainOwners 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 lastA 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 storeBearer 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 deciderProd 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 credentialLeasing 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 relayThe wrong database at the wrong time, including through mehen fetch --run.
Run maximum life: 12 hours, 2 hours for approved protected runsA run, and its placeholders and approval, living forever by renewal.
The checkout's reported remote checked against the project's bound remotesAccidental 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 workspaceCiphertext swapped between rows or workspaces.
Sealing secret fields in the browserPlaintext 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 reviewEchoed secrets reaching the agent in those forms, including in a compression the redactor cannot read.
Redirects not followedBouncing a credential to another host.
Run CA name constraints on both DNS names and IP addresses, path length 0, key in memoryA stolen run CA impersonating anything else.
Hash-chained audit, only appended by the applicationAn 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 transactionFurther 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 pathA 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:

Cannot:

Honest limits:

A compromised machine#

Malware, or a hostile process, running as the same user as the agent on an enrolled machine.

Can:

Cannot:

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:

Cannot:

Honest limits:

A malicious upstream#

An API the credential is bound to that misbehaves: echoes the secret back, redirects elsewhere, or returns hostile content.

Can:

Cannot:

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:

Cannot:

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:

Cannot:

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:

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:

A co-owner, an admin or a colleague#

Someone inside the workspace with real rights, hostile or careless.

Can:

Cannot:

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#

Open#