Filter JSON-RPC requests based on method with NGINX / OpenResty

Filter JSON-RPC requests based on method with NGINX / OpenResty

In this article, we will learn how to filter JSON-RPC APIs to deny requests based on the method name.

While exploring the Avalanche P-Chain API, we came across an issue: requesting the platform.getValidatorsAt method at an early height is a costly operation and often fails with timeouts. The bigger problem in this case is that while this call is being processed, the entire P-Chain API (/ext/bc/P) becomes unavailable for querying on the Avalanche node.

This is a big issue for RPC providers, and we needed to address it. Is there a way to filter JSON-RPC requests based on the method that is being used? The goal here is to have a configurable system in which we can allow/deny methods on a JSON-RPC API endpoint.

Note: In this article, we will focus on applying filters to AvalancheGo's API interface but the same logic can be applied to any software implementing the JSON-RPC specification.

State of JSON-RPC reverse proxy options

While Googling for an answer, we realized that there are not many options available. This was confirmed by Claus Strommer's article: The bleak landscape of protocol aware JSON RPC proxies:

There is still a clear need for proxies that can do payload filtering on arbitrary protocols.

We could not agree more!

Though this article came out in 2020, we still came across some interesting projects that have been released since. Before expanding on the solution we ended up choosing, here are some honorable mentions:

  • Subway: a project built in Rust for the Substrate framework. While promising, it seemed that the JSON-RPC method filtering was not implemented yet.

  • Apache Tuweni: an Apache-incubated Java library for Blockchain-related development. While it seemed that JSON-RPC method filtering is a possible use case for Tuweni, the laconic documentation and having to run a Java application made us look for another solution.

Note: We did not try these projects for this specific need.

Digging on GitHub, we found the ethereum-nginx-proxy by adetante. This one gained our interest because it is using NGINX, an industry standard reverse proxy.

As we were already using NGINX as part of our infrastructure, we decided to give this project a try.

Installing OpenResty

OpenResty 1.17.8.1 released - OpenResty Official Blog

OpenResty is an NGINX distribution packed with extra modules and their external dependencies. Since the ethereum-nginx-proxy project is essentially a LUA script leveraging the lua-nginx-module, it makes sense to install OpenResty.

Note: It is also possible to manually compile the module into NGINX but we have not tried this option.

As IT automation maximalists, we set out to develop a quick Ansible role to manage the installation/configuration of OpenResty. If you'd rather install OpenResty manually, you can follow the official installation documentation.

Note: The following resources can be found on our GitHub.

---
- hosts: validator01
  become: true
  tasks:
    - name: Import OpenResty GPG key
      get_url:
        url: https://openresty.org/package/pubkey.gpg
        dest: /etc/apt/trusted.gpg.d/openresty.asc

    - name: Add OpenResty APT repository
      apt_repository:
        repo: "deb http://openresty.org/package/ubuntu jammy main"
        filename: openresty
        state: present

    - name: Install OpenResty
      apt:
        name: openresty
        state: present

    - name: Template Nginx OpenResty configuration
      template:
        src: nginx.conf.j2
        dest: /etc/openresty/nginx.conf
        owner: root
        group: root
        mode: 0644

    - name: Download eth-jsonrpc-access.lua
      get_url:
        url: https://raw.githubusercontent.com/adetante/ethereum-nginx-proxy/master/eth-jsonrpc-access.lua
        dest: /usr/local/openresty/nginx/eth-jsonrpc-access.lua
        owner: root
        group: root
        mode: 0644

    - name: Start OpenResty
      service:
        name: openresty
        state: started
        enabled: true

This simple playbook installs OpenResty from its APT repository and templates the following configuration file:

user  www-data;

worker_processes  "12";


events {
    worker_connections  1024;
    multi_accept off;
}

http {
    include       /usr/local/openresty/nginx/conf/mime.types;
    default_type  application/octet-stream;

    server_names_hash_bucket_size 64;

    client_max_body_size 64m;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                       '$status $body_bytes_sent "$http_referer" '
                       '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /usr/local/openresty/nginx/access.log main buffer=16k flush=2m;

    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;

    keepalive_timeout  75;
    keepalive_requests 600;

    server_tokens on;

    # validator01.ash.local
    server {
        listen       80;
        server_name  validator01.ash.local;

        location / {
            set $jsonrpc_blacklist '{{ jsonrpc_blacklist }}';
            access_by_lua_file 'eth-jsonrpc-access.lua';
            proxy_pass http://localhost:9650;
        }
    }
}
What's localhost:9650?
This is the endpoint of AvalancheGo. Check out our tutorial "Local Test Network Creation" to get started with a local VM-based AvalancheGo cluster. You can also use the avalanche-cli.

We can now run the following command to install OpenResty and configure it to deny access to the platform.getValidatorsAt method:

ansible-playbook playbooks/openresty.yml -i inventories/local -e jsonrpc_blacklist=platform.getValidatorsAt

Note: In this case, we only blacklist a single method but the jsonrpc_blacklist parameter can accept a comma-separated list of methods.

We can then check that it's working by trying to call the platform.getValidatorsAt method through NGINX/OpenResty:

curl -X POST --data '{
    "jsonrpc":"2.0",
    "id"     :1,
    "method" :"platform.getValidatorsAt",
    "params" :{ 
        "height": 0,
        "subnetID": "11111111111111111111111111111111LpoYY"
    }
}' -H 'content-type:application/json;' -H 'Host: validator01.ash.local' http://localhost/ext/bc/P

Perfect! We successfully managed to filter JSON-RPC requests based on the used method.

Note: In this case, the methods blocked through NGINX/OpenResty can still be reached by reaching the AvalancheGo HTTP API port directly (9650 in this case). A good practice is to keep --http-host to localhost and use NGINX/OpenResty to filter outside requests.

What's next?

We have just seen how to use the ethereum-nginx-proxy script to filter JSON-RPC requests based on the method used. At Ash, we are planning to improve this script to allow using glob expressions to match multiple methods (e.g.: jsonrpc_blacklist=platform.* ) or having in-depth filtering based on params (e.g.: platform.getValidatorsAt is allowed but with height > X)

JSON-RPC method filtering will be a native feature of the Ash Console, coming out later this year.

Stay tuned for more content by subscribing below or by following us on Twitter @ash_avax!