What Permaura protects
Permaura gives agents permission instead of keys. This page says exactly where that holds, because a security promise without a stated edge is worth less than a smaller one with a clear edge.
The short answer
| If you run Permaura… | The promise holds | Worth knowing |
|---|---|---|
| On your own server or VPS | Yes, as written | Anyone with root on that machine has everything on it |
| As a hosted worker | Yes, for the agent | A worker decrypts your secret to make the call, so our infrastructure is inside the trust boundary. Not zero-knowledge |
| With remote access on | Yes, as written | A leaked agent token then works from anywhere, not just your desk. With a managed tunnel, Permaura and Cloudflare carry the traffic for your address (what that allows) |
| On your laptop, alongside agents running as you | Only for what a device has to sign | The rest of this page |
If you are in the last row, read on. If you are not, the rest of this page is background.
What the gateway does for you
When an agent calls a capability, the gateway checks your policy, adds the real credential on its own side, runs the call, and cleans the result before returning it.
The agent holds a token that says what it may do. The gateway never hands it the credential, and every call a policy decides on, allowed, held or denied, lands on the audit log; a call refused before that point (unauthenticated, rate-limited, too large, or naming a capability the gateway does not have) is answered with an error and no entry. That is the part that does not change no matter where you run it.
Cleaning the result matters because some upstream APIs send the key back. Fields whose names are known to carry secrets are blanked, and from gateway 1.3.44 so is the header, query parameter or environment variable the call's own credential is sent under, in its hyphen and underscore spellings. Also from 1.3.44, the gateway replaces each occurrence of the credential it opened for that call (for an OAuth connection, its tokens and client secret) in the result, the response headers, the error text and the diagnostics, provided it is at least 8 bytes long. It looks for the value as stored or trimmed, JSON-escaped, in base64 (standard or URL-safe), in the forms an HTTP Basic header carries, each line of a PEM key, and any percent-encoding of those. Before 1.3.44 only field names are matched, so an API that echoes the key under some other name, or inside an error message, hands it to the agent. Even from 1.3.44 the value pass does not catch a credential shorter than 8 bytes (a short SSH password, for example), a key returned as hex, as a hash or in part, a value encoded twice, or any secret the call did not open; those are caught only if they sit under a field name on the list. So a capability that returns account details is worth leaving off unless an agent needs it.
Where it stops, and why
The gateway keeps three things, owned by whichever user account it runs as. On Linux, and with a gateway built from source on macOS, all three are files in one folder; a released gateway on macOS keeps the vault key in a Keychain item instead, and on Windows it is in Credential Manager:
~/.permaura/
vault-kek the key that unlocks every credential you have stored
operator.token the token that authorizes any action on this gateway
gateway.db the store: credential values sealed under vault-kek, and policies,
grants, agents and the audit log in plain rows
Those files are readable only by that user account. On a server that is you and nobody else, and the story ends there.
On a laptop it is different, and the difference is the whole point of this page. An agent you run is running as you. Your operating system cannot tell your terminal apart from a coding agent, or from a script that came with a package you installed. As far as it is concerned they are all you, so they can all read operator.token and gateway.db, and on Windows and Linux the vault key as well.
An agent that reads operator.token and presents it gets a valid session. Nothing was broken to do that. It asked using your credentials and got a correct answer.
Any tool that keeps a usable credential in your home folder works this way, whether it says so or not. We would rather write it down.
What stands in the way
A physical device has to approve the things that matter. While the approval floor is not lowered, these all wait for a paired device to sign them:
- Exporting your vault
- Deleting a stored credential
- Deleting a connection, and everything that depends on it
- Deleting an agent, a policy or a grant
On a Mac that is the desktop app, which asks for Touch ID before it signs. A copied token cannot sign, because the app's signing key is kept in its own Keychain item rather than in a file; the signature proves the key was used, not that a fingerprint was given.
Lowering the approval floor switches this off. That is Allow console approvals in the desktop app, or permaura approver floor approve, and it takes your device's signature, so a copied token cannot do it for you. Once the floor is lowered, none of the four waits for a device: on the gateway's own machine, anything holding the operator token can export the vault and delete things alone, and the console's deletes go ahead too. On a gateway you run, a vault export from off its machine is still refused while the floor is lowered; a hosted gateway's console exports once you confirm it's you.
If you have no device paired, these go ahead as normal, because a gateway you can be locked out of is worse than one you can empty. The exception, from gateway 1.3.44, is a request from a web page, through permaura.com or from another machine: with no device paired, the gateway refuses a vault export, an import that would replace or attach to a credential already there, a new connection that would take over a stored credential, and an edit that changes where an SSH connection's credential is sent or how it is presented, and says to do it on the gateway's own machine or pair a device first. Until a device is paired, or while the approval floor is lowered, permaura.com's console code can still configure a self-run gateway and use its credentials through an agent it approves, without reading them. Pair more than one where you can: losing every paired device puts you into account recovery rather than a quick fix.
Your sign-in stays in the system keychain. The desktop app keeps your account tokens in the macOS Keychain or Windows Credential Manager, not in a file. On a Mac, a program that is not the app gets an approval prompt instead of your tokens; Credential Manager on Windows has no such check, as the Windows paragraph below explains. Before desktop 1.4.25, a program running as you could get around that by setting PERMAURA_VAULT_CUSTODY=file in the app's environment, and the app's next sign-in then wrote the tokens to a plaintext file, and its next pairing did the same with its approval signing key. From desktop 1.4.25, a shipped app ignores PERMAURA_VAULT_CUSTODY and CREDENTIALS_DIRECTORY for its own secrets, your sign-in and its approval signing key among them, and, the next time it can read or write that secret in the Keychain or Credential Manager, deletes a plaintext copy an earlier override left behind. Deleting that file does not revoke a session or a key someone already copied from it. From desktop 1.4.24 it also ignores PERMAURA_AS_URL and PERMAURA_API, which could otherwise send its sign-in to another server. This does not change the gateway's own encryption key, which keeps the custody described next.
The gateway keeps its own encryption key in the OS credential store where it can: on macOS a released gateway, signed with Permaura's Developer ID, keeps it in a Keychain item tied to that signature (a gateway built from source keeps an owner-only file instead), and on Windows it keeps it in Credential Manager, which ties it to your user account rather than to any program. On Linux it is an owner-only file, vault-kek in the gateway's data directory, whether you installed the desktop app or the gateway alone; the gateway never uses your desktop keyring. An install run as root on a machine with systemd seals it to the machine's TPM whenever there is one (--no-tpm opts out), into a root-owned systemd credential the gateway is handed at start.
Exactly what is exposed, and where
The three files above are not equally well protected, and the difference is not the same on every operating system. This is the honest version.
| What a program running as you can get | macOS | Windows | Linux | Linux with --service-user |
|---|---|---|---|---|
operator.token, and with it every action on the gateway | Yes | Yes | Yes | No |
| The vault key | It has to ask you first, once the gateway has stored the key (see the macOS paragraph below) | Yes, silently, from Credential Manager | Yes, it is a file you own | No |
| The store file: every policy, grant and audit entry, with credential values still sealed | Yes | Yes | Yes | No |
| Any credential you have stored, in plaintext, by asking the API for it | Yes | Yes | Yes | No |
| Lowering the approval floor | Only with a signature from a paired device | Only with a signature from a paired device | Only with a signature from a paired device | Only with a signature from a paired device |
| Exporting your vault, deleting things | Only with a signature from a paired device, while the approval floor is not lowered | Only with a signature from a paired device, while the approval floor is not lowered | Only with a signature from a paired device, while the approval floor is not lowered | Only with a signature from a paired device |
Read the last two rows first: on a laptop they are doing most of the work. They hold only while the approval floor is not lowered, and they have one gap, open to anything that can write the gateway's own files on its machine:
- The list of trusted devices is part of the store. From gateway 1.3.37,
permaura approver resetwill not untrust a paired device without one of your recovery codes, but the list it protects is an ordinary entry ingateway.db, or in the keyring file if you setPERMAURA_APPROVER_KEYS_PATH. A program running as you can stop the gateway, clear that entry and start it again. After that the operator token alone approves everything, vault export included, until a device is paired.
With --service-user this is not open to a program running as you, because it can neither read the token nor write the store.
Gateways before 1.3.37 had a second gap: over loopback, the operator token could point an SSH connection at another host and switch it to password authentication, and the next call sent the stored credential there. From 1.3.37, with a device paired, that change waits for the device to sign it.
macOS protects the vault key best, once the gateway has stored it. A released gateway keeps the key in a Keychain item that admits only its own signed code, so a different program asking for it afterwards gets a system dialog rather than the key. That holds only for an item the gateway created: it reads and writes the item through your default keychain and adopts one already standing under its name, so a program running as you that acts before the gateway first stores the key (creating that item itself with an access list that lets it in, or pointing your default keychain at one it controls) can choose or read the key. It does not protect the operator token, because that is an ordinary file, and a token is enough on its own.
Windows is the weakest of the three, and there is currently nothing that closes it. Credential Manager is per-user with no equivalent of that code-identity check, so a program running as you reads the vault key without a prompt. It is still better than leaving the key in a file, and there is no --service-user on Windows. Pair a device.
Linux is the only one where you can move the boundary into the kernel. By default the key is in an owner-only file, which does not stop a program running as you. --service-user does.
Moving the line further out
On Linux you can give the gateway its own user account, which is the only option here that makes your operating system enforce the boundary rather than Permaura. An install run as root on a machine with systemd does this by default. Anywhere else, ask for it:
sudo sh -c "curl -fsSL https://permaura.com/install | sh -s -- --service-user"
The gateway then runs as its own permaura account, which owns its store, key and token in /var/lib/permaura and nothing else. The binary in /opt/permaura/bin, the settings in /etc/permaura and a TPM-sealed key belong to root, and the service logs to the journal, so code running as that account cannot change anything root later reads or runs. Nothing running as you can read the store, the key or the token. An agent on your machine ends up in the same position as one on the other side of the world: it can use the API with an agent token and do nothing else.
That is the boundary working, not a fault, but plan for it. You run the gateway from your Permaura account instead, the same way the web console already does, or locally with sudo -u permaura. If you have neither an account nor sudo on that machine, you will be locked out of your own gateway. That is why it is the default only for an install run as root, and --no-service-user opts out of it.
It is Linux only. On macOS the gateway would have to give up the Keychain item that holds its encryption key to gain a separate account, which is a swap rather than an improvement; on Windows, where Credential Manager does not keep the key from a program running as you, there is no --service-user option.
Account recovery is a trust choice
Turning remote access on makes your gateway an OAuth resource server for permaura.com, and that has a consequence worth stating plainly. A permaura.com recovery ticket becomes a third authority that can loosen trust on the gateway, alongside your paired device and your recovery codes.
It exists for a real case. If the device is lost and the codes went with it, that ticket is how you get back in rather than starting over with a vault you can no longer open. A ticket is issued only to an owner or admin of the gateway's organization who has just confirmed it's them with the account password and a second factor (two different methods on an account with no password), and the gateway refuses one that does not show that, or that is more than five minutes past that confirmation. A phone that became their account's approver in the last 24 hours does not count toward it.
Most other confirmations take one authentication, not two. Someone who holds both a live session of yours and your account password can confirm it's you for those (pairing or removing an approval device, lowering a hosted gateway's approval floor, exporting its vault or decommissioning it) without a second factor, unless the password changed in the last seven days. The session itself took a second factor or a passkey to sign in, but a stolen session carries that with it, so a stolen session together with your password is enough for those confirmations. It is not enough for a recovery, for new recovery codes, or for the account changes that would let someone keep a way in (new backup codes, turning off two-step verification, removing your last passkey, adding a passkey or an authenticator app, showing an authenticator's setup key again), which take the password and a second factor every time, or two different methods on an account with no password.
The cost is that an account-recoverable gateway trusts the account, and the server that issues the ticket, as part of its custody. A compromised authorization server could assert the re-authentication itself. That reaches your devices: a ticket naming the owner can remove a paired device at once, the last one included, which switches off the holds a device provides, and on a gateway hosted by Permaura, whose first device is paired with a ticket, the same authority could then pair a device of its own. Each step is written to the audit log. A gateway with no remote access configured has no such route and no such trust, which also means one fewer way back.
When the device is gone, these are the ways back:
- A recovery code. On the gateway's own machine, stop the gateway and run
permaura approver reset --yes --recovery-code, then type the code when asked. From gateway 1.3.46 the input is hidden, so the code never reaches your shell history or the process list. It spends the code, untrusts every paired device and clears the approval-floor override, so the operator token alone approves again until you pair a replacement. With the gateway running,permaura approver revoke <approver-id> --recovery-coderemoves just the lost device the same way. Where there is no terminal to type into, setPERMAURA_RECOVERY_CODEfor that one command from somewhere other than the command line, such as a password manager's CLI. Typing the code straight after--recovery-codestill works but prints a warning, because the code is then in your shell history and, while the command runs, in the process list; if that run does not spend it, remove it from the history. - A recovery ticket, only where remote access is configured. Remove the device under Workspace → Devices and confirm it's you with your password and a second factor (two different methods if your account has no password) when asked. The gateway accepts the single-use ticket in place of the device's signature or a code.
- Starting over. With the device and the codes both gone on a gateway with no remote access, no reset keeps the vault, because
approver resetrefuses without a code while any device is still trusted. What is left ispermaura uninstall --purge-data, which removes the gateway and its data. Your connections, credentials, policies, grants and agents go with it, and so does the local audit chain, so read the audit log out first if you want it.
So keep your recovery codes somewhere other than the device you paired, or turn remote access on, if starting over is not a cost you can carry.
On a self-run gateway, issuing a new set of recovery codes from the Permaura app goes through permaura.com, so from gateway 1.3.46 it asks for your account password and a second factor (two different methods if your account has no password) on top of what the gateway always requires: a paired device's signature (the app asks for Face ID, Touch ID or your passcode) or one of your current codes. A new set replaces the old one: codes you wrote down earlier stop working. Nothing on the gateway's own machine asks for the account confirmation, permaura approver codes and the desktop app included. A gateway hosted by Permaura has no recovery codes; its recovery is a sign-in to your Permaura account.
Neither answer is the right one for everybody, which is why it follows remote access rather than being a setting of its own. Turning remote access on is the moment you accept the ticket as an authority.
The managed tunnel is a trust choice too
permaura.com creates a managed permaura.app tunnel in Permaura's Cloudflare account, and Cloudflare terminates TLS for it. So Permaura (permaura.com and its Cloudflare account) and Cloudflare can still see the traffic for your gateway's permaura.app hostname, or send it somewhere else, and because Permaura created the tunnel, they can run a second connector for it.
What they cannot do is point the connector on your machine at anything else there without your machine stopping it. From gateway 1.3.48 and desktop 1.4.21, the connector's only destination is http://127.0.0.1:<gateway port> for that hostname, written on your machine by the gateway or the desktop app. That file does not hold it there on its own: cloudflared would still apply a configuration pushed to it, and would carry Cloudflare private-network (WARP) traffic to any address your machine can reach if Permaura's Cloudflare account routed some to the tunnel. So the gateway (or the desktop app, while it is running) checks the connector's running configuration and its private-network counters twice a second, and stops the connector within about a second, and says so, when either is not what your machine set up. Requests or connections that arrive in that second can reach the pushed destination. If the app quits unexpectedly, the next launch stops a connector it left running. We tested the first: Cloudflare's API accepts a configuration for a tunnel configured this way, makes the tunnel remotely managed, and the connector applies it the next time it connects, which is the moment the gateway or the app stops it. We have not tested the second, and the stop is there in case it happens. Permaura's Cloudflare account can also read the connector's logs, which include the requests it carries for your hostname, and see your machine's network address.
If you do not accept that, use your own tunnel or domain, where you decide where the traffic goes and Permaura only registers the address, or keep the gateway loopback-only.
What to do
- Running on a server? Nothing. The promise holds as written.
- Running on your laptop with agents? Pair a device, and do not lower the approval floor. It is the only control that works on all three operating systems, and apart from the gap above it is what stands between an agent that read your token and an emptied vault. Lowering the floor takes that away on the gateway's own machine.
- On Linux, and want the boundary the kernel enforces? Install as root on a machine with systemd, where it is the default, or pass
--service-user. Read the warning above first. - Turned remote access on? Revoke an agent token you no longer trust. A token is a bearer: until you revoke it, whoever holds it is that agent.
- Turned remote access on? Know that a permaura.com recovery ticket can now loosen trust on this gateway. That is the trade for being able to recover an account when the device and the codes are both gone.
- Using the managed tunnel? Know that Permaura and Cloudflare carry the traffic for your public address, and what that still allows. Use your own tunnel or domain if you would rather they did not.
- On macOS? Run a released gateway, installed by the desktop app or the installer: it is signed with Permaura's Developer ID, which is what keeps the vault key in a Keychain item tied to that signature rather than in a file. Start it soon after installing, before other software you run has a chance to create that Keychain item first.
- Anywhere: hand the vault key to the gateway in a file it reads at startup, never in an environment variable. Anything running as you can read a variable out of the process, every program the gateway starts inherits it, and clearing it later does not help, because the value a reader sees is the one the process started with. From gateway 1.3.5 the old
PERMAURA_VAULT_PASSPHRASEis ignored outright and the gateway says so on startup, so if you are still setting it, move the value toPERMAURA_VAULT_PASSPHRASE_FILEbefore you upgrade.