The production checklist is the short list to work through before launch. This page explains the reasoning behind the security items on it, and goes further for organizations that move significant value.
Policies, root quorum, and MFA
Turnkey decides whether a request goes ahead using three controls. They answer different questions, and most of this page is about choosing the right ones for each kind of user.
In short: policies say what users may do and who has to approve it. The root quorum lets root users, acting together, do anything without a policy. MFA asks the same person for more proof.
A few things about how they fit together:
- Policies deny by default. A non-root user can do nothing that a policy does not allow, apart from a few built-in permissions such as reading data and managing their own credentials. If one policy allows an activity and another explicitly denies it, the deny wins.
- One policy can both allow and require approvals. Its
conditionsays what is allowed, and itsconsensussays who has to approve it. There is no separate kind of approval policy. - The root quorum sits above policies, but with a threshold above one, a single root user does not. Once enough root users approve, Turnkey does not evaluate policies at all. With a threshold of one, a single root user is the whole quorum. With a higher threshold, a root user acting alone normally waits for the rest of the quorum. The exception is a matching allow policy: one with no
consensusrequirement lets that root user perform the activity alone, without the other root users. See Using root users with policies. - MFA applies to every approver. When a policy asks for two approvers, each of them is still held to their own MFA policies.
- Your team in the parent organization. Root quorum members are protected by the threshold itself, together with passkeys. Team members outside the root quorum act under policies, with consensus on anything sensitive. See Protect the people in your parent organization.
- Your services and agents, in any organization. They should be non-root users acting under narrow allow policies, and anything large needs approval from people whose credentials are not on the same system. See If your backend is compromised.
- Your end users, if they have their own sub-organizations. An end user is then usually the only root user of their sub-organization, with a threshold of one, so they are the whole quorum and policies do not constrain them. MFA policies do. See Require MFA for your end users.
The root quorum is not bound by policies
The root quorum can execute any action and bypasses the policy engine entirely. Once the root threshold is met, Turnkey allows the request without evaluating your policies, so even anEFFECT_DENY policy does not stop it. This is deliberate, since it is how an organization recovers from policies that lock it out, but it means the root quorum is effectively a master key.
- Raise the root threshold above one. With a threshold of one, any single root credential can do anything in your organization, regardless of the policies you have written. The production checklist recommends at least three members with a threshold of at least two.
- Keep root credentials off servers. Root quorum members should be people signing in with passkeys. Avoid giving a root user an API key that lives in a backend or a CI system.
- Use non-root users for everything routine. Anything you intend to constrain with a policy should be done by a non-root user, because once the root threshold is met, policies no longer apply.
- Check which allow policies match root users. Policies do apply to a root user acting alone, so an allow policy with no
consensusrequirement that matches a root user lets them do that activity without the other root users. A broad allow written with non-root users in mind can quietly turn a threshold of two into a threshold of one for that activity. Scope allow policies to the users or tags you intend, and grant a root user solo permissions only deliberately.
Protect the people in your parent organization
Your parent organization is your Turnkey account: the organization your team signs in to, as opposed to any sub-organizations you create, for example one per end user. MFA policies are mainly a tool for your end users. For the people in your parent organization, including root quorum members and anyone who approves transactions or policy changes, the protection comes from strong credentials and from requiring more than one person:- Use passkeys. A passkey can live on a phone or laptop, or on a hardware security key such as a YubiKey. Either way it is bound to the site that registered it, so a phishing site cannot reuse it, and it cannot be read out over the phone the way a one-time code can.
- Register at least two authenticators per person, ideally of different types, such as a passkey on a laptop and one on a hardware security key, so that a lost device does not lock a quorum member out.
- Keep admin credentials away from untrusted code. Many attacks start on engineers’ laptops, through tampered developer tools and “take-home assignments” from fake job interviews. A passkey does not help once malware is running inside a signed-in session. Root quorum members and other administrators are better off using a separate device, or at least a separate browser profile, for Turnkey than the one they use to install tools and run other people’s code.
- Make sensitive actions need more than one person. A root threshold of two or more, and consensus policies for non-root users, mean one compromised person, or one compromised laptop, cannot act alone.
If an approver is deceived
The credentials above stop an attacker from approving in someone else’s name. They do not stop a real approver from approving the wrong thing.- Approve only what you can read. Turnkey evaluates a request when it is signed, against the policies in force at that moment. It cannot judge whether a transaction is what the approver believes it is. Approvers should see the decoded destination, amount, and contract call before approving.
- Avoid pre-signing. Turnkey’s signing activities return a signature without broadcasting anything, and whoever holds that signature can submit the transaction at any time. On Solana, a durable nonce keeps it valid indefinitely. A transaction signed in advance, for someone else to submit later, can therefore land in a context the approvers never saw. Where you can, sign and broadcast in one step with transaction management.
Require MFA for your end users
Most account takeovers start with one compromised credential: a session put to use by malicious script injected into your site or by malware on a user’s device, a one-time code read out to a phisher. MFA policies make that one credential insufficient. When an activity matches the policy’s condition, Turnkey holds it until the user also proves possession of a second, independent credential, such as a passkey on their device. This section applies if your end users have sub-organizations of their own. Each is then usually the only root user of their sub-organization, with a threshold of one, so organization policies do not constrain them. MFA policies do: they apply to root users, and they are checked even when a policy would allow the activity. That makes them the strongest control you can put in front of an end user’s wallet. Use them wherever your product allows, alongside session profiles, which limit what a given session can do. What to cover. At a minimum, require MFA for:- Signing, either all of it or above a value you choose.
- Exports of wallets and private keys.
- Changes to the user’s credentials and to MFA policies themselves (see below).
Treat this as a starting point, not something to paste into production. Before you enforce it, check that every user can satisfy it and that your recovery flow still works.
- MFA is set per user. There is no organization-wide switch. Each MFA policy is created for one user, with its own activity, after that user exists. Turning MFA on means creating a policy for every user who should have one.
- Cover changes to MFA and credentials. By default, users can add and remove their own credentials and MFA policies. MFA can cover those actions too, but only if your condition includes them. If it covers only signing, a compromised session can register the attacker’s own passkey, or delete the MFA policy, and then sign. Include
activity.resource == 'CREDENTIAL'andactivity.resource == 'MFA_POLICY'in the condition, as the example above does. - MFA is evaluated for the user who stamped the request. If your backend submits logins for end users with its own API key, the end user’s MFA policies are never consulted. See MFA is evaluated for the user who stamped before relying on MFA at login.
- Plan for lost factors before you enforce. A user who loses their only passkey cannot satisfy a passkey requirement. Register a second factor at signup, and choose a recovery design from MFA enforcement and recovery.
Account recovery is a way in
Every way a user can regain access is also a way an attacker can gain it, so an account is only as strong as its weakest recovery path. Recovery does not reset a password. It attaches a new credential that can then do everything the old one could. For your end users, MFA enforcement and recovery covers recovery designs in depth. The practices that matter most:- Make recovery at least as strong as sign-in. Whatever methods you support, such as email, SMS, social logins, passkeys, or wallets, avoid letting any single one complete recovery on its own. Email inboxes and social accounts get taken over, and phone numbers get moved by SIM swaps. Require two independent factors, and check that they really are independent: an email address and a social login backed by the same Google account count as one.
- Land recovery in a session that can only add a new authenticator, using a session profile, so that even a successful attack on recovery cannot move funds directly.
- Treat recovery as a security event. Notify the user on every channel when a credential is added, give them a fast way to report it, and consider holding withdrawals for a day or more after recovery.
- Your backend. If your backend can add credentials to end users’ sub-organizations, for example through a delegated access user, then whoever compromises your backend can take over any user. Limit that user to what your recovery flow needs, and make the flow depend on something the end user proves, not only on your backend’s decision.
- Your support team. Attackers impersonate users to get credentials added on their behalf. Support should not add or reset credentials because a ticket asks for it. Route recovery through the factors the user already has.
If an API key leaks
An API key carries the full permissions of the user it belongs to. Assume that any key stored on a server will eventually leak, and limit what it can do when it does.- Give every application, service, and agent its own non-root, API-only user, with only the permissions that service needs. A leaked key then exposes one service’s permissions, and you can delete that key without affecting anything else. See Users best practices.
- Restrict where keys can be used from. An IP allowlist limits requests to known networks, either for the whole organization or for a specific key. It applies to the parent organization only; requests to sub-organizations are not checked against it.
- Use short-lived keys where you can. A policy can deny API keys that expire after more than seven days. An expiring key cannot be a user’s only credential, though: every user needs at least one long-lived credential, such as a non-expiring API key or a passkey. Keep that long-lived credential away from the system that uses the short-lived keys, so that compromising the system does not expose it.
- Keep keys out of source code and plain environment variables. Store them in a secrets manager.
- When a key leaks, deleting it is not the whole response. There is no separate revoke step: you revoke a key by deleting it with
DELETE_API_KEYSand issuing a replacement. Users can manage their own credentials unless a policy denies it or an MFA policy covers it, so whoever held the key may also have added another API key or authenticator to the same user. List that user’s credentials and delete anything you did not create, or delete the user and recreate it.
If your backend is compromised
Your backend probably runs its own checks before it asks Turnkey to sign: withdrawal limits, fraud rules, internal approvals. Those checks run on the same system an attacker would control. Once they are inside your backend, they can skip the checks and call Turnkey directly. So put the limits that matter in Turnkey policies, where they are enforced before anything is signed, whatever your backend asks for. And for transfers above a threshold you choose, require approval from people whose credentials are not on that backend. For example, use two policies with non-overlapping conditions: one lets the service sign small transfers alone, and one requires two human approvers for anything larger.ACTIVITY_STATUS_CONSENSUS_NEEDED until two users with the approver tag approve it.
These policies only limit native ETH. An ERC-20 transfer is sent to the token contract, so it is allowed only if you add that contract to the list, and then its recipient and amount, which are in the calldata, are not checked. Treat example policies as a starting point, not something to paste into production, and test them against the transactions you actually send. See Policies for wallets that move money and the Ethereum policy examples.
Send transactions through Turnkey
How you sign decides what your policies can check. A full transaction signed withSIGN_TRANSACTION can be parsed and checked; a hash or other raw payload cannot. With transaction management, Turnkey goes further: it builds, signs, and broadcasts the transaction for you through ETH_SEND_TRANSACTION and SOL_SEND_TRANSACTION, with optional gas sponsorship. Besides removing gas and nonce handling from your code, that has security benefits:
- Policies see every call, even when sponsored or batched.
eth.txandsolana.txdescribe your call: its destination, value, and calldata, witheth.tx.fromset to your wallet even when a paymaster relays a sponsored transaction. In an EVM batch, each call is evaluated on its own, and the batch is allowed only if every call is allowed. - You can close the raw-payload gap. A user that sends all its transactions through transaction management has no need to sign them as raw payloads, so you can withhold that permission, or limit it to the message types it actually signs. Raw payload signing is the main way around transaction policies, whether through blind signing or a hidden EIP-7702 authorization.
- Sponsorship without opaque signatures. On EVM chains, gas sponsorship works through an EIP-7702 delegation to Turnkey’s gas station delegate contract. You get gasless and batched transactions while the policy engine still sees every call, which is not the case for ERC-4337 user operations signed as hashes. It also keeps the set of delegations your wallets need small and known.
- Spend is capped and visible. Gas spend limits cap sponsorship for the organization and for each sub-organization, and
SEND_TRANSACTION_STATUS_UPDATESwebhooks report every transaction’s progress, so unexpected outflows show up in your monitoring.
Policies for wallets that move money
Policies are only as strong as what they can see. Every wallet that holds meaningful value should have policies covering approved destinations, value limits, and who has to approve large transfers. Sending through transaction management gives those policies the most to work with. Write them with the following limits in mind.- Native value only covers native transfers.
eth.tx.valueis the amount of ETH sent. An ERC-20 transfer has a value of zero, and its amount and recipient are inside the calldata. To limit token transfers, upload a smart contract interface and write conditions on the parsed arguments. Where no ABI is available, you can match on slices of the raw calldata ineth.tx.data, such as the function selector. See the Ethereum policy examples for both. - Limits apply per transaction, not per day. A per-transaction limit does not bound how much leaves a wallet over a day, because many transactions under the limit are each allowed. Size your limits with that in mind, and keep the human-approval threshold low enough that repeated small transfers are noticed (see Detect and respond).
- Raw payload signing skips transaction parsing. A transaction can be signed as a raw payload, and Turnkey cannot apply transaction conditions such as
eth.tx.toto it. If a user is meant to sign transactions under an allowlist, avoid also allowing that user to sign raw payloads, send its transactions through transaction management so it has no need to, or restrict raw payload signing to typed data your policies can inspect, such as EIP-712 messages. - Not every chain is parsed. The policy engine parses Ethereum and other EVM chains, Solana, Tron, Bitcoin, and Tempo transactions. On other chains, signing is a raw payload, and conditions on destination or amount cannot apply. For hot wallets on those chains, consider requiring human approval for every signature, keeping balances low, or adding your own parsing with a TVC policy sidecar.
- ERC-4337 user operations are signed as raw hashes. Policies cannot see their sender, target, or calldata. If you need sponsored or batched transactions with policy coverage, see Send transactions through Turnkey. If you use ERC-4337, the controls on those wallets have to come from somewhere other than transaction conditions.
- Exported keys are outside Turnkey’s control. Once a wallet or private key is exported, nothing signed with it elsewhere is checked against your policies. Treat permission to export like root permission, and use policies to restrict which accounts can be exported.
- Client-side signing is an export. With client-side signing, a key is exported to the user’s browser so it can sign locally. Following our recommended setup, it is exported into an iframe using the iframe stamper, which keeps the key out of your page’s JavaScript. Handling an exported key any other way, such as decrypting it in your own page, removes that protection. Even in the iframe, any script running in your page, whether an XSS bug, a malicious extension, or a drainer bookmarklet, can still ask it to sign, and nothing it signs is checked by policies or MFA. The bigger risk is the session that loads the keys: whoever can use it can export the keys themselves, and unlike a session, an exported key cannot be revoked. If you use it:
- Issue sessions from a session profile that cannot export or add credentials, so loading keys into the iframe requires a passkey.
- Export only a dedicated wallet holding what you would accept losing, keep the rest in wallets that are never exported, and limit what the session can sign from them.
- Load only the keys you need, clear them when you are done, and lock your page down with a strict Content Security Policy.
- Protect the policies themselves. Anyone who can change a policy can remove the limits above. Require multiple approvers for policy changes, and do the same for creating users and adding credentials to other users.
Add your own checks with Turnkey Verifiable Cloud
Some rules cannot be written as policies, or depend on data Turnkey does not have. With Turnkey Verifiable Cloud (TVC, currently in private beta), you can run them as a policy sidecar: your own service, running in a Turnkey secure enclave, that receives activity webhooks, checks each request against your rules, and approves or rejects it as a separate service user. Require its approval in a policy’sconsensus, and the activities that policy covers cannot complete without it.
Because the sidecar runs in an enclave rather than on your backend, it addresses several of the gaps on this page:
- An approver your backend cannot impersonate. Its credential exists only inside the enclave, and a new version of its code goes live only after the deployment approvals you require. Compromising your backend gives an attacker neither.
- Checks beyond native parsing. It can parse transactions on chains Turnkey does not parse natively, and apply recipient and amount limits to them.
- Checks that need outside data, such as address screening, or value limits based on oracle prices.
- Rules about administrative changes, such as who may create policies.
EIP-7702 delegations
Malicious EIP-7702 authorizations are now a common way to drain EVM wallets, both in attacks on companies and in phishing aimed at individual users. An EIP-7702 authorization lets a smart contract act on behalf of a wallet. Once signed, it stays in force onchain until the account signs a new one, so an attacker who gets a single authorization signed can keep moving funds long after you have revoked their access or shut down your own systems. Make signing an authorization a deliberate decision rather than something any key can do:- For your own wallets, use policies to limit authorizations to the delegate contracts and chains you actually use. The policy engine can inspect EIP-7702 authorizations, and the Ethereum policy examples show how. An authorization with
chain_id0 is valid on every EVM chain, so be especially wary of those. - For end users with their own sub-organizations, remember that policies do not bind them, since they are usually the only root user of their sub-organization. If your product lets users sign arbitrary payloads, for example on behalf of connected apps, a malicious site can ask for an authorization. Consider whether your product needs to offer that at all, show users clearly what they are authorizing, and use session profiles to narrow what a session can sign.
- Raw payloads can hide authorizations. A payload signed as hexadecimal or UTF-8 can also be an authorization, so conditions on the EIP-7702 encoding alone do not cover every case. See the notes in the Ethereum policy examples.
- Know how to undo one. An EIP-7702 delegation stays active until the account is undelegated, which Turnkey supports as a single undelegate activity. See Detect and respond.
AI agents
An agent that can sign is a user like any other, and should be constrained the same way.- Give each agent its own non-root user. Avoid letting an agent act with a person’s credentials or session.
- Put the agent’s limits in Turnkey policies. Limits written in the agent’s prompt or its own code can be talked around. Policies are evaluated in the enclave before anything is signed, whatever the agent was told. For rules a policy cannot express, a TVC policy sidecar can approve or reject the agent’s requests with checks of your own.
- See Agentic wallets and Delegated access for patterns.
Detect and respond
Prevention reduces what an attacker can do. Detection decides how long they have to do it.- Subscribe to activity webhooks.
ACTIVITY_UPDATESwebhooks deliver signed notifications for every activity. Alert on the activities that change who controls your organization or its funds: root quorum updates, policy changes, new users, new API keys or authenticators, exports, and EIP-7702 authorizations. - Alert on new root credentials. A credential an attacker adds to a root user can do anything in that organization, and with a threshold of one, as in most end-user sub-organizations, it needs no one else’s approval. Consider alerting on
CREATE_AUTHENTICATORS,CREATE_API_KEYS, andCREATE_OAUTH_PROVIDERSactivities whose target user is one of your root users, and on changes to a user’s email address or phone number, since those are sign-in and recovery paths too. Webhooks on your parent organization include activities in its sub-organizations, so one subscription covers your end users. An alert fires after the credential exists. To stop it from being added in the first place, require MFA foractivity.resource == 'CREDENTIAL'. - Log who approved what. Record activity IDs and the credentials that approved them, as described in the production checklist, so that you can reconstruct an incident.
- Review access regularly. Remove users, keys, and policies that are no longer used, and check that every root quorum member can still sign in.
- Decide your response in advance. Know who can delete a compromised user or key, and confirm they can do it quickly. If only the root quorum can, under a threshold of two that means reaching two people. Deleting a service’s API keys is usually the fastest way to stop it. Adding a deny policy can be slower if policy changes need several approvals, and it does not bind the root quorum.
- Plan for what keeps working after you stop. Shutting down your own systems does not undo what was already signed. A transaction that was signed and broadcast stays valid until it is mined or replaced with another transaction using the same nonce. An EIP-7702 delegation stays active until the account is undelegated, which Turnkey supports as a single undelegate activity. Make sure the people in your incident plan are allowed to run it, and try it once before you need it.
- Rehearse your response. Run tabletop exercises that walk through an incident end to end, such as a leaked API key or a phished approver. They show where your plan and reality differ, such as a missing permission or an approver who cannot be reached, before you find out during a real incident.