Nginx for managing your API access

Tommaso Allevi
Mia-Platform
Published in
6 min readApr 27, 2018
The typical nginx configuration

In microservice environment, the first gate we can thought to is the API access point. There, all your requests arrive. There, your security is important. There, your company should invest its attention for security enhancement. It is important.

The permission management

The first kind of security is user permission. But the permission management is hard. In the most of case, the tools that implement this kind of security are very complex and difficult to manage without some expertise. So, your company should make a choice: uses some commercial solution or builds the own one. But in every enterprise context, there’s need one. In this article, we’ll see how it could be implemented using nginx.

The simplest security enhancement could be implemented checking if the user is able to access to an endpoint. This is our goal in this article. In particular we’ll able to declare that a path can be called by a subset of our user base. To manage that requirement, let’s say we are tagging our users with some labels called groups: each user can have zero, one or more strings that represent the grants given to him.

So, our goal is to find a technical solution to implement, for instance, those restrictions:

  • GET /service1: all users
  • POST /service1: only logged users
  • DELETE /service1: only admin users
  • GET /service2: only the users that are admin and editor
  • POST /service2: only the user that are admin or editor
  • GET /service3: only the users that are either admin or editor but not both
  • POST /service3: users that are admin but not editor
  • all other APIs are forbidden for all users

In this article we’ll see how to build this simple access management using only nginx and a little custom application.

Group login expression

Before staring, we should translate the restriction requirements into a logic expressions: in this way, we’ll use the logic expression to checking the user grants.

  • GET /service1: all users ⇔ true
  • POST /service1: only logged users ⇔ logged
  • DELETE /service1: only admin users ⇔ admin
  • GET /service2: only the users that are admin and editor admin && editor
  • POST /service2: only the user that are admin or editor admin || editor
  • GET /service3: only the users that are either admin or editor but not both ⇔ (!admin && editor) || (admin && !editor)
  • POST /service3: users that are admin but not editoradmin && !editor
  • the other paths are forbidden for all users ⇔ false

NB: the logged group is a special group used to identify all logged users.

Now we have the requirements formatted in logic expression!

This is powerful. For each request, we should understand which user calls the backend resolving its session, fetches its groups and evaluates the logic expression associated to that path to know if he has the grant to make that request or not.

In the next chapter we’ll see which tools allow us to make those checks.

Set up

In my previous article Nginx for surviving in microservice era, we had seen how to setup docker, docker-compose and how we use them to simulate a production environment. If you don’t read it yet, please do it!

For our purpose, we need some files:

  • my-server.conf nginx configuration file
  • docker-compose.yml file

For our purpose, the following docker-compose.yml fits well

version: '3'services:
reverse-proxy:
image: nginx
ports:
- "8888:80"
volumes:
- ./my-server.conf:/etc/nginx/conf.d/default.conf:ro
be1:
image: jmalloc/echo-server
environment:
- PORT=80
be2:
image: tutum/hello-world
be3:
image: breerly/hello-server
environment:
- HELLO_PORT=80
- HELLO_RESPONSE_DELAY=1
auth:
image: allevo/nginx_cerbero
environment:
- MONGODB_URL=mongodb://mongo/auth
- REDIS_URL=redis://redis
- HTTP_PORT=80
link:
- mongo
- redis
mongo:
image: mongo
redis:
image: redis

The first part of this file is the same of my previous post: three application backend and a reverse-proxy implemented by nginx. We added auth service that will be described in the follow chapters. mongo and redis services are used by auth service for storing the users and their sessions respectively.

We’ll study my-server.conf nginx configuration file in the next chapters too.

auth_request directive

We’ll use reverse-proxy service as entry point in our system. Before each request reaches the backend, the auth service is queried to ask if the current user is able to access to that backend.

To do that, nginx offers a simple directive called auth_request. That directive allows us to make a subrequest to another service to check if the incoming request could be performed. From documentation:

If the subrequest returns a 2xx response code, the access is allowed. If it returns 401 or 403, the access is denied with the corresponding error code. Any other response code returned by the subrequest is considered an error.

So the auth service interface is clear: nginx automatically blocks the incoming requests if the auth service return a non 2xx status code.

Our auth service implements this interface: it responses 204 No Content if the group logic expression returns true; 403 Forbidden otherwise.

Because we want to keep our permission configuration in a one place, we will store it in the nginx conf. So let’s start to write it!

map $request_method-$uri $groupExpression {
default "0";
"~^GET-/auth/" "true";
"POST-/auth/login" "true";
"POST-/auth/signup" "true";
"GET-/service1" "true";
"POST-/service1" "logged";
"DELETE-/service1" "admin";
"GET-/service2" "admin && editor";
"POST-/service2" "admin || editor";
"GET-/service3" "(!admin && editor) || (admin && !editor)";
"POST-/service3" "admin && !editor";
}map $uri $upstreamName {
"~^/auth" auth;
"~^/my-service1" service1;
"~^/my-service2" service2;
"~^/my-service3" service3;
}
server {
listen 80;
server_name localhost;
default_type text/plain;
location / { # A
auth_request /auth; # 1

proxy_pass http://$upstreamName; # 4
}
location = /auth { # B
proxy_set_header 'group-expression' $groupExpression; # 2
proxy_pass http://auth; # 3
}
}

For each request, nginx check witch location block matches. It found the (A) block.

In that block the first thing defined there is the auth_request directive(1). Because of this directive, nginx makes another request before continuing with the incoming one. So, nginx “forwards” the request to /auth.

nginx looks for which block matches that path finding the location (B). In this block, nginx add a new header to the request called group-expression (2). The value of this header is calculated by the $groupExpression map. As you can see, the group logic expressions are defined there. Than, forwards the request to the auth service (3). This service resolves the group check and returns 204 or 403.

If auth responses 204, nginx continues with the incoming request. Finally, nginx proxies the incoming request to the backend evaluating $upstreamName map.

The tests

For our purpose some CURLs work fine.

First of all we need to register an user. In this example, auth exposes /signup API to insert the user in mongo database. In your context, this is made by your application.

curl http://localhost:8888/auth/signup -XPOST -d '{"username":"my_user","password":"my_password","groups":["admin", "editor"]}' -H'Content-type: application/json'

With this CURL, a new user is registered. Obviously during the user registration, the client should not declare him groups!

After the user registration, we need to log in.

curl http://localhost:8888/auth/login -XPOST -d '{"username":"my_user","password":"my_password"}' -H'Content-type: application/json' -vvvv

The output of this CURL is like this:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8888 (#0)
> POST /auth/login HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.54.0
> Accept: */*
> Content-type: application/json
> Content-Length: 47
>
* upload completely sent off: 47 out of 47 bytes
< HTTP/1.1 200 OK
< Server: nginx/1.13.8
< Date: Fri, 26 Jan 2018 21:39:28 GMT
< Content-Type: application/json
< Content-Length: 83
< Connection: keep-alive
< Set-Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW; Path=/
<
* Connection #0 to host localhost left intact
{"_id":"5a6b9f61af897500108d1dcc","username":"my_user","groups":["admin","editor"]}

As you can see, the backend sends a cookie. Please copy it: we’ll use it for the following CURLs.

Now we are ready to start our test!

Our user has the groups “admin” and “editor”. So, we expect that GET /service1 (true), POST /service1 (logged), DELETE /service1 (admin), GET /service2 (admin && editor), POST /service2 (admin || editor) work well.

In fact the following CURLs work fine!

curl http://localhost:8888/service1 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW'
curl http://localhost:8888/service1 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW' -X POST
curl http://localhost:8888/service1 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW' -X DELETE
curl http://localhost:8888/service2 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW'
curl http://localhost:8888/service2 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW' -X POST

Instead the “my_user” user has not grant to access to GET /service3 ((!admin && editor) || (admin && !editor)) and POST /service3 (admin && !editor).

curl http://localhost:8888/service3 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW'
curl http://localhost:8888/service3 -H 'Cookie: sid=Wn4E5hZjwcssv90eCOj00dsZUi3897tW' -X POST

Both of last CURLs return 403 Forbidden.

Conclusion

As you can see, using this approach, you configuration

--

--