Custody security: best practices
Hardening the withdrawal and signing pipeline for crypto exchanges and custodians, based on lessons from the 2025 Bybit and 2026 Bitget incidents. Written for teams integrating the Trustodian API and SDK into their own stack.
Core principle
Protect the intent, not just the key.
MPC, multisig, and HSMs protect private keys. They do not, on their own, verify that a transaction is one you actually meant to send.
Both attacks produced valid signatures without stealing a key
In both the Bybit (February 2025, about $1.5B) and Bitget (September 2026, about $387.5M) incidents, attackers did not steal private keys. Instead, they got the operator's own signing system to produce valid signatures for transfers the operator never intended to send.
Bybit
The multisig signing interface was tampered with, so human signers saw one transaction and approved another.
Bitget
A vulnerability in a third-party security product gave attackers internal credentials. They compromised the wallet backend, forged transaction data, and drove the normal authorization flow.
The shared failure: one signing pipeline trusted one upstream source of truth. Once that source was compromised, the signing technology did exactly what it was built to do, on forged instructions.
Assume any single component can be fully compromised at any time
| Component | Example compromise | Primary control |
|---|---|---|
| Withdrawal backend | Forged transaction data sent to signers | Independent verification at each signer (section 1) |
| Admin or service credentials | Stolen via vendor flaw or phishing | Separate trust roots, least privilege (sections 2, 8) |
| Signing UI | Displays a different transaction than the one signed | Clear signing on independent devices (section 6) |
| Third-party tooling | Security or monitoring agent with broad access | Vendor isolation (section 7) |
| A single signer node | Malicious or buggy policy | Threshold across independently run nodes (section 2) |
Design goal: no single compromise can move funds beyond a small, pre-defined, bounded amount.
Three independent policy engines, three signers, one direct halt
Dashed boxes are read or control paths that bypass the transaction builder: each reads the ledger independently, and the reconciliation monitor can halt every signer without going through the backend.
- Every signer has its own policy engine that validates independently.
- Policy engines read the ledger through separate paths, not through the tx builder.
- The reconciliation monitor can halt signing directly, without going through the backend.
Verify intent at the signer
The signer must never sign something just because the backend asked. Each signing party (every MPC node, multisig co-signer, or HSM policy layer) should independently confirm all of the following:
- A matching withdrawal request exists in the user ledger (same user, asset, amount, destination).
- The user's balance covered the amount at request time.
- The request is signed by the user's device key and the signature is valid (end-to-end request signing).
- The destination is allowed for this wallet tier (section 3).
- The amount is within per-transaction and rolling velocity limits (section 4).
- The request has not already been signed (replay and double-spend protection).
- The raw transaction the signer is about to sign actually decodes to the validated intent: no hidden calls, approvals, or delegatecalls.
Example policy check (pseudocode)
def approve_signing(raw_tx, request):
intent = decode(raw_tx) # decode what will actually be signed
assert verify_user_sig(request) # request came from the user's device
record = ledger_readonly.get(request.id) # independent read, not via backend
assert record and record.matches(intent) # ledger agrees with the tx
assert not already_signed(request.id) # no replay
assert destination_allowed(intent, wallet_tier) # tier flow rules
assert within_velocity_limits(intent) # per-tx and rolling caps
assert not circuit_breaker_tripped() # global halt
return TrueIf any check fails, the signer refuses and raises an alert.
Separate trust roots
Multiple signers add little if they all trust the same upstream system.
- Run each signer and its policy engine on separate infrastructure, ideally separate cloud accounts or providers.
- Have different teams operate different signers, with no shared admin credentials.
- Give each policy engine its own read path to the ledger: a read replica, signed ledger snapshots, or an independent reconciliation service.
- Where practical, use different software implementations for policy checks so one bug or one exploit does not affect all signers.
- Make sure no single person, credential, or service can reach a threshold of signers.
Wallet tiering and flow restrictions
Keep the amount at risk small, and restrict where each tier can send.
| Tier | Share of assets (example) | Allowed destinations | Signing |
|---|---|---|---|
| Hot | As small as operations allow | User withdrawals within limits | Automated, policy-gated |
| Warm | Buffer for refilling hot | Only the operator's own hot wallets | Policy-gated plus human approval above threshold |
| Cold | Majority of assets | Only the operator's own warm wallets | Offline, multi-party, human approval, time delay |
- Enforce flow rules at the signer, and on-chain where possible (smart contract wallets with allowlists).
- Refill hot wallets in small, frequent batches rather than large transfers.
- Size hot balances from actual withdrawal data, and review regularly.
Velocity limits and time delays
- Per-transaction caps per asset and per tier.
- Rolling window caps (for example, per 10 minutes and per hour) across all transactions.
- Per-destination caps, especially for new or rarely used addresses.
- Time delays on large transfers, with a cancellation window monitored by a separate team.
- Limits must be enforced inside each signer's policy engine, not only in the backend.
Circuit breakers
Detection only helps if it triggers action automatically.
- Continuous reconciliation of on-chain balances against the internal ledger.
- Automatic signing halt when reconciliation fails, velocity limits are hit, or anomalous destinations appear.
- The halt signal goes directly to signers through an independent channel, not through the backend.
- Resuming signing requires multi-party human approval.
- Target: halt within seconds, not minutes. In the Bitget incident, a discrepancy was flagged about seven minutes after the first large transfer, yet on-chain outflows continued for hours.
Human approval and clear signing
For transfers above threshold and all cold or warm movements:
- Approvers verify the decoded transaction on a dedicated device (hardware wallet or air-gapped signer), never only on a web UI.
- Use clear signing: the device shows destination, amount, and contract call in human-readable form.
- Require approvers from different teams, using different devices and networks.
- Serve signing UIs from pinned, integrity-checked builds (subresource integrity, reproducible builds, no dynamic third-party scripts).
- Compare the transaction hash on at least two independent devices before approving.
Third-party and vendor risk
The Bitget entry point was reportedly a flaw in a third-party security product.
- Keep an inventory of every vendor tool with access to production.
- No vendor tool should have a network path to signing infrastructure.
- Give agents (EDR, monitoring, backup) least privilege; avoid domain-admin-style credentials.
- Patch vendor software quickly, and subscribe to vendor security advisories.
- Isolate vendor management consoles on separate networks with separate credentials.
- Assess vendors for secure development practices and incident history.
Access control and infrastructure
- Network-segment signing infrastructure; allow only required, authenticated connections.
- Hardware-backed MFA (FIDO2 keys) for all privileged access.
- Just-in-time, time-bound privileged access with approval and logging.
- No standing production access for developers.
- Signed and reviewed deployments for backend, policy engines, and signing UIs.
- Tamper-evident, centralized audit logs stored outside the production trust domain.
- Routine credential rotation, plus immediate rotation after any suspected compromise.
Incident response
Prepare before you need it.
Halt
all signing (circuit breaker) and pause withdrawals.
Contain
revoke credentials, isolate affected systems, preserve forensic evidence.
Trace
work with on-chain analytics firms to label attacker addresses.
Freeze
contact stablecoin issuers, other exchanges, and bridges using pre-established contacts.
Communicate
publish an accurate, timely notice to users.
Recover
consider bounties for freezing and recovering funds.
Restore
resume withdrawals in phases only after the root cause is fixed and verified.
Report
publish a post-mortem with the root cause and remediation.
- Maintain a user protection fund or insurance sized to realistic worst-case hot and warm exposure.
- Run tabletop exercises at least twice a year.
Testing and assurance
- Red-team exercises that assume the backend is fully compromised and try to get a signature.
- Chaos tests: inject forged transactions and confirm signers refuse them.
- Test circuit breakers end to end, including halt latency.
- Independent audits of policy engines, signing UIs, and key management.
- Regular proof of reserves with liabilities, audited by a third party.
Fifteen controls to tick off
Signing and intent
- Every signer independently validates the decoded transaction against the ledger
- Withdrawal requests are signed end to end from the user's device
- Replay protection on signing requests
Trust separation
- Signers run on separate infrastructure with separate operators
- Policy engines have independent read paths to the ledger
- No single credential can reach a threshold of signers
Limits
- Wallet tier flow rules enforced at signer level
- Per-transaction and rolling velocity caps in every signer
- Time delays on large transfers with a monitored cancel window
Detection and response
- Automated reconciliation with direct signing halt
- Incident runbook tested in the last six months
- Pre-established contacts with issuers and analytics firms
People and vendors
- Clear signing on dedicated devices for human approvals
- Vendor tools have no path to signing infrastructure
- Hardware MFA and just-in-time access for all privileged roles
You can't stop an exchange being hacked, but you can limit your exposure
- Keep only what you are actively trading on exchanges.
- Store long-term holdings in a hardware wallet you control.
- Spread funds across more than one platform.
- Prefer platforms with audited proof of reserves and a meaningful protection fund.
- Enable withdrawal allowlists and hardware 2FA. These protect against account takeover, not a platform-level breach.
Sources for the incident details above
CoinDesk: Bitget's hack happened via spoofed transfers, not private keys
ReadThe Crypto Times: GoPlus analysis of how attackers moved funds without stealing keys
ReadHalborn: Explained, the Bitget hack
ReadSuggestions and corrections are welcome. Email [email protected] and include sources for any incident details.
Build on it
Trustodian enforces the quorum on-chain. Your integration should enforce the intent.
The SDK gives you per-key scopes, IP whitelists, spending limits, business hours, and address-book whitelisting. Pair them with the controls above so no single compromised system can produce a signature you didn't intend.