Tác giả: HolySheep AI Team — Chuyên gia tích hợp API tài chính số

Mở đầu: Câu chuyện thật từ một dự án bị giữ tiền

Tháng 3/2025, một đội ngũ phát triển game blockchain tại Việt Nam gặp phải tình huống kinh hoàng: $480,000 USDT bị khóa trong ví multisig của OKX. Nguyên nhân? Một developer mới vô tình có quyền API withdrawal đầy đủ đã trigger một giao dịch test vào đúng thời điểm hệ thống giám sát bảo trì.

May mắn họ phát hiện sớm và nhờ hỗ trợ từ OKX để freeze giao dịch. Tuy nhiên, sự cố này cho thấy một thực tế: hầu hết các team không phân tách quyền API đúng cách khi vận hành ví đa chữ ký cho doanh nghiệp.

Bài viết này sẽ hướng dẫn chi tiết cách cấu hình phân quyền API multi-signature cho Binance, OKX và Bybit — giúp bạn xây dựng hệ thống kiểm soát rủi ro chặt chẽ như các quỹ đầu tư chuyên nghiệp.

Multi-Signature Wallet là gì và tại sao cần phân tách quyền

Multi-signature (multisig) wallet yêu cầu nhiều hơn một chữ ký để thực hiện giao dịch. Thay vì một private key đơn lẻ có thể bị compromise, multisig phân tán rủi ro cho nhiều người giữ khóa.

3 mô hình multisig phổ biến

Mô hình Cấu hình Use case Rủi ro
M-of-N Standard 2-of-3, 3-of-5 Quỹ công ty, DAO treasury Trung bình
Role-Based Access Admin/Operator/Viewer Exchange, payment gateway Thấp
Time-Locked Recovery 24h delay + override key High-value storage Rất thấp

Trong bài viết này, chúng ta tập trung vào Role-Based Access Control (RBAC) được triển khai qua API của các sàn giao dịch lớn.

Kiến trúc hệ thống đề xuất

┌─────────────────────────────────────────────────────────────────┐
│                    ENTERPRISE TREASURY SYSTEM                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐       │
│  │   VIEWER     │    │  OPERATOR    │    │    ADMIN     │       │
│  │   API Key    │    │   API Key    │    │   API Key    │       │
│  │  Read-only   │    │  Withdraw    │    │  All Access  │       │
│  │              │    │  (Limited)   │    │  + Settings  │       │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘       │
│         │                   │                   │               │
│         └───────────────────┼───────────────────┘               │
│                             │                                   │
│                    ┌────────▼────────┐                          │
│                    │   APPROVAL      │                          │
│                    │   GATEWAY       │                          │
│                    │  (Custom Logic) │                          │
│                    └────────┬────────┘                          │
│                             │                                   │
│         ┌───────────────────┼───────────────────┐                │
│         │                   │                   │                │
│  ┌──────▼──────┐    ┌──────▼──────┐    ┌──────▼──────┐          │
│  │  BINANCE    │    │    OKX      │    │   BYBIT     │          │
│  │ Sub-Account │    │   Master    │    │   Unified   │          │
│  │  + Sub-User │    │  Account    │    │   Account   │          │
│  └─────────────┘    └─────────────┘    └─────────────┘          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Phần 1: Binance Sub-Account với Phân Quyền API

Bước 1: Tạo Master Account API Key

Đăng nhập Binance vào Dashboard → API Management → Create API. Chọn loại System (generated) để Binance tự sinh key thay vì tự quản lý.

# ============================================

BINANCE: Tạo Sub-Account với API Key riêng

============================================

Base URL: https://api.binance.com

