DEV Community

Boris Lyashev
Boris Lyashev

Posted on

Nginx Configuration Tips for Secure Communication: Enabling mTLS and checking client fingerprint

This paper is intended for beginners looking to enhance web security by configuring mutual Transport Layer Security (mTLS) in Nginx and verifying the client’s certificate fingerprint or securing specific URLs with mTLS. The paper provides a comprehensive guide on how to implement these security measures in Nginx, offering step-by-step instructions and practical advice. The resulting Nginx configuration is available at https://github.com/stjam/mtls_demo/blob/master/nginx.conf. By following this guide, readers can improve the security of their web applications and prevent unauthorized access to sensitive data.

A demo project has been prepared and is available at https://github.com/stjam/mtls_demo/tree/master. The project is a simple web application with two endpoints: /api/private, which echoes the headers of requests, and /api/public, which returns the current time. All traffic is routed through Nginx, which proxies requests to the backend.

I have used the following software versions to prepare this tutorial:

Maven 3.8.7
Docker Desktop 4.16.1
Java 19
LibreSSL 3.3.6

To build backend you can use the next set of commands

  1. ./mvnw package -Pnative -Dquarkus.native.container-build=true
  2. docker build -f src/main/docker/Dockerfile.native -t quarkus/mtls_demo . To run everything, you need to use docker-compose, which I have included here: https://github.com/stjam/mtls_demo/blob/master/docker-compose.yaml

Now, let’s make access to our backend more secure. Initially, we have the following nginx.conf file, which simply proxies requests to our backend service.

events {
    worker_connections 100;
    multi_accept on;
}

