Root Cause Analysis - Linkfy TLS Certificate Failure: A Kubernetes ACME Incident
Published on Sat Sep 12 2026Note: An LLM was used extensively to investigate and resolve this incident, and this root cause analysis was created with the help of the LLM. I used my own Kubernetes, networking, and infrastructure knowledge together with the LLM to narrow down the problem quickly, validate the findings, and resolve the incident.
When Linkfy’s SSL Certificates Started Failing: A Kubernetes ACME Incident
Recently, users of Linkfy, a SaaS platform I manage, started experiencing problems with SSL certificates for their custom domains.
Linkfy allows customers to use their own domains through a whitelabel setup. When a new domain is configured, the platform automatically provisions a TLS certificate using Let’s Encrypt through cert-manager.
Suddenly, certificate issuance started failing.
The interesting part was that, although the issue initially pointed directly at cert-manager or Let’s Encrypt. The actual problem was deeper inside the Kubernetes control plane.
The symptom
The initial cert-manager error looked like this:
Waiting for HTTP-01 challenge propagation:
wrong status code '503', expected '200'
The certificate request was using the normal Let’s Encrypt HTTP-01 challenge mechanism.
The expected flow was:
Let's Encrypt
|
| HTTP request
v
Ingress
|
v
ACME solver Service
|
v
ACME solver Pod
|
v
HTTP 200
Instead, Let’s Encrypt was receiving:
HTTP 503
At first glance, this looked like an ingress, cert-manager, DNS, or solver problem.
It wasn’t.
First investigation: the ACME solver pods
The first thing I checked was whether cert-manager was actually creating the temporary ACME solver pods.
Eventually, the solver pod existed and was healthy:
1/1 Running
The corresponding Service also existed.
But there was an important problem.
The Service had no endpoints.
That meant Kubernetes had a Service such as:
cm-acme-http-solver-49pbc
but no backend:
Endpoints: <none>
This explained the HTTP 503.
Nginx was receiving the request, forwarding it toward the Kubernetes Service, and Kubernetes had no backend endpoint to send the request to.
So the next question became: Why wasn’t Kubernetes creating an endpoint for a healthy pod?
Looking at the EndpointSlice
Kubernetes now primarily uses EndpointSlices to represent Service backends.The Service selector matched the ACME solver pod correctly. The pod was also Ready. So the expected EndpointSlice should have been created automatically. But it wasn’t. This shifted the investigation away from cert-manager and toward the Kubernetes control plane.
The scheduler was also failing
Around the same time, I discovered something more serious. The Kubernetes scheduler was producing Unauthorized errors. The scheduler’s job is to watch for unscheduled pods and assign them to nodes.
That explained another part of the incident. The ACME solver pods were initially stuck in Pending because the scheduler wasn’t functioning correctly.
I verified several things:
- The scheduler client certificate was valid.
- The certificate and private key matched.
- The certificate was issued for
system:kube-scheduler. - The scheduler ClusterRoleBinding was correct.
- The scheduler kubeconfig was valid.
- Kubernetes authorization checks succeeded when tested manually.
- There was no proxy configuration interfering with API access.
Yet the scheduler process itself was still receiving:
Unauthorized
This was an important clue. The configuration was correct. The running process was the problem.
The stale container behind the static pod
Kubernetes control-plane components in this cluster are managed as static pods. The scheduler manifest was located at:
/etc/kubernetes/manifests/kube-scheduler.yaml
At first, I was looking at the scheduler as a Kubernetes Pod. But in this case, the distinction between the Pod and the container running inside it turned out to be important.
I inspected the actual container through the container runtime rather than relying only on Kubernetes’ view of the Pod. That revealed something surprising: the scheduler’s actual container had been running since July 7.
The Pod existed. Kubernetes knew about the Pod. But the underlying scheduler container was effectively stale.
This explained why some of the normal Kubernetes-level actions did not resolve the problem. Removing the static Pod object did not address the underlying container that was still running with its broken API authentication state.
I needed kubelet to actually recreate the scheduler container.
A harmless change to the static Pod manifest triggered kubelet to reconcile the manifest and create a new container. Once the new container started, the Unauthorized errors disappeared and the ACME solver pods were immediately able to schedule.
This distinction was one of the most important findings in the incident:
The problem wasn’t simply a stale Kubernetes Pod. The actual control-plane container running behind the Pod was stale.
But the certificate still wasn’t working
At this point the solver pod was running. However, the Service still had no endpoint. That led to the second major discovery. The kube-controller-manager was also stale. Again, the container runtime showed that its actual container had been created on July 7.
The controller-manager was also producing:
error retrieving resource lock kube-system/kube-controller-manager:
Unauthorized
The controller-manager is responsible for many Kubernetes reconciliation loops, including the controllers involved in maintaining Service and EndpointSlice state. Therefore, we now had two independent control-plane components with broken API authentication:
Kubernetes API
|
+------------+------------+
| |
v v
kube-scheduler kube-controller-manager
| |
X X
Unauthorized Unauthorized
| |
v v
Solver pod stuck EndpointSlice missing
The result was exactly what we were seeing.
Recreating kube-controller-manager
I again forced kubelet to recreate the actual static container by making a harmless change to:
/etc/kubernetes/manifests/kube-controller-manager.yaml
The new container was created successfully, and as expected the Unauthorized errors stopped Almost immediately, Kubernetes created the missing EndpointSlice. The Service finally had a backend:
192.168.167.187:8089
with the endpoint marked ready.
The ACME HTTP-01 path was now complete:
Let's Encrypt
|
v
Ingress
|
v
ACME solver Service
|
v
192.168.167.187:8089
|
v
HTTP 200
The certificate recovered
After the control-plane components were healthy again, cert-manager was able to complete the ACME challenge. The Order eventually reached:
STATE: valid
The CertificateRequest became:
READY: True
And the TLS Certificate was also:
READY: True
There was no need to reinstall cert-manager or rebuild the Let’s Encrypt integration. The ACME system itself was working correctly.
Root cause
The primary root cause was stale Kubernetes control-plane static pod containers.
Both:
kube-scheduler
kube-controller-manager
had been running since July 7 and were experiencing API authentication failures. The scheduler failure caused newly created ACME solver pods to remain unscheduled. The controller-manager failure prevented the necessary Service endpoint reconciliation.
Together, this produced the external symptom:
Let's Encrypt
|
v
HTTP-01 challenge
|
v
Ingress
|
v
Kubernetes Service
|
X
No backend endpoint
|
v
HTTP 503
This made the incident initially look like a cert-manager or Let’s Encrypt problem, but the actual failure was in the Kubernetes control plane.
Why was this difficult to diagnose?
- The most misleading part was that Kubernetes appeared healthy at first.
- The API server was accessible.
- The node was Ready.
- The control-plane pods appeared to exist.
- The scheduler and controller-manager manifests were correct.
- The certificates were valid.
- RBAC was correct.
But the actual containers running underneath those static pods were stale.
That distinction between the Kubernetes object’s apparent state and the actual container runtime state was the key to solving the problem.
Checking crictl was what exposed it.
For example:
crictl ps --name kube-scheduler
and:
crictl ps --name kube-controller-manager
showed the real container creation and start times.
What fixed the incident?
The final remediation was surprisingly simple. I forced kubelet to recreate the stale static pods by making harmless changes to their manifests:
/etc/kubernetes/manifests/kube-scheduler.yaml
/etc/kubernetes/manifests/kube-controller-manager.yaml
Once the containers were genuinely recreated:
- Scheduler API authentication recovered.
- ACME solver pods were scheduled.
- Controller-manager API authentication recovered.
- EndpointSlices were created.
- ACME solver Services received endpoints.
- HTTP-01 validation succeeded.
- Let’s Encrypt issued the certificate.
No cert-manager reinstall was required.
Lessons learned
1. Don’t assume a Kubernetes pod object means the underlying container is healthy
For static pods in particular, check the container runtime when something doesn’t make sense.
crictl ps
crictl inspect <container-id>
The runtime can tell you when the actual container was created and started.
2. Follow the dependency chain
The original error was:
HTTP 503
It would have been easy to focus exclusively on ingress. Instead, following the chain backwards was more useful:
503
↓
Service
↓
No endpoints
↓
EndpointSlice controller
↓
controller-manager
↓
Unauthorized
↓
stale container
At the same time:
ACME solver Pending
↓
scheduler
↓
Unauthorized
↓
stale container
The two symptoms eventually converged on the same underlying problem: stale control-plane components.
3. Don’t reinstall the application that reports the error too quickly
cert-manager was reporting the failure, but cert-manager wasn’t broken. It was correctly reporting that the HTTP-01 challenge endpoint returned 503. The actual failure was lower in the stack.
4. Whitelabel systems need resource hygiene
Linkfy can create a large number of Kubernetes and cert-manager resources because every customer domain can potentially require its own certificate. Domains with incorrect or missing DNS records can leave behind failed certificate requests and related resources.
That means a production whitelabel system should have monitoring and cleanup policies around:
- CertificateRequests
- Orders
- Challenges
- failed certificates
- abandoned domains
- invalid DNS configurations
This incident reinforced the importance of keeping those resources under control.
Final outcome
The certificate issuance pipeline is now working again. The important conclusion is that Let’s Encrypt, cert-manager, ingress, and the ACME solver were not the fundamental problem.
The actual issue was a stale Kubernetes control plane. Once the scheduler and controller-manager were genuinely recreated, the entire certificate issuance pipeline recovered without reinstalling or replacing the certificate-management components.
This was a good reminder that Kubernetes incidents often require looking one layer below what the Kubernetes API appears to be telling you.
Root Cause Summary
Primary root cause: Stale kube-scheduler and kube-controller-manager static pod containers with broken API authentication state.
Impact: Linkfy users could not successfully provision TLS certificates for custom/whitelabel domains.
Immediate symptom: Let’s Encrypt HTTP-01 validation received HTTP 503 instead of 200.
Failure chain:
Stale scheduler
↓
ACME solver pods not scheduled
Stale controller-manager
↓
EndpointSlices not reconciled
↓
ACME Service had no endpoints
↓
Ingress returned HTTP 503
↓
Let's Encrypt HTTP-01 validation failed
↓
Certificate issuance failed
Resolution: Force recreation of the stale scheduler and controller-manager static pod containers.
Cert-manager reinstall required: No.
Let’s Encrypt problem: No.
Ingress problem: No.
Main lesson: When Kubernetes behaves inconsistently, inspect the actual control-plane containers through the container runtime instead of relying solely on the Kubernetes API view.