Skip to main content
A service account authenticates its sensitive calls with User Action Signing: it signs a server-issued challenge with its credential private key. If that credential key is a DFNS MPC key, the signature is produced by 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 with Keys: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
  • serviceAccountsCanApprove enabled 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 kind Wallets: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 keyId or keyTags filter, 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 on walletTags. 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 TransactionAmountLimit or TransactionRecipientWhitelist extract fields from a transaction payload. A user-action challenge is opaque bytes, so those rules fail closed and trigger. Use AlwaysTrigger for 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-accounts expects.

Set up the gate

The admin identity creates everything except the service account. Creating a service account is session-only, because POST /auth/service-accounts rejects personal access tokens, so that one step is done in the dashboard.

Step 1: Create the MPC key

Keep key.id and key.publicKey (hex).

Step 2: Create a tagged proxy wallet from the key

This wallet exists only to carry the tag the policy filters on. Requires Keys:Reuse and Wallets:Tags:Add.

Step 3: Create the quorum policy

List approvers explicitly. An empty approvers object means “any human user” and does not admit service-account approvers. If a service account must approve, set serviceAccountsCanApprove: true on the group and list it in approvers.userId.in, because the engine requires both.

Step 4: Create a minimal permission

Grant the service account only what it needs, for example Wallets: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.
Copy the access token the dashboard shows once. The service account is now backed by the gated key.

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.
The service account created a wallet only because two approvers signed off on the signature that authorized 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. Keep autoRejectTimeout 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 pending userActionPayload 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 holds Keys: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

Set initiatorCanApprove: false in production so the identity requesting the signature cannot also approve it. Keep the approver set distinct from the automation.

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
Last modified on September 11, 2026