import requests import hashlib import hmac import time BINANCE_API_KEY = "YOUR_BINANCE_API_KEY" BINANCE_SECRET_KEY = "YOUR_BINANCE_SECRET_KEY" BASE_URL = "https://api.binance.com" def binance_signature(params, secret_key): """Tạo signature cho Binance API""" query_string = '&'.join([f"{k}={v}" for k, v in params.items()]) signature = hmac.new( secret_key.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256 ).hexdigest() return signature def create_sub_account(email, api_key, ip_whitelist): """ Tạo sub-account với quyền hạn chế - Spot trading: enabled - Withdrawal: DISABLED by default - Internal transfer: enabled """ endpoint = "/sapi/v1/sub-account/virtualSubAccount" params = { "email": email, "recvWindow": 5000, "timestamp": int(time.time() * 1000) } params["signature"] = binance_signature(params, BINANCE_SECRET_KEY) headers = { "X-MBX-APIKEY": BINANCE_API_KEY, "Content-Type": "application/x-www-form-urlencoded" } response = requests.post( f"{BASE_URL}{endpoint}", headers=headers, data=params ) return response.json()

Ví dụ: Tạo sub-account cho Trading Bot

result = create_sub_account( email="[email protected]", api_key="trading-bot-key", ip_whitelist="203.0.113.0/24" # Chỉ cho phép IP nội bộ ) print(result)

Bước 2: Cấu hình Sub-Account Permissions

# ============================================

BINANCE: Phân quyền chi tiết cho Sub-Account

============================================

def set_sub_account_permissions(master_api_key, sub_email, permissions): """ Cấu hình permissions chi tiết cho sub-account Permissions available: - enableSpotTrading: bool - enableMarginTrading: bool - enableFuturesTrading: bool - enableVanillaOptions: bool - enableSubAccountApiKey": bool - enableUniversalTransfer: bool - enableDerivatives: bool - enableWithdrawals: bool (Rất quan trọng!) """ endpoint = "/sapi/v1/sub-account/updateUserPermission" params = { "subEmail": sub_email, "canTrade": permissions.get("canTrade", True), "canDeposit": permissions.get("canDeposit", False), "canWithdraw": permissions.get("canWithdraw", False), # Mặc định OFF "canInternalTransfer": permissions.get("canInternalTransfer", True), "recvWindow": 5000, "timestamp": int(time.time() * 1000) } params["signature"] = binance_signature(params, BINANCE_SECRET_KEY) headers = {"X-MBX-APIKEY": BINANCE_API_KEY} response = requests.post(f"{BASE_URL}{endpoint}", headers=headers, data=params) return response.json() def create_sub_api_key(sub_email, permissions): """ Tạo API key riêng cho sub-account với permissions cụ thể """ endpoint = "/sapi/v1/sub-account/apiRestriction" params = { "email": sub_email, "subAccountApiKey": "", # Để trống = tạo mới "permissions": { "enableSpotAndMarginTrading": permissions.get("spot", False), "enable futures": permissions.get("futures", False), "enableWallet": permissions.get("wallet", False), "enableWithdrawals": permissions.get("withdraw", False), # LUÔN = False cho bot "enableInternalTransfer": permissions.get("internal_transfer", True), "enableVanillaOptions": permissions.get("options", False), "enableReading": permissions.get("read_only", True), }, "ipRestrict": True, "allowedIps": "203.0.113.0/24,198.51.100.0/24", # Whitelist IPs "recvWindow": 5000, "timestamp": int(time.time() * 1000) } params["signature"] = binance_signature(params, BINANCE_SECRET_KEY) response = requests.post(f"{BASE_URL}{endpoint}", headers=headers, data=params) return response.json()

============================================

VÍ DỤ THỰC TẾ: Cấu hình cho 3 vai trò

============================================

1. VIEWER - Chỉ đọc, không thao tác

viewer_config = { "spot": False, "futures": False, "wallet": False, "withdraw": False, "internal_transfer": False, "options": False, "read_only": True }

2. OPERATOR - Giao dịch được, rút tiền HẠN CHẾ

operator_config = { "spot": True, "futures": True, "wallet": False, # Không rút tiền trực tiếp "withdraw": False, # Chỉ qua internal transfer "internal_transfer": True, # Chuyển sang ví chính "options": False, "read_only": False }

3. ADMIN - Full access nhưng vẫn cần multi-approval

admin_config = { "spot": True, "futures": True, "wallet": True, "withdraw": True, # Cần ít nhất 2/3 keys "internal_transfer": True, "options": True, "read_only": False }

Áp dụng cấu hình

viewer_result = create_sub_api_key("[email protected]", viewer_config) operator_result = create_sub_api_key("[email protected]", operator_config) admin_result = create_sub_api_key("[email protected]", admin_config) print(f"Viewer API: {viewer_result}") print(f"Operator API: {operator_result}") print(f"Admin API: {admin_result}")

Phần 2: OKX Account API với Multi-Permission

Tạo Sub-Account và API Key với OKX

# ============================================

OKX: Multi-SubAccount với RBAC

====================================

Base URL: https://www.okx.com

import jwt import datetime OKX_API_KEY = "YOUR_OKX_API_KEY" OKX_SECRET_KEY = "YOUR_OKX_SECRET_KEY" OKX_PASSPHRASE = "YOUR_OKX_PASSPHRASE" BASE_URL = "https://www.okx.com" def okx_sign(timestamp, method, path, body, secret_key): """Tạo signature cho OKX API v2""" message = timestamp + method + path + (body if body else "") mac = hmac.new( secret_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).digest() return base64.b64encode(mac).decode() def okx_headers(method, path, body=None): """Tạo headers cho OKX API v5""" timestamp = datetime.datetime.utcnow().isoformat() + 'Z' signature = okx_sign(timestamp, method, path, body, OKX_SECRET_KEY) return { 'Content-Type': 'application/json', 'OK-ACCESS-KEY': OKX_API_KEY, 'OK-ACCESS-SIGN': signature, 'OK-ACCESS-TIMESTAMP': timestamp, 'OK-ACCESS-PASSPHRASE': OKX_PASSPHRASE, } def create_sub_account(sub_account_name, label): """ Tạo sub-account trên OKX """ endpoint = "/api/v5/users/create-subaccount" payload = { "subAccount": sub_account_name, "label": label } headers = okx_headers("POST", endpoint, json.dumps(payload)) response = requests.post( f"{BASE_URL}{endpoint}", headers=headers, data=json.dumps(payload) ) return response.json() def set_sub_account_permission(sub_account, permissions): """ Cấu预设权限 cho sub-account Permission values: - "readOnly": chỉ đọc - "trade": giao dịch - "withdraw": rút tiền - "transfer": chuyển tiền nội bộ """ endpoint = "/api/v5/users/subaccount/set-permission" payload = { "subAccount": sub_account, "perm": permissions.get("perm", "readOnly"), "apikey": { "apiKey": permissions.get("apiKey", ""), "perm": permissions.get("apiPerm", "readOnly"), "ipWhitelist": permissions.get("ipWhitelist", []) } } headers = okx_headers("POST", endpoint, json.dumps(payload)) response = requests.post( f"{BASE_URL}{endpoint}", headers=headers, data=json.dumps(payload) ) return response.json()

============================================

VÍ DỤ: Cấu hình 3-tier permission

============================================

1. Tạo 3 sub-accounts

sub_accounts = { "viewer": { "name": "vault-viewer-01", "label": "Treasury Viewer - Read Only" }, "operator": { "name": "vault-operator-01", "label": "Treasury Operator - Limited Trade" }, "admin": { "name": "vault-admin-01", "label": "Treasury Admin - Full Control" } }

Tạo sub-accounts

for role, config in sub_accounts.items(): result = create_sub_account(config["name"], config["label"]) print(f"Created {role}: {result}")

2. Cấu hình permissions chi tiết

permission_configs = { "viewer": { "perm": "readOnly", "apiKey": "", "apiPerm": "readOnly", "ipWhitelist": ["203.0.113.0/24"] }, "operator": { "perm": "trade", "apiKey": "", "apiPerm": "trade", "ipWhitelist": ["203.0.113.0/24", "198.51.100.0/24"] }, "admin": { "perm": "trade,withdraw,transfer", "apiKey": "", "apiPerm": "trade,withdraw,transfer", "ipWhitelist": ["203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"] } }

Áp dụng permissions

for role, perms in permission_configs.items(): sub_acct = sub_accounts[role]["name"] result = set_sub_account_permission(sub_acct, perms) print(f"Permission {role}: {result}")

Phần 3: Bybit Unified Account API

Unified Account với Permission Groups

# ============================================

BYBIT: Unified Account với API Permission

====================================

Base URL: https://api.bybit.com

BYBIT_API_KEY = "YOUR_BYBIT_API_KEY" BYBIT_SECRET_KEY = "YOUR_BYBIT_SECRET_KEY" BASE_URL = "https://api.bybit.com" def bybit_sign(api_key, timestamp, recv_window, param_str, secret_key): """Tạo signature cho Bybit API v3""" message = str(timestamp) + api_key + recv_window + param_str mac = hmac.new( secret_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).digest() return mac.hex() def create_unified_api(sub_uid, api_key_label, permissions): """ Tạo API key cho unified account với permissions Permission flags: - readOnly: chỉ đọc dữ liệu - trade: giao dịch spot/futures - withdraw: rút tiền (cần 2FA bổ sung) - transfer: chuyển tiền nội bộ - unified: quản lý tài khoản """ endpoint = "/v5/user/create-sub-api" params = { "subUid": sub_uid, "apiKey": api_key_label, "permissions": { "readOnly": permissions.get("readOnly", True), "trade": permissions.get("trade", False), "contractTrade": permissions.get("contractTrade", False), "wallet": permissions.get("wallet", False), "withdraw": permissions.get("withdraw", False), "isMaintMode": permissions.get("isMaintMode", False), "transfer": permissions.get("transfer", False), "usdtPerm": permissions.get("usdtPerm", False), }, "ips": permissions.get("ips", ["203.0.113.0/24"]), "validDate": permissions.get("validDate", 90), # Hết hạn sau 90 ngày "recvWindow": 5000, "timestamp": int(time.time() * 1000) } param_str = json.dumps(params) timestamp = int(time.time() * 1000) recv_window = "5000" sign = bysign(BYBIT_API_KEY, timestamp, recv_window, param_str, BYBIT_SECRET_KEY) headers = { "Content-Type": "application/json", "X-BAPI-API-KEY": BYBIT_API_KEY, "X-BAPI-SIGN": sign, "X-BAPI-SIGN-TYPE": "2", "X-BAPI-TIMESTAMP": str(timestamp), "X-BAPI-RECV-WINDOW": recv_window } response = requests.post(f"{BASE_URL}{endpoint}", headers=headers, data=param_str) return response.json()

============================================

CẤU HÌNH THỰC TẾ: 3 vai trò cho Bybit

============================================

VIEWER: Chỉ đọc, không thao tác gì

viewer_bybit = { "readOnly": True, "trade": False, "contractTrade": False, "wallet": False, "withdraw": False, "transfer": False, "usdtPerm": False, "ips": ["203.0.113.0/24"], "validDate": 365 # Dài hạn cho monitoring }

OPERATOR: Giao dịch được, không rút tiền

operator_bybit = { "readOnly": False, "trade": True, "contractTrade": True, "wallet": True, # Xem ví "withdraw": False, # KHÔNG BAO GIỜ rút tiền trực tiếp "transfer": True, # Chỉ chuyển nội bộ "usdtPerm": True, "ips": ["203.0.113.0/24", "198.51.100.0/24"], "validDate": 90 # Hết hạn sau 90 ngày }

ADMIN: Full access nhưng giới hạn IP và có 2FA

admin_bybit = { "readOnly": False, "trade": True, "contractTrade": True, "wallet": True, "withdraw": True, # Cần thêm 2FA verification "transfer": True, "usdtPerm": True, "ips": ["203.0.113.0/24", "198.51.100.0/24"], "validDate": 30 # Hết hạn nhanh, cần renew định kỳ }

Tạo API keys

viewer_result = create_unified_api("VIEWER_UID", "vault-viewer", viewer_bybit) operator_result = create_unified_api("OPERATOR_UID", "vault-operator", operator_bybit) admin_result = create_unified_api("ADMIN_UID", "vault-admin", admin_bybit) print("Bybit API Setup Results:") print(f"Viewer: {viewer_result}") print(f"Operator: {operator_result}") print(f"Admin: {admin_result}")

Approval Gateway: Triển khai Multi-Approval Logic

Sau khi có API keys với permissions riêng biệt, bạn cần một Approval Gateway để enforce quy trình multi-approval cho các giao dịch lớn.

# ============================================

APPROVAL GATEWAY: Multi-Signature Request Flow

============================================

import redis from typing import List, Dict from dataclasses import dataclass from enum import Enum class TransactionType(Enum): SMALL_WITHDRAW = "small" # < $10,000 MEDIUM_WITHDRAW = "medium" # $10,000 - $100,000 LARGE_WITHDRAW = "large" # > $100,000 CRITICAL = "critical" # > $500,000 @dataclass class ApprovalRequest: request_id: str requester_id: str amount: float currency: str destination: str required_approvals: int approvals: List[str] status: str created_at: float expires_at: float class MultiSigApprovalGateway: """ Gateway quản lý multi-approval cho các giao dịch lớn """ def __init__(self, redis_host="localhost", redis_port=6379): self.redis = redis.Redis(host=redis_host, port=redis_port, decode_responses=True) self.approval_thresholds = { TransactionType.SMALL_WITHDRAW: 1, # 1 người approve TransactionType.MEDIUM_WITHDRAW: 2, # 2 người approve TransactionType.LARGE_WITHDRAW: 3, # 3 người approve TransactionType.CRITICAL: 5, # 5 người + CEO sign } def classify_transaction(self, amount_usd: float) -> TransactionType: """Phân loại giao dịch dựa trên số tiền""" if amount_usd >= 500000: return TransactionType.CRITICAL elif amount_usd >= 100000: return TransactionType.LARGE_WITHDRAW elif amount_usd >= 10000: return TransactionType.MEDIUM_WITHDRAW else: return TransactionType.SMALL_WITHDRAW def create_withdrawal_request( self, requester_id: str, amount: float, currency: str, destination: str, exchange: str, api_key: str ) -> Dict: """ Tạo yêu cầu rút tiền - tự động xác định số approvals cần thiết """ tx_type = self.classify_transaction(amount) required = self.approval_thresholds[tx_type] request = ApprovalRequest( request_id=f"req_{uuid.uuid4().hex[:12]}", requester_id=requester_id, amount=amount, currency=currency, destination=destination, required_approvals=required, approvals=[], status="pending", created_at=time.time(), expires_at=time.time() + 3600 # 1 giờ ) # Lưu vào Redis self.redis.setex( f"approval:{request.request_id}", 3600, json.dumps(request.__dict__) ) # Gửi notification self._notify_approvers(request, tx_type) return { "request_id": request.request_id, "required_approvals": required, "transaction_type": tx_type.value, "status": "pending", "expires_in": 3600 } def approve_request(self, request_id: str, approver_id: str) -> Dict: """ Approve một yêu cầu - kiểm tra xem đã đủ chữ ký chưa """ key = f"approval:{request_id}" data = self.redis.get(key) if not data: return {"error": "Request not found or expired"} request = json.loads(data) # Kiểm tra approver có quyền không if approver_id in request["approvals"]: return {"error": "Already approved by this approver"} # Thêm approval request["approvals"].append(approver_id) # Kiểm tra đủ số lượng chưa if len(request["approvals"]) >= request["required_approvals"]: request["status"] = "approved" self.redis.setex(key, 300, json.dumps(request)) # 5 phút để execute return { "status": "approved", "can_execute": True, "execution_window": 300 } else: remaining = request["required_approvals"] - len(request["approvals"]) self.redis.setex(key, 3600, json.dumps(request)) return { "status": "pending", "approvals": request["approvals"], "remaining": remaining } def execute_withdrawal(self, request_id: str, exchange: str) -> Dict: """ Thực hiện withdrawal sau khi đủ approvals """ key = f"approval:{request_id}" data = self.redis.get(key) if not data: return {"error": "Request expired"} request = json.loads(data) if request["status"] != "approved": return {"error": "Request not approved"} # Thực hiện withdrawal qua exchange API result = self._call_exchange_api(request, exchange) # Cleanup self.redis.delete(key) return result

============================================

SỬ DỤNG: Ví dụ flow cho withdrawal $250,000

============================================

gateway = MultiSigApprovalGateway()

1. Tạo yêu cầu rút $250,000

request = gateway.create_withdrawal_request( requester_id="bot-trading-01", amount=250000, currency="USDT", destination="0x742d35Cc6634C0532925a3b844Bc9e7595f3f234", exchange="binance", api_key="binance_operator_key" ) print(f"Request created: {request}")

Output: {

"request_id": "req_a1b2c3d4e5f6",

"required_approvals": 3,

"transaction_type": "large",

"status": "pending",

"expires_in": 3600

}

2. 3 approvers approve lần lượt

approvers = ["[email protected]", "[email protected]", "[email protected]"] for approver in approvers: result = gateway.approve_request(request["request_id"], approver) print(f"Approval from {approver}: {result}")

3. Sau khi đủ 3 approvals, execute

execution = gateway.execute_withdrawal(request["request_id"], "binance") print(f"Execution result: {execution}")

Lỗi thường gặp và cách khắc phục

Lỗi 1: API Key không có quyền Withdrawal

# ============================================

LỖI: "81001 - Withdrawal is not allowed for this account"

============================================

""" NGUYÊN NHÂN: - API key được tạo với permission mặc định không có withdraw - Trên Binance: sub-account có thể bị giới hạn bởi master account - Trên OKX: sub-account cần enable withdrawal permission riêng - Trên Bybit: unified account cần kích hoạt withdrawal permission GIẢI PHÁP: """

=== Bước 1: Kiểm tra quyền hiện tại của API Key ===

def check_api_permissions(exchange, api_key): """Kiểm tra permissions của API key""" if exchange == "binance": # Binance: Query sub-account API key permissions endpoint = "/sapi/v1/sub-account/apiRestriction" # Gọi API với master key elif exchange == "okx": # OKX: Query sub-account permissions endpoint = "/api/v5/users/subaccount/permission" elif exchange == "bybit": # Bybit: Query API key info endpoint = "/v5/user/query-api" return permissions

=== Bước 2: Cập nhật permissions ===

Binance: Update sub-account permissions

def enable_withdrawal_binance(sub_email): """Bật withdrawal cho sub-account Binance""" endpoint = "/sapi/v1/sub-account/updateUserPermission" params = { "subEmail": sub_email, "canWithdraw": True, # Bật withdrawal "canInternalTransfer": True, "canTrade": True, "timestamp": int(time.time() * 1000) } # ... rest of signature logic

OKX: Enable withdrawal permission

def enable_withdrawal_okx(sub_account, api_key): """Bật withdrawal cho sub-account OKX""" endpoint = "/api/v5/users/subaccount/set-permission" payload = { "subAccount": sub_account, "perm": "trade,withdraw,transfer", # Thêm withdraw "apikey": { "apiKey": api_key, "perm": "trade,withdraw,transfer" } } # ... rest of request logic

Bybit: Update API key permissions

def enable_withdrawal_bybit(api_key): """Bật withdrawal cho Bybit unified API""" endpoint = "/v5/user/update-api" payload = { "apiKey": api_key, "permissions": { "withdraw": True, "readOnly": False, "trade": True } } # ... rest of request logic

Lỗi 2: IP Whitelist không khớp

# ============================================

LỖI: "IP address not in whitelist" hoặc "Invalid IP"

============================================

""" NGUYÊN NHÂN THƯỜNG GẶP: 1. Server của bạn có dynamic IP 2. Cloud provider thay đổi IP khi restart instance 3. Request đến từ IP khác với whitelist 4. Format IP whitelist không đúng (thiếu /32, sai CIDR) GIẢI PHÁP: """

=== Giải pháp 1: Sử dụng Static IP qua Cloud NAT ===

def setup_static