# PKI - Demo

## Introduction

In the previous articles, we explored the foundations of [digital certificates](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/pki-digital-certificates), the role of [PKI](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/pki-foundation), and how [Hardware Security Modules (HSMs)](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/pki-hsm) 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](https://smallstep.com/docs/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](https://smallstep.com/docs/step-ca/acme-basics/).

### Step 1: Install Step-CA and Dependencies

On your CA server, install the required tools:

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

Add the Smallstep APT repository and install `step-ca`:

```bash
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:

```bash
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:

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2F5IaRKbeXeSUkVeDTJQit%2Fimage.png?alt=media&#x26;token=ae7ac838-dd7f-4aee-a099-984cd2a662af" alt=""><figcaption></figcaption></figure>

```bash
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:

```bash
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.

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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FQb7mUZrpbAgef0ulVvtA%2Fimage.png?alt=media&#x26;token=0dfd97f1-45da-4c27-921a-bf07913e6906" alt=""><figcaption></figcaption></figure>

### Step 4: Run Step-CA as a System Service

Create and install a `systemd` service for Step-CA:

```bash
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:

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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FpSKT89jhQ0QtJlD6vlT5%2Fimage.png?alt=media&#x26;token=e7c89dd4-02a2-4f70-b60b-3613c45161be" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FJqP7O6pwwE7qYlKQ23TP%2Fimage.png?alt=media&#x26;token=0a588f3e-bb22-48c3-9788-46a2f80baf49" alt=""><figcaption></figcaption></figure>

### Step 5: Enable the ACME Provisioner

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

```bash
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.&#x20;

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2Fvrr6ibR8e2ZnXM3j97r9%2Fimage.png?alt=media&#x26;token=5586689f-827b-425c-b7cf-de8b87e0b687" alt=""><figcaption></figcaption></figure>

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 `snap`if not yet available on your distro.

```bash
sudo apt install snapd
sudo snap install snapd
sudo snap install lego
```

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

```bash
export PATH=$PATH:/snap/bin
```

To make it permanent for all sessions:

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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FyNabZ133NfeHNBoFa4q8%2Fimage.png?alt=media&#x26;token=e1332303-d329-4a90-96ef-d7ae7f68842e" alt=""><figcaption></figcaption></figure>

### 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:

```bash
scp /etc/step-ca/certs/root_ca.crt your-user@client-ip:~
```

2. Update the client’s trusted certificates:

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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FYSnwbuQIlxabfM0vrGB7%2Fimage.png?alt=media&#x26;token=5571f56a-0aba-44ee-90bb-0d6971a21325" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FxS07cG6WUQ0HPmcFyBnd%2Fimage.png?alt=media&#x26;token=6cd1229f-6e78-4222-939e-0e860b69a1ba" alt=""><figcaption></figcaption></figure>

From the **client**, run the following command:

```bash
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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FFvygBWa43t8fFwiRhUkZ%2Fimage.png?alt=media&#x26;token=80d6d32e-540d-4299-a042-315608d7c456" alt=""><figcaption></figcaption></figure>

#### 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:

```html
http://<your-domain>/.well-known/acme-challenge/<token>
```

3. 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.

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FbzgxVIjMw9NSg7zBRC7J%2Fimage.png?alt=media&#x26;token=2984231e-9fc9-41d8-9bf7-ccea56cd2e6a" alt=""><figcaption></figcaption></figure>

### Step 5: Renewing Certificates

Lego only renews certificates expiring in less than 30 days.

#### Manual renewal:

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

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2F9F5dnaB0SyZRlWR8r5mE%2Fimage.png?alt=media&#x26;token=18efd25b-ef09-43c9-9117-48930c695c90" alt=""><figcaption></figcaption></figure>

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

```bash
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:

```bash
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:

```bash
crontab -e
```

Add the following line:

```bash
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.

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FoLZ1TA8nNfvJV5Wn7UaJ%2Fimage.png?alt=media&#x26;token=a8cda3a1-8330-4609-9325-9ba6f145b199" alt=""><figcaption></figcaption></figure>

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

```bash
[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

```bash
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.

&#x20;`/etc/nginx/sites-available/website-demo`

```bash
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:

```bash
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.&#x20;

```bash
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 :

```bash
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.

<figure><img src="https://3366121826-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fdo9UTLEVhvOrL0zBVbm6%2Fuploads%2FauIiIj34w0K9PflGgqDX%2Fimage.png?alt=media&#x26;token=31e94488-97d4-4615-867a-8f926a92dbfa" alt=""><figcaption></figcaption></figure>

You can find resources related to this demo on my personal [github](https://github.com/kmanu225/pki-web-app-demo).

## 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**](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/digital-certificates) **are** and why they're foundational to secure communications
* **How** [**PKI** ](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/pki-public-key-infrastructure)**works**, including the role of Certificate Authorities, trust chains, and provisioning
* **The importance of** [**HSMs** ](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/pki-public-key-infrastructure)in protecting private keys and ensuring cryptographic integrity
* [**How to build a fully functional private PKI**](https://kmanu225.gitbook.io/cs/cybersecurity/secrets-management/demo-pki-deployment), 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.
