springboot-kong-plugins
The goal of this project is to create a simple Spring Boot
REST API and securing it with Kong
using the LDAP Authentication
and Basic Authentication
plugins. Besides, we will explore more plugins that Kong
offers like: Rate Limiting
and Prometheus
plugins.
Project Diagram
Application
-
simple-service
Spring Boot
Java Web application that exposes two endpoints:/api/public
: that can be access by anyone, it is not secured;/api/private
: that must be accessed only by authenticated users.
Prerequisites
Run application during development using Maven
-
Open a terminal and navigate to
springboot-kong-plugins
root folder -
Run the command below to start
./mvnw clean spring-boot:run --projects simple-service
-
Open another terminal and call application endpoints
curl -i localhost:8080/api/public curl -i localhost:8080/api/private curl -i localhost:8080/actuator/beans curl -i localhost:8080/actuator/health
-
To stop, go to the terminal where the application is running and press
Ctrl+C
Build application Docker Image
-
In a terminal, make sure you are in
springboot-kong-plugins
root folder -
Build Docker Image
- JVM
./docker-build.sh
- Native
./docker-build.sh native
- JVM
Test application Docker Image
-
In a terminal, run the following command
docker run --rm -p 8080:8080 --name simple-service ivanfranchin/simple-service:1.0.0
-
Open another terminal and call application endpoints
curl -i localhost:8080/api/public curl -i localhost:8080/api/private curl -i localhost:8080/actuator/beans curl -i localhost:8080/actuator/health
-
To stop, go to the terminal where the application is running and press
Ctrl+C
Initialize Environment
-
In a terminal, make use you are in
springboot-kong-plugins
root folder -
Run the following script
./init-environment.sh
Note:
simple-service
application is running as a Docker container. The container does not expose any port to HOST machine. So, it cannot be accessed directly, forcing the caller to useKong
as gateway server in order to access it.
Import OpenLDAP Users
The LDIF
file that we will use, ldap/ldap-mycompany-com.ldif
, has already a pre-defined structure for mycompany.com
. Basically, it has 2 groups (developers
and admin
) and 4 users (Bill Gates
, Steve Jobs
, Mark Cuban
and Ivan Franchin
). Besides, it's defined that Bill Gates
, Steve Jobs
and Mark Cuban
belong to developers
group and Ivan Franchin
belongs to admin
group.
Bill Gates > username: bgates, password: 123
Steve Jobs > username: sjobs, password: 123
Mark Cuban > username: mcuban, password: 123
Ivan Franchin > username: ifranchin, password: 123
There are two ways to import those users: by running a script or using phpldapadmin
Import users running a script
-
In another terminal, make use you are in
springboot-kong-plugins
root folder -
Run the following script
./import-openldap-users.sh
-
Check users imported using
ldapsearch
ldapsearch -x -D "cn=admin,dc=mycompany,dc=com" \ -w admin -H ldap://localhost:389 \ -b "ou=users,dc=mycompany,dc=com" \ -s sub "(uid=*)"
Import users using phpldapadmin
-
Access https://localhost:6443
-
Login with the credentials
Login DN: cn=admin,dc=mycompany,dc=com Password: admin
-
Import the file
ldap/ldap-mycompany-com.ldif
-
You should see something like
Kong
In a terminal, follow the steps below to configure Kong
Check Status
-
Before starting, check if
Kong
admin API is accessiblecurl -I http://localhost:8001
It should return
HTTP/1.1 200 OK
Add Service
-
You can use
application/x-www-form-urlencoded
orapplication/json
content type-
application/x-www-form-urlencoded
SIMPLE_SERVICE_ID=$(curl -s -X POST http://localhost:8001/services/ \ -d "name=simple-service" \ -d "protocol=http" \ -d "host=simple-service" \ -d "port=8080" | jq -r '.id') echo "SIMPLE_SERVICE_ID=$SIMPLE_SERVICE_ID"
-
application/json
.Note: in order to set
protocol
,host
,port
andpath
at once, theurl
shorthand attribute can be usedSIMPLE_SERVICE_ID=$(curl -s -X POST http://localhost:8001/services/ \ -H 'Content-Type: application/json' \ -d '{ "name": "simple-service", "url":"http://simple-service:8080" }' | jq -r '.id') echo "SIMPLE_SERVICE_ID=$SIMPLE_SERVICE_ID"
-
-
[Optional] To list all services run
curl -s http://localhost:8001/services | jq .
Add routes
-
One default route for the service, no specific
path
includedPUBLIC_ROUTE_ID=$(curl -s -X POST http://localhost:8001/services/simple-service/routes/ \ -d "protocols[]=http" \ -d "hosts[]=simple-service" | jq -r '.id') echo "PUBLIC_ROUTE_ID=$PUBLIC_ROUTE_ID"
-
Another route specifically for
/api/private
endpoint (it will be secured and only accessible by LDAP users)PRIVATE_ROUTE_ID=$(curl -s -X POST http://localhost:8001/services/simple-service/routes/ \ -H 'Content-Type: application/json' \ -d '{ "protocols": ["http"], "hosts": ["simple-service"], "paths": ["/api/private"], "strip_path": false }' | jq -r '.id') echo "PRIVATE_ROUTE_ID=$PRIVATE_ROUTE_ID"
-
Finally, one route for
/actuator/beans
endpoint (it will be secured and only accessible by pre-defined users)BEANS_ROUTE_ID=$(curl -s -X POST http://localhost:8001/services/simple-service/routes/ \ -H 'Content-Type: application/json' \ -d '{ "protocols": ["http"], "hosts": ["simple-service"], "paths": ["/actuator/beans"], "strip_path": false }' | jq -r '.id') echo "BEANS_ROUTE_ID=$BEANS_ROUTE_ID"
-
[Optional] To list all
simple-service
routes runcurl -s http://localhost:8001/services/simple-service/routes | jq .
Call endpoints
-
Call
/api/public
endpointcurl -i http://localhost:8000/api/public -H 'Host: simple-service'
It should return
HTTP/1.1 200 It is public.
-
Call
/api/private
endpointcurl -i http://localhost:8000/api/private -H 'Host: simple-service'
It should return
HTTP/1.1 200 null, it is private.
Note: This endpoint is not secured by the application, that is why the response is returned. The idea is to use
Kong
to secure it. It will be done on the next steps. -
Call
/actuator/beans
endpointcurl -i http://localhost:8000/actuator/beans -H 'Host: simple-service'
It should return
HTTP/1.1 200 {"contexts":{"simple-service":{"beans":...
Note: As happened previously with
/api/private
,/actuator/beans
endpoint is not secured by the application. We will useKong
to secure it on the next steps.
Plugins
In this project, we are going to add these plugins: LDAP Authentication
, Basic Authentication
, Rate Limiting
and Prometheus
. Please refer to https://konghq.com/plugins for more.
Add LDAP Authentication plugin
The LDAP Authentication
plugin will be used to secure the /api/private
endpoint.
-
Add plugin to route
PRIVATE_ROUTE_ID
LDAP_AUTH_PLUGIN_ID=$(curl -s -X POST http://localhost:8001/routes/$PRIVATE_ROUTE_ID/plugins \ -d "name=ldap-auth" \ -d "config.hide_credentials=true" \ -d "config.ldap_host=openldap" \ -d "config.ldap_port=389" \ -d "config.start_tls=false" \ -d "config.base_dn=ou=users,dc=mycompany,dc=com" \ -d "config.verify_ldap_host=false" \ -d "config.attribute=cn" \ -d "config.cache_ttl=60" \ -d "config.header_type=ldap" | jq -r '.id') echo "LDAP_AUTH_PLUGIN_ID=$LDAP_AUTH_PLUGIN_ID"
Note: If you need to update some
LDAP Authentication
plugin configuration, run the followingPATCH
call informing the field you want to update, for examplecurl -X PATCH http://localhost:8001/plugins/${LDAP_AUTH_PLUGIN_ID} -d "config.base_dn=ou=users,dc=mycompany,dc=com"
-
Try to call
/api/private
endpoint without credentialscurl -i http://localhost:8000/api/private -H 'Host: simple-service'
It should return
HTTP/1.1 401 Unauthorized {"message":"Unauthorized"}
-
Call
/api/private
endpoint using Bill Gates base64 encode credentialscurl -i http://localhost:8000/api/private \ -H "Authorization:ldap $(echo -n 'Bill Gates':123 | base64)" \ -H 'Host: simple-service'
It should return
HTTP/1.1 200 Bill Gates, it is private.
Add Basic Authentication plugin
The Basic Authentication
plugin will be used to secure the /actuator/beans
endpoint
-
Add plugin to route
BEANS_ROUTE_ID
BASIC_AUTH_PLUGIN_ID=$(curl -s -X POST http://localhost:8001/routes/$BEANS_ROUTE_ID/plugins \ -d "name=basic-auth" \ -d "config.hide_credentials=true" | jq -r '.id') echo "BASIC_AUTH_PLUGIN_ID=$BASIC_AUTH_PLUGIN_ID"
-
Try to call
/actuator/beans
endpoint without credentials.curl -i http://localhost:8000/actuator/beans -H 'Host: simple-service'
It should return
HTTP/1.1 401 Unauthorized {"message":"Unauthorized"}
-
Create a consumer
IFRANCHIN_CONSUMER_ID=$(curl -s -X POST http://localhost:8001/consumers -d "username=ivanfranchin" | jq -r '.id') echo "IFRANCHIN_CONSUMER_ID=$IFRANCHIN_CONSUMER_ID"
-
Create a credential for consumer
IFRANCHIN_CREDENTIAL_ID2=$(curl -s -X POST http://localhost:8001/consumers/ivanfranchin/basic-auth \ -d "username=ivan.franchin" \ -d "password=123" | jq -r '.id') echo "IFRANCHIN_CREDENTIAL_ID2=$IFRANCHIN_CREDENTIAL_ID2"
-
Call
/api/private
endpoint usingivan.franchin
credentialcurl -i -u ivan.franchin:123 http://localhost:8000/actuator/beans -H 'Host: simple-service'
It should return
HTTP/1.1 200 {"contentDescriptor":{"providerVersion":...
-
Let's create another consumer just for testing purpose
ADMINISTRATOR_CONSUMER_ID=$(curl -s -X POST http://localhost:8001/consumers -d "username=administrator" | jq -r '.id') echo "ADMINISTRATOR_CONSUMER_ID=$ADMINISTRATOR_CONSUMER_ID" ADMINISTRATOR_CREDENTIAL_ID=$(curl -s -X POST http://localhost:8001/consumers/administrator/basic-auth \ -d "username=administrator" \ -d "password=123" | jq -r '.id') echo "ADMINISTRATOR_CREDENTIAL_ID=$ADMINISTRATOR_CREDENTIAL_ID"
Add Rate Limiting plugin
We are going to add the following rate limitings:
/api/public
and/actuator/health
: one request per second/api/private
: 5 requests a minute/actuator/beans
: 2 requests a minute or 100 requests an hour
-
Add plugin to route
PUBLIC_ROUTE_ID
PUBLIC_RATE_LIMIT_PLUGIN_ID=$(curl -s -X POST http://localhost:8001/routes/$PUBLIC_ROUTE_ID/plugins \ -d "name=rate-limiting" \ -d "config.second=1" | jq -r '.id') echo "PUBLIC_RATE_LIMIT_PLUGIN_ID=$PUBLIC_RATE_LIMIT_PLUGIN_ID"
-
Add plugin to route
PRIVATE_ROUTE_ID
PRIVATE_RATE_LIMIT_PLUGIN_ID=$(curl -s -X POST http://localhost:8001/routes/$PRIVATE_ROUTE_ID/plugins \ -d "name=rate-limiting" \ -d "config.minute=5" | jq -r '.id') echo "PRIVATE_RATE_LIMIT_PLUGIN_ID=$PRIVATE_RATE_LIMIT_PLUGIN_ID"
-
Add plugin to route
BEANS_ROUTE_ID
BEANS_RATE_LIMIT_PLUGIN_ID=$(curl -s -X POST http://localhost:8001/routes/$BEANS_ROUTE_ID/plugins \ -d "name=rate-limiting" \ -d "config.minute=2" \ -d "config.hour=100" | jq -r '.id') echo "BEANS_RATE_LIMIT_PLUGIN_ID=$BEANS_RATE_LIMIT_PLUGIN_ID"
-
Make some calls to these endpoints
-
Test
/api/public
curl -i http://localhost:8000/api/public -H 'Host: simple-service' curl -i http://localhost:8000/actuator/health -H 'Host: simple-service'
-
Test
/actuator/beans
curl -i -u ivan.franchin:123 http://localhost:8000/actuator/beans -H 'Host: simple-service' curl -i -u administrator:123 http://localhost:8000/actuator/beans -H 'Host: simple-service'
-
Test
/api/private
curl -i http://localhost:8000/api/private \ -H "Authorization:ldap $(echo -n 'Bill Gates':123 | base64)" \ -H 'Host: simple-service' curl -i http://localhost:8000/api/private \ -H "Authorization:ldap $(echo -n 'Mark Cuban':123 | base64)" \ -H 'Host: simple-service'
-
-
After exceeding some calls in a minute, you should see
HTTP/1.1 429 Too Many Requests {"message":"API rate limit exceeded"}
Add Prometheus plugin
-
Add plugin to
simple-service
PROMETHEUS_PLUGIN_ID=$(curl -s -X POST http://localhost:8001/services/simple-service/plugins \ -d "name=prometheus" | jq -r '.id') echo "PROMETHEUS_PLUGIN_ID=$PROMETHEUS_PLUGIN_ID"
-
Make some requests to
simple-service
endpoints -
You can see some metrics
curl -i http://localhost:8001/metrics
Shutdown
In a terminal and, inside springboot-kong-plugins
root folder, run the following script
./shutdown-environment.sh
Cleanup
To remove the Docker image created by this project, go to a terminal and, inside springboot-kong-plugins
root folder, run the script below
./remove-docker-images.sh
Issues
When upgrading postgres to a version above 13.x
(using current kong version), there is an error while running kong-database migration
Running kong-database migration
-------------------------------
Error: module 'openssl.rand' not found:No LuaRocks module found for openssl.rand
no field package.preload['openssl.rand']
no file './openssl/rand.lua'
no file './openssl/rand/init.lua'
no file './openssl/rand.lua'
no file './openssl/rand/init.lua'
no file '/usr/local/openresty/site/lualib/openssl/rand.ljbc'
no file '/usr/local/openresty/site/lualib/openssl/rand/init.ljbc'
no file '/usr/local/openresty/lualib/openssl/rand.ljbc'
no file '/usr/local/openresty/lualib/openssl/rand/init.ljbc'
no file '/usr/local/openresty/site/lualib/openssl/rand.lua'
no file '/usr/local/openresty/site/lualib/openssl/rand/init.lua'
no file '/usr/local/openresty/lualib/openssl/rand.lua'
no file '/usr/local/openresty/lualib/openssl/rand/init.lua'
no file './openssl/rand.lua'
no file '/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/openssl/rand.lua'
no file '/usr/local/share/lua/5.1/openssl/rand.lua'
no file '/usr/local/share/lua/5.1/openssl/rand/init.lua'
no file '/usr/local/openresty/luajit/share/lua/5.1/openssl/rand.lua'
no file '/usr/local/openresty/luajit/share/lua/5.1/openssl/rand/init.lua'
no file '/home/kong/.luarocks/share/lua/5.1/openssl/rand.lua'
no file '/home/kong/.luarocks/share/lua/5.1/openssl/rand/init.lua'
no file '/usr/local/openresty/site/lualib/openssl/rand.so'
no file '/usr/local/openresty/lualib/openssl/rand.so'
no file './openssl/rand.so'
no file '/usr/local/lib/lua/5.1/openssl/rand.so'
no file '/usr/local/openresty/luajit/lib/lua/5.1/openssl/rand.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
no file '/home/kong/.luarocks/lib/lua/5.1/openssl/rand.so'
no file '/usr/local/openresty/site/lualib/openssl.so'
no file '/usr/local/openresty/lualib/openssl.so'
no file './openssl.so'
no file '/usr/local/lib/lua/5.1/openssl.so'
no file '/usr/local/openresty/luajit/lib/lua/5.1/openssl.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
no file '/home/kong/.luarocks/lib/lua/5.1/openssl.so'
Run with --v (verbose) or --vv (debug) for more details