Friday, March 5, 2021

Node.JS : Enable HTTPS on Express JS


How to enable HTTPS on Express Node.JS?

Step 1 : $ npm install express
Step 2 : Download & Install OpenSSL
Step 3 : Generate file *.crt and *.key using OpenSSL
             $ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt -config "C:\OpenSSL-Win64\share\openssl.cnf"
Step 4 : Write Code 
Step 5 : Done

You can follow the syntax below to enable HTTPS on Express Node.JS 

/*
	NodeJS HTTPS Server Example
	--------------------------------------------------

	Step 1 : $ npm install express
	Step 2 : Download & Install OpenSSL
		 - http://gnuwin32.sourceforge.net/packages/openssl.htm
	Step 3 : Generate file *.crt and *.key using OpenSSL
		 $ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt -config "C:\OpenSSL-Win64\share\openssl.cnf"
	Step 5 : Write Code 
	Step 6 : Run
		 $ node static-file-webserver.js
	Step 7 : Done

	--------------------------------------------------
	Reference :
		- https://stackoverflow.com/a/11745114/9278668
		- https://stackoverflow.com/a/32169444/9278668
		- https://stackoverflow.com/a/19400134/9278668
*/

var fs = require('fs');
var http = require('http');
var https = require('https');
var privateKey  = fs.readFileSync('localhost.key', 'utf8');
var certificate = fs.readFileSync('localhost.crt', 'utf8');

var credentials = {key: privateKey, cert: certificate};

var path = require('path');
var express = require('express');
var app = express();

var dir = path.join(__dirname, '');

app.use(express.static(dir));

var httpServer = http.createServer(app);
var httpsServer = https.createServer(credentials, app);

httpServer.listen(80, function() {
	console.log('HTTP Server Running...');
});
httpsServer.listen(443, function() {
	console.log('HTTPS Server Running...');
});


Reference :

How to Generate File .CRT and .KEY using OpenSSL


How to generate file certification .crt and .key using OpenSSL?

Step 1 : Download & Extract / Install OpenSSL
http://gnuwin32.sourceforge.net/packages/openssl.htm

Step 2 : Generate file *.crt and *.key using OpenSSL

$ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt -config "C:\OpenSSL-Win64\share\openssl.cnf"

Step 3 : Done!


Reference :

Thursday, August 6, 2020

Nginx Extra Security & Performance Tuning Example


Advanced configuration for Nginx

There are some small changes you can make to make your website faster, and a little more secure.

Security enhancements

The original configuration will keep you safe, but it is always good to see what else can be done.

Giving less information to attackers

By default, the nginx.conf will return "403 Forbidden" errors for system files that are not supposed to be accessed directly. However, there's a good alternative, like so:

server {
    ...

    ## Begin - Index
    # for subfolders, simply adjust:
    # `location /subfolder {`
    # and the rewrite to use `/subfolder/index.php`
    location / {
        try_files $uri $uri/ @index;
    }

    location @index {
        try_files = /index.php?_url=$uri&$query_string;
    }
    ## End - Index

    ## Begin - Security
    # set error handler for these to the @index location
    error_page 418 = @index;
    # deny all direct access for these folders
    location ~* /(\.git|cache|bin|logs|backup|tests)/.*$ { return 418; }
    # deny running scripts inside core system folders
    location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 418; }
    # deny running scripts inside user folder
    location ~* /user/.*\.(txt|md|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 418; }
    # deny access to specific files in the root folder
    location ~ /(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt|\.htaccess) { return 418; }
    ## End - Security
    ...
}

What happens here is the following:

  • Try and see if the file exists on disk, and if not, give the request to the @index location.
  • The new @index location will reroute requests to the /index.php as usual.
  • Instead of returning a "403 Forbidden" error, we now return a 418.
  • Because we set the error_page 418, any 418 will be handled by the @index location.
  • Grav's /index.php will pick up the route it is given, determine if there's a matching route,
    and if not, simply return a 404 by itself.

Normally, we route all non-existing files to Grav. However, returning any status code from nginx itself, will give a different kind of error than if it had been routed through Grav. That gives the attacker the information that those files are special and actually exist. More than that, that you explicitly don't want them to try and read those. They will try harder.

