Proxy iDempiere-Rest Through KrakenD

From iDempiere en

Quick notes from Carlos Ruiz here about proxying idempiere-rest through krakend

Goal

Initial goal is just to protect the iDempiere installation from the public, as the idempiere-rest is too open, it can be subject to attacks, so the recommended approach for these cases is to avoid exposing the full API and use a gateway API.

Gateway APIs can help with many different things:

  • Proxy, authenticate, authorize, security, audit
  • Reporting, analytics
  • Billing, etc

Gateway APIs

There are ready-to-use providers, like from Amazon, Google, or Microsoft

There are also open source alternatives like:

KrakenD

Because of its simplicity and open sourceness, I chose KrakenD for this test.

KrakenD is an open source project from Spain, developed in golang.

It seems well documented, and there is a very helpful slack channel, as well as easy to reach paid support.

Steps for this test

Installation in Ubuntu 20.04

The installation in a server with Ubuntu 20.04 was very straight:

# as root
apt-key adv --keyserver keyserver.ubuntu.com --recv 5DE6FD698AD6FDD2 
echo "deb https://repo.krakend.io/apt stable main" | tee /etc/apt/sources.list.d/krakend.list 
apt-get update 
apt-get install krakend

As of 2024-10-02 these commands installed the version 2.7.2-0.

The installer adds:

  • a single 114M binary in /usr/bin/krakend (no libraries required)
  • a user krakend
  • a default configuration file in /etc/krakend/krakend.json

To restart the program (for example after changing krakend.json)

systemctl restart krakend

To check the status of the program:

systemctl status krakend

Initially it shows that krakend port 8080 collided with the same port used in idempiere, so, later we need to change the port in /etc/krakend/krakend.json

Local tests with docker

I also managed to do some local tests with docker, it is basically:

docker pull devopsfaith/krakend
docker run -p 8085:8085 -v $PWD:/etc/krakend/ devopsfaith/krakend

Note here two things:

  • The folder where you start the docker container will become the /etc/krakend in the "server", so, it can contain the configuration files. In principle it must contain the file krakend.json
  • I changed the default port here from 8080 to 8085, and consequently that needs to be changed in the krakend.json configuration file

Create a krakend.json configuration file

The easiest way to start is creating the krakend.json file in the designer: https://designer.krakend.io/

Step 1

  • Name = idrest
  • Port = 8085 # cannot use 8080 because is used by iDempiere
  • Available hosts =
    • Static address resolution
    • Disable sanitize = Y
    • http://localhost:8080
      • # NOTE: when using krakend with docker, then use the IP address of the host, localhost doesn't work in this case

Step 2

  • Add an Endpoint:
  • KrakenD Endpoint = /idrest/auth/tokens
    • Method = POST
    • Output = JSON
    • Backend API calls =
      • Host = select the host configured above
      • Backend endpoint = /api/v1/auth/tokens
      • HTTP Verb = POST
      • Response Parsing Decode as = JSON
  • KrakenD Endpoint = /idrest/auth/tokens
    • Method = PUT
    • Output = JSON
    • Headers passing to backend =
      • Authorization
      • Content-Type
    • Enable JWT Validation
      • # NOTE: JWT Validation is not mandatory, enable it if you want an extra security measure
    • JWK URI = http://localhost:8080/api/v1/auth/jwk
      • # NOTE: for this to work you must add a System Configurator record in iDempiere as System for IDEMPIERE_REST_EXPOSE_JWK = Y
    • Algorithm = HS512
    • Issuer = idempiere.org
    • Enable caching = Y
    • Disable JWK security = Y
  • Backend API calls =
    • Host = select the host configured above
    • Backend endpoint = /api/v1/auth/tokens
    • HTTP Verb = PUT
    • Response Parsing Decode as = JSON
  • Logging and metrics
  • Logging = Y
  • Level = DEBUG
  • Logging prefix = [KRAKEND]
  • Message format = default
  • Send logs to Stdout = Y
  • Send logs to Syslog = N
    • # NOTE: this is for testing, in production is the opposite, you would like logging in Syslog
  • Save the configuration and use it

From here is just about adding more endpoints for every need.

This is the simplest example, there are better ways to define the krakend.json, for example to avoid repeating the JWT validation information on every endpoint

Refining the configuration

Once you understand how is the configuration managed, in the documentation you find the tips to refine it, reusing pieces, creating templates, filtering, etc.

Sample krakend.json working for idempiere-rest

Tokens endpoints (Login)

Here you can find a sample file that works for login on idempiere-rest:

{
  "$schema": "https://www.krakend.io/schema/v3.json",
"version": 3,
  "host": [
    "http://127.0.0.1:8080"
  ],
  "extra_config": {
    "telemetry/logging": {
      "level": "DEBUG",
      "prefix": "[KRAKEND]",
      "syslog": false,
      "stdout": true,
      "format": "default"
    }
  },
  "timeout": "3000ms",
  "cache_ttl": "300s",
  "name": "idrest",
  "port": 8085,
  "endpoints": [
    {
      "endpoint": "/idrest/auth/tokens",
      "method": "POST",
      "backend": [
        {
          "url_pattern": "/api/v1/auth/tokens",
          "method": "POST",
          "disable_host_sanitize": true
        }
      ]
    },
    {
      "endpoint": "/idrest/auth/tokens",
      "method": "PUT",
      "extra_config": {
        "auth/validator": {
          "alg": "HS512",
          "jwk_url": "http://127.0.0.1:8080/api/v1/auth/jwk",
          "issuer": "idempiere.org",
          "cache": true,
          "disable_jwk_security": true
        }
      },
      "backend": [
        {
          "url_pattern": "/api/v1/auth/tokens",
          "method": "PUT",
          "disable_host_sanitize": true
        }
      ],
      "input_headers": [
        "Authorization",
        "Content-Type"
      ]
    }
  ]
}

