(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 bothA 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:
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 snap
if 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.
Copy the root certificate from the server:
scp /etc/step-ca/certs/root_ca.crt your-user@client-ip:~
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:
Start a temporary HTTP server on port 80.
Respond to a specific challenge request from Step-CA at:
http://<your-domain>/.well-known/acme-challenge/<token>
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:
Open
Settings → Privacy & Security
Scroll to Certificates
Click View Certificates
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