Configuring Client Certificate Authentication on Nginx in Docker

Basic HTTP Server Setup in Docker
Create a directory where we will configure the container and navigate into it.
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.
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.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location / {
add_header Content-Type text/plain;
return 200 "Hello to client from $remote_addr!\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.
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.
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.
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
.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
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 "Hello to client from $remote_addr!\n";
}
# Needed for obtaining certificates from Let's Encrypt
location ~ /.well-known/acme-challenge {
allow all;
root /var/certtmp;
}
}
Reload the Nginx configuration and test. Your site should now be accessible via https.
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.
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.
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.
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.
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.
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.
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.
openssl verify -verbose -CAfile companyname.ca.crt user1.crt
To test if everything works correctly, try connecting to the server using the certificate.
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.
openssl pkcs12 -export -in user1.crt -inkey user1.key -out user1.p12 -passout pass:SuperPassword
Checking the obtained container.
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.
#!/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:
chmod +x adduser.sh
Then create a user:
./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.
cd ~/mycompanycert
mkdir demoCA
touch demoCA/index.txt
echo 1000 > demoCA/crlnumber
Although we currently have no revoked certificates, we can already generate a file for them that will be added to the Nginx configuration. It needs to be copied into the settings directory for the website. In our case, that is /opt/docker/nginx/.
openssl ca -keyfile companyname.ca.key -cert companyname.ca.crt -gencrl -out companyname.crl.pem
sudo cp companyname.crl.pem /opt/docker/nginx/
Now add the following configuration to Nginx to check for revoked certificates.
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.
openssl ca -revoke user1.crt -keyfile companyname.ca.key -cert companyname.ca.crt
Update the file containing the list of revoked certificates.
openssl ca -keyfile companyname.ca.key -cert companyname.ca.crt -gencrl -out companyname.crl.pem
sudo cp companyname.crl.pem /opt/docker/nginx/
Finally, reload the Nginx configuration and test it.