Kushal Das

FOSS and life. Kushal Das talks here.


Using onion services over unix sockets and nginx

I have explained before about how to create Onion services, this provides an easy solution to expose any service from inside of your home network to the Internet, in a secured manner (authorized services). But, in all of those examples I used an IP/port combination to expose/talk to the internal service. Instead you can also use unix sockets to do the same.

To do so, use the following style in your torrc file, this example is from my blog.

HiddenServiceDir /var/lib/tor/hidden/
HiddenServiceVersion 3
HiddenServicePort 80 unix:/var/run/tor-hs-kushal.sock
HiddenServicePort 443 unix:/var/run/tor-hs-kushal-https.sock

And the corresponding nginx configuration parts:

server {
    listen unix:/var/run/tor-hs-kushal.sock;

    server_name kushal76uaid62oup5774umh654scnu5dwzh4u2534qxhcbi4wbab3ad.onion;
    access_log /var/log/nginx/kushal_onion-access.log;

    location / {
        return 301 https://$host$request_uri;


server {
    listen unix:/var/run/tor-hs-kushal-https.sock ssl http2;

    server_name kushal76uaid62oup5774umh654scnu5dwzh4u2534qxhcbi4wbab3ad.onion;
    access_log /var/log/nginx/kushal_onion-access.log;

Now if you start tor and also nginx pointing to the same unix domain, things will go fine. But, nginx will fail to restart, you will have to remove the socket files by hand to restart. This happens due to a bug in nginx. You can edit the restart process and fix this issue.

systemctl restart nginx

Add the following the configuration file in the correct location (between the comments):

### Editing /etc/systemd/system/nginx.service.d/override.conf
### Anything between here and the comment below will become the new contents of the file

### Editing /etc/systemd/system/nginx.service.d/override.conf Anything between here and the comment below will become the new contents of the file
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry TERM/5 --pidfile /run/nginx.pid

### Lines below this comment will be discarded

If you go and read the original ExecStop value, you will find that it is using SIGQUIT, but that does not remove the socket files, only a SIGTERM does. You can read more in the []upstream bug](https://trac.nginx.org/nginx/ticket/753).

After this nginx should be able to restart without any trouble.

Thank you reader who emailed and asked for this.

Setting up local mTLS environment using mkcert

mTLS or mutual TLS is a way of doing mutual authentication. When we talk about TLS in general, we only about TLS for the servers/services. There the clients can verify that they are connected to the right server. But, the server does not know much about the clients themselves. This can be done via mTLS, say for services talking to each other. To know more please read the Cloudflare writeup on mTLS.

In this blog post we will see how we can use the mkcert from Filippo Valsorda to setup a local environment, so that you can play-around & learn.

Install nss-tools package for your system

For Fedora, I installed it via dnf.

$ sudo dnf install nss-tools -y

Getting mkcert

I grabbed the latest release from the gitub release page.

$ wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64
$ mv mkcert-v1.4.3-linux-amd64 ~/bin/mkcert
$ chmod +x ~/bin/mkcert

Setting up the local CA

$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊

This will create two important files inside of your user home directory.

❯ ls -l .local/share/mkcert/
.r--------@ 2.5k kdas 20 Dec 12:14 rootCA-key.pem
.rw-r--r--@ 1.8k kdas 20 Dec 12:14 rootCA.pem

Note:: The rootCA-key.pem is an important file and it can allow people to decrypt traffic from your system. Do not share or randomly copy it around.

The rootCA.pem file contains the public key, we can use the openssl tool to inspect it.

❯ openssl x509 -text -noout -in ~/.local/share/mkcert/rootCA.pem
        Version: 3 (0x2)
        Serial Number:
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = kdas@localhost.localdomain (Kushal Das), CN = mkcert kdas@localhost.localdomain (Kushal Das)
            Not Before: Dec 20 11:14:33 2021 GMT
            Not After : Dec 20 11:14:33 2031 GMT
        Subject: O = mkcert development CA, OU = kdas@localhost.localdomain (Kushal Das), CN = mkcert kdas@localhost.localdomain (Kushal Das)
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (3072 bit)
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Subject Key Identifier: 
    Signature Algorithm: sha256WithRSAEncryption

If you look closely at the X509v3 extensions section of the output, you will notice two important things:

  • It is a CA certificate
  • pathlen 0 means it can not sign/create any new CA but only sign leaf certificates. Do man x509v3_config to learn more.

Setting up certificate for local development

❯ cd ~/code/mtls-example
❯ mkcert localhost ::1

Created a new certificate valid for the following names 📜
 - "localhost"
 - ""
 - "::1"

The certificate is at "./localhost+2.pem" and the key at "./localhost+2-key.pem" ✅

It will expire on 20 March 2024 🗓

Starting a nginx podman container with the certificate

Next we will start a podman nginx container to try to out the certificate. On my Fedora machine, I will also have take care of SELinux. First let us create a default.conf.

server {
  listen [::]:443 ssl http2 ipv6only=on;
  listen 443 ssl http2;
  server_name  localhost;
  ssl_protocols TLSv1.3;
  ssl_certificate /etc/nginx/conf.d/localhost+2.pem;
  ssl_certificate_key /etc/nginx/conf.d/localhost+2-key.pem;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root   /usr/share/nginx/html;

Then, I will copy the rootCA.pem file in the current directory and start the container.

❯ cp ~/.local/share/mkcert/rootCA.pem .
❯ chcon -Rt svirt_sandbox_file_t .
❯ podman run --rm -p 8080:443 -v $PWD:/etc/nginx/conf.d/ nginx

and from another terminal I can verify the setup using curl.

❯ curl --tlsv1.3 https://localhost:8080
<!DOCTYPE html>
<title>Welcome to nginx!</title>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>

Same via Python httpx module (includes commands to create and activate the Python virtualenv).

❯ python3 -m venv .venv
❯ source .venv/bin/activate
❯ python3 -m pip install httpx
>>> import httpx
>>> r = httpx.get("https://localhost:8080/", verify="./rootCA.pem")
>>> r
<Response [200 OK]>

Now let us enable client side certificate and verification in nginx

We will modify the default.conf to the following.

server {
  listen [::]:443 ssl http2 ipv6only=on;
  listen 443 ssl http2;
  server_name  localhost;
  ssl_protocols TLSv1.3;
  ssl_certificate /etc/nginx/conf.d/localhost+2.pem;
  ssl_certificate_key /etc/nginx/conf.d/localhost+2-key.pem;
  ssl_client_certificate /etc/nginx/conf.d/rootCA.pem;
  ssl_verify_client on;
  ssl_verify_depth  3;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root   /usr/share/nginx/html;

and restart the podman container.

Now, let us try the same curl command and Python code.

❯ curl --tlsv1.3 https://localhost:8080
<head><title>400 No required SSL certificate was sent</title></head>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
>>> r = httpx.get("https://localhost:8080/", verify="./rootCA.pem")
>>> r
<Response [400 Bad Request]>

Creating a client side certificate and using the same

Here we are saying to use the name nehru in the client certificate. Note: I am running the commands in a different day, that is why the dates will not match with the CA certificate date :)

❯ mkcert -client nehru

Created a new certificate valid for the following names 📜
 - "nehru"

The certificate is at "./nehru-client.pem" and the key at "./nehru-client-key.pem" ✅

It will expire on 22 March 2024 🗓

If you use the openssl x509 -text -noout -in ./nehru-client.pem and check the details of the certificate, you will notice the following:

        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Client Authentication, TLS Web Server Authentication

Next, we will use the same certificates in curl.

❯ curl --tlsv1.3 --key nehru-client-key.pem --cert nehru-client.pem https://localhost:8080

And then in Python.

>>> cert = ("./nehru-client.pem", "./nehru-client-key.pem")
>>> r = httpx.get("https://localhost:8080/", verify="./rootCA.pem", cert=cert)
>>> r
<Response [200 OK]>

I hope this will help you to start trying out mTLS on your local development environment. In future posts we will learn more in depth examples.