POST /keys/{keyId}/signatures, an activity the policy engine evaluates. Put a Wallets:Sign policy on that key and the service account cannot complete a single user action until an approval quorum signs off. DFNS gates DFNS.
This turns an autonomous machine identity into a quorum-gated one without changing how the service account calls the API. The account still holds a bearer token; it simply cannot mint the user-action signature that privileged calls require until humans approve.
Get the code
dfns/dfns-solutions: gated-service-account
When to use this
- High-privilege automation: a service account that can create wallets, move treasury funds, or change permissions, where a stolen token must not be enough to act
- Break-glass identities: an account used rarely but powerfully, where every use should require named human sign-off
- Regulated operations: machine actions that need the same multi-party authorization as human ones, with a signed, auditable approval per action
How it works
The service account’s public key registered on the credential is the MPC key’s public key. Every user action the account signs routes through the gated key, so the quorum authorizes each one individually. Two identities appear by necessity, not by choice. Requesting the MPC signature is itself a user action, and the service account’s only credential is the gated key, so it can never bootstrap its own signature request. Any non-gated identity withKeys:Signatures:Create can play the operator role: the walkthrough below uses the admin identity for brevity, and a production deployment typically uses a dedicated operator service account holding only that permission.
What you’ll need
- A DFNS account and an admin identity that can create keys, wallets, policies, permissions, and service accounts
serviceAccountsCanApproveenabled on your organization if a service account sits in the approval group (contact our )- Node.js v22.18+
- A dev or staging organization to try this on first
Key facts this relies on
The design depends on four behaviors, each verified against the API:- Only signature generation is policy-gated on a key. The policy engine evaluates
POST /keys/{keyId}/signatures(activity kindWallets:Sign). Key lifecycle operations (create, import, export, delegate, delete) are governed by permissions and are not policy-evaluated. This solution gates use, not the key’s lifecycle. - Policies filter by wallet, not by key. There is no
keyIdorkeyTagsfilter, and keys carry no tags. A key-based signature request is matched to a policy through the wallets built on that key. To scope the policy, create one wallet from the key and tag it; the policy filters onwalletTags. A key with no wallets can only be gated by an unfiltered (org-wide) policy. - Value-based rules cannot read a raw signature. Rules like
TransactionAmountLimitorTransactionRecipientWhitelistextract fields from a transaction payload. A user-action challenge is opaque bytes, so those rules fail closed and trigger. UseAlwaysTriggerfor this pattern, since every signature then requires approval, which is exactly the intent. - An EdDSA/ed25519 key is the right choice. Ed25519 signs the raw message with no pre-hashing, and its 32-byte public key converts directly to the SPKI PEM that
POST /auth/service-accountsexpects.
Set up the gate
The admin identity creates everything except the service account. Creating a service account is session-only, becausePOST /auth/service-accounts rejects personal access tokens, so that one step is done in the dashboard.
Step 1: Create the MPC key
key.id and key.publicKey (hex).
Step 2: Create a tagged proxy wallet from the key
Keys:Reuse and Wallets:Tags:Add.
Step 3: Create the quorum policy
Step 4: Create a minimal permission
Grant the service account only what it needs, for exampleWallets:Create and Wallets:Read.
Step 5: Register the service account with the MPC key
Convert the key’s 32-byte hex public key to SPKI PEM and register it in the dashboard (Settings → Service Accounts → New), assigning the permission from step 4.The gated round-trip
Whenever the service account performs a privileged call, its user action is signed by the gated key.1
Initiate the challenge (as the service account)
2
Request the MPC signature (this is the gated moment)
Build the client data and sign it with the key. The policy holds it for approval.
3
Approve (the quorum)
Each approver posts a signed decision. When the quorum is met, the signature executes.
4
Complete the user action and make the call (as the service account)
Fetch the signed result, assemble the assertion, exchange it for a user-action token, and use it.
Design considerations
Challenge lifetime versus approval latency
The challenge from step 1 must still be valid when the assertion completes in step 4, so the quorum has to approve within the challenge’s lifetime. That window is comfortable but bounded: a challenge completed successfully after a five-minute delay in testing, but do not assume hours. KeepautoRejectTimeout aligned with how fast your quorum realistically responds, and design the orchestrator to treat an expired challenge as a restart. A fresh challenge means a new signature and therefore a new approval. For quorums that may take longer than a few minutes, page approvers before initiating the challenge so the human delay happens up front, not inside the window.
Approvers see opaque bytes
The pending approval shows a signature request over hex bytes. The client data contains only the challenge and a type; the real action payload is bound to the challenge server-side, so the approval screen does not render “create wallet X” or “transfer Y”. Surface the pendinguserActionPayload to approvers out of band, or attach a decoded summary to the decision reason, so the quorum reviews the action rather than a hash.
The signing identity is the residual surface
Whatever identity requests the signature holdsKeys:Signatures:Create. That is acceptable only because every signature it can request on the gated key is quorum-gated, which is why scoping matters. If you add a second key, create it with its tagged wallet in the same step, or it is born ungated. Audit periodically that no untagged wallet exists on the gated key.
Separate initiation from approval
SetinitiatorCanApprove: false in production so the identity requesting the signature cannot also approve it. Keep the approver set distinct from the automation.
Related
Policies for signature requests
How the engine evaluates signing requests
Build programmable approval policies
Automate the approval side with a service account
Define treasury policies
Spending limits and approval quorums
Govern wallet access
Permissions and access control