This is better, because if you reroute it to Grav, Grav will handle it like any other non-existing file.

No direct access to other .php-files

In the example nginx.conf, all requests to files ending in .php are sent to the PHP-handler. This is not necessary, as Grav only uses the /index.php to route requests. Every other location is handled internally.

Some vulnerabilities in CMS's are targeted specifically at plug-ins, themes or other third-party libraries. There is no reason to keep direct access to them open (unless you run Grav combined with other pieces of software in the same webroot!).

So, alternatively, only route /index.php to the PHP-handler, and block the rest! Like so:

server {
    ...
    ## Begin - PHP
    location = /index.php {
        # Choose either a socket or TCP/IP address
        fastcgi_pass unix:/var/run/php7.0-fpm.sock;
        # fastcgi_pass 127.0.0.1:9000;

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    ## End - PHP

    ## Begin - Security
    ...
    # deny access to other .php-scripts
    location ~ \.php$ { return 418; }
    ...
    ## End - Security
    ...
}

With this, any .php-file that is not /index.php, will be rerouted to and be handled by /index.php.

Performance

nginx is a very capable webserver, but it is also very capable of doing advanced caching.

Do not check for the existence of directories

Many, many examples use this line:

try_files $uri $uri/ @index;

Few people actually realise that it does, which is:

  1. Check for the existence of the file.
  2. If it does not exist, try and see if there's a directory with that name.
  3. Otherwise, fall back to @index (/index.php).

But, in reality, you only really use $uri/ (step 2) if you really are linking to a directory that will have its own index.php or index.html. Considering you're using Grav, you will only go to /, and that already goes to /index.php in step 3.

If you're not going to use it, don't keep it around, and you will take out an extra filesystem stat():

try_files $uri @index;

Caching filesystem metadata of files

The open file cache caches metadata about files. If they exist or not, what file permissions they have, if they are readable or not.. This can help a little on local filesystems, especially that are not on SSD. It shines more in environments with network storage (like NFS).

Keep in mind that this goes outside of the server{}-block, directly into the http{}-context:

open_file_cache                 max=10000 inactive=5m;
open_file_cache_valid           1m;
open_file_cache_min_uses        1;
open_file_cache_errors          on;

server {
    ...
}

In this example, we told nginx to:

  • Keep a maximum of 10k entries.
  • Delete metadata of a file from the cache if they it is not used for 5 minutes.
  • Refresh the metadata it has every minute.
  • Put the metadata of a file in the cache immediately upon accessing it the first time.
  • Cache errors like "Permission denied", "Not found", as well.

Precompressing resources

In the example nginx.conf, we enable GZip-compression. While excellent, it also means that the output is compressed on a per-request basis. This in turn means that with every request, it adds extra CPU-cycles and latency (you have to wait or the compression to be done).

There is an alternative in nginx, which is gzip_staticgzip_static will make nginx look for the same file, but with a .gz extension. So if I have main.css, it will try and see if there is already a main.css.gz present, and send that instead.

To enable this, use:

server {
    ...

    ## Begin - Index
    ...

    location / {
        try_files $uri @index;

        location /assets {
            gzip_static on;
        }
    }

    ...
    ## End - Index
    ...
}

By using a nested location /assets, you will not incur extra filesystem stat()s for the rest of Grav.
If enabled, the /assets location will contain pipelined/minified assets.

!! nginx does not automatically compress the files for you. You will have to do this yourself.

To compress the files (on UNIX-based systems), you can do the following:

cd assets
for asset in *.css *.js; do gzip -kN9 "$asset"; done

Note that these are automatically deleted when you clear your (asset) cache, and you will have to redo it after new resource files are created. There is an outstanding Pull Request to allow automatic precompression of assets upon creation of these files.

Enable FastCGI caching.

** DO NOT USE THIS KIND OF CACHING IF YOU HAVE DYNAMIC PAGE CONTENT **

The following example is safe to use with authentication, and the Admin interface. It is also safe for dynamic page content, but your dynamic content will not be dynamic anymore (as it is aggressively statically cached).

nginx has caching for FastCGI. In the example below, we will leverage this. Note that if anything on the site sets a cookie or similar dynamic content headers, the cache is invalid and will not be used at all.

First, we are going to need to make a map{} in the main http{}-context (so outside/before the server{}-block) to convert our optional session cookie into a unique identifier for the cache (so users with different sessions do not share the same cached resource, all of them will have a unique copy, but cached for their own session):

# This is to have caching enabled when site sessions are turned on.
# It makes FastCGI caching safe to use with authenticated content.
map $http_cookie $sessionkey {
    default '';
    ~grav-site-(?<hash>[0-9a-f]+)=(?<sessionid>[^\;]+) $hash$sessionid;
}

server {
    ...
}

If you renamed your site session name (setting session.name) in your Grav config, update its name in the example above! The regular expression will combine the unique identifier in the cookie name with the session id in the cookie into a new variable $sessionkey, which we can later use in the fastcgi_cache_key setting. Next, define a cache zone in the same context right under it:

fastcgi_cache_path      /path/to/cache/on/disk          levels=1:2
                        keys_zone=fastcgi:10m           max_size=200m
                        inactive=60m                    use_temp_path=off;

You should change the path of where the cache is stored on disk, but what it does is:

  1. Set the path. You can use this to cache on disks that might be faster. (Tip: Use /dev/shm/something on Linux systems to use a RAM-disk instead!).
  2. Define the directory hierarchy structure of 1 character, then 2 characters. (i.e. /path/to/cache/on/disk/c/29/...)
  3. Give the cache zone a name ("fastcgi" here) and an initial size (10 MB).
  4. Allow a maximum of 200 MB to be cached.
  5. Delete a resource from the cache if it hasn't been used for 60 minutes.
  6. Do not base the cache path off of fastcgi_temp_path.

Next, you can use this cache zone in your configuration:

server {
    ...
    ## Begin - PHP
    location = /index.php {
        ## Begin - FastCGI caching
        fastcgi_cache           fastcgi;
        fastcgi_cache_key       "$scheme$request_method$host$request_uri$sessionkey";
        fastcgi_cache_valid     200 30m;
        fastcgi_cache_valid     404 5m;
        fastcgi_cache_valid     any 1m;
        fastcgi_ignore_headers  "Cache-Control"
                                "Expires"
                                "Set-Cookie";

        fastcgi_cache_use_stale error
                                timeout
                                updating
                                http_429
                                http_500
                                http_503;

        fastcgi_cache_background_update on;
        ## End - FastCGI caching

        # Choose either a socket or TCP/IP address
        fastcgi_pass unix:/var/run/php7.0-fpm.sock;
        # fastcgi_pass 127.0.0.1:9000;

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    ## End - PHP
}

Now, what is happening here, is the following:

  • The cache zone named "fastcgi" will be used for this (we defined that earlier).
  • The cache key to lookup a cached resource is set to something like "httpsGETwww.example.com/".
  • Cache times:
    • 200 "OK"-responses: 30 minutes.
    • 404 "Not Found"-responses: 5 minutes.
    • Any other status code: 1 minute.
  • Ignore the Cache-ControlExpires and Set-Cookie headers (since we're in control here).
  • Keep serving our cache when the following happens:
    • There is an error.
    • There is a timeout.
    • nginx is busy updating the cache.
    • There is a 429 Too Many Requests on the backend (backend is too busy).
    • There is a 500 Internal Server Error on the backend (backend has a misconfiguration).
    • There is a 503 Gateway Timeout to the backend (backend has other problems).
  • And last but not least: Upgrade the cache in the background, don't make our clients wait.

Please note, once again, that this will not (and should not) work if you're using the Admin panel, or use any other sessions on your site. It is not recommended to use this caching with any authenticated content, for security reasons.

If you wish to test if your cache is working, you can add a simple header (remove on production):

add_header X-Cache "$upstream_cache_status - $scheme$request_method$host$request_uri$sessionkey";

You can view the header with cURL (or any other tool):

$ curl -I https://www.example.com/
...
X-Cache: MISS - httpsHEADexample.com/en - 7b2fdab20bb2fbi85l61eounburtlxavo
...
$ curl -I https://www.example.com/
...
X-Cache: HIT - httpsHEADexample.com/en - 7b2fdab20bb2fbi85l61eounburtlxavo
...
$

Again, you might want to disable the header after confirming it works.

If you feel the need to purge your cache entirely:

  1. Stop nginx.
  2. Remove the directory.
  3. Start ngnix.

The cache is automatically updated and pruned in the background, so you shouldn't need to do so.

More fine-grained control over (not) caching

If you use the admin interface, you might want to disable caching globally once the admin cookie has been set. Note that an admin cookie is set when you go to the /admin URL, and that anyone can go there by default. You don't need to login to disable caching.

You can use the following example as a basis. Put the map{}s outside your server{} and the fastcgi_*-directives with the rest of the caching directives:

# This is used by fastcgi_cache_bypass and fastcgi_no_cache.
# If you don't want certain URI's cached, add them here with a value of 1.
map $request_uri $no_cache1 {
        default                 0;
        ~^/(../|)admin          1;
}

# This is used by fastcgi_cache_bypass and fastcgi_no_cache.
# To disable caching based on cookie names, add them here with a value of 1.
map $http_cookie $no_cache2 {
    default 0;
    ~grav-site-([0-9a-f]+)-admin=([^\;]+) 1;
}

server {
    ...
    location = /index.php {
        ...
        fastcgi_cache_bypass $no_cache1 $no_cache2;
        fastcgi_no_cache     $no_cache1 $no_cache2;
        ...
    }
}


Nginx Block User Agent Configuration


Try adding something like the following directives to your config to block user agent:

# Disallow User Agent
if ($http_user_agent ~* "agent1|Cheesebot|Catall Spider|LWP::Simple|BBBike|wget|libwww-perl|python|nikto|curl|scan|java|winhttp|HTTrack|clshttp|archiver|loader|email|harvest|extract|grab|miner" ) {
	return 404;
}



HTTP Flood & DDoS Filter via Nginx


Try adding something like the following directives to your nginx config to prevent HTTP Flooding & Distributed Denial of Service (DDoS) :

http {
	limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
	limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=5r/s;

	server {
		limit_conn conn_limit_per_ip 10;
		limit_req zone=req_limit_per_ip burst=10 nodelay;
	}
}



NOTE: http://www.botsvsbrowsers.com/details/504401/index.html says the above user agent is not a known bot



WordPress Security via Nginx


This tutorial goes over how to set up basic security rules for hosting a WordPress website behind an Nginx reverse proxy that also serves cached and static content.

One of the biggest advantages of Apache as a web server is its flexibility to implement custom-level access and security rules through its .htaccess file. For example, many security plugins make heavy use of .htaccess to implement their policies.

If you’re using Nginx to serve some content dynamically, or even to serve most content, it makes sense to have these rules implemented at the Nginx level, since Apache will not get to see a large proportion of requests.

We can create security rules in external files, which we can then call from within the Nginx server configuration files. This way we only need to update rules once, and the new rules apply to all sites you host. Let’s create a set of basic rules.

root@system:~# nano /etc/nginx/snippets/secrules.conf

This is just a set I’ve put together from other people have shared. I have modified it to include particular nuisance requests that hit some of my servers:

# secrules.conf, a snippet containing security rules for Nginx hosts

# Block request methods that are unnecessary for serving your content
if ($request_method !~ ^(GET|POST|HEAD)$ ) {
	return 444;
	}

# Block scripts from being executed from your uploads folder. They will be served as text.
# If you don't serve scripts at all, you could block them altogether instead.
location ~* ^/wp-content/uploads/.*.(php|pl|py|jsp|asp|htm|html|shtml|sh|cgi)$ {
	types { }
	default_type text/plain;
	}

# Block attempts to access PHPMyAdmin. If you actually use it, don't include this rule!
location ~* .(administrator|[pP]hp[mM]y[aA]dmin) {
	deny all;
	}

# Disallow common hacks
location ~* .(display_errors|set_time_limit|allow_url_include.*disable_functions.*open_basedir
	|set_magic_quotes_runtime|webconfig.txt.php|file_put_contentssever_root
	|wlwmanifest) {
		deny all;
		}

location ~* .(globals|encode|localhost|loopback|xmlrpc|revslider) {
	deny all;
	}

# Disallow access to sensitive files
## WARNING - The first rule \. interferes with access to the .well-known directory,
## and will disallow LetsEncrypt using webroot. In this case you may want to change
## it to \.ht
location ~ /(\.|wp-config.php|readme.html|license.txt|nginx.conf|wp-config-sample.php
    |readme.txt|dbconfig.php) {
		deny all;
		}

# Disallow scripts
location ~* \.(pl|cgi|py|sh|lua)$ { return 444; }

# Help guard against SQL injection
location ~* .(\;|'|\"|%22).*(request|insert|union|declare|drop)$ {
	deny all;
	}

# Disallow access to parts of wp-includes
# Many sites recommend blocking wp-includes altogether but in my experience this breaks WordPress
location ~* wp-admin/includes { deny all; }
location ~* wp-includes/theme-compat/ { deny all; }
location ~* wp-includes/js/tinymce/langs/.*.php { deny all; }

Full disclosure: This file is modified from content in lamosty.com and geekytuts.net, changing rules that haven’t worked for me in the past (or adding warnings), and including some others.

Now that we have our basic secrules, we can call it from within the Nginx server block. An example configuration following on from our previous tutorial could be like this:

server {
	listen 80;
	server_name example.com www.example.com;
	root /var/www/example.com/html;
	index index.php;

	include snippets/supercache.conf
	include snippets/secrules.conf

	location / {
		try_files $cachefile $uri $uri/ /index.php;
	}

	location ~ \.php$ {
        proxy_pass http://localhost:8080$request_uri;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

	location ~ /\. {
		deny all;
	}

}

Before doing anything else, test the new Nginx configurations:

root@system:~# nginx -t

If all goes well, reload Nginx and enjoy the warm and fuzzy feeling that your server is a little better protected from basic attacks. We can’t really be immune to geting hacked, just try not to be the low-hanging fruit.




Saturday, October 19, 2019

How to Enable Spring Boot with SSL (HTTPS) and HTTP/2 on localhost



The web is moving to HTTPS. More and more sites are only accessible with HTTP over TLS. Thanks to Let's Encrypt you have access to free TLS certificates and with the ACME protocol a way to automate the certificates management.
But there is one area where TLS is not that prevalent, in our development environment. This is a bit of a problem because more and more features in the browsers require a secure context. For example Geolocation, Service Workers, Web Crypto and others. These features only work when the page is served over HTTPS, but fortunately browsers make an exception for connections to localhost and 127.0.0 and you can work with these features in your development environment with HTTP over plaintext TCP.
But there is one feature that requires a TLS connection, HTTP/2. If you want to use HTTP/2 in your development, you have to have TLS enabled. There is a specification for using HTTP/2 over cleartext TCP, but browsers and Spring Boot did not implement it.
Another reason to use TLS in your development environment is the mixed content issue. For example, you have a HTML page that references the jQuery library with HTTP.
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Mixed Content</title>
</head>
<body>
    <script src="http://code.jquery.com/jquery-3.3.1.slim.min.js"></script> 
</body>
</html>
In your development environment you use HTTP over cleartext TCP and everything looks good and works. Then you deploy this web page to a production server which serves the resources over HTTPS. And suddenly your application is no longer working, because browsers refuse to load resources over an insecure connection when the host page has been loaded over a secure connection.
The browser prints this error message in the developer tools console:
Mixed Content: The page at 'https://localhost:8443/index.html' was loaded over HTTPS, but requested an
insecure script 'http://code.jquery.com/jquery-3.3.1.slim.min.js'. This request has been blocked; 
the content must be served over HTTPS.
You see that there are valid reasons to always develop and test your application with the same protocol that you use in your production.

mkcert

Unfortunately, it is not that easy to set up TLS on your local machine, because you can't simply get a TLS certificate for localhost or 127.0.0.1. You could create self signed certificates, but browsers show you some ugly warning messages and it's not very convenient.
Another workaround is to use tools like ngrok and localtunnel. They give you a HTTPS address to your application, but the drawback is that the traffic is routed from your computer to the ngrok resp. localtunnel servers and then back to your computer. So it won't work when you don't have an Internet connection, for example if you want to do some development during a flight. Although, these services are very convenient when you want to give somebody outside of your network access to your computer. For example presenting a co-worker or customer a web application that your are working on.
The better solution is to install your own private CA (certification authority) and create TLS certificates that are signed with this CA and also configure your operating system and browsers to trust this CA.
Setting this up is a bit complicated but it is possible doing it from scratch with tools like openssl or the keytool from Java.
But in this example we use a tool that simplifies the setup process quite a lot: mkcert
It's a command line tool written in Go and runs on Windows, Linux and macOS. See the readme on how to install it. For this blog post I'm going to demonstrate the tool on Windows 10. I downloaded the executable from the release page and saved it in an arbitrary directory.
First run the -install command. This creates a private CA, configures the operating system and browsers to trust this CA. It also automatically adds the CA to Java if it finds a JAVA_HOME environment variable.
mkcert-v1.4.0-windows-amd64.exe -install
This creates two files rootCA.pem and rootCA-key.pem in your home directory (C:\Users\<USER>\AppData\Local\mkcert). It registers the CA in the Windows system certification store. Browsers like Chrome and Firefox read root certificates from this store.
You can verify the entry with the Windows certification manager tool
certmgr.msc
You find the CA under Trusted Root Certification Authorities -> Certificates
Next, we create a TLS certificate for the domains localhost127.0.0.1 and ::1, that is signed by our own private CA. By default, mkcert creates certificates in the PEM format. Because we want to use the certificate in a Java application (Spring Boot) and Java can't load PEM certificates, we have to create the certificate in the PKCS#12 format.
mkcert-v1.4.0-windows-amd64.exe -pkcs12 localhost 127.0.0.1 ::1
This creates a new file localhost+2.p12 in the current directory. The PKCS#12 bundle is secured with the password changeit.

Spring Boot

In this section we create a trivial Spring Boot application and enable TLS and HTTP/2 with our newly created TLS certificate.
I usually bootstrap my Spring Boot applications with a visit to https://start.spring.io. In this case I utilize the curl method. Run the following command in your command prompt:
curl https://start.spring.io/starter.zip -d dependencies=web,thymeleaf -d javaVersion=11 -d groupId=ch.rasc -d artifactId=h2demo -o h2demo.zip
Unzip the h2demo.zip file and copy the certificate localhost+2.p12 into the root folder of your project.
Open src/main/resources/application.properties, it's empty by default, and insert the following content:
server.http2.enabled=true
server.port=8443

server.ssl.enabled=true
server.ssl.key-store=./localhost+2.p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=changeit
With these settings we enable TLS and HTTP/2 and set the listening port to 8443. When you enable TLS in Spring Boot, you also have to specify the key store, the format and the password.
To test, if everything works, we write a simple RestController and a GET endpoint.
@SpringBootApplication
@RestController
public class Application {

 public static void main(String[] args) {
  SpringApplication.run(Application.class, args);
 }

 @GetMapping("/")
 public String helloWorld() {
  return "Hello World";
 }
}
Start the application from the command line or inside your IDE.
.\mvnw.cmd spring-boot:run
Check the network tab in the browser developer tools
You see that this request has been served with HTTP/2 over TLS

Charles

Charles is a HTTP proxy, monitor, reverse proxy to inspect all the HTTP and HTTPS traffic between any application on your machine and the Internet. Very similar to the network tool in the browser developer tools, but it is not limited to browsers it can intercept traffic from all applications running on your computer.
To inspect the TLS connection between our browser and Spring Boot we need to install our private root certificate into Charles.
mkcert creates the root certificate in the PEM format, which Charles can't read. We therefore have to convert the file into a PKCS#12 file.
If you don't remember, where mkcert has stored the root certificate, run the following command. It tells you the directory containing the root certificate.
mkcert-v1.4.0-windows-amd64.exe -CAROOT
In a command prompt change into this directory and execute the following command.
openssl pkcs12 -export -out rootCA.pkcs12 -inkey rootCA-key.pem -in rootCA.pem
Enter the password changeit and openssl creates a new file rootCA.pkcs12 from the PEM file.
Start Charles, open the menu Proxy -> SSL Proxying Settings, open the tab Root Certificate and select the pkcs12 file we've just created.
Open the SSL Proxying tab and enable SSL Proxying. Add a new entry to the location list (localhost:8443).
Open the URL https://localhost:8443/ in your browser, you should see the traffic in Charles if everything is configured correctly.

HTTP/2 Push

With a working TLS and HTTP/2 Spring Boot application we can now start experimenting a bit with HTTP/2 push, a new way to send content from a server to the client.
A typical workflow of a HTTP request for a HTML page looks like this
  1. Browser sends GET request
  2. Server responds with the HTML page
  3. Browser parses the HTML code and looks for references to other files in tags like <img><link><script> and others
  4. Browser sends requests for all the referenced resources
  5. Server sends back the requested resources
  6. Browser displays the page
With HTTP/2 the server has the ability to push resources to the client. For example, the following page contains an <img> tag. We, as the developer of the page, know when a browser requests this HTML page he also needs the image, so why not send it together with the HTML page and that is what HTTP/2 push provides.
<!DOCTYPE html>
<html lang="en">
<head>
    <title>HTTP2 Push Test</title>
</head>
<body>
    <img src="cat.webp">
</body>
</html>
The workflow for a HTTP request for this particular page with HTTP/2 push follows these steps
  1. Browser sends GET request
  2. Server responds with the HTML page AND the image cat.webp
  3. Browser parses the HTML page, sees the <img> tag and looks for it in the push cache (a special cache for resources that have been pushed from the server). Because he finds the image in there he immediately displays the page without sending any further requests.
There is one caveat, the browser cache. Without using push, after the browser has parsed the HTML page, he checks if any of the referenced files are stored in one of his caches. If they are, he does not send additional requests to the server and retrieves the resources from the local cache. This saves bandwidth and the browser can display the page faster.
With HTTP/2 push this is different, the server has no knowledge of whether the files are cached or not. He always pushes the files to the client. This would waste a lot of bandwidth, but fortunately browsers solve that problem by cancelling the push connection as soon as they have checked the cache and find the resources in there.

To observe this behaviour I've created two endpoints in my Spring Boot application. In Spring Boot everything is already built-in, we only have to specify the resources we want to push.
  @GetMapping("/withoutPush")
  public String withoutPush() {
    return "index";
  }
  
  @GetMapping("/withPush")
  public String withPush(PushBuilder pushBuilder) {
    if (pushBuilder != null) {
        pushBuilder.path("cat.webp").push();
    }
    return "index";
  }
index references the HTML page, mentioned above, that is stored in the src/main/resources/template folder. javax.servlet.http.PushBuilder is a class from the Servlet 4 implementation and allows us to specify which resources we want to push in addition to the HTML page.
Start the application and then open the network tab in the browser.
When we call /withoutPush with an empty browser cache, we see the typical waterfall of requests. The browser receives the HTML, parses it and requests the image.
With a populated browser cache the browser does not have to send an additional request for the image, he can retrieve it from the local cache
With push and an empty browser cache we see that the browser also sends just one request to the server.
When the image is already stored in the cache, the browser cancels the push stream. Because you can't see that in the browser developer tools, I show you here a screenshot of Charles. There you see that Chrome closes the stream before the server is able to send the complete picture. 
This was just a brief overview about HTTP/2 push. If you wan to dig deeper into HTTP/2 push, I recommend reading this article from Jack Archibald. He writes about all the pitfalls and different browser implementations of HTTP/2 push: https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/

You have seen in this article that setting up TLS on your localhost is not that complicated, thanks to the mkcert tool. With a valid TLS certificate, setting up TLS on Spring Boot and Java 11 is also very easy, because it provides everything out of the box for running a TLS and HTTP/2 server.


Reference: