Secure Spring Boot REST API with Apache APISIX API Gateway

Secure Spring Boot REST API with Apache APISIX API Gateway

Introduction

In this walkthrough, we are going to look at some of the Apache APISIX API Gateway built-in plugins for securing your Spring Boot REST APIs and demonstrate how to effectively use them. You will learn to run the multi-container app (backend, db, apisix and etcd together) with Docker compose. It contains also conceptual knowledge about API Security, the important role of the API gateway in handling cross-cutting concerns, and how Apache APISIX can help you to simplify and accelerate the task of securing APIs.

Here, is a short summary of what we do:

  • ✅ Overview of API Security.
  • ✅ The API Gateway as a gatekeeper.
  • ✅ How does an APISIX gateway secure your APIs?
  • ✅ Clone the demo repository apisix-security-java-spring from GitHub.
  • ✅ Understand the structure of the project and docker-compose.yaml file.
  • ✅ Build a multi-container APISIX via Docker CLI.
  • ✅ Allow only whitelisted IP address access your API with IP restriction plugin in action.
  • ✅ Block direct access to some API resources with Uri-blocker plugin.
  • ✅ Make sure that only allowed users can request your API with Consumer restriction plugin.

Key things we need:

💁Before we get started, let's quickly enhance our theoretical knowledge about API security and API Gateway concepts.

Overview of API Security

API security is a term referring to practices and products that prevent malicious attacks on, Application program interfaces (API). Because APIs have become key to programming web-based interactions, they have become a target for hackers. As a result, basic authentication, requiring only user names and passwords, has been replaced with various forms of security methods, such as multifactor authentication (MFA) or tokens like JWT Web Tokens.

API Security

In our modern era, API security has become increasingly important. Many hardening techniques are available:

  • TLS encryption
  • API Firewalls
  • Validating request data
  • Throttling for protection
  • Continuously monitoring
  • Auditing
  • Logging
  • IP restriction
  • And more.

Representational state transfer (REST) API security is one of the most common API securities available. With REST API security, you have a Hypertext Transfer Protocol (HTTP) Uniform Resource Identifier (URI), which controls which data the API accesses as it operates. REST API security can thus prevent attacks involving malicious data an attacker is trying to introduce using an API.

An API Gateway as a gatekeeper👮

An API gateway is an essential component of an API management solution. It is key to API security and protects the underlying data like a gatekeeper checking authentication and authorization and managing traffic. As a thin layer between your user clients and your upstream services, the API gateway is further extended with countless plugins to handle authentication, authorization, traffic control, and security.

Apache APISIX is a lightweight open-source API gateway that sits in front of your upstream services. Easy to configure and quick to deploy, APISIX API Gateway serves as a central point for routing all incoming requests to their intended destinations, whether that be an upstream API server, a third-party service, a database, or even a serverless.

How does an APISIX gateway secure your APIs?

API security at APISIX is the highest priority. As it serves as an inline proxy point of control over APIs.

  • Verifies the identity associated with API requests through credential and token validation, as well as other authentication means. You can check available authentication plugins.
  • Determines which traffic is authorized to pass through the API to backend services.
  • Controls the traffic flowing through the APIs using rate limiting and throttling. There are traffic control plugins available for that purpose.
  • Observes all requests and applies runtime policies to enforce governance. There are many observability plugins that can be easily enabled for your APIs.
  • Implements all industry-standard encryptions.

Apache APISIX API Gateway Security

With the enough knowledge, now we can start to apply some security plugins in action for our demo Spring Boot App. Let's get started 🚀

Clone the demo repository

For this demonstration, we’ll leverage the demo projectapisix-security-java-spring I prepared in advance. You can see it on Github.

Use git to clone the repository:

git clone 'https://github.com/Boburmirzo/apisix-security-java-spring'

Go to root directory of apisix-security-java-spring

cd apisix-security-java-spring

You can open the project in your favorite code editor. I used IntelliJ Idea community edition. You’ll see the following project directory structure:

APISIX project directory

Understand the structure of the project

In the project folders, here is the list of main components you can take a look at:

  • APISIX's config files - All the services are configured by mounting external configuration files in the project onto the docker containers: /apisix_conf/conf.yaml defines the configs for apisix. Similarly, configs for etcd, prometheus, and grafana are located in /etcd_conf/etcd.conf.yml, /prometheus_conf/prometheus.yml, and /grafana_conf/config respectively.
  • APISIX's logs - in the apisix_log folder, APISIX Admin and Control API requests are logged such as access.logor error.log
  • Spring Boot project - Next, the spring-postgres folder keeps Java application with Spring framework (Controller, repository and entity classes) and a PostgreSQL database setup with dummy schema and data.

