I'm theorizing what the ideal patterns are for developing applications that deal with centralized payment processing.
I've come up with the following list, please let me know if there is more to add or if you think something is wrong.
There have been many hacks, which is often the fault of monolith systems without proper isolation and authentication.
I haven't looked into topics such as cold storage, anomaly detection, rate limiting and would love to hear ideas.
1. Use microservice pattern
Following the microservice pattern is a must. Payment processors must force interaction through an API. The sensitive payment processing code must have its own database with limited privileges (eg. INSERT, UPDATE, SELECT).
Ask yourself whether deletes are necessary, I recommend not giving it that ability such that there is an audit trail after an incident.
- Forces you to think about authentication.
- Isolation of the sensitive code and credentials from the main application.
- Reduces the attack surface.
2. Split the cryptocurrencies into their own microservices and containers
A malicious insertion into the source of one cryptocurrency, should not affect any other cryptocurrency.
- Containers/virtualization is your friend, isolate as much as possible.
- Docker is your friend but be distrustful of container images created by other people. Use private repositories.
- Bind containers that execute sensitive cryptographic operations to a single dedicated core, use CPU affinity.
- Each cryptocurrency should have its own database, but the API for interaction should be identical.
3. Process payments from a queue, not on-demand
- Users shouldn't be able to trigger an asynchronous withdrawal, the corresponding cryptocurrency microservice should maintain a queue which is processed every X amount of time.
- Use mutex locking to prevent ANY possibility of concurrent processing of payments, if a processing request accidentally wants to process the queue, it must happen in sync.
- Spamming transaction attacks may trigger payment processing requests that overlap and can therefore cause undefined behavior if no mutex locking is provided.
- As a general rule: deposits should be processed before withdrawals (to prevent a withdrawal from using coins of an unconfirmed deposit.
- Always use UTXOs (txid + vout) and their amount to track payments (rpc command listunspent). Txid's on their own mean nothing.
- Always check whether a UTXO is already present in the database.
- Register deposit transactions eagerly and mark them as "unconfirmed", then periodically monitor the transactions for confirmations, and only when confirmed, update the balance of the user. (rpc fields 'safe' and 'solvable')
- The balance of a user requesting a withdrawal should subtract the balance eagerly and verify that the remaining balance is a positive amount, only then push the withdrawal on the queue of "to be processed".
- Evict the withdrawal from the queue before actually executing the transaction. A failed withdrawal should not stay in the queue as it may repeat itself and drain all the funds.
- Think about corner cases where a withdrawal is made to an address within the system and would also have to account for a deposit!
- Rollback at any error on a transaction. Make sure that the eviction from the "to process" queue (both deposit and withdrawal) is not rollbacked though.
4. Authentication for microservice X should not be valid for microservice Y
Make clever use of authentication. I recommend using OpenID Connect JWT tokens that a cryptographically signed by the authentication microservice.
JWTs should:
- Be fully verified before even inspecting them.
- Only be valid for this particular service, tokens generated for other microservices should not provide access.
- Only be valid for a single request and provide replay protection.
Then the user turns to the authentication microservice and ask its to include the hash(challenge + API function + API request) in the creation of the JWT token specific for the cryptocurrency microservice.
5. Optimize for security not for performance
- Realistically, how many transactions does your company expect to process?
- If you're using SQL, make sure to always used prepared statements and transactions where necessary.
- If you're using SQL, make sure to test every endpoint for SQL injections. Doing this retroactively is often PITA to do manually. There are automated tools that can also crawl the website for you.
0. Obvious notes
- HTTPS everywhere...