Skip to main content

Security best practices: Denial of service

Intermediate
Security
Concept

Protect against DoS and DDoS attacks

Security concern

A Denial of Service (DoS) attack aims to make a system unavailable by overwhelming it with requests or data. A Distributed Denial of Service (DDoS) attack is a more sophisticated version, where the attack originates from multiple sources, making it harder to block. An attacker will typically search for operations that are free to be executed by anyone but which are expensive for the application in terms of certain resources such as storage, memory usage, network bandwidth, computing resources, etc. In the case of canisters, such attacks can aim to deplete cycles, making the canister unable to process legitimate requests. The reverse gas model means that a dapp needs to implement strategies to deal with this.

Recommendation

To protect your canisters from DoS and DDoS attacks, consider the following strategies:

  • Bot prevention techniques: Use methods like captchas or proof of work to ensure only legitimate users can access your canister. Captchas help verify that the user is human, while proof of work requires the user to spend computational resources to proceed, deterring automated attacks. Internet Identity has a captcha implementation that can serve as an example for implementing this in other projects.
  • Monitor cycles usage: Regularly track your canisters cycles consumption and set alerts for any sudden spikes that may indicate an attack.
  • Ingress message charging: While charging for ingress messages (external requests to the canister) is not natively supported, custom solutions could be implemented to make sure that any expensive actions have costs associated with them.
  • Filter ingress messages using inspect message: Certain non-critical checks can be placed in the inspect message function to filter out ingress update messages before they are executed by all nodes of a subnet. Since this code only runs on a single node, the execution does not consume cycles but it also shouldn't be relied upon for security critical checks such as access control. However, they can efficiently reject certain ingress messages early. Read the corresponding documentation and security best practice carefully for the caveats.

Protect against noisy neighbors

Security concern

In a shared resource environment like the Internet Computer, multiple canisters can run on the same subnet. If one canister consumes too many resources (CPU, memory, etc.), it can negatively impact the performance of others on the same subnet. This is known as the "noisy neighbor" problem.

Recommendation

To mitigate the "noisy neighbor" issue, manage your canister's resource allocation effectively:

  • Memory allocation: Memory can be reserved per canister by setting memory_allocation, ensuring that your canister can always allocate memory up to the requested memory_allocation and preventing other canisters from using up the subnets available memory. Note that this also reduces the upper bound of memory the canister can allocate to the same value. Monitoring actual memory usage against this value is important to avoid availability issues.
  • Compute reservation: Similar to memory, computing power can also be reserved by setting compute_allocation to a value between 0 and 100, which denotes the percentage of one CPU core to be reserved for this canister. A value of 50 means that every 2 rounds, the canister will be scheduled to execute a message. This guarantees the minimal progress you canister can make, which protects against noisy neighbors. Both allocations are reserving resources for your canister on the subnet, which prevents the other canisters from using them. Hence, they come at a cost. Memory allocation is charged as if all that memory would be allocated. Compute allocation is currently charged at 10M cycles per percentage point. Learn more about managing memory and compute resources in the Storage and Compute guides.
  • Subnet and canister distribution: Implement a smart canister deployment strategy by monitoring the load on subnets. You can choose to deploy new canisters on less busy subnets or adopt a multi-canister architecture that balances the load across subnets. Be mindful to minimize inter-subnet communication for canisters that frequently interact with each other. Additionally, avoid deploying to known high-traffic subnets where possible, though keep in mind that resource usage can change unexpectedly with new dapps.

When the subnet grows above 450GiB, then the new reservation mechanism activates. Every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is. For example, it may cover days, months, or even years of payments for the newly allocated bytes. It is important to note that the reservation mechanism applies only to the newly allocated bytes and does not apply to the storage already in use by the canister. See more at Reseourse reservations.

Handle expensive calls

Security concern

Some calls (update or query) might be expensive in terms of the memory or cycles they consume. For example, any function using chain-key signing or HTTPS outcalls are relatively expensive. See the additional documentation on cycles cost details and for other examples.

An attacker will target expensive calls to drain cycles balance or available memory quickly.

Recommendation

  • Use captchas: Expensive operations should require a captcha to be solved. Try to use a library to implement a captcha instead of a cloud service, as such a service would require HTTPS outcalls and isn't decentralized.
  • Use PoW (proof-of-work): Require a proof-of-work challenge to be solved by the client for any expensive operation. The parameters need to be carefully chosen to require sufficient computation per call to the expensive operation without creating too much impact for legitimate clients. Don't forget to consider clients on slow and older mobile devices whilst protecting against attackers on modern multi-GPU systems. Certain algorithms can limit the performance increase of GPUs to improve this uneven battlefield.
  • Charge for expensive calls: You can require that certain expensive calls from other canisters include cycles to compensate for the resources consumed. In addition, one can charge for ingress messages. However, that is not currently supported by the protocol itself, and a custom solution, such as pre-paying a certain amount, would need to be designed.
  • Differentiate between update and query calls: Expensive computations should generally be avoided for update calls unless absolutely necessary. While query calls are not authenticated, they are faster and less resource-intensive. To check whether a method was called as a query or update call, you can use ic0.in_replicated_execution().

Further recommendations

  • Automatically monitor cycles consumption and set appropriate alerts for cycles consumption rate and balance. Sudden spikes in cycles consumption could indicate an attack.
  • Implement early authentication and rate limiting for your canisters.
  • Be aware of attacks targeting high cycles-consuming calls.
  • See the "Cycle balance drain attacks section" in how to audit an ICP canister.