spring-postgres project structure

  • Docker compose file - The docker-compose.yml file defines an application with some services:

    • apisix-dashboard - It runs APISIX Dashboard on port 9000. ℹ️ The Dashboard offers another way to interact with APISIX Admin API and we can achieve the same configuration result with the CLI as with the Dashboard.

    • apisix- deploys APISIX and exposes it on port 9080. On your local machine, you can access APISIX Admin API by sending requests to the following URL http://127.0.0.1:9080/

    • etcd - APISIX uses etcd to save and synchronize configuration.
    • prometheus - APISIX can fetch metrics data about upstream APIs together with prometheus plugin.
    • grafana - Metrics exported by the [prometheus plugin]apisix.apache.org/docs/apisix/plugins/prome.. can be also graphed in Grafana and you can see the running dashboard on port 3000.
    • backend - While deploying the application, docker compose maps port 8080 of the backend service container to port 8080 of the host
    • db - PostgreSql database exposed on 5432 port that backend service connects, gets data from.

☝️You may notice all services are mapped to apisix network.

The apisix-security-java-spring project makes use of the similar example APISIX docker compose template.

version: "3"

services:
  apisix-dashboard:
    image: apache/apisix-dashboard:2.10.1-alpine
    restart: always
    volumes:
    - ./dashboard_conf/conf.yaml:/usr/local/apisix-dashboard/conf/conf.yaml
    ports:
    - "9000:9000"
    networks:
      apisix:

  apisix:
    image: apache/apisix:2.13.1-alpine
    restart: always
    volumes:
      - ./apisix_log:/usr/local/apisix/logs
      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
    depends_on:
      - etcd
    ##network_mode: host
    ports:
      - "9080:9080/tcp"
      - "9091:9091/tcp"
      - "9443:9443/tcp"
      - "9092:9092/tcp"
    networks:
      apisix:

  etcd:
    image: bitnami/etcd:3.4.15
    restart: always
    volumes:
      - etcd_data:/bitnami/etcd
    environment:
      ETCD_ENABLE_V2: "true"
      ALLOW_NONE_AUTHENTICATION: "yes"
      ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
      ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
    ports:
      - "12379:2379/tcp"
    networks:
      apisix:

  prometheus:
    image: prom/prometheus:v2.25.0
    restart: always
    volumes:
      - ./prometheus_conf/prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    networks:
      apisix:

  grafana:
    image: grafana/grafana:7.3.7
    restart: always
    ports:
      - "3000:3000"
    volumes:
      - "./grafana_conf/provisioning:/etc/grafana/provisioning"
      - "./grafana_conf/dashboards:/var/lib/grafana/dashboards"
      - "./grafana_conf/config/grafana.ini:/etc/grafana/grafana.ini"
    networks:
      apisix:

  backend:
    build: spring-postgres/backend
    ports:
      - "8080:8080"
    environment:
      - POSTGRES_DB=example
    networks:
      apisix:

  db:
    image: postgres
    restart: always
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      apisix:
    environment:
      - POSTGRES_DB=example
      - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
    expose:
      - 5432
networks:
  apisix:
    driver: bridge
secrets:
  db-password:
    file: spring-postgres/db/password.txt
volumes:
  db-data:
  etcd_data:
    driver: local

Build a multi-container APISIX via Docker CLI

Now we can start our application by running docker compose command from the root folder of the project:

docker-compose -p docker-apisix up -d

Sample output:

docker compose run result

The running container list you can see by running docker compose ps CLI command or using docker desktop:

APISIX with other services on docker desktop

Once the containers are running, navigate to http://localhost:8080/get in your web browser and you will see the following output:

Hello Apache APISIX

This response actually is coming from the simple Spring Boot REST API. If you check the project folder spring-postgres, the src/main/java directory contains our project’s source code. There is a GreetingController controller class annotated with @RestController where single /get endpoint serves our requests. Basically, it retrieves the data from PostgreSQL database and replies to the request with name attribute in Greeting entity class.

We will enable different Apache APISIX security plugins for this endpoint in the next steps.

@RestController
public class GreetingController {

    private final GreetingRepository repository;

    public GreetingController(GreetingRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/get")
    public String getApisixGreeting() {
        Greeting apisixGreeting = repository.findById(1).orElse(new Greeting("Not Found 😕"));
        return apisixGreeting.getName();
    }
}

3 APISIX security plugins

Enable IP addresses restriction

Now we can enable IP restriction plugin for the API endpoint. IP restriction is a technique is to limit the IPs of clients that can send requests by whitelisting/blacklisting IP addresses list. We can not access the API with IPs other than the allowed ones.

The first step, we need to create a upstream for our backend service. We use curl commands to interact with APISIX's Admin API:

curl "http://127.0.0.1:9080/apisix/admin/upstreams/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
{
  "type": "roundrobin",
  "nodes": {
    "backend:8080": 1
  }
}'

Upstream object will point to target backend service or multiple backend services, in our case, it is referencing to Spring Boot REST API.

In the next step, we define a Route and bind the route to the Upstream. It can be mapped by upstream_id in a specific Route. Also, we will enable ip-restriction plugin in the route configuration.

curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/get",
    "plugins": {
       "ip-restriction": {
            "whitelist": [
                "127.0.0.2"
            ]
        }
    },
    "upstream_id": "1"
}'