Check the configuration

Check the configuration

krakend check --config /etc/krakend/krakend.json --debug 

Run krakend

systemct restart krakend

Change on endpoint URL (idrest)

Note that the example above is configured to answer on the URL /idrest instead of /api/v1 - and is configured to answer in port 8085, so, if testing with postman for example you need to configure the endpoints as:

http://locahost:8085/idrest/auth/tokens

Because of this you would need to open the 8085 port in your firewall, not recommended, or configure KrakenD to use SSL.

But, my preferred way of doing this is:

Proxying KrakenD through nginx

As explained in Proxy iDempiere Through Nginx you can use nginx as a proxy for iDempiere, in a similar way, I used nginx to proxy krakend adding these lines to the configuration file:

   location /idrest {
       proxy_pass        http://localhost:8085/idrest;
       proxy_set_header  X-Real-IP $remote_addr;
       proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header  Host $http_host;
   }

Adding more endpoints

We already have the configuration above to do the login using idempiere-rest, but obviously we want to add more endpoints to expose in our system, following are examples for this:

GET Warehouses

This is an example for the endpoint GET warehouses.

NOTE the usage of querystring_params to instruct krakend to accept the URL parameters client, role and organization.

In a similar way are configured the endpoints auth/roles and auth/organizations:

    {
      "endpoint": "/idrest/auth/warehouses",
      "method": "GET",
      "extra_config": {
        "auth/validator": {
          "alg": "HS512",
          "jwk_url": "http://127.0.0.1:8080/api/v1/auth/jwk",
          "issuer": "idempiere.org",
          "cache": true,
          "disable_jwk_security": true
        }
      },
      "input_query_strings": [
        "client",
        "role",
        "organization"
      ],
      "backend": [
        {
          "url_pattern": "/api/v1/auth/warehouses",
          "method": "GET",
          "disable_host_sanitize": true,
          "is_collection": false,
          "target": ""
        }
      ],
      "input_headers": [
        "Authorization",
        "Content-Type"
      ]
    }

GET windows

The endpoint to get information from windows requires the usage of different querystring_params, for example note here the configuration for $filter, $expand, $select, $orderby, $top and $skip

    {
      "endpoint": "/idrest/windows",
      "method": "GET",
      "extra_config": {
        "auth/validator": {
          "alg": "HS512",
          "jwk_url": "http://127.0.0.1:8080/api/v1/auth/jwk",
          "issuer": "idempiere.org",
          "cache": true,
          "disable_jwk_security": true
        }
      },
      "input_query_strings": [
        "$filter",
        "$expand",
        "$select",
        "$orderby",
        "$top",
        "$skip"
      ],
      "backend": [
        {
          "url_pattern": "/api/v1/windows",
          "method": "GET",
          "disable_host_sanitize": true,
          "is_collection": false,
          "target": ""
        }
      ],
      "input_headers": [
        "Authorization",
        "Content-Type"
      ]
    }

GET model (restricted to some tables)

Here is an example for the GET model configuration, note here additionally the usage of parameters (table) as part of the URL, and the ability to validate those parameters in check_expr.

In this case the URLs /idrest/models/m_product and /idrest/models/ad_user are allowed, but the attempt to read any other table will throw an error.

    {
      "endpoint": "/idrest/models/{table}",
      "method": "GET",
      "extra_config": {
        "auth/validator": {
          "alg": "HS512",
          "jwk_url": "http://127.0.0.1:8080/api/v1/auth/jwk",
          "issuer": "idempiere.org",
          "cache": true,
          "disable_jwk_security": true
        },
        "validation/cel": [
          {
            "check_expr": "req_params.Table.matches('m_product') || req_params.Table.matches('ad_user')"
          }
        ]
      },
      "input_query_strings": [
        "$filter",
        "$expand",
        "$select",
        "$orderby",
        "$top",
        "$skip"
      ],
      "backend": [
        {
          "url_pattern": "/api/v1/models/{table}",
          "method": "GET",
          "disable_host_sanitize": true,
          "is_collection": false,
          "target": ""
        }
      ],
      "input_headers": [
        "Authorization",
        "Content-Type"
      ]
    }

POST processes (running)

Here is an example that allows to run JUST the process rv_bpartneropen, of course, more processes can be allowed adding more conditions to the check_expr:

    {
      "endpoint": "/idrest/processes/{process}",
      "method": "POST",
      "extra_config": {
        "auth/validator": {
          "alg": "HS512",
          "jwk_url": "http://127.0.0.1:8080/api/v1/auth/jwk",
          "issuer": "idempiere.org",
          "cache": true,
          "disable_jwk_security": true
        },
        "validation/cel": [
          {
            "check_expr": "req_params.Process.matches('rv_bpartneropen')"
          }
        ]
      },
      "backend": [
        {
          "url_pattern": "/api/v1/processes/{process}",
          "method": "POST",
          "disable_host_sanitize": true,
          "is_collection": false,
          "target": ""
        }
      ],
      "input_headers": [
        "Authorization",
        "Content-Type"
      ]
    }
Cookies help us deliver our services. By using our services, you agree to our use of cookies.