(Demo) PKI deployment

Introduction

In the previous articles, we explored the foundations of digital certificates, the role of PKI, and how Hardware Security Modules (HSMs) safeguard cryptographic keys. Now, let’s bring the theory to life by building a practical, automated private PKI using open-source tools. This guide will walk you through deploying your own Certificate Authority (CA) and use it to ensure TLS communication on a web application.

A private PKI empowers organizations to issue, manage, and revoke digital certificates for internal services, devices, and users, without relying on external certificate vendors. This approach enhances security, supports automation, and gives you full control over your trust model

Before you begin this demo, you’ll need:

  • Two Debian-based Linux machines (e.g., Ubuntu 20.04+):

    • One for hosting your Step-CA server

    • One or more as clients (the systems requesting certificates)

  • Root or sudo access on both

  • A basic understanding of how TLS certificates work

  • Network connectivity between the client(s) and the CA server (e.g., you can use two virtual machines with bridge connexion on your host machine).

Part 1 : Deploy your Certificate Authority (CA)

Step-CA is an open-source Certificate Authority server designed for secure, automated certificate management. It supports both X.509 (TLS) and SSH certificates, and integrates easily with automation tools and protocols like ACME.

Step 1: Install Step-CA and Dependencies

On your CA server, install the required tools:

sudo apt update && sudo apt install -y curl vim gpg ca-certificates

Add the Smallstep APT repository and install step-ca:

curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o /etc/apt/trusted.gpg.d/smallstep.asc

echo 'deb [signed-by=/etc/apt/trusted.gpg.d/smallstep.asc] https://packages.smallstep.com/stable/debian debs main' \
  | sudo tee /etc/apt/sources.list.d/smallstep.list

sudo apt update && sudo apt install -y step-ca step-cli

Step 2: Initialize Your Certificate Authority

Run the initialization wizard:

step ca init

During the interactive setup:

  • Enter a name for your CA (e.g., My Internal CA)

  • Select standalone deployment mode

  • Use an available port (e.g., 2222)

  • Set and save a strong password to a file for later use:

echo "your-password-here" > password.txt

By this step, the following commands will be executed as root.

Step 3: Harden Your CA Configuration

Create a dedicated system user and move the configuration:

sudo useradd --system --home /etc/step-ca --shell /bin/false step
sudo mv /root/.step/ /etc/step-ca
sudo mv password.txt /etc/step-ca/

Private key should be stored in a secure and protected location.

Update all references to the original path in config files and set proper ownership.