In the whitelist attribute of ip-restriction plugin, we are allowing only single IP address 127.0.0.2 to access /get URI. If you try to access the API with disallowed IP address, APISIX will throw a 403 Forbidden Http error:

curl http://127.0.0.1:9080/get -i --interface 127.0.0.1

Sample output:

HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.13.1

{"message":"Your IP address is not allowed"}

☝️ Single IPs, multiple IPs or ranges in CIDR notation like 10.10.10.0/24 can be used in the plugin configuration. The plugin also supports IPv4 and IPv6 addresses.

Now we know one way to secure our APIs by allowing/disallowing IP addresses with APISIX.

Block direct access to an API resource

Next, let's see more enhanced URI blocking option with uri-blocker plugin by applying regex filter rules. For example, we might have a scenario when an API endpoint should be blocked if an user is trying to access and download the private sample-file.csv file.

We can enable uri-blocker plugin similar to ip-restriction plugin configuration we applied in the previous step:

curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/*",
    "plugins": {
       "uri-blocker": {
          "block_rules": ["sample-file.csv+"]
        }
    },
    "upstream_id": "1"
}'

Once you have configured the Plugin as shown above, you can try accessing the file with curl command:

curl -i http://127.0.0.1:9080/sample-file.csv

You should get another Http 403 Forbidden error:

HTTP/1.1 403 Forbidden

Now we restricted direct access to a URI resource. You can also set a rejected_msg plugin config property to customize the response body message.

Allow users request the API

One of the techniques is to make corresponding access restrictions to API resources for specific users or services. Consumer Restriction (consumer-restriction) – makes corresponding access restrictions to your services or routes based on different users selected. If unknown user tries to send a request, Apache APISIX will meet them with an error.

Assume that we have two consumers of our APIs (consumer1 and consumer2). We want to give an access only to one of them. To do so, let’s create two consumers and enable consumer-restriction plugin for our existing example route.

To create the first consumer, run the following command:

curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "username": "consumer1",
    "plugins": {
        "basic-auth": {
            "username":"consumer1",
            "password": "123456"
        }
    }
}'

To create the second consumer, run the following command:

curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "username": "consumer2",
    "plugins": {
        "basic-auth": {
            "username":"consumer2",
            "password": "654321"
        }
    }
}'

Then, enable consumer-restriction plugin and allow only consumer1 access the /get endpoint:

curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/get",
    "plugins": {
        "basic-auth": {},
        "consumer-restriction": {
            "whitelist": [
                "consumer1"
            ]
        }
    },
    "upstream_id": "1"
}'

The consumer-restriction has the type property which is enumerated type (it support the following values: _consumer_name, serviceid and _routeid), we can specify it to allow access to correspond object.

By default, the plugin returns a generic {"message":"The consumer_name is forbidden."} if the object is not allowed. It’s possible to configure a more friendly message via the plugin attribute.

Now we can test if the consumer consumer1 can request the API resource with the provided credentials:

curl -u consumer1:123456 http://127.0.0.1:9080/get -i

Output:

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 19
Connection: keep-alive
Server: APISIX/2.13.1

Hello Apache APISIX

It is obvious that consumer1 can request the endpoint because we whitelisted the consumer in the consumer-restriction plugin config.

However, our the second consumer has no access. Now we can try to request the same path with different consumer credentials:

curl -u consumer2:123456 http://127.0.0.1:9080/get -i

Output:

HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.13.1

{"message":"The consumer_name is forbidden."}

What's next

In this blog post, we have demonstrated how to deploy our Spring Boot REST API app together with Apache APISIX API Gateway by harnessing Docker compose capability for smoother deployment. Also, we learned how to use basic security plugins so you can check other available plugins to satisfy more advanced API security scenarios or create your own plugin for the API Gateway with the help of Java Plugin Runner.

➔ Watch Video Tutorial Getting Started with Apache APISIX

➔ Read the blog post Overview of Apache APISIX API Gateway Plugins

➔ Read the blog post Centralized Authentication with Apache APISIX Plugins

➔ Read the blog post API Security with OIDC by using Apache APISIX and Microsoft Azure AD

➔ Read the blog post API Observability with Apache APISIX Plugins

Community⤵️

Did you find this article valuable?

Support Bobur's Blog by becoming a sponsor. Any amount is appreciated!