# Configuring Client Certificate Authentication on Nginx in Docker


<!--more-->

## Basic HTTP Server Setup in Docker

Create a directory where we will configure the container and navigate into it.

```shell
sudo mkdir /opt/docker && cd $_
```

### Container Configuration in Docker Compose

Create a file named `docker-compose.yml` with two containers — one for Nginx and one for Certbot for updating the
**Let's Encrypt** certificates.

```yaml
services:
  nginx:
    image: nginx:latest
    restart: unless-stopped
    # route ports
    ports:
      - "80:80"
      - "443:443"
    # mount directories from the current folder into the container
    volumes:
      - certbot-crt:/etc/letsencrypt:ro
      - certbot-tmp:/var/certtmp:ro
      - ./nginx:/etc/nginx/conf.d:ro
    command: "/bin/sh -c 'while :; do sleep 12h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  certbot:
    image: certbot/certbot:latest
    restart: unless-stopped
    volumes:
      - certbot-crt:/etc/letsencrypt
      - certbot-tmp:/var/certtmp
    depends_on:
      - nginx
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  certbot-crt:
  certbot-tmp:
```

The `/var/certtmp` directory in the **nginx** container must match the path specified in the site's configuration file
with `root /var/certtmp;`.

Create a simple configuration for your site in the file `nginx/example.com.conf`. Make sure that your domain points to
the IP of this server.

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location / {
        add_header Content-Type text/plain;
        return 200 "SSL Client Verify: $ssl_client_verify\nSSL Serial: $ssl_client_serial\nSSL DN: $ssl_client_s_dn\nSSL Cert: $ssl_client_cert\n";
    }
    
    # Needed for obtaining certificates from Let's Encrypt
    location ~ /.well-known/acme-challenge {
        allow all;
        root /var/certtmp;
    }
}
```

Do not forget to replace all instances of `example.com` with your own domain. Reload the Nginx configuration.

```shell
docker compose exec nginx nginx -s reload
```

### Obtaining a Certificate from Let's Encrypt

First, run in test mode with the `--dry-run` parameter to check that the configuration is correct.

```shell
docker compose run --entrypoint "certbot certonly --webroot --webroot-path=/var/certtmp --dry-run -d example.com -d www.example.com" certbot
```

If everything is fine, you should see the following message:

    [+] Creating 1/0
     ✔ Container docker-nginx-1  Running                    0.0s 
     Saving debug log to /var/log/letsencrypt/letsencrypt.log
     Account registered.
     Simulating a certificate request for example.com
     The dry run was successful.

Now, request the certificate.

```shell
docker compose run --entrypoint "certbot certonly --webroot --webroot-path=/var/certtmp -d example.com -d www.example.com" certbot
```

Agree to the terms and conditions and obtain the certificate. If everything is successful, the output will include the
path where the certificate files are saved. Next, update your configuration in `nginx/example.com.conf`.

Leave the Let's Encrypt check block in place, otherwise we won't be able to update the certificate later, as their bot
won't pass our security. And for all other requests, we set up a redirection to HTTPS.

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    
    location / {
        return 301 https://$server_name$request_uri;
    }

    # Needed to obtain a certificate from Let's Encrypt.
    location ~ /.well-known/acme-challenge {
        allow all;
        root /var/certtmp;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    location / {
        add_header Content-Type text/plain;
        return 200 "SSL Client Verify: $ssl_client_verify\nSSL Serial: $ssl_client_serial\nSSL DN: $ssl_client_s_dn\nSSL Cert: $ssl_client_cert\n";
    }
}
```

Reload the Nginx configuration and test. Your site should now be accessible via **https**.

```shell
curl https://example.com
```

---

## Configuring Client Certificates

### Configuring the Certificate Authority

First, we need to create a certificate for the Certificate Authority (CA). We will use it to sign client certificates
and verify their authenticity.

{{< admonition type=info open=true >}}
**Important!** When creating the CA and the client certificates, the **Organization Name** fields must be different!
Otherwise, client authentication will fail.
{{< /admonition >}}

Create a separate directory to store certificates and perform all the operations there. For security reasons, this
directory should not be kept inside a container. Create it, for example, in the user’s home directory.

Create the CA’s private key and generate the CA certificate.

```shell
mkdir ~/mycompanycert && cd $_
openssl ecparam -name secp384r1 -genkey -out companyname.ca.key
openssl req -new -x509 -days 36500 -sha512 -key companyname.ca.key -out companyname.ca.crt
```

Be sure to specify the original **Organization Name** for the Certificate Authority. It is recommended to fill in the
other fields with real data.

Copy the generated certificate **companyname.ca.crt** into the configuration folder `/opt/docker/nginx`. Then add your
created CA into your site's Nginx configuration for client certificate verification and enable forced client certificate
verification.

```nginx
server {
    ...
    ssl_client_certificate conf.d/companyname.ca.crt;
    ssl_verify_client on;
    ...
}
```

Reload the Nginx configuration. After this setting, access to the site without a client certificate should be blocked.

```shell
docker compose -f /opt/docker/docker-compose.yml exec nginx nginx -s reload
```


### Creating User Certificates

Ideally, user keys should be created on their devices so that the private key never leaves them or is transmitted over
the network. For signing, only the `.csr` (certificate signing request) file is needed. However, often users are not
able to create them and the entire process falls to the administrator.

Create a private key for the user and generate a certificate signing request. It is advisable to correctly fill in the
certificate fields to facilitate later control.

```shell
openssl ecparam -name secp384r1 -genkey -out user1.key
openssl req -new -key user1.key -out user1.csr
```

Create a user certificate signed by our Certificate Authority.

```shell
openssl x509 -req -CA companyname.ca.crt -CAkey companyname.ca.key -CAcreateserial -days 36500 -sha512 -in user1.csr -out user1.crt
```

You can verify the correctness of the created certificate. There should be no message regarding a self-signed
certificate.

```shell
openssl verify -verbose -CAfile companyname.ca.crt user1.crt
```

To test if everything works correctly, try connecting to the server using the certificate.

```shell
curl --cert user1.crt --key user1.key  https://example.com
```


### Transferring Certificates to Client Devices

It is not recommended to transfer the private key in plain text. For this, you need to create a secure container, such
as in the `.p12` format. All modern devices support this format.

It is not necessary to transfer our center's CA certificate to users. It is not used during client authorization. It
should only be installed on users' systems if you intend to issue certificates for your HTTPS server yourself (for
example, for servers with local names or that are not accessible from the Internet). In that case, users’ browsers will
not flag them as untrusted.

```shell
openssl pkcs12 -export -in user1.crt -inkey user1.key -out user1.p12 -passout pass:SuperPassword
```

Checking the obtained container.

```shell
openssl pkcs12 -info -nodes -in user1.p12
```

Pay attention to the line with **PKCS7 Encrypted data** -- it should not contain any DES, 3DES, MD5, or other deprecated
formats. If it does, please update your **openssl** version.

Now you can send this file to the users, and transmit its password via another secure channel along with instructions on
how to import it into the system.


### User Certificate Creation Automation Script

Create a file named `adduser.sh` to add a user. Edit the **SUBJ** parameter to set your organization, city, and region
name.

```shell
#!/bin/sh

USER=$1
PASSWORD=$2
CACRT=companyname.ca.crt
CAKEY=companyname.ca.key
SUBJ="/C=RU/ST=State/L=City/O=Organization Name/CN=$USER"
DAYS=36500

if [ -z "$1" ]; then
  echo $0 \"User Name\" [Password for p12]
  exit 1
fi

if [ ! -z "$PASSWORD" ]; then
  PASSWORD="-passout pass:$PASSWORD"
fi

openssl ecparam -name secp384r1 -genkey -out "$USER.key"
openssl req -new -subj "$SUBJ" -key "$USER.key" -out "$USER.csr"
openssl x509 -req -CA "$CACRT" -CAkey "$CAKEY" -CAcreateserial -days "$DAYS" -sha512 -in "$USER.csr" -out "$USER.crt"
openssl verify -verbose -CAfile "$CACRT" "$USER.crt"
openssl pkcs12 -export -in "$USER.crt" -inkey "$USER.key" -out "$USER.p12" $PASSWORD
```

Make the script executable:

```shell
chmod +x adduser.sh
```

Then create a user:

```shell
./adduser.sh "Harry Smith" SuperPassword
```

---

## Revoking Client Certificates

All information about revoked certificates will be stored in the same directory as the certificates. For this, create a
subdirectory **demoCA** (or use another name if you have modified the **/etc/ssl/openssl.cnf** file; you can check the
parameter **dir** in the **[ CA_default ]** section) along with the necessary files. They will contain a registry of
revoked certificates, from which a signed file with the revoked certificates named `crl.pem` will be created for
use—for example, in Nginx.

```shell
cd ~/mycompanycert
mkdir demoCA
touch demoCA/index.txt
echo 1000 > demoCA/crlnumber
```

We don’t have any revoked certificates yet, but we can already generate the CRL file and add it to the Nginx
configuration.

Note the `default_crl_days` setting in **/etc/ssl/openssl.cnf**. By default, it is set to **30**, which means your CRL
file remains valid for 30 days. If you don’t supply an updated CRL within that period, Nginx will block access to all
certificates issued by that CA.

If you’re building an automated management system, make sure the updated CRL is regenerated and distributed to all
relevant servers on time.

If you handle CRL updates manually, you may want to increase this value. You can change it in the configuration file 
**/etc/ssl/openssl.cnf** or override it with the `-crldays` option.

After generating the CRL file, distribute it to every server that uses it. In our case, copy it into the site
configuration directory at **/opt/docker/nginx/**.

```shell
openssl ca -gencrl -crldays 36500 -keyfile companyname.ca.key -cert companyname.ca.crt -out companyname.crl.pem
sudo cp companyname.crl.pem /opt/docker/nginx/
```

Now add the following configuration to Nginx to check for revoked certificates.

```nginx
server {
    ...
    ssl_crl conf.d/companyname.crl.pem;
    ...
}
```

Reload the Nginx configuration and test the setup. At this point, all certificates should be accepted, and access
without a valid certificate should be blocked.

Next, revoke the user's certificate.

```shell
openssl ca -revoke user1.crt -keyfile companyname.ca.key -cert companyname.ca.crt
```

Update the file containing the list of revoked certificates.

```shell
openssl ca -gencrl -crldays 36500 -keyfile companyname.ca.key -cert companyname.ca.crt -out companyname.crl.pem
sudo cp companyname.crl.pem /opt/docker/nginx/
```

Finally, reload the Nginx configuration and test it.