sudo sed -i 's|/root/.step/|/etc/step-ca/|g' /etc/step-ca/config/*.json
sudo chown -R step:step /etc/step-ca

Step 4: Run Step-CA as a System Service

Create and install a systemd service for Step-CA:

sudo curl -o /etc/systemd/system/step-ca.service https://raw.githubusercontent.com/smallstep/certificates/master/systemd/step-ca.service

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now step-ca
sudo systemctl status step-ca

Ensure DNS resolution allows access to your CA via its domain name.

Step 5: Enable the ACME Provisioner

To allow clients to request certificates using the ACME protocol (like Let’s Encrypt), add an ACME provisioner:

sudo step ca provisioner add acme --type=ACME \
  --ca-url=https://<CA-IP>:<PORT>\
  --admin-password-file=/etc/step-ca/password.txt \
  --root=/etc/step-ca/certs/root_ca.crt \
  --ca-config=/etc/step-ca/config/ca.json
sudo systemctl restart step-ca

Replace <CA-IP> and <PORT> with your CA server’s IP address and TCP port.

Your private CA is now ready to issue ACME certificates!

Understanding ACME and Its Challenges

The ACME protocol (Automatic Certificate Management Environment) automates certificate issuance and renewal. It uses domain validation challenges to confirm you control the domain before issuing a certificate. The main challenge types are:

Challenge
How it Works
Use Case / Limitations

HTTP-01

CA requests a token at http://<domain>/.well-known/acme-challenge/<token>

Easiest for web servers with port 80 open; doesn’t support wildcards

DNS-01

Client creates a DNS TXT record under _acme-challenge.<domain>

Needed for wildcard certs; requires DNS API or manual update

TLS-ALPN-01

Client serves a special certificate via ALPN on port 443

Good for HTTPS-only environments; less common; no wildcard support

Choose the challenge that best fits your infrastructure and automation needs.

Part 2: Issue and Manage Certificates

Now that the CA is set up, let’s configure a client machine to request certificates from it. We will use Lego which is a versatile, open-source ACME client written in Go. It supports all major ACME challenge types and works with a wide range of DNS providers. Lego can request, renew, and revoke certificates from any ACME-compatible CA, including your Step-CA.

Step 1: Install Lego on the Client

On your client machine, install Lego via snap, install snapif not yet available on your distro.

sudo apt install snapd
sudo snap install snapd
sudo snap install lego

Ensure /snap/bin is in your environment's PATH. For example:

export PATH=$PATH:/snap/bin

To make it permanent for all sessions:

sudo sh -c 'echo "export PATH=\$PATH:/snap/bin" >> /etc/profile'
source /etc/profile

Step 2: Trust the CA’s Root Certificate

The client must trust the Step-CA root certificate before it can validate issued certificates.

  1. Copy the root certificate from the server:

scp /etc/step-ca/certs/root_ca.crt your-user@client-ip:~
  1. Update the client’s trusted certificates:

cd ~
sudo mv root_ca.crt /usr/local/share/ca-certificates/<pki name>_root_ca.crt
sudo update-ca-certificates

Now the client system will trust certificates issued by your CA.

Step 3: Request a TLS Certificate with Lego

Ensure DNS resolution allows access to your CA via its domain name.

From the client, run the following command:

lego -a \
  --email="user@example.com" \
  --http \
  -d your.client-domain.com \
  --server https://<CA-IP>:<PORT>/acme/acme/directory \
  run

Explanation of flags:

  • -a: Accept ACME Terms of Service automatically

  • --email: Email address for registration

  • --http: Use the HTTP-01 challenge (requires port 80)

  • -d: Domain name to issue the certificate for

  • --server: Your Step-CA’s ACME directory endpoint

How the HTTP-01 Challenge Works

The HTTP-01 challenge verifies that you control the domain by requiring the ACME client to:

  1. Start a temporary HTTP server on port 80.

  2. Respond to a specific challenge request from Step-CA at:

http://<your-domain>/.well-known/acme-challenge/<token>
  1. If Step-CA can access and validate the token, the domain is verified and a certificate is issued.

⚠️ Make sure port 80 is open and not used by another process (like Apache or Nginx), or the challenge will fail.

Step 4: Locate Your Certificate

Lego saves certificates to:

/var/snap/lego/common/.lego/certificates/

You’ll find:

  • .crt: The domain certificate

  • .key: The private key

  • .issuer.crt: The CA chain You can now use these files in your web server or application.

Step 5: Renewing Certificates

Lego only renews certificates expiring in less than 30 days.

Manual renewal:

lego -a \
  --email="user@example.com" \
  --http \
  -d your.client-domain.com \
  --server https://<CA-IP>:<PORT>/acme/acme/directory \
  renew

Force early renewal (e.g., renew if expiring in 45 days):

lego -a \
  --email="user@example.com" \
  --http \
  -d your.client-domain.com \
  --server https://<CA-IP>:<PORT>/acme/acme/directory \
  renew --days 45

Run a script on renewal:

lego -a \
  --email="user@example.com" \
  --http \
  -d your.client-domain.com \
  --server https://<CA-IP>:<PORT>/acme/acme/directory \
  renew --renew-hook="./myscript.sh"

Step 6: Automate Renewal with Cron

Manually renewing certificates on a large number of devices quickly becomes impractical. Automating this process ensures that certificates are always up to date, reducing administrative overhead and minimizing the risk of expired certificates disrupting your services.

To automate certificate renewal on each client, you can use cron to schedule regular renewal attempts.

Open the crontab editor:

crontab -e

Add the following line:

0 2 * * * /snap/bin/lego --email="user@example.com" --domains="your.client-domain.com" --http renew --days 30 >> /var/log/lego-renew.log 2>&1
  • This command attempts to renew certificates every day at 2:00 AM.

  • All output is logged to /var/log/lego-renew.log for monitoring and troubleshooting.

Automating renewals in this way ensures that your certificates are consistently maintained without manual intervention.

If you manage a large fleet of client devices, consider using automation tools like Ansible to deploy and manage these cron jobs across your infrastructure. For initial certificate deployment, you can script the process to securely distribute the first certificate to each device, further streamlining your certificate management workflow.

Bonus : Set website certificate

Now let’s walk through deploying a secure HTTPS web application using a certificate issued by your private CA via the Lego ACME client. We’ll run a simple Flask app behind Nginx, which will handle TLS termination.

Web app deployment

The Flask app lives in /srv/www/demo and uses a virtual environment.

To ensure our Flask app starts on boot and runs reliably, we’ll use systemd. Create the service file /etc/systemd/system/flask-demo.service

[Unit]
Description=Flask Demo Website
After=network.target

[Service]
User=debian
WorkingDirectory=/srv/www/demo
Environment="PATH=/srv/www/demo/venv/bin"
ExecStart=/srv/www/demo/venv/bin/python app.py
Restart=always

[Install]
WantedBy=multi-user.target

Enable and start the service

sudo systemctl daemon-reload
sudo systemctl start flask-demo
sudo systemctl enable flask-demo
sudo systemctl status flask-demo

Configure Nginx with HTTPS

Let’s use Nginx as a reverse proxy that handles HTTPS using the certificate issued by your private PKI.

/etc/nginx/sites-available/website-demo

server {
    listen 443 ssl;
    server_name website.demo.com;

    ssl_certificate     /var/snap/lego/common/.lego/certificates/website.demo.com.crt;
    ssl_certificate_key /var/snap/lego/common/.lego/certificates/website.demo.com.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

server {
    listen 80;
    server_name website.demo.com;
    return 301 https://$host$request_uri;
}

Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/website-demo /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Trust the private CA certificate

Install the CA root certificate on web site client

On devices which have to reach the website via HTTS you need to install the root CA certificate.

sudo cp <pki_name>_root_ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

Replace <pki_name>_root_ca.crt with the actual filename of your CA's root certificate.

Optional: Trust the CA in webbrowser (Firefox)

Firefox uses its own certificate store:

  1. Open Settings → Privacy & Security

  2. Scroll to Certificates

  3. Click View Certificates

  4. Import your CA certificate under the Authorities tab

or simply run :

sudo cp <pki_name>_root_ca.crt /usr/share/ca-certificates/mozilla/
sudo update-ca-certificates

Your website is now accessible securely via https://website.demo.com. You can see the lock pad on the image below.

You can find resources related to this demo on my personal github.

Conclusion

Congratulations, you've successfully walked through the complete setup of a private PKI using open-source tools like Step-CA and Lego, and you've seen how to secure internal web applications with trusted, automated certificate management.

Over the course of this series, you've learned:

  • What digital certificates are and why they're foundational to secure communications

  • How PKI works, including the role of Certificate Authorities, trust chains, and provisioning

  • The importance of HSMs in protecting private keys and ensuring cryptographic integrity

  • How to build a fully functional private PKI, automate certificate issuance with ACME, and deploy certificates securely across your infrastructure

Most importantly, you now understand how to earn the padlock in your browser, not through a commercial CA, but via your own trust infrastructure tailored to your internal environment.

Last updated