loading...

Implement HTTPS for API Access on localhost with Nginx

can_atac profile image ATAC Jan ・6 min read

Our open source app, FlexOffice which provides employees a way to find and book a desk in, has a technical stack based on React with Typescript, Node and mongodb.

Our app is composed of two-parts : a web Front which access resources through JSON calls to RestAPI provided by a web back-end.
Our production target is to have a Front-end with a different URL than the Back-end. Front-end makes a cross-domain request to access back-end resources. As such, we are assigned to follow CORS policy by specifying the allowed origin URL to get resources.

As we have at least two different ports on localhost, serving each the Front and the Back end, we make de facto cross-domain requests when accessing back-end APIs.

In this post, we'll use a radically different domain name for our back-end : local.web.com, but you're free to choose another one, and we'd like to secure API access and test it locally.

To attach this domain name to our IPv4 loopback (127.0.0.1), you must add a record on /etc/hostsas such :

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1       localhost
127.0.0.1       local.web.com 
(...)

To use HTTPS protocol, we need local self-signed certificates, plus a root CA certificate which will give full confidence to our browsers (with a green lock).

Let's create our local certificates :
Let's just follow this great gist here:

Generate :
The root certificate : RootCA.crt
The key : RootCA.key
The pem : RootCA.pem

$ openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout RootCA.key -out RootCA.pem -subj "/C=US/CN=FlexOffice-Root-CA"
$ openssl x509 -outform pem -in RootCA.pem -out RootCA.crt

First, create a file domains.ext that lists all your local domains

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = local.web.com

Generate localhost.key, localhost.csr, and localhost.crt:

$ openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=FR/ST=IDF/L=Paris/O=FlexOffice-Certificates/CN=local.web.com"
$ openssl x509 -req -sha256 -days 1024 -in localhost.csr -CA RootCA.pem -CAkey RootCA.key -CAcreateserial -extfile domains.ext -out localhost.crt

Once you get your certificate, you can declare them in nginx.conf.

The back end is accessible locally at http://local.web.com:3000, but we'd like to avoid that and force API users to pass through https://local.web.com:443. Our Front is located at http://local.web.com:1025, and must be authorized to access the Back-end. This can be done by adding a header with Access-Control-Allow-Origin valued with the Front URL a.k.a the 'origin' of the request.

# nginx Configuration File
    # http://wiki.nginx.org/Configuration


    # Run as a less privileged user for security reasons.
    #user nginx;

    worker_processes auto;

    events {
        worker_connections 1024;
    }

    #pid /var/run/nginx.pid;

    http {

        sendfile        off;

        upstream backend {
            server local.web.com:3000;
        }        

    #Redirect to https, using 307 instead of 301 to preserve post data
        server {
            listen 80 default_server;
            server_name _;
            return 307 https://$host$request_uri;
        }

        server {
            listen 443 ssl ;

            server_name local.web.com;

            # Protect against the BEAST attack by not using SSLv3 at all. If you need to support older browsers (IE6) you may need to add
            # SSLv3 to the list of protocols below.
            ssl_protocols TLSv1.2 TLSv1.1 TLSv1 ;


            # Ciphers set to best allow protection from Beast, while providing forwarding secrecy, as defined by Mozilla - https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx

            ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK;
            ssl_prefer_server_ciphers on;

            # Optimize SSL by caching session parameters for 10 minutes. This cuts down on the number of expensive SSL handshakes.
            # The handshake is the most CPU-intensive operation, and by default it is re-negotiated on every new/parallel connection.
            # By enabling a cache (of type "shared between all Nginx workers"), we tell the client to re-use the already negotiated state.
            # Further optimization can be achieved by raising keepalive_timeout, but that shouldn't be done unless you serve primarily HTTPS.
            ssl_session_cache shared:SSL:10m; # a 1mb cache can hold about 4000 sessions, so we can hold 40000 sessions
            ssl_session_timeout 24h;


            # Use a higher keepalive timeout to reduce the need for repeated handshakes
            keepalive_timeout 300; # up from 75 secs default

            # remember the certificate for a year and automatically connect to HTTPS
            #add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';
            add_header Strict-Transport-Security 'max-age=0;';

            ssl_certificate /<CERTIFICATE_PATH>/<your_certificate>.crt; // <- CHANGE IT !
            ssl_certificate_key /<KEY_PATH>/<your_key>.key; // <- CHANGE IT !

            access_log /<LOG_PATH>/<your_log>.log; // <- CHANGE IT !

            location / {

                add_header 'Access-Control-Allow-Origin'  '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
                    #
                    # Custom headers and headers various browsers *should* be OK with but aren't
                    #
                add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;             add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin';
                add_header    'Access-Control-Allow-Credentials' 'true' always;

                proxy_pass http://local.web.com:3000;
                proxy_set_header Connection "";
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $remote_addr;

                if ($request_method = 'OPTIONS') {

                    #
                    # Tell client that this pre-flight info is valid for 20 days
                    #
                    add_header 'Access-Control-Max-Age' 1728000;
                    add_header 'Content-Type' 'application/json;';
                    #add_header 'Content-Length' 0;
                    return 204;
                }
                if ($request_method = 'POST') {
                    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
                }
                if ($request_method = 'GET') {
                    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
                }
                proxy_pass_request_headers      on;
            }
            location /api/ {

                proxy_pass http://local.web.com:3000;

                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header authorization $http_authorization;
                proxy_pass_request_headers      on;
                proxy_no_cache $cookie_nocache  $arg_nocache$arg_comment;
                proxy_no_cache $http_pragma     $http_authorization;
                proxy_cache_bypass $cookie_nocache $arg_nocache $arg_comment;
                proxy_cache_bypass $http_pragma $http_authorization;

                    #
                    # Custom headers and headers various browsers *should* be OK with but aren't
                    #

                if ($request_method = 'OPTIONS') {

                    #
                    # Tell client that this pre-flight info is valid for 20 days
                    #
                    add_header 'Access-Control-Max-Age' 1728000;
                    add_header 'Access-Control-Allow-Origin'  'http://local.web.com:1025' always;
                    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, HEAD' always;
                    add_header 'Access-Control-Allow-Headers' 'authorization, Origin, X-Requested-With, Content-Type, Accept' always;
                    add_header  'Access-Control-Allow-Credentials' 'true' always;
                    return 204;
                }
                if ($request_method = 'POST') {
                    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
                    add_header 'Access-Control-Allow-Origin'  'http://local.web.com:1025' always;
                    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, HEAD' always;
                    add_header 'Access-Control-Allow-Headers' 'authorization, Origin, X-Requested-With, Content-Type, Accept' always;
                    add_header    'Access-Control-Allow-Credentials' 'true' always;
                }
                if ($request_method = 'GET') {
                    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
                    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
                    add_header 'Access-Control-Allow-Origin'  'http://local.web.com:1025' always;
                    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, HEAD' always;
                    add_header 'Access-Control-Allow-Headers' 'authorization, Origin, X-Requested-With, Content-Type, Accept' always;
                    add_header    'Access-Control-Allow-Credentials' 'true' always;

                }

            }

        }
    }



Reload or stop and restart nginx.
You could run nginx -t to test your configuration file and verify the file path.

$ nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful

Start nginx

$ nginx

You can now call your back-end through HTTPS.

Please feel free to comment or suggest improvements.

Tested with environment :
macOS v10.13.6 (High Sierra)
node v10.15
npm v6.4.1
MongoDB shell version v3.6.6
curl 7.60.0
nginx v1.5.7
Firefox Quantum 69.0.2 (64 bits)
Chrome v77.0.3865.90 (official Build) (64 bits)
Safari v13.0.1 (13608.2.11.1.10)

Useful resources :

[1] CORS, detailed explanation, https://www.moesif.com/blog/technical/cors/Authoritative-Guide-to-CORS-Cross-Origin-Resource-Sharing-for-REST-APIs/
[2] Root CA creation, https://gist.github.com/cecilemuller/9492b848eb8fe46d462abeb26656c4f8

Posted on by:

Discussion

pic
Editor guide