For years, my internal PKI ran on next to nothing: a manually managed OpenSSL certificate authority, a root key in a folder, and a habit—whenever a service needed HTTPS, I’d run the command openssl x509 -req, sign a two-year certificate, deploy it, and move on.
It worked. For a long time. The problem with a setup that works is that you never question it—until the day the number of services makes it unsustainable. This first part recounts the move away from that DIY approach: the switch to step-ca, an online certificate authority that issues and renews short-term certificates on its own. And most importantly, how to make this switch without breaking the trust already established throughout my personal infrastructure.
> We’ll focus here solely on the Linux world and appliances. The Microsoft side—Active Directory, autoenrollment, RDP—will be the subject of Part 2, because it involves a different approach and a different certificate authority.
The Infrastructure Context
To make the rest of this post easier to follow, here’s the setup—anonymized but faithful to the actual structure. Internal domain example.internal, with IP addressing in the 192.0.2.0/24 range (as specified in RFC 5737).
ca-01: the host that hosts the certificate authority. Running on Linux. This is where the old OpenSSL CA used to reside, and this is where step-ca will reside.web-01,web-02: web services behind nginx and Apache (a secret manager, an asset inventory, etc.).dns-01: a DNS filtering service with a web interface that needs to be protected.pve-01,pbs-01: virtualization and backup appliances. They natively support ACME—we’ll come back to this; it’s the ideal scenario.
The root is called myCA. Its certificate was already deployed everywhere: in the trusted store on Windows workstations via GPO, in Linux trust stores, and in web browsers. This detail will become the cornerstone of the entire migration.
A Word on What a PKI Really Is
Before getting into the technical details, let’s define the terminology—because a PKI seems obscure until you understand the three components that make it up.
A public key infrastructure (PKI) is a chain of trust. It has three levels:
- The root (root CA): the supreme authority. Its certificate is self-signed, and it is this certificate that we install in the trusted stores. A machine that trusts the root, by extension, trusts everything it has signed. The root is precious: if its key is compromised, the entire chain collapses. It is therefore kept offline as much as possible.
- The intermediate (issuing CA): an authority signed by the root, which handles the day-to-day task of issuing certificates. It is exposed to the public and used frequently—and if it is compromised, it is revoked without affecting the root.
- The leaf: the final certificate, the one that
web-01presents when your browser connects to it. It is signed by the intermediate CA.
Trust is verified step by step: the browser receives the leaf certificate, traces back to the intermediate CA that signed it, and then to the root CA—which it knows. If a link is missing, the chain breaks and the padlock turns red.
My old OpenSSL CA had only two of these three components: a root certificate, and leaf certificates signed directly by it. No intermediaries. No automation. The root was used for everything, all the time, manually.
{{< figure src="/images/PKI/pki-hierarchy.svg" alt="PKI hierarchy before and after: Manual OpenSSL CA vs. step-ca as an intermediary" caption="Before: The root signs everything, manually, for two years. After: step-ca is inserted as an intermediary under the same root and issues short leaf certificates automatically." >}}
Why the artisanal approach no longer scales
Signing a certificate manually once is no big deal. The problem is the accumulation.
Every service wanted its own certificate. Each time: generate a key, draft a request, sign it, place the file in the right location with the correct permissions, and restart the service. And since signing was such a hassle, I’d issue two-year certificates—just so I wouldn’t have to go through it again. It’s an understandable reflex, and it’s exactly the wrong one.
A two-year certificate means:
- a private key that remains the same for two years. If it’s leaked, the window of opportunity for exploitation is huge.
- no rotation policy. We never practice renewal, so when the day comes that we need to do it urgently, we’ve forgotten how.
- no visibility. How many certificates are in circulation? Which ones expire when? My only answer was a text file—the kind of file that’s never up to date.
The industry is actually moving in the opposite direction of my two-year approach: the lifespan of public certificates is shrinking year after year, precisely to force automation. The lesson applies internally as well—a short-lived certificate is only a problem if you renew it manually. When automated, it’s a non-issue.
Hence the goal: short certificates that renew themselves. This requires a certificate authority that issues on demand, without human intervention. That’s exactly what step-ca does.
The architectural decision: keep the root, slip step-ca underneath it
The first instinct when discovering step-ca would be to start from scratch: new root, new CA, a clean slate. A very bad idea in my case.
Because my myCA root was already deployed everywhere. Years of GPOs, trust stores, and configurations that reference this root certificate. Starting over with a new root means having to redeploy trust across the entire infrastructure before anything works. A massive undertaking, with zero benefit.
The right approach takes advantage of a key feature of step-ca: it can act as an intermediary for an existing root certificate.
The process:
- We keep the
myCAroot certificate as-is—it remains the anchor of trust, already known to everyone. - We generate an intermediate key pair for step-ca and have it signed by the root
myCA. - step-ca now issues leaf certificates using this intermediate certificate.
Result: The leaf certificates issued by step-ca trace back, via the intermediate certificate, to myCA—which everyone already knows. No machines need to be reconfigured on the trust side. The transition is transparent to clients.
> Key takeaway: We don’t replace the root certificate; we extend it. The root certificate becomes a simple offline anchor; step-ca handles the day-to-day work underneath. It’s the difference between moving the entire house and adding a room.
In practice, step-ca is initialized by providing it with the existing root and its key, so that it can generate the intermediate certificate signed by the root. Once the intermediate certificate is in place, the root key no longer needs to be online: step-ca signs with the intermediate certificate, not with the root certificate. We can therefore remove the root certificate from the machine and store it offline—a security consideration I’ll revisit at the end of this series.
step-ca in action: the self-service certificate authority
Once initialized, step-ca runs as a persistent service. It listens on a port (:9000 by default) and offers two ways to request a certificate:
- an API queried by the
stepcommand-line client; - an ACME endpoint—the same protocol used by well-known public certificate authorities—which an increasing number of tools and appliances support.
To authorize requests, step-ca uses the concept of a provisioner: an authentication method that determines who is authorized to have a certificate signed. A provisioner can be a password-protected key, an ACME account, a token… In my case, two are sufficient: an ACME provisioner for appliances that support ACME, and a password-based provisioner for script-driven issuance.
Issuing a certificate from a Linux host then takes just one command:
step ca certificate "web-01.example.internal" \
/etc/ssl/web-01/web-01.crt \
/etc/ssl/web-01/web-01.key \
--provisioner admin@example.internal
step-ca verifies the provisioner, signs a certificate valid for 90 days, and places the certificate and its key in the specified paths. We restart the service, and that’s it.
But 90 days means renewing four times a year. Doing this manually would be worse than the two-year cycle we had before. This is where it all comes down to: renewal must be automatic.
The Heart of the Matter: Automatic Renewal
The goal is simple: each certificate must renew itself before it expires and notify us when it has done so. On Linux, systemd provides everything needed with two components: a service that does the work, and a timer that triggers it periodically.
The renewal service calls step ca renew. The subtlety—and this is a lesson I learned the hard way—lies in the condition for renewal:
# /etc/systemd/system/cert-renewer@.service
[Unit]
Description=Certificate renewal for %i
[Service]
Type=oneshot
ExecStart=step ca renew --expires-in 720h \
--exec "/usr/local/bin/cert-renewed.sh" \
/etc/ssl/%i/%i.crt /etc/ssl/%i/%i.key
The key parameter is --expires-in 720h. It specifies: “ renew only if there are less than 720 hours (30 days) of validity remaining”. Without it—with a --force, for example—the certificate would be reissued every time the timer runs. However…
> The pitfall I stumbled upon: my first draft renewed unconditionally, with every hourly run. The result: a new certificate every hour, and most importantly, a notification email every hour. My inbox was flooded overnight. The rule is simple: renew only when the expiration date is approaching. --expires-in isn’t just an option—it’s the condition that makes automation manageable.
The timer, for its part, simply wakes up the service at regular intervals:
# /etc/systemd/system/cert-renewer@.timer
[Unit]
Description=Checks for certificate renewal for %i
[Timer]
OnCalendar=*:0 # at the start of every hour
Persistent=true
[Install]
WantedBy=timers.target
Every hour, the service wakes up, checks if there are fewer than 30 days remaining, and does nothing 99% of the time. On the day the threshold is crossed, it renews the certificate, runs the hook, and goes back to sleep.
The hook cert-renewed.sh is the third component. It does the work after the new certificate is obtained: it restarts the relevant service so it uses the new certificate, and sends the notification.
#!/bin/bash
# /usr/local/bin/cert-renewed.sh — executed after a successful renewal
set -e
systemctl reload nginx
printf "Certificate renewed on %s\nExpires on: %s\n" \
"$(hostname -f)" \
"$(step certificate inspect /etc/ssl/web-01/web-01.crt --format json \
| jq -r '.validity.end')" \
| mail -s "[PKI] Renewal $(hostname -s)" admin@example.internal
The reloading process (nginx, apache2, or restarting a dashboard) depends on the machine. This is the only part that varies from one host to another—and it’s precisely what will need to be configured when we scale this up.
{{< figure src="/images/PKI/pki-cycle-vie.svg" alt="Automated certificate lifecycle: issuance, deployment, periodic verification, renewal, notification" caption="Once the process is set up, the cycle runs on its own. The timer checks every hour; it only renews as the expiration date approaches, restarts the service, and sends a notification. We only intervene if an expected email doesn’t arrive.” >}}
The Ideal Scenario: ACME-Compatible Appliances
Everything mentioned above applies to Linux hosts that are managed via scripts. However, some appliances—such as hypervisors and backup servers—include a native ACME client. For these, there’s no need to write a script: they know how to request and renew a certificate on their own, just as they would with a public certificate authority.
All you need to do is point them to step-ca’s ACME URL and trust them at the root. On a virtualization appliance, registering an ACME account and requesting a certificate takes just two or three lines using its own tools, and then renewal is fully handled by the appliance via its daily update task. No timers to write, no hooks to maintain.
This is the most convenient scenario, and a strong argument for choosing ACME-compatible devices whenever possible. The only thing to watch out for: the ACME challenge validation (the mechanism by which the appliance proves to step-ca that it is indeed who it claims to be) must be able to succeed over the network—something to verify when the appliance and the CA are not on the same network segment.
Standardization: the same pattern, everywhere
At this point, I had a pattern that worked, host by host: deploy the root to the trust store, install the step client, issue the leaf certificate, set up the hook, and start the timer. Doing this manually on every machine would have simply shifted the burden elsewhere. The real way to move beyond manual labor is to automate the deployment of the pattern itself.
This is the role of an automation controller (in my case, an idempotent playbook) that applies the same sequence to an entire group of machines:
- deploy the root certificate to the host’s trust store;
- install the
stepclient and the email sending tool; - issue the certificate via step-ca, retrieving the provisioner’s password from an encrypted secret vault (never in plain text within the playbook);
- deploy the hook and trigger the renewal timer.
What varies from one machine to another—the certificate name, the command to restart the service—is described in host-specific variables. The playbook itself remains the same. We add a service to the fleet by writing three lines of variables, not by rewriting the procedure.
{{< figure src="/images/PKI/pki-industrialisation.svg" alt="Industrialized deployment pattern: a controller applies the PKI pattern to the entire fleet via a playbook and a secret vault" caption="The controller applies the same pattern everywhere: issuance via step-ca, certificate deposit, and renewal configuration. The ACME appliances handle the rest on their own. The differences between machines are contained within a few variables." >}}
The Pitfall of Mass Deployment
Industrialization is powerful. It’s also dangerous, and I have to be honest about this because I’ve fallen into this trap myself.
A playbook that applies a pattern to a group of machines applies it to all machines in the group—including those that shouldn’t have received it. The day the pattern installs a component that conflicts with a particular role of a machine (typically: an email-sending tool deployed on a host that already is the mail server), we don’t just break one service— you break the service that all the others depend on.
> The lesson: A playbook’s idempotence guarantees that it produces the same state with every execution. It does not guarantee that this state is the correct one for all machines in the group. Hosts with specific roles—mail server, controller, anything that is unique—must be explicitly excluded from tasks that do not concern them, before the first run. A forgotten exclusion isn’t obvious when reviewing the playbook: it becomes apparent when the service goes down.
Since then, any pattern applied en masse begins with the question: “Which machines in this group have a role that makes one of these tasks harmful?” — and those machines are excluded by condition, by name, before anything else.
What I’ve learned
- Reuse the root certificate rather than replace it. Using step-ca as an intermediary for an already deployed root certificate ensures a seamless transition for clients. Starting from a brand-new root certificate means you’ll have to redeploy trust everywhere for no reason.
- A short-lived certificate is only a problem if you renew it manually. When automated, 90 days is safer and simpler than two years. It’s automation that makes a short duration manageable—not the other way around.
--expires-inis the cornerstone of renewal. Unconditional renewal means reissuing the certificate with every tick of the timer and getting buried under notifications. We only renew as the expiration date approaches.- Prioritize devices that support ACME. When an appliance natively supports ACME, there’s nothing to script or maintain—it renews itself. This is a selection criterion in its own right.
- Standardization amplifies everything—both good actions and mistakes. Applying the same pattern across an entire fleet saves hours. Applying it to a machine that shouldn’t have been touched costs an entire evening. Single-role hosts are explicitly excluded before the first run.
In Part 2, we’ll leave the Linux world behind and head over to the Microsoft side: why I maintain a separate second authority there, integrated with Active Directory, and how it supports server authentication — RDP foremost among them — via native autoenrollment. Two authorities, two worlds, a deliberate coexistence.
Sources and References
Official step-ca (Smallstep) documentation:
- step-ca — overview and capabilities — including the ability to operate as an online intermediary under an existing root, with the root remaining offline.
- Getting Started — CA initialization,
step ca init, bootstrapping trust via the root fingerprint. - Import an existing root or intermediate CA into step-ca — the exact procedure for having the step-ca intermediate CA signed by an existing root (the focus of this article).
- Provisioners — methods for authorizing requests (ACME, JWK, etc.).
- Certificate renewal options — automatic renewal via systemd timers (recommended approach) and the
cert-renewtemplates.service/.timer. step ca renewreference — including the--expires-inflag: renewal is not performed as long as there is more than the specified time remaining before expiration (with a random jitter ofduration/20).- Production considerations — lifetimes, revocation, and best practices for operation.
- Run your own private ACME server using step-ca — how ACME works on a private server and challenge validation.
- smallstep/certificates (GitHub repository) — the project’s source code.
Standards and RFCs:
- RFC 8555 — Automatic Certificate Management Environment (ACME) — the certificate automation protocol.
- RFC 5280 — Internet X.509 Public Key Infrastructure Certificate and CRL Profile — the profile for X.509 certificates and trust chains.
- RFC 5737 — IPv4 Address Blocks Reserved for Documentation — the ranges
192.0.2.0/24,198.51.100.0/24, and203.0.113.0/24used here for anonymization.
Sources verified online as of the date of writing. The links point to the official documentation from the publishers and the IETF.