Approvals
Some actions are too consequential to run unattended — a refund, a production deploy, rotating a secret. Permaura lets you pause exactly those for a human, without slowing down the routine ones.
The approval ladder
Every action a policy governs sits at one of five rungs. You choose the rung per action, per connection.
| Rung | What happens |
|---|---|
| Allow | Runs immediately. |
| Notify | Runs immediately, but you're told it happened. |
| Approve | Pauses until a human approves or denies it in the console. |
| Device-signed | Pauses until approved and cryptographically signed on an enrolled device. |
| Deny | Never runs. |
Set these on a connection's policy. Everything defaults to deny until a grant says otherwise, so you're always opting actions in, never trying to remember what to lock down.
The queue
Anything at Approve or Device-signed lands in Control → Approvals. Each request shows the agent, the exact action and connection, and the arguments, and each is single-use — approve it and that one call proceeds, and only that one. Ignore it and it waits: a held request does not expire on its own, so nothing happens until you approve or deny it.

Device-signed approvals
For your highest-risk actions, a click in a browser isn't enough. Device-signed approvals require a signature from a phone you've enrolled, so that even a compromised gateway can't forge an approval — the signing key lives only on your device, and the gateway merely verifies it.
You approve from the Permaura app:
- Enrol the device as an approval device. How depends on where your gateway runs; see below.
- Mark the action Device-signed in its policy.
- When an agent triggers it, your phone gets a push. Approve with Face ID / Touch ID and the phone signs the result; the gateway checks the signature before the call runs.
An enrolled phone is an approver, not a place your secrets run. It holds a signing key and answers yes/no — nothing more.
Enrolling an approval device
The signing key is generated on the device and never leaves it. What differs by placement is who is allowed to say "trust this key", and the console is never that party: there is no pair button in the web console, on any placement, by design. The console is served by Permaura, and Permaura is exactly who shouldn't be able to hand itself the power to sign your approvals.
This is a different thing from the PGW-… pairing code that links a gateway to your account. That one connects a machine; this one trusts a signing key.
Local or tunnelled gateway. The app shows a permaura approver pair --blob … command with the proof already in it. Copy it and run it in a terminal on the machine running your gateway, within two minutes. Running it there is the consent: only someone at that machine can enrol a key, which is what stops a compromised cloud from enrolling one of its own. An Apple Watch hands its proof to the iPhone, which shows the same command.
Hosted gateway. There is no machine of yours to run that command on, so the app enrols the device itself, and asks you to sign in again first:
- In the app, choose to pair, and prove it's you again with your password or a two-step code. Permaura issues a single-use ticket that is good for three minutes and locked to that one key.
- The first device on a hosted gateway is trusted there and then. Your account plus a fresh sign-in is the whole authorisation, because an empty keyring has nothing that could confirm it.
- Every device after it waits for a device you already paired to confirm the enrolment from its own approval queue. A console click cannot stand in, because the confirmation has to carry that device's signature. The new device cannot confirm itself.
- Either way, the account owner is emailed that an approval device was enrolled, naming the device and the key.
The owner notification is a detection control, not a prevention one. By the time it arrives the gateway has already decided: the first device is trusted, or a later one is queued for confirmation. If you get one you didn't expect, treat it as an incident: revoke the device under Workspace → Devices, check the audit log for permaura.approver.pair, and change your account password.
On a hosted gateway, revoking your last approval device asks for that same fresh sign-in. Revocation is normally the safe direction and asks for nothing: while another usable device remains, the keyring still holds something that has to confirm the next enrolment. Removing the last one changes that. It puts the gateway back to trusting the next device on first use, which makes it the one revocation that loosens rather than tightens, so it costs what pairing costs. Revoking a device while another still works is unaffected, as is every revocation on a local or tunnelled gateway.
That is also why recovering a hosted gateway whose only device is gone takes two sign-ins rather than one: the proof that revokes cannot enrol, and the proof that enrols cannot revoke. Playbooks has the order to do them in.
From the moment you pair a device, the gateway stops accepting unsigned approvals: by default every pending action waits for a signature from a paired device, and the web console and CLI can no longer approve on their own. Revoking that device does not lower the bar again on its own, by design, so that nothing which merely holds your account session can revoke its way back to waving actions through.
Lowering it is a deliberate, separate act. If you'd rather keep one-click console approvals for your everyday Approve-rung actions and reserve signatures for the Device-signed ones, you can:
- Desktop app — Settings, on the Approve on this device card, under Web console & CLI approvals. Confirmed with Touch ID on a Mac.
- Terminal —
permaura approver floor approve, run on the machine your gateway is on.permaura approver floor device_signedputs it back;permaura approver listshows where it currently sits. - Hosted gateway — console, Workspace → Devices, on the approval-devices card. There is no machine of yours to run the terminal command on, so this is the route.
On a local or tunnelled gateway that is the whole story: lowering is accepted only from the machine running the gateway, never over the network. On a hosted gateway it takes the same fresh sign-in as pairing, and while you still have a working device it also takes that device's signature; the sign-in alone is enough only when no usable device is left, which is the recovery case. Device-signed actions always require a signature regardless, at either setting.
A hosted worker treats every request as arriving over the network, because it does. That composes with the rule above in a way worth knowing before you meet it: once any approval device is enrolled on a hosted gateway, every control-plane change that isn't strictly tightening (a new policy, a rung lowered, a grant widened, approving an agent, un-pausing the broker) stops applying when you click save and waits for a signature from your device instead. The console shows it as pending rather than done.
Tightening still applies immediately, and so does everything on a local gateway you edit from its own machine. But if you pair a phone with a hosted gateway, expect to be confirming your own console edits on it from then on.
Two-person approval
For the actions where one person's judgement isn't enough, a policy can require signatures from several different paired devices before the action runs. Set device_signed_quorum on the policy holding the action at the Device-signed rung:
{
"device_signed": ["payments.refund"],
"device_signed_quorum": 2
}
The action then waits until two distinct devices have each signed it. Until then nothing runs, and each approver sees how far along it is ("1 of 2") along with who has already signed.
The rule is built so one person cannot satisfy it alone:
- Signing twice doesn't count twice. A repeated signature from the same device is treated as the same approval, not a second one.
- One device paired twice is still one device. Distinctness is checked against the signing key, not just the device name, so a second pairing of the same phone adds nothing.
- Whoever asked for the action can't approve it. Separation of duty applies to every signature, not only the first.
- Revoking a device takes back the approval it already gave. If you revoke a lost phone that had already signed a pending action, its signature stops counting immediately, so the remaining device can't finish the job alone. The same applies to a phone that was wiped and re-paired: the new key is not the key that signed.
- Each signature is bound to the exact request. A second approver cannot be handed a different action to sign and have it counted toward this one.
A single refusal stops it. Two-person approval means two people have to agree to proceed; it never means one person can be outvoted into proceeding. The first denial ends the action there and then.
An action needing more signatures than you have paired devices can never be approved. Held actions do not expire, so it waits in the queue indefinitely rather than failing on its own — still a fail-safe, not a fail-open, since nothing lets it through. To clear it, deny it from any one paired device: a refusal never waits for a quorum, because two-person approval means two people must agree to proceed, not that one person can be outvoted into proceeding. A frustrating way to discover the setting either way, so pair the devices first.
Getting notified
Pending approvals surface as a badge on the Approvals item in the sidebar and in the top bar, so a waiting request is always one glance away. Device-signed requests also push to the enrolled phone.
Slack, Discord and Buzz
Your gateway can post to a team chat channel whenever an action is waiting, and whenever a Notify-rung action has already run. For Slack and Discord, create an incoming webhook for the channel you want (Slack, Discord) and set it on the machine running your gateway:
export PERMAURA_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/…"
export PERMAURA_DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/…"
Buzz works differently, because a Buzz message is signed rather than posted to a URL. Put the buzz CLI on the gateway's PATH and give it an identity:
export PERMAURA_BUZZ_RELAY_URL="https://buzz.example.com"
export PERMAURA_BUZZ_PRIVATE_KEY="<64-hex>"
export PERMAURA_BUZZ_CHANNEL="<channel-uuid>"
# optional: PERMAURA_BUZZ_AUTH_TAG, PERMAURA_BUZZ_BINARY
Give the gateway its own Buzz identity, never your own. Everything it posts is signed as that key, so an agent reading the channel sees messages from "the gateway", and revoking it later costs you nothing personally. A key that is not exactly 64 hex characters disables the channel rather than being trimmed — half a key is not a smaller version of one.
Set any of these, or all of them — every event goes to every channel you have configured. All the URLs must be https; a webhook URL is itself a credential, and so is the text riding on it.
Two optional variables make the link in each message land more precisely. PERMAURA_CONSOLE_URL points at a console other than https://permaura.com, and PERMAURA_WORKSPACE_ID makes the link switch to this gateway's workspace first, rather than dropping you wherever your browser session last was.
The message names the capability, the connection, and the approval message from your policy, and links through to the console. It deliberately contains none of the request's arguments and none of its results — a channel has a longer memory and a wider audience than your audit log. Nothing in it can ping your whole server either: Discord messages are sent with mentions disabled, so an @everyone that appears in a policy message is text and nothing more.
If a channel is unreachable — a revoked webhook, a buzz binary that isn't installed — the approval is unaffected. It is still in the console queue and still on your paired devices; chat is an extra way to hear about it, never the thing holding it.
Only the Approve and Notify rungs post. A device-signed action is one you decided a click in a browser wasn't good enough for, and a chat channel is weaker still: anyone who has ever been in it, plus anyone who administers the workspace or server, can read it. Posting those requests would advertise your most consequential pending actions on the one surface that cannot resolve them.
Nothing in a Slack or Discord message can approve anything. There is no token in it and no action button — the link goes to the console, which asks who you are. Chat tells you something is waiting; the decision still happens somewhere that authenticates you.