Tuesday, 10 March 2020

Blogger python cli

Blogger python cli

Blogger python cli

2020-03-09T18:46:41Z



Introduction

We will be creating a commandLine utility in python to upload a post in Google blogger.

Account Preparation

  • You need to have a google account (gmail) (Paid account NOT required).
  • Once you have a google account, Login to https://console.developers.google.com/apis/credentials and create a new Project here.
  • Click on Dashboard on left hand column, and click on `+ ENABLE APIS AND SERVICES`.
    • Type in blogger in search bar, and select Blogger API v3 and then click ENABLE.`
  • Click on OAuth consent screen on left hand column, and select User Type to External and click CREATE.
    • On next screen type in Application Name as Blogger CLI (It can be any string).
    • Click on Add Scope and select both the Blogger API v3 options and click ADD and click Save.
  • Click on Credentials on the left hand side and you will be presented with following two options of creating credentials for Blogger API.
    • API Keys
    • OAuth 2.0 Client IDs
    • Service Accounts
    • Click on `+ CREATE CREDENTAILSon the top and selectOAuth Client ID` from drop down menu.
    • Select Application Type to other.
    • Type in Name to Python Blogger CLI (it can be any string) and click create.
    • You will see an entry with name Python Blogger CLI under OAuth 2.0 Client IDs.
    • Download the credential files by clicking on a down arrow button.
    • This will be a json file and we need it to authenticate it with google OAuth2.0 services.
  • Login to blogger site https://www.blogger.com
    • Sign-in to blogger.
    • Enter a display name you like.
    • Create a NEW BLOG and give it a name you like in Ttile
    • Type in address. It has to be a unique e.g somethingrandom23455.blogspot.com and click create blog
    • On next screen, look at the address bar of browser, it will be like https://www.blogger.com/blogger.g?blogID=3342324243435#allposts
    • Note down blog ID (a string after blogID=, excluding #allposts) in above URL. We need this ID to put in python script.

Python environment creation

I am using following python version.

$ python --version
Python 3.7.0

We will be creating a python virtual environment.

mkdir BloggerCli
cd BloggerCli
python -m venv python-2.7.0
source python-2.7.0/bin/activate

Install required libraries.

pip install google-api-python-client
pip install --upgrade google-auth-oauthlib

Copy credential script.

mkdir python-cli

Copy above downloaded credential json file in python-cli folder and rename it to OAuth2.0_secret.json.

Note: You can rename it any name.

Create Python script bloggerCli.py

cd python-cli

Create following python script in python-cli folder.

#! /Users/<username>/BloggerCli/python-2.7.0/bin/python

# https://github.com/googleapis/google-api-python-client/blob/master/docs/README.md
# https://github.com/googleapis/google-api-python-client/blob/master/docs/oauth-server.md
# https://github.com/googleapis/google-api-python-client/blob/master/docs/oauth-installed.md
# http://googleapis.github.io/google-api-python-client/docs/dyn/blogger_v3.html
# http://googleapis.github.io/google-api-python-client/docs/dyn/blogger_v3.posts.html
# https://developers.google.com/identity/protocols/googlescopes
# https://console.developers.google.com/apis/credentials
# pip install --upgrade google-auth-oauthlib

# Note: This script does NOT check the duplicate title of posts while inserting the post in Blog.

from google.oauth2 import service_account
import googleapiclient.discovery
from google_auth_oauthlib.flow import InstalledAppFlow
import os
import sys
import argparse


usage = f"{os.path.basename(__file__)} -f <html file> -t <post title> -l <label_string1> -l <label_string2> -l .. -l .."

parser = argparse.ArgumentParser(prog=os.path.basename(__file__), usage=usage, description='Upload a post to Blogger')
parser.add_argument('-f', '--uploadfile', action='store', dest='fileToPost', help='html file to post', required=True)
parser.add_argument('-l', '--labels', action='append', dest='labels', default=[], help='-l <label1> -l <label2>')
parser.add_argument('-t', '--title', action='store', dest='title', help='-t <Post Title string>', required=True)
parser.add_argument('-b', '--blogid', action='store', dest='blogid', help='-b <blogid string>', required=True)
arguments = parser.parse_args()

if len(sys.argv) == 1:
    parser.print_help()
    parser.error("You must specify command line flags as mentioned above.")


FILE_TO_POST = arguments.fileToPost
LABELS_FOR_POST = arguments.labels
POST_TITLE = arguments.title
BLOG_ID = arguments.blogid

SCOPES = ['https://www.googleapis.com/auth/blogger']
SERVICE_ACCOUNT_FILE = 'blogger-api-credentials.json'
OAUTH2_ACCOUNT_FILE = 'OAuth2.0_secret.json'
FLOW_SERVER_PORT = 9090
API_SERVICE_NAME = 'blogger'
API_SERVICE_VERSION = 'v3'

# create a API client from service account
def create_client_from_serviceAccount():
    credentials = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_FILE, scopes=SCOPES)

    blogger = googleapiclient.discovery.build(API_SERVICE_NAME, API_SERVICE_VERSION, credentials=credentials)
    return blogger

# create a client from an AOUTH2.0 account.
def create_client_from_outhAccount():
    flow = InstalledAppFlow.from_client_secrets_file(OAUTH2_ACCOUNT_FILE, scopes=SCOPES)
    credentials = flow.run_local_server(host='localhost', port=FLOW_SERVER_PORT, authorization_prompt_message='Please visit this URL: {url}', 
                        success_message='The auth flow is complete; you may close this window.', open_browser=True)
    blogger = googleapiclient.discovery.build(API_SERVICE_NAME, API_SERVICE_VERSION, credentials=credentials)
    return blogger

def print_number_of_posts(postList):
    if not 'items' in postList:
        print("No of posts in blog = 0")
    else:    
        print("No of posts in blog = {}".format(len(postList['items'])))

# From a service account you cannot create a new post in blogger. It might need some special permissions that I am not aware of.
# However, Service account can read posts and fetch them.
# blogger = create_client_from_serviceAccount()

# Below will open a browser window to authenticate yourself.
blogger = create_client_from_outhAccount()

postList = blogger.posts().list(blogId=BLOG_ID).execute()
print_number_of_posts(postList)

print("====== Inserting a new post =======")

with open(FILE_TO_POST) as f:
    contents = f.read()

blogBody = {
    "content": contents,
    "kind": "blogger#post",
    "title": POST_TITLE,
    "labels": LABELS_FOR_POST,
        }

result = blogger.posts().insert(blogId=BLOG_ID, body=blogBody).execute()
postList = blogger.posts().list(blogId=BLOG_ID).execute()
print_number_of_posts(postList)

Create a test file to be uploaded.

echo 'This is a first post' > sometext.txt

Note: You can create an HTML file as well.

Run the script.

$ ./bloggerCli.py -f sometext.txt -t "My First Post" -b 34456769666334 -l label1 -l label2 -l label3
Please visit this URL: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=26652434353-e9u77isb2sdsd4343sdsd343434.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A9090%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fblogger&state=BkaXVyiW15sdsdsdADH7sadsH8hS&access_type=offline
No of posts in blog = 0
====== Inserting a new post =======
No of posts in blog = 1

Note-1: While running above script, this will open a browser and wil ask you to login to your google account.

Note-2: You may need to change the FLOW_SERVER_PORT in above script.

References used:

  • https://github.com/googleapis/google-api-python-client/blob/master/docs/README.md
  • https://github.com/googleapis/google-api-python-client/blob/master/docs/oauth-server.md
  • https://github.com/googleapis/google-api-python-client/blob/master/docs/oauth-installed.md
  • http://googleapis.github.io/google-api-python-client/docs/dyn/blogger_v3.html
  • http://googleapis.github.io/google-api-python-client/docs/dyn/blogger_v3.posts.html
  • https://developers.google.com/identity/protocols/googlescopes
  • https://console.developers.google.com/apis/credentials

Convert markup to blogger compatible html

Convert markup to blogger compatible html

Convert markup to blogger compatible html

2020-03-10T11:12:00Z



Introduction

We will be converting a markup file to a blogger compatible html.

Install pandoc and GitHUB html5 Template

Install pandoc using URL https://pandoc.org/installing.html . Create a workspace and clone following git repos in that workspace area.

mkdir $HOME/panDocs
cd $HOME/panDocs
git clone https://github.com/tajmone/pandoc-goodies.git
cd pandoc-goodies/templates/html5/github/src

Modify above GitHUB html5 template source code for blogger.

Make the padding and max-width changes in _github-markdown.scss file as follows:

$ git diff templates/html5/github/src/_github-markdown.scss
diff --git a/templates/html5/github/src/_github-markdown.scss b/templates/html5/github/src/_github-markdown.scss
index 479b5fc..54c9abb 100644
--- a/templates/html5/github/src/_github-markdown.scss
+++ b/templates/html5/github/src/_github-markdown.scss
@@ -48,9 +48,9 @@
   word-wrap: break-word;
   box-sizing: border-box;
   min-width: 200px;
-  max-width: 980px;
+  max-width: 100%;
   margin: 0 auto;
-  padding: 45px;
+  padding: 45px 5px;

   a {
     color: #0366d6;

Change the css source file name in src/css-injector.js as follows:

$ git diff css-injector.js
diff --git a/templates/html5/github/src/css-injector.js b/templates/html5/github/src/css-injector.js
index 861494d..14b5597 100644
--- a/templates/html5/github/src/css-injector.js
+++ b/templates/html5/github/src/css-injector.js
@@ -16,7 +16,7 @@ This script will:
 */

 templateFile  = "GitHub_source.html5" // Template with CSS-injection placeholder
-cssFile       = "GitHub.min.css"      // CSS source to inject into placeholder
+cssFile       = "GitHub.css"      // CSS source to inject into placeholder
 outFile       = "../GitHub.html5"     // Final template output file
 placeHolder   = "{{CSS-INJECT}}"      // Placeholder string for CSS-injection

Compile scss file.

Note: You need to install node on your system in order to compile.

cd pandoc-goodies/templates/html5/github/src
npm init -y
npm install node-sass
./node_modules/node-sass/bin/node-sass --source-map true --output-style=expanded --indent-type space GitHub.scss > GitHub.css
node css-injector.js

Note: You can clone pre-compiled code from https://github.com/spareslant/markup_to_GitHUB_HTML5_and_PDF.git as well.

  • pandoc-goodies is to generate html files.

Create a sample markup file.

Create a markup file in $HOME/panDocs say markupContents.md with following contents.

    # A top Level Heading
    This is a heading.

    ## Second top level heading
    Another heading

    ### Following is a Bash code.
    ```bash
    $ ls -l $HOME
    ```
    ### Following is just a text file.
    ```
    This is a plain text
    or it can be any kind of text.
    ```

    ### Some highligts in `heading`.
    ```
    This Text is colorized.
    This Text is not colorized.
    ```

Create a conversion script.

Create a shell script say generate_html_for_blogger.sh with following contents in $HOME/panDocs folder.

#! /bin/bash

[[ $1 == "" ]] && echo "pass a html filename as first argument" && exit 2

#output_file="output.html"
input_file="$1"
output_file="$(basename $input_file .md).html"
output_file_code_tag_removed="$(basename $input_file .md).code_tag_removed.html"

pandoc "$input_file" -f markdown --template pandoc-goodies/templates/html5/github/GitHub.html5 --self-contained --toc --toc-depth=6 -o $output_file

cat "$output_file" | perl -wpl -e 's!\<code\>(.+?)\</code\>!\<span class=\"code"\>$1\</span\>!g' > "$output_file_code_tag_removed"
echo "file geneated: $output_file_code_tag_removed"

Run the script.

$ ./generate_html_for_blogger.sh markupContent.md
[WARNING] This document format requires a nonempty <title> element.
  Defaulting to 'markupContent' as the title.
  To specify a title, use 'title' in metadata or --metadata title="...".
file geneated: markupContent.code_tag_removed.html

html content for blogger is generated in markupContent.code_tag_removed.html file. You can copy/paste the contents in blogger post without any modification.

Note: Blogger does NOT recognize <code> tag. Therefore we had to replace <code> tag with <span>. This also gives us the flexiblity to add custom colors in final HTML. You cannot colorize text in markup though.

Colorizing text for blogger in Markup.

In order to colorize the texts in blogger, add CSS styles as following in the markup file. We also added <pre> tag to colorize the text we want. Following style will also colorize text in single backquotes in markup.

    <style>
    /* To highlight text in Green in pre tag */
    .hl {color: #008A00;}
    /* To highlight text in Bold Green in pre tag */
    .hlb {color: #008A00; font-weight: bold;}
    /* To highlight text in Bold Red in pre tag */
    .hlbr {color:#e90001; font-weight: bold;}
    /* <code> tag does not work in blogger. Use following class with span tag */
    .code {
        color:#7e168d; 
        background: #f0f0f0; 
        padding: 0.1em 0.4em;
        font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
    }
    </style> 
    # A top Level Heading
    This is a heading.

    ## Second top level heading
    Another heading

    ### Following is a Bash code.
    ```bash
    $ ls -l $HOME
    ```
    ### Following is just a text file.
    ```
    This is a plain text
    or it can be any kind of text.
    ```

    ### Some highligts in `heading`.
    <pre>
    This Text is not colorized.
    <span class="hlb">This Text is colorized.</span>
    </pre>

Run the script.

$ ./generate_html_for_blogger.sh markupContent.md
[WARNING] This document format requires a nonempty <title> element.
  Defaulting to 'markupContent' as the title.
  To specify a title, use 'title' in metadata or --metadata title="...".
file geneated: markupContent.code_tag_removed.html

markupContent.code_tag_removed.html will have contents with CSS styles applied.

Sunday, 2 February 2020

Apache ReverseProxySSL SSLClientAuth

Apache ReverseProxySSL SSLClientAuth

Apache ReverseProxySSL SSLClientAuth

2019-08-04T14:54:01+01:00



Introduction

We will be configuring Apache in reverse proxy mode. Apache will be accepting connections on secure port with SSL client authentication and forward that request to a backend application server Gunicorn. Communication between apache and gunicorn will also be on secure port and SSL authenticated. Gunicorn will run a simple flask app that will be writing some text to file.

Purpose of this excersize is to introduce end to end secure communication from client to apache to app server with SSL client authentication at every stage. Please note that this exercise represents just the minimal configuration to work and no other authentication mechanism will be mentioned.

Note: we will be using self-signed CA authority and all certificates will be signed by this authority.

Environment Used

$ cat /etc/fedora-release
Fedora release 30 (Thirty)

$ uname -a
Linux localhost.localdomain 5.0.9-301.fc30.x86_64 #1 SMP Tue Apr 23 23:57:35 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

Prepare Environment

Create python virtual environment

$ python3 -m  venv apacheSLL
$ source apacheSLL/bin/activate
$ pip install flask
$ pip install gunicorn

Create all digital certificates

We will be using a PKI setup mentioned in [this link]({{< ref “/posts/SSL-certificate.md” >}}) We will be creating following certificates.

  • client-side certificate used by curl
  • apache server-side certificate used in interfacing with client.
  • apache client-certificate used in interfacing with gunicorn
  • gunicorn server-side certificate.

Create client-side certificate

create CSR

openssl req \
-config <(cat opensslConf/csr_openssl.cnf opensslConf/client_san_names.cnf) \
-new \
-newkey rsa:2048 \
-nodes \
-days 365 \
-out CSRs/aClient.csr \
-keyout privateKeys/aClientKey.pem \
-reqexts client \
-subj "/emailAddress=client@somecompany.com/C=GB/ST=London/L=City of London/O=client Organization/OU=Engg/CN=client.full.domain.name"

Sign CSR with CA

openssl ca \
-config opensslConf/CA_openssl.cnf \
-policy policy_anything \
-cert caCert/myCA.crt \
-keyfile caCert/myCAkey.pem \
-in CSRs/aClient.csr \
-out newCreatedCerts/aClientCert.pem

Create apache server-side certificate

If you have followed the structure of PKI mentioned in [this link]({{< ref “/posts/SSL-certificate.md” >}}), then create a san file for apache like following.

$ cat opensslConf/apache_san_names.cnf
[ san_names ]
DNS.1 = apache.server.name

create CSR

openssl req \
-config <(cat opensslConf/csr_openssl.cnf opensslConf/apache_san_names.cnf) \
-new \
-newkey rsa:2048 \
-nodes \
-days 365 \
-out CSRs/apacheServer.csr \
-keyout privateKeys/apacheServerKey.pem \
-reqexts server \
-subj "/emailAddress=apacheAdmin@admin.com/C=GB/ST=London/L=City of London/O=My Organization/OU=Engg/CN=apache.server.name"

Sign CSR with CA

openssl ca \
-config opensslConf/CA_openssl.cnf \
-policy policy_anything \
-cert caCert/myCA.crt \
-keyfile caCert/myCAkey.pem \
-in CSRs/apacheServer.csr \
-out newCreatedCerts/apacheServerCert.pem

Create apache client-side certificate

create CSR

openssl req \
-config <(cat opensslConf/csr_openssl.cnf opensslConf/apache_san_names.cnf) \
-new \
-newkey rsa:2048 \
-nodes \
-days 365 \
-out CSRs/apacheAsClient.csr \
-keyout privateKeys/apacheAsClientKey.pem \
-reqexts client \
-subj "/emailAddress=apacheAdmin@admin.com/C=GB/ST=London/L=City of London/O=My Organization/OU=Engg/CN=apache.server.name"

Sign CSR with CA

openssl ca \
-config opensslConf/CA_openssl.cnf \
-policy policy_anything \
-cert caCert/myCA.crt \
-keyfile caCert/myCAkey.pem \
-in CSRs/apacheAsClient.csr \
-out newCreatedCerts/apacheAsClientCert.pem

Create gunicorn server-side certificate

If you have followed the structure of PKI mentioned in [this link]({{< ref “/posts/SSL-certificate.md” >}}), then create a san file for gunicorn like following.

$ cat opensslConf/gunicorn_san_names.cnf

[ san_names ]
DNS.1 = gunicorn.app.server
$ openssl req \
-config <(cat opensslConf/csr_openssl.cnf opensslConf/gunicorn_san_names.cnf) \
-new \
-newkey rsa:2048 \
-nodes \
-days 365 \
-out CSRs/gunicornServer.csr \
-keyout privateKeys/gunicornServerKey.pem \
-reqexts server \
-subj "/emailAddress=apacheAdmin@admin.com/C=GB/ST=London/L=City of London/O=My Organization/OU=Engg/CN=gunicorn.app.server"

Sign CSR

$ openssl ca \
-config opensslConf/CA_openssl.cnf \
-policy policy_anything \
-cert caCert/myCA.crt \
-keyfile caCert/myCAkey.pem \
-in CSRs/gunicornServer.csr \
-out newCreatedCerts/gunicornCert.pem

In whole, following files will should be created.

$ ls -1 caCert newCreatedCerts privateKeys/
caCert:
myCA.crt
myCAkey.pem

newCreatedCerts:
12.pem
13.pem
14.pem
15.pem
aClientCert.pem
apacheAsClientCert.pem
apacheServerCert.pem
gunicornCert.pem

privateKeys/:
aClientKey.pem
apacheAsClientKey.pem
apacheServerKey.pem
gunicornServerKey.pem

Note: you will never need myCAkey.pem file except for signing CSRs. Also ignore pem files having numerals in their names. Each certification generation process creates a cert file and a corresponding numeral file. Contents of both the files are same. Hence can be ignored.

Apache Preparation

Make sure apache is installed. In case of fedora apache is recognized as httpd rpm package.

Install apache ssl module.

$ sudo dnf -y install mod_ssl

Create Certificate Directory

$ sudo mkdir /etc/httpd/sslCerts
$ sudo cp newCreatedCerts/apacheServerCert.pem newCreatedCerts/apacheAsClientCert.pem \
privateKeys/apacheServerKey.pem privateKeys/apacheAsClientKey.pem \
caCert/myCA.crt \
/etc/httpd/sslCerts/

prepare certificate to be used by apache reverse proxy SSL engine.

$ openssl rsa -in apacheAsClientKey.pem > apacheAsClientKeyWithRSAHeader.pem
$ openssl x509 -in apacheAsClientCert.pem > apacheAsClientCertPEMformat.pem
$ cat apacheAsClientCertPEMformat.pem apacheAsClientKeyWithRSAHeader.pem > apacheAsClientCertAndKeyCombined.pem
$ chmod 400 apacheServerKey.pem apacheAsClientCertAndKeyCombined.pem

Note: We do NOT need apacheAsClientKey.pem, apacheAsClientCert.pem, apacheAsClientKeyWithRSAHeader.pem and apacheAsClientCertPEMformat.pem files and they can be deleted.

Prepeare apache config file

Create a file /etc/httpd/conf.d/reverseProxy.conf with following contents.

$ cat /etc/httpd/conf.d/reverseProxy.conf

Listen 7771
<VirtualHost *:7771>
    ServerName apache.server.name
    # LogLevel debug

    SSLEngine On
    SSLCACertificateFile /etc/httpd/sslCerts/myCA.crt
    SSLCertificateFile /etc/httpd/sslCerts/apacheServerCert.pem
    SSLCertificateKeyFile /etc/httpd/sslCerts/apacheServerKey.pem
    SSLVerifyClient require
    SSLVerifyDepth 5

    SSLProxyEngine On
    SSLProxyVerify require
    SSLProxyVerifyDepth 5
    # SSLProxyCheckPeerCN Off
    SSLProxyCheckPeerName Off
    SSLProxyCACertificateFile /etc/httpd/sslCerts/myCA.crt
    SSLProxyMachineCertificateFile /etc/httpd/sslCerts/apacheAsClientCertAndKeyCombined.pem

    <Location "/">
        ProxyPreserveHost On
        ProxyPass "https://gunicorn.app.server:6662/"
        # SSLRequireSSL
        # SSLRequire %{SSL_CLIENT_S_DN_CN} eq "some.host.name"
        # SSLRequire %{SSL_CLIENT_SAN_DNS_0} eq "some.host.name"
        # SSLRequire %{REMOTE_HOST} in {"some.host.name", "another.host.name"}
    </Location>

</VirtualHost>

Note: You may need to inrease SSLVerifyDepth and SSLProxyVerifyDepth to higher number. This represents the depth of you certificate chain (Root and intermediary certs). Number need to be exact. It can be higher but no lower. Lower number will result in error.

Start Apache

$ sudo systemctl restart  httpd

Note: You may need to tweak selinux policy for apache to run at 7771 port. I had disabled selinux for this excersize.

Make client, apache and gunicorn addresses resolvable

As root user enter the following line in /etc/resolv.conf file above all nameserver lines.

nameserver 127.0.0.1

Run a temp DNS server in foreground.

$ sudo dnsmasq --no-daemon \
--listen-address=127.0.0.1 \
--address=/client.full.domain.name/127.0.0.1 \
--address=/apache.server.name/127.0.0.1 --address=/gunicorn.app.server/127.0.0.1  \
--log-queries

Create a Simple flask app

mkdir flaskApp
mkdir flaskApp/certs
touch flaskApp/myFlaskApp.py

Contents of flaskApp/certs should be following

$ ls -og flaskApp/certs
total 16
-rw-rw-r--. 1 5021 Aug  4 17:37 gunicornCert.pem
-rw-------. 1 1704 Aug  4 17:37 gunicornServerKey.pem
-rw-rw-r--. 1 1493 Aug  4 17:37 myCA.crt

Contents of myFlaskApp.py are below.

$ cat myFlaskApp.py
from flask import Flask, jsonify
app = Flask(__name__)

api_url = "/"

@app.route(api_url, methods=["GET"])
def write_to_file():
    return jsonify({"success": "request received successfully"}), 201

start serving flask app using following command.

$ cd flaskApp/
$ gunicorn --bind 0.0.0.0:6662 myFlaskApp:app \
--ca-certs certs/myCA.crt \
--certfile certs/gunicornCert.pem \
--keyfile certs/gunicornServerKey.pem \
--cert-reqs 2

[2019-08-04 18:20:48 +0100] [37404] [INFO] Starting gunicorn 19.9.0
[2019-08-04 18:20:48 +0100] [37404] [INFO] Listening at: https://0.0.0.0:6662 (37404)
[2019-08-04 18:20:48 +0100] [37404] [INFO] Using worker: sync
[2019-08-04 18:20:48 +0100] [37407] [INFO] Booting worker with pid: 37407

Note: argument 2 for --cert-reqs corresponds to ssl.CERT_REQUIRED

Create client environment

$ mkdir client
$ mkdir client/certs

contents of client/certs are below

$ ls -og client/certs/
total 16
-rw-rw-r--. 1 5062 Aug  4 17:40 aClientCert.pem
-rw-------. 1 1704 Aug  4 17:41 aClientKey.pem
-rw-rw-r--. 1 1493 Aug  4 17:41 myCA.crt

Test the whole setup

Test gunicorn server connectivity directly

Gunicorn server connectivity.

$ curl https://gunicorn.app.server:6662 \
--cacert certs/myCA.crt \
--cert certs/aClientCert.pem \
--key certs/aClientKey.pem 

{"success":"request received successfully"}

Test gunicorn server connectivity directly (no certs, insecure)

$ curl https://gunicorn.app.server:6662 \
--cacert certs/myCA.crt \
--insecure

curl: (56) OpenSSL SSL_read: error:1409445C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required, errno 0

insecure connection attempt

insecure connection

$ cd client/
$ curl https://apache.server.name:7771/ --insecure

curl: (56) OpenSSL SSL_read: error:1409445C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required, errno 0

Above connection failed because server requires authentication. Client also need to provide its certificate.

Authenticated connection attempt.

$ curl https://apache.server.name:7771/ \
--cacert certs/myCA.crt \
--cert certs/aClientCert.pem \
--key certs/aClientKey.pem 

{"success":"request received successfully"}

Note: You do not need to generate so many certificates. You can generate certificate having both the extensions of TLS Web Server Authentication and TLS client Authentication and that certificate can be deployed at all places.