Certificate automation with Kubernetes, Jenkins, and Ansible
I built a certificate lifecycle that renews one wildcard, deploys it across Kubernetes, Linux servers, and appliances, and verifies what each service actually presents over TLS. Jenkins coordinates the pipeline; cert-manager handles issuance; Ansible handles registered Linux deployments; Prometheus and Grafana track the result.
Shorter certificate lifetimes reduce the window for stolen keys and stale ownership information, but increase renewal work. Let’s Encrypt is moving its default certificates to 45 days in 2028. I wanted the repeatable work in automation.
A renewed certificate still had to reach every service
The wildcard renewed in Kubernetes, but its copies also lived on Linux web servers and appliances. A healthy cert-manager resource did not tell me whether Ansible had updated a host, whether Nginx had reloaded, or whether an appliance was still serving the old certificate.
I also found two valid wildcard certificates behind Traefik. It could select the older one even when an application namespace held a newer copy. I changed the design to one cert-manager owner, one source TLS Secret, and verified distribution to every registered consumer.
How Kubernetes, Jenkins, and Ansible do the work
- Kubernetes and cert-manager renew the source. cert-manager uses the ACME protocol with Let’s Encrypt. A scoped Cloudflare API credential creates the DNS-01 TXT challenge. The signed certificate and private key land in a Kubernetes TLS Secret as
tls.crtandtls.key. - Jenkins validates and distributes the certificate. The certificate-sync pipeline reads that Secret, checks the certificate/key pair and remaining validity with OpenSSL, and updates the existing TLS Secrets in registered Kubernetes namespaces. Traefik watches those Secrets and loads the replacement.
- Jenkins runs Ansible for the Linux consumers. The
wildcard-cert-portals.ymlplaybook deploys to Gitea, NetBox, Greenbone, and Wazuh, one host at a time. Ansible replaces complete certificate files, applies ownership and permissions, and runs the service’s reload or restart handler. Restricted SSH helpers and product APIs handle the appliance paths. - OpenSSL and Prometheus verify the running service. Fresh TLS handshakes check the served certificate against the source. Behind Cloudflare, the pipeline checks the origin using the application hostname as SNI; public probes check the edge separately. Blackbox exporter, Prometheus alerts, and Grafana expose expiry, deployment drift, and missing coverage.
The completion check: a successful Jenkins stage or Ansible file copy is not enough. The service has to load the replacement, and a new TLS connection has to prove it.
Did Kubernetes, Ansible, and the appliance adapters deploy the same certificate?
This Grafana table puts the wildcard source beside the certificates served by its consumers. If a destination still presents the previous certificate, I check the Jenkins distribution stage, Ansible handler, or appliance reload. Source issuance and consumer activation are separate checks.
These panels expose expiry and lifecycle metadata with public labels. Certificate subjects, internal hostnames, secret values, and credential IDs are not shown.
Wildcard certificate deployments
Open this live panel ↗Checking live state.
Checking the live dashboard before loading.
The live panel is unavailable. Open the panel directly or try again when the dashboard service recovers.
Mail, Vault, and Active Directory have their own renewal paths
The shared wildcard is only one part of the inventory. Mail renews through its own Let’s Encrypt ACME client. Vault uses an internal CA with attended renewal. The directory services use the Windows certificate path. I keep those paths separate in Grafana so each certificate has a clear issuer, renewal process, and served expiry.
Other managed certificates
Open this live panel ↗Checking live state.
Checking the live dashboard before loading.
The live panel is unavailable. Open the panel directly or try again when the dashboard service recovers.
Prometheus shows whether the renewal reached the running services
Blackbox exporter opens fresh TLS connections while cert-manager exports source expiry. Prometheus stores those measurements and evaluates alerts; Grafana shows the trend. A renewal produces an upward step. A served copy that keeps declining after the source renews needs investigation. Cloudflare edge and origin certificates are checked separately.
Certificate days remaining
Open this live panel ↗Checking live state.
Checking the live dashboard before loading.
The live panel is unavailable. Open the panel directly or try again when the dashboard service recovers.
Secrets had the same problem: replacement was only the first step
Once certificate distribution worked, I applied the same pattern to Microsoft Entra client secrets, API tokens, and service credentials. Creating a new credential at the provider did not update the Kubernetes workload or Linux application that used it. I needed a controlled handoff from the provider to Vault to the consumer.
Jenkins coordinates rotation; Vault holds the lifecycle state
A Jenkins worker reads a reviewed registry entry that names the provider, Vault field, application, activation method, and validation checks. A scoped Vault AppRole gives that worker the access it needs. The provider creates a replacement, and a compare-and-set write updates the registered Vault field without overwriting another process’s changes.
Entra SSO applications
Open this live panel ↗Checking live state.
Checking the live dashboard before loading.
The live panel is unavailable. Open the panel directly or try again when the dashboard service recovers.
Kubernetes and application adapters activate the replacement
For a Kubernetes consumer, the adapter updates one key in an existing Secret and rolls the owning Deployment or StatefulSet. A Linux adapter sends the value through standard input to a restricted SSH helper. Other applications read Vault at startup or use a provider API. Each path ends with workload health checks and a real login or API request.
Overlap gives me a recovery path
Proven paths rotate on a 60-day cycle with a seven-day overlap. The previous credential remains available while the replacement is in use. The worker repeats its checks before retiring the exact predecessor. A failed check stops retirement and leaves the rotation pending.
Prometheus and Grafana expose active, overlap, pending, and overdue states from Entra and Vault metadata. Canary, attended, and product-managed paths remain explicitly labeled. A worker is automatic only after the complete activation, verification, and retirement sequence has passed.
Grafana keeps active credentials, overlap, and exceptions visible
The service-token panel shows the registered rotation process, consumer state, and next lifecycle action. An overlap row means the replacement is active and the predecessor remains available. Pending means a required stage still needs to pass. I use the row to find the responsible Jenkins worker and its validation logs.
Service token lifecycle
Open this live panel ↗Checking live state.
Checking the live dashboard before loading.
The live panel is unavailable. Open the panel directly or try again when the dashboard service recovers.
Implementation details and failure cases
Outcome, operating scope, and evidence
At a glance
- Outcome
- The shared public certificate renews ahead of expiry and reaches every registered consumer. Proven credential paths rotate on a 60-day cycle with a seven-day recovery overlap.
- Operating scope
- The certificate source fans out to Kubernetes, servers, and appliances. Credential workers update Vault and the applications registered for unattended activation; explicit exceptions remain on the same board.
- Evidence
- The live dashboard measures source and served expiry, rotation state, and coverage. Fresh TLS handshakes and real authentication requests test what consumers use.
- Primary lesson
- Issuance or provider success is supporting evidence. The job verifies the live consumer before it retires the working predecessor.
Certificate architecture, Ansible delivery, and service reloads
One wildcard certificate now feeds every copy
The wildcard covers the first level of application names. Let’s Encrypt signs it, cert-manager renews it in Kubernetes, Cloudflare carries the DNS validation record, and Jenkins sends the resulting certificate and key to the services that use them.
This division matters because cert-manager finishes its work at the source Secret. It has no knowledge of a certificate file on a Linux VM, an appliance certificate object, or a copy in another Kubernetes namespace. The Jenkins job knows those destinations and how each service reloads.
The six-step certificate renewal run
These are the six things one run has to accomplish. Steps one through three create a replacement. Steps four and five put it into use. Step six proves the live result.
- cert-manager opens a renewal order with Let’s Encrypt. It uses ACME, the automated certificate request protocol, and starts early enough to leave recovery time. No application changes yet.
- Cloudflare publishes the DNS validation record. cert-manager creates a temporary TXT record with a scoped API credential. That record shows Let’s Encrypt that the requester can change DNS for the requested name, so it signs the certificate.
- cert-manager writes the pair into a Kubernetes Secret. The Secret holds sensitive workload data: the certificate in
tls.crtand private key intls.key. Jenkins verifies the pair and remaining lifetime before deployment. - Jenkins sends the pair to the registered destinations. Kubernetes isolates resources in namespaces, so the monthly job updates an existing TLS Secret in each registered namespace and stages the same pair for services outside the cluster.
- Each service loads the pair. Traefik, the Kubernetes ingress controller, watches Secret changes. Linux web servers swap complete files and reload or restart. Appliances use a restricted API or helper.
- Jenkins checks what a new client receives. A fresh TLS handshake returns the served certificate. Jenkins compares its fingerprint, a unique hash, with the source and fails if the service still presents the previous certificate.
Why one source matters
Two independent wildcard Certificates once existed behind the same Traefik controller. Both were valid, but Traefik’s global TLS selection could serve the older lineage for a hostname whose namespace contained the newer copy. The fix was architectural: keep one cert-manager owner, copy its result, and verify every destination against that source.
How each consumer reloads
| Consumer class | Delivery | Activation | Proof |
|---|---|---|---|
| Kubernetes ingress | Patch the existing TLS Secret in the application namespace | Traefik watches the Secret | Compare the certificate served for each application name |
| Linux web server | Atomic Ansible or SSH file replacement | Reload or restart the owning service | Compare the live TLS fingerprint with the source |
| Proxmox family | Forced-command helper accepts bounded input | Product certificate command restarts the proxy | Read the custom certificate back and test the live UI port |
| OPNsense | Update one managed Trust certificate through its API | Restart only the web interface service | Require the named certificate object and the live fingerprint to match |
Atomic replacement swaps complete files, so a service cannot read a half-written pair. A forced-command SSH key can run the certificate helper but cannot open a general shell.
OPNsense stores certificates as Trust objects. Its API updates one object and restarts only the web interface.
How newly discovered consoles enter the managed inventory
Static inventory is reliable only when someone remembers to update it. I added a discovery workload that reads two places where a new console must appear: the public DNS inventory and Kubernetes Ingress objects. Every five minutes it removes names already covered by the reviewed probes, opens a normal trusted TLS connection to each remaining name, and records only endpoints whose hostname and certificate chain validate.
A passing handshake creates monitoring, not renewal authority. The private dashboard marks the endpoint New, records when it first appeared and when its served certificate expires, and labels the rotation process Needs onboarding. The public dashboard publishes only the number of waiting consoles, so the coverage gap is visible without publishing a hostname.
Enrollment is a reviewed repository change. It records the certificate issuer and source, the destination that receives the replacement, the reload or restart action, the outside TLS probe, and the rollback path. On the next discovery pass, the endpoint leaves the New queue and appears in the managed table whose renewal process now owns it.
Internal PKI: AD CS, Windows auto-enrollment, and Linux CSRs
Public and internal certificates solve different trust problems
Public browsers already trust Let’s Encrypt. Private services use Active Directory Certificate Services, Microsoft’s domain certificate authority, whose root is distributed to managed computers.
Windows auto-enrollment uses Group Policy and an AD CS template, which defines purpose, names, lifetime, and enrollment permission. Windows requests and renews without moving the private key between machines.
Linux creates its private key locally and submits a CSR, a request containing the public key and names. AD CS returns the signed certificate and chain; Linux installs them and reloads the service.
Blackbox exporter validates the name, trust chain, and live expiry on both paths, catching an expired leaf, untrusted chain, or missed reload. Some internal leaves are still watched rather than automatically renewed.
Secret rotation architecture, Vault writes, and activation adapters
Secrets needed the same handoff and a different last mile
A client secret is a password an application uses when it authenticates as itself. API tokens and service keys serve a similar purpose for other products. Creating a replacement at the provider does not make an application use it. The replacement still has to reach the application’s configuration, the process has to reload, and a real authentication request has to succeed.
I call the system that creates and revokes a credential its provider. Microsoft Graph manages the Entra client secrets. Grafana, Zabbix, Jenkins, NetBox, Cloudflare, OPNsense, and LiteLLM expose product APIs for their own credentials. Where a product has no secret-safe update API, a small helper on the host accepts the value through standard input and can change one allowlisted setting.
The seven-step secret rotation run
Certificates share one deployment source. Credentials do not because each provider creates them differently and each application stores them differently. A reviewed registry describes those differences so the worker does not infer where a value belongs.
- Jenkins reads one registry entry. It names the provider, Vault field, consuming application, activation method, and required checks. An unmatched credential stays in inventory but cannot enter an automatic write path.
- The worker decides whether rotation is due. The policy starts at age 60 days and creates a 90-day replacement, leaving time for the seven-day overlap and a failed attempt.
- The provider creates one replacement. The worker retrieves its scoped provider authority through a Vault AppRole. The returned value is not printed or sent to metrics; its safe provider identifier is recorded for retirement.
- The worker updates Vault with compare-and-set. It changes the registered field only if the version it read is still current, preserves neighboring fields, and stops when another process edited the record first.
- The registered adapter updates the application. It changes one Kubernetes Secret, supplies the next script invocation, sends stdin to a forced host command, or updates one provider resource.
- The worker tests the new path. It checks provider authentication, the Vault-to-consumer binding, workload health, and the real login or API route.
- The previous credential remains for seven days. The worker repeats every check after overlap and deletes the recorded predecessor. A failed check leaves both credentials and marks the rotation pending.
The activation adapters are the practical part
| Consumer pattern | What the worker changes | How it proves success |
|---|---|---|
| Kubernetes application | One key in one existing Secret, then one Deployment or StatefulSet rollout | Provider authentication, Secret-to-Vault match, workload health, and the application’s sign-in route |
| Read-at-start script | Vault only; the next invocation retrieves the replacement | A read-only provider request using the exact application permission |
| Linux-hosted application | Stdin-only forced command updates one allowlisted setting | Pinned host identity, stored-value fingerprint, process health, and a live login or API check |
| Provider-hosted configuration | One exact identity-provider or integration resource through its API | Read-back of stable metadata plus a new authorization or service request |
| Product with no secret-safe interface | No unattended mutation | Attended activation remains visible until the product offers a safe input path |
Prometheus, Blackbox exporter, and coverage signals
I verify the result outside the process that made the change
Jenkins could prove that it wrote a file or Secret, but not what a browser received. The observation path measures the service independently.
Each exporter converts one source into metrics. Blackbox exporter opens the TLS connection; the Entra and Vault exporters collect metadata without receiving existing secret values.
Prometheus stores the metrics and evaluates alerts. Grafana groups the resulting series by application and renewal path.
| Question | Signal | Why it exists |
|---|---|---|
| What certificate is the service presenting? | Blackbox exporter TLS handshake expiry | Catches stale files, missed reloads, the wrong SNI certificate, and broken trust |
| Did cert-manager issue a usable source? | Certificate readiness and expiration metrics | Separates an issuance failure from a fan-out failure |
| Did a new HTTPS console appear? | DNS and Kubernetes discovery followed by a trusted TLS handshake | Starts monitoring before an undeclared endpoint can silently age outside the managed inventory |
| Which Entra credentials exist and when do they expire? | Read-only Microsoft Graph metadata exporter | Finds new, unmapped, overlapping, approaching, or overdue credentials without reading secret values |
| Is every Vault record classified and on the promised path? | Metadata-only Vault lifecycle exporter | Turns new or unclassified records into visible gaps rather than silent inventory growth |
| Did a rotation finish or stop in overlap? | Value-free rotation-state records | Shows active, overlap, pending, failed, and retirement timing without leaking the credential |
Jenkins schedules and renewal cadence
How it runs now
Most of the time, no rotation is happening. The controllers and exporters watch. Jenkins wakes only when a schedule or an operator starts a run.
| When | What runs | What a quiet result means |
|---|---|---|
| Continuously | cert-manager watches the Certificate; exporters refresh live TLS and metadata; Prometheus evaluates alerts. | Sources and consumers remain observable between changes. |
| Every five minutes | Console discovery compares DNS and Kubernetes Ingress with the declared probe inventory, then validates TLS for unknown names. | A trusted new console appears as Needs onboarding without granting the workload permission to change it. |
| Monthly | Jenkins validates and deploys the wildcard. | Destinations either remain matched or receive the newer source. |
| Daily | Credential workers evaluate due, pending, and retirement-ready entries. | An item that is not due causes no replacement. |
| During overlap | The replacement is active and the predecessor remains available. | A failure leaves both credentials in place. |
| After a change | The worker checks authentication, binding, workload health, and the real route. | The dashboard records completion, overlap, or the pending stage. |
The current monthly distribution schedule must be reassessed as certificate lifetimes shrink. Automated issuance alone does not establish that the distribution cadence is sufficient.
What broke and how I fixed it
What broke
The observer failed and made healthy services look down
Summary
A certificate-monitoring merge left two modules under the same YAML key. The blackbox exporter refused to start. Prometheus then lost the shared probe target, and the NOC made many healthy services look down.
What made it confusing
The failure was in the observation path, not in every application. The dashboard initially could not distinguish “the probes are unavailable” from “every service failed.” That distinction now matters in both the tests and the dashboard coverage signals.
Root cause
Each parent branch contained one valid module. The merge kept both definitions. The old YAML test used a parser that silently accepted the last duplicate key, so CI approved a configuration the exporter rejected.
Recovery
The two definitions were reconciled, dependent probes were checked, the generated ConfigMap revision changed, and the exporter rolled out. A direct probe and the Prometheus target then returned healthy.
Prevention
CI now performs strict duplicate-key validation. The dashboard also treats missing coverage as its own failure class, so a dead observer cannot masquerade as a fleet-wide outage.
The private inventory was right while the public table was stale
Summary
The private certificate inventory had the complete wildcard fan-out, but the public table showed only part of it. The missing rows made a correct deployment look incomplete.
What made it confusing
The Grafana dashboard synchronized successfully, so the visible layout looked current. Its public queries depended on separate Prometheus recording rules that had not been reconciled. Updating one half of the view did not update the data projection underneath it.
Root cause
The dashboard and its privacy-filtered recording rules had different deployment owners. The public rule set required an operator apply, while the dashboard followed the normal synchronization job.
Recovery
I reconciled the wildcard inventory, regenerated the public rules, and verified one source plus every registered consumer in the rendered table. I then placed the public projection under its own Argo CD application.
Prevention
Argo CD now continuously reconciles the public recording rules. Tests derive certificate coverage from the same declared probe inventory, so a new probe cannot pass without a lifecycle classification.
A correct edge certificate looked like a failed wildcard deployment
Summary
On 2026-09-03, the first synchronization run for a newly registered console updated its Kubernetes Secret but failed the final fingerprint comparison.
What made it confusing
The public hostname intentionally terminated at a content-delivery edge. A client received the edge provider's valid public certificate, while the Kubernetes ingress correctly held the wildcard certificate. Comparing those fingerprints made a healthy layered certificate path look broken.
Root cause
The verifier assumed public DNS always reached the certificate consumer directly. That assumption was true for the earlier routes and false for the new edge-protected route.
Recovery
I verified the source Secret, destination Secret, and Traefik origin independently. All three matched. I then changed the deployment check to reach the Kubernetes origin while keeping the public hostname as SNI, the name supplied during the TLS handshake.
Prevention
The deployment pipeline now distinguishes origin proof from public-edge proof. Jenkins checks that the application origin loaded the wildcard. Blackbox monitoring separately checks that the public edge presents a trusted certificate for the hostname and reports its own expiry.