http {
    server {
      listen 80;
      location / {
          proxy_pass http://quarkus:8080;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          }
      }
  }
Enter fullscreen mode Exit fullscreen mode

And our private API is not secure, as we can directly call it.

I recommend changing the content of https://github.com/stjam/mtls_demo/blob/master/nginx.conf with every snippet of configuration that I’ve put here, as it will make understanding the key steps of this tutorial better.

First of all, we need to create a server certificate and enable SSL in Nginx. To generate a server certificate, use the following command. You can read more about the parameters of this command in https://medium.com/@stjamlb/openssl-made-easy-a-practical-guide-to-generating-and-signing-x-509-certificates-da85e8f2db3e.

openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt

For convince you can combine key and server certificate in one file

cat server.key server.crt > server.pem

Modified nginx.conf:

events {
    worker_connections 100;
    multi_accept on;
}

http {
    server {
      listen 443 ssl;
      ssl_certificate /etc/nginx/server.pem;
      ssl_certificate_key /etc/nginx/server.pem;

      location / {
          proxy_pass http://quarkus:8080;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          }
      }
  }
Enter fullscreen mode Exit fullscreen mode

Now we listen on port 443, and our clients should use HTTPS to call our service. We’ve also added paths to our server certificate and key. If we call without HTTPS, we get the following error:

After modifying our request to call via HTTPS, we get a successful response.

When configuring an HTTPS connection, it’s important to verify the connection between the client and server. This can be done using the following command:

openssl s_client -connect 127.0.0.1:443

This command will connect to the server on port 443 and print out detailed information about the SSL/TLS handshake process, including the server’s certificate details, the supported cipher suites, and the SSL/TLS session parameters:

Now that the traffic between our client and server is encrypted, clients can verify the server certificate. However, in some cases, a TLS connection alone may not be sufficient. If you need to verify the client’s certificate on the server side, you should configure mutual TLS (mTLS)

events {
    worker_connections 100;
    multi_accept on;
}

http {
    server {
      listen 443 ssl;
      ssl_certificate /etc/nginx/server.pem;
      ssl_certificate_key /etc/nginx/server.pem;
      ssl_client_certificate /etc/nginx/server.crt;
      ssl_verify_client on;

      location / {
          proxy_pass http://quarkus:8080;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          }
      }
  }
Enter fullscreen mode Exit fullscreen mode

We have added the option ‘ssl_verify_client on;’ and the path to the client certificate ‘ssl_client_certificate’ in our Nginx configuration. This means that Nginx will now require a client certificate for a connection. However, we encountered an error when trying to establish a connection using OpenSSL. Let’s fix this.

we need client certificate. I should notice that both the client and server need to have certificates that are signed by the same Certificate Authority (CA). This ensures that both parties can trust each other’s identity during the SSL/TLS handshake. Without this trust relationship, the client and server will not be able to establish a secure connection.

  1. openssl req -new -newkey rsa:2048 -nodes -keyout client.key -out client.csr

  2. openssl x509 -req -days 365 -in client.csr -CA server.crt -CAkey server.key -CAcreateserial -out client.crt

    The first command generates a certificate signing request (CSR) for the client certificate, and the second command signs the CSR with the CA key to generate the client certificate. After executing these commands, you will have a client.crt file containing the client certificate.

Now we can check the OpenSSL connection.

openssl s_client -connect 127.0.0.1:443 -key client.key -cert client.crt -CAfile server.crt
After execution you should see something like this:

And now, in order to call our private API, we must provide the -cert and -key options with the path to the client.key and client certificate.

curl --cert client.crt --key client.key https://127.0.0.1:443/api/private --insecure

Everything is working as expected, but we need to remember that we have a public endpoint that doesn’t require client certificate verification. Let’s configure Nginx to only check the certificate for the /api/private endpoint. We just need to add a new location and make ssl_verify_client optional. We also add an if condition to check if the ssl_connection was successful or not. If it was not successful, we return a 403 error. Otherwise, we proxy the request to our backend.

events {
    worker_connections 100;
    multi_accept on;
}

http {
    server {
      listen 443 ssl;
      ssl_certificate /etc/nginx/server.pem;
      ssl_certificate_key /etc/nginx/server.pem;
      ssl_verify_client optional;
      ssl_client_certificate /etc/nginx/server.crt;

      location / {
          proxy_pass http://quarkus:8080;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          }

      location /api/private {
                if ($ssl_client_verify != SUCCESS) {
                    return 403;
                }
                proxy_pass http://quarkus:8080;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
        }
      }
  }
Enter fullscreen mode Exit fullscreen mode

Now we only need to provide a certificate with our request if we are accessing the /api/private endpoint, as the /api/public endpoint is available directly without any additional security measures. In this case, we have two options: we can either check the client certificate fingerprint or the client CN. To find the fingerprint using openssl, we can use the following command:

openssl x509 -noout -fingerprint -sha1 -inform pem -in client.crt

After adding the fingerprint check to our configuration, our updated nginx configuration looks like this:

events {
    worker_connections 100;
    multi_accept on;
}

http {
    server {
      listen 443 ssl;
      ssl_certificate /etc/nginx/server.pem;
      ssl_certificate_key /etc/nginx/server.pem;
      ssl_verify_client optional;
      ssl_client_certificate /etc/nginx/server.crt;

      location / {
          proxy_pass http://quarkus:8080;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          }

      location /api/private {
                if ($ssl_client_verify != SUCCESS) {
                                    return 403;
                            }

                if ($ssl_client_fingerprint != 7dc7ec3379c699dc2dcf8a44e67acae5ae09e06f) {
                        return 403;
                    }



                proxy_pass http://quarkus:8080;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
        }
      }
      log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$ssl_client_fingerprint" "$ssl_client_s_dn"';
      access_log /var/log/nginx/access.log main;

  }
Enter fullscreen mode Exit fullscreen mode

I should note that when checking the fingerprint, you must provide the SHA1 fingerprint without any colons. Additionally, I’ve included an example of how to enrich the information in the Nginx access.log with useful details such as the client certificate fingerprint or client certificate CN.

Conclusion

We have demonstrated the steps required to configure mutual TLS (mTLS) in Nginx, and explained how to verify the connection using OpenSSL.

After that, we worked with a location block in our Nginx configuration to check client certificates only for a specific endpoint (/api/private), and not for the public endpoint (/api/public).

Finally, we have discussed how to enrich the information in the Nginx access log with useful details such as client certificate fingerprints or CN. By following these steps, we can ensure that our communication is secure and only authorized parties can access our API endpoints. This dialog provides a comprehensive guide to configuring mTLS in Nginx for secure communication between the client and server.

Top comments (0)