Creating an API with python: Part 6: HTTPS and Proxying

In my previous post, Creating an API with python: Part 5: Authentication, I added oauth2 authentication to the FastAPI API. In this post, I add an nginx proxy. This enables three things:

  1. The API can be served over HTTPS
  2. Requests to HTTP will be forwarded to HTTPS
  3. We can serve the API over a more ‘normal’ URL, without needing the port 8000 part

Prerequisites

These prerequisites are assumed for this post, however the nginx proxy setup steps involved could be applied to any API implementation served over port 8000 (or any other port).

  1. Creating an API with python: Part 1: GET Endpoints
  2. Creating an API with python: Part 2: MariaDB Database
  3. Creating an API with python: Part 3: POST Endpoints
  4. Creating an API with python: Part 4: DELETE Endpoints
  5. Creating an API with python: Part 5: Authentication

Step 1: Create Self-Signed SSL Certs

In order to serve the API over HTTPS, we will need SSL certs. For a production environment, we would need to get certificates for the domain over which we are serving the API. We would get them from an official certificate authority (such as a web service provider). However, for the purposes of development, we can create our own self-signed certs. Note that these will result in browsers issuing security warnings, however you can usually choose to ignore the warnings and proceed to the site anyway. Another option is to upload the CA root cert that you create to the browser. There are many ways to create self-signed certs, but I found the article here particularly helpful: https://devopscube.com/create-self-signed-certificates-openssl/. The following instructions are based on that article, with a few minor tweaks.

  1. Navigate to the home directory of your virtual machine. Create an openssl directory and change directory to it.
    $ cd
    $ mkdir openssl && cd openssl
    
  2. Run the following to generate a root cert and key. Be sure to replace <YOUR_IP> with the IP of your virtual machine.
    $ openssl req -x509 \
                -sha256 -days 356 \
                -nodes \
                -newkey rsa:2048 \
                -subj "/CN=<YOUR_IP>/C=US/L=San Fransisco" \
                -keyout rootCA.key -out rootCA.crt 
    
  3. Generate a server private key:
    $ openssl genrsa -out server.key 2048
    
  4. Create a CSR (Certificate Signing Request) configuration. Be sure to replace each instance of <YOUR_IP> with the IP of your virtual machine, and the O and OU entries with your project name:
    $ cat > csr.conf <<EOF
    [ req ]
    default_bits = 2048
    prompt = no
    default_md = sha256
    req_extensions = req_ext
    distinguished_name = dn
    
    [ dn ]
    C = US
    ST = California
    L = San Fransisco
    O = AllTheCoding
    OU = AllTheCoding Dev
    CN = <YOUR_IP>
    
    [ req_ext ]
    subjectAltName = @alt_names
    
    [ alt_names ]
    DNS.1 = <YOUR_IP>
    IP.1 = <YOUR_IP>
    
    EOF
    
  5. Generate a CSR (Certificate Signing Request):
    $ openssl req -new -key server.key -out server.csr -config csr.conf
    
  6. Create a cert.conf file. Again, replace each instance of <YOUR_IP> with your virtual machine IP:
    $ cat > cert.conf <<EOF
    
    authorityKeyIdentifier=keyid,issuer
    basicConstraints=CA:FALSE
    keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
    subjectAltName = @alt_names
    
    [alt_names]
    DNS.1 = <YOUR_IP>
    IP.1 = <YOUR_IP>
    
    EOF
    
  7. Generate the SSL certificate:
    $ openssl x509 -req \
        -in server.csr \
        -CA rootCA.crt -CAkey rootCA.key \
        -CAcreateserial -out server.crt \
        -days 365 \
        -sha256 -extfile cert.conf
    
  8. Once you’ve created your certs, copy them to the /etc/ssl/certs directory on your server:
    $ sudo su
    $ cp server.key /etc/ssl/certs
    $ cp server.crt /etc/ssl/certs
    $ cp rootCA.crt /etc/ssl/certs
    $ cp rootCA.key /etc/ssl/certs
    

Step 2: Install Nginx

Nginx is open-source web server software. We are going to use it to proxy requests from the server URL https://<YOUR_IP>/api to the actual location of the API, at http://<YOUR_IP>:8000. In addition, we will forward any requests over HTTP to HTTPS, to make sure we are always encrypting all traffic.

  1. Install the epel-release repo:
    $ sudo yum install epel-release
  2. Install nginx:
    $ sudo yum install nginx
  3. If you have firewalld running on your server, you will need to run the following commands to allow traffic over HTTP and HTTPS:
    $ sudo firewall-cmd --permanent --zone=public --add-service=http
    $ sudo firewall-cmd --permanent --zone=public --add-service=https
    $ sudo firewall-cmd --reload
    
  4. Start nginx:
    $ sudo systemctl start nginx
  5. Enable nginx to start at boot:
    $ sudo systemctl enable nginx
  6. If you now navigate to http://<YOUR_IP>/ (replace <YOUR_IP> with your server IP), you should see the default CentOS7 nginx web page. If so, that means nginx is working.

Step 3: Configure nginx

Now we need to configure nginx to proxy HTTP to HTTPS and forward requests from https://<YOUR_IP>/api to http://<YOUR_IP>:8000.

  1. Create a new nginx configuration file:
    $ cd /etc/nginx/conf.d
    $ sudo vim api.conf
    
    Inside the file, paste in the following configuration:
    server {
            listen       443 ssl http2;
            listen       [::]:443 ssl http2;
            server_name  _;
    
            ssl_certificate    /etc/ssl/certs/server.crt;
            ssl_certificate_key    /etc/ssl/certs/server.key;
            ssl_session_cache shared:SSL:1m;
            ssl_session_timeout  10m;
            ssl_ciphers HIGH:!aNULL:!MD5;
            ssl_prefer_server_ciphers on;
    
            access_log /var/log/nginx/nginx.vhost.access.log;
            error_log /var/log/nginx/nginx.vhost.error.log;
                    
    		location ^~/api/ {
    				rewrite ^/api/(.*)$ /$1 break;
    				proxy_pass  http://127.0.0.1:8000;
    				proxy_set_header Host $host;
    				proxy_set_header X-Real-IP $remote_addr;
    				proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    				proxy_set_header X-Forwarded-Proto $scheme;
    				proxy_set_header X-Request-Id $http_x_request_id;
    				proxy_set_header X-Cert-Issuer-DN $ssl_client_i_dn;
    				proxy_set_header X-Cert-Subject-DN $ssl_client_s_dn;
    		}
        }
    
    server {
            listen       80;
            listen       [::]:80;
            server_name  _;
            return 301 https://$host$request_uri;
        }
    
    
  2. If SELinux is running, you may need to permit httpd to connect over the network. To check if SELinux is running, run:
    $ sestatus
    
    If you see this at the top of the output, then SELinux is enabled:
    SELinux status:                 enabled
    
    If it’s enabled, run this command to enable httpd_can_network_connect:
    $ setsebool httpd_can_network_connect on -P
    
  3. Now restart nginx:
    $ sudo systemctl restart nginx
    

Step 4: Start FastAPI

We have to tell FastAPI that it’s running at a different base path (i.e. /api). This is so that internal links from the Swagger and ReDoc docs will be correct.

  1. On your server, change to the code directory (~/vboxshare/fastapi should be replaced with the path to your FastAPI python code):
    $ cd ~/vboxshare/fastapi
    
  2. Run the FastAPI server with a slightly modified command:
    $ . ~/.venv-fastapi/bin/activate
    (.venv-fastapi) $ uvicorn --host 0.0.0.0 main:app --root-path /api --reload
    Note the addition of --root-path /api.

Step 5: Test new URLs

Test that the FastAPI API is served over the new URLs.

  1. Navigate to https://<YOUR_IP>/api/docs in your browser. Be sure to replace <YOUR_IP> with your server IP. The swagger page should load. If you try authenticating and making requests, the requests should go to the correct URL (https://<YOUR_IP>/api/) and return responses correctly.
  2. Navigate to http://<YOUR_IP>/api/docs in your browser. Check that the request is forwarded to https://<YOUR_IP>/api/docs.

Conclusion

You should now have a FastAPI API running with a proxy, which serves the API over a cleaner URL over HTTPS and forwards any requests over HTTP to HTTPS.

If you want to find out how to add CORS (Cross-Origin Resource Scripting) support to the API, see my follow-on post, Creating an API with python: Part 7: CORS.

Thanks for reading!