Troubleshooting¶
Common failure modes and where to look. Never paste credential values, private keys or protected CMP DER into tickets or Events.
Reading the controller log¶
The controller log is the first place to look for anything that resource conditions do not explain.
kubectl logs -n cmp-issuer-system deploy/cmp-issuer-controller-manager -c manager -f
Useful variations:
# Follow by label rather than by Deployment name
kubectl logs -n cmp-issuer-system -l app.kubernetes.io/name=cmp-issuer -c manager -f
# What the previous container said before it restarted
kubectl logs -n cmp-issuer-system deploy/cmp-issuer-controller-manager -c manager --previous
# Only the last few minutes
kubectl logs -n cmp-issuer-system deploy/cmp-issuer-controller-manager -c manager --since=5m
# Everything about one request
kubectl logs -n cmp-issuer-system deploy/cmp-issuer-controller-manager -c manager | grep <certificaterequest-name>
The container is named manager. Adjust the namespace and Deployment name if you installed under a
different release name or namespace.
Which build is running¶
The first line the manager writes names the build and the install it came from, which is the first thing to establish before reporting a problem:
{"level":"info","logger":"setup","msg":"Starting manager",
"version":"v0.1.0","gitCommit":"1081fb2ff26a","buildDate":"2026-08-19T01:15:49Z",
"goVersion":"go1.26.5","platform":"linux/amd64",
"image":"ghcr.io/misiektoja/cmp-issuer:v0.1.0",
"chart":"cmp-issuer-0.1.0","release":"cmp-issuer"}
| Field | Meaning |
|---|---|
version |
The release the binary was built from. A build that is not a release carries the commit description instead, and one built with no version information at all reports development |
gitCommit |
The commit the binary was built from, shortened to 12 characters, with -dirty when the tree was modified |
buildDate |
The commit date, so rebuilding the same commit produces the same value |
goVersion, platform |
The Go toolchain and the operating system and architecture the binary runs on |
image |
The image reference the manager was deployed from. Set by the Helm chart, so it names your mirror when you mirror the image. A manifest install reports the reference the image was published under instead |
chart |
The Helm chart and chart version that installed the manager. Absent on an install from the manifest, which uses no chart |
release |
The Helm release name. Absent on an install from the manifest |
The same information is available without reading the log, which is useful when the manager will not start:
kubectl exec -n cmp-issuer-system deploy/cmp-issuer-controller-manager -- /manager --version
An image that reports development was not built by a release. If you did not build it yourself,
check that the Deployment pulls the tag you intended.
What the log contains¶
Every enrollment produces one line for its outcome at the default verbosity. A completed enrollment identifies the request, the issuer, the endpoint, the CMP transaction and the certificate itself:
{"level":"info","logger":"Reconcile","msg":"Issued certificate",
"CertificateRequest":{"name":"demo-tls-1","namespace":"demo"},
"issuer":"CMPIssuer/demo/demo-issuer","endpoint":"http://ca.example.com/pkix/",
"transactionID":"5f3c1d0e9a4b7c26d81f0a3b5e7c9d11",
"subject":"CN=workload.example.com","serialNumber":"3b8f2a41",
"notBefore":"2026-05-04T09:12:31Z","notAfter":"2026-08-02T09:12:31Z",
"issuingCA":"CN=Example CA,O=Example","keyType":"RSA","keySize":2048,
"signatureAlgorithm":"SHA256-RSA","chainLength":2,
"dnsNames":["workload.example.com"],
"confirmation":"Explicit","polls":0,"duration":"412ms"}
That answers what was issued, by which authority and how long it took, without decoding the Secret.
controller-runtime adds its own fields to every line, such as controller and reconcileID, which are
left out of the examples on this page.
transactionID is the CMP transaction identifier the server sees, so it is the field to search for in
the CMP server's own log when you need both sides of one exchange.
| Message | Level | Meaning |
|---|---|---|
Issued certificate |
info | The certificate was validated, recorded and returned to cert-manager |
Waiting for the CMP server to issue the certificate |
info | The server answered waiting; the line carries polls, maximumPolls, retryAfter and deadline |
Waiting for the CMP server to confirm the issued certificate |
info | The certificate exists and certConf has not been answered with pkiConf yet |
Resumed CMP transaction |
info | A restart or retry picked up an unfinished transaction, with its phase |
Returned the certificate already recorded for this CMP transaction |
info | A restart after issuance returned the recorded chain and sent no CMP message |
CMP enrollment failed |
error | The typed failure, as operation, failure and classification |
An asynchronous enrollment is therefore followable line by line: a wait, each poll interval, the resumption after a restart and finally the certificate. A silent gap means nothing is happening.
A failure is logged with the typed failure that caused it, followed by the retry decision, which
carries the same message that appears on the CertificateRequest condition:
{"level":"error","logger":"Reconcile","msg":"CMP enrollment failed",
"CertificateRequest":{"name":"demo-tls-1","namespace":"demo"},
"issuer":"CMPIssuer/demo/demo-issuer","endpoint":"http://ca.example.com/pkix/",
"transactionID":"5f3c1d0e9a4b7c26d81f0a3b5e7c9d11",
"operation":"process PKIStatus","failure":"badRequest","classification":"Permanent",
"error":"CMP process PKIStatus failed: pkicmp: status rejection, failInfo: badRequest, ..."}
{"level":"error","logger":"Reconcile","msg":"Got an error, will be retried.", ...}
classification tells you what happens next without reading the message:
| Classification | Meaning |
|---|---|
Permanent |
The request cannot succeed as sent. cert-manager marks it failed and issues a new one |
Retryable |
Transport or server unavailability. The same transaction is retried under its recorded identifier |
Security |
An authenticated transaction invariant failed, such as protection, trust or a nonce. Treated as permanent and never retried |
The log never contains credential values, private keys, CSR bodies or protected CMP message bytes. Server-supplied status text is included in failure messages, because that is usually the reason you need, but it is stripped of line breaks and truncated first, so a server cannot flood the log or forge a log line. Raising verbosity is therefore safe on a running system.
Increasing verbosity¶
Raising the level to debug adds the CMP message level: every request sent and every response
received with its body type and size, the transaction record written before the first message, and the
confirmation decisions. It also adds controller-runtime internals such as cache syncs, leader election
and client activity:
helm upgrade cmp-issuer cmp-issuer/cmp-issuer \
--namespace cmp-issuer-system \
--reuse-values \
--set logging.level=debug
| Value | Purpose |
|---|---|
logging.level |
debug, info, error, or an integer where higher is more verbose. Default info |
logging.stacktraceLevel |
Level at and above which a stack trace is attached, info, error or panic. Default panic |
logging.encoder |
json for log collectors, console for reading by eye. Default json |
Set logging.encoder=console while debugging by hand, since JSON is hard to scan in a terminal. One
enrollment then reads as its CMP exchange, ending in the same outcome line the default level prints:
DEBUG Recorded CMP transaction before sending the first message {"operation": "P10CR v2", ...}
DEBUG Sending CMP request {"operation": "p10cr", "bytes": 1193}
DEBUG Received CMP response {"operation": "p10cr", "body": "cp", "bytes": 2841}
DEBUG Confirming the issued certificate {"certReqID": 0, "polls": 0}
DEBUG Sending CMP request {"operation": "certConf", "bytes": 402}
DEBUG Received CMP response {"operation": "certConf", "body": "pkiconf", "bytes": 331}
INFO Issued certificate {"subject": "CN=workload.example.com", ...}
Only the body type and the size of each message are logged. The message content is never written to the log.
With the manifest installation, pass the same settings as container arguments instead:
--zap-log-level=debug, --zap-stacktrace-level=error and --zap-encoder=console.
Return the level to info afterwards, since debug is noisy on a busy cluster.
Issuer not Ready¶
| Symptom | Likely cause | Action |
|---|---|---|
| Ready=False, message names a Secret | Credential or trust Secret missing or unreadable | Create the Secret; for CMPIssuer authorize the namespace with credentialNamespaces or the RoleBinding from Credential Secret access |
| Ready=False after install | Controller not running | kubectl -n cmp-issuer-system get pods |
CertificateRequest stuck or failed¶
kubectl describe certificaterequest <name> -n <namespace>
kubectl get cmptransactions -n <namespace>
kubectl logs -n cmp-issuer-system deploy/cmp-issuer-controller-manager -c manager
| Condition message pattern | Likely cause |
|---|---|
| P10CR CP certReqId | Server returned an unexpected identifier; adjust p10crResponseCertReqId or use default |
| Response protection / trust | Wrong CMP trust anchors or unexpected signer |
message is signature-protected but MAC-based protection is required |
spec.protocol.macResponseProtection is Strict and the server signs its answer to a PasswordBasedMac request. Remove the value to fall back to the AllowSignature default, or configure the server to protect the answer with the shared secret |
| Response sender does not name the configured recipient | The server answers under a different name than spec.protocol.recipient. Set the recipient to the name the server puts in its responses. Attribute order does not matter, so only a genuine difference in attributes or values causes this |
| Transaction deadline / maximum polls | Server queue too slow; increase spec.transaction limits |
| Connection refused / timeout | Wrong URL, network policy or server down |
| HTTP redirect rejected | Endpoint redirects; use the final CMP URL |
| Recorded CMP transaction enrolls a different request | The CMPTransaction was edited or corrupted; delete it so the request enrolls again |
transactionIdInUse, or badRequest naming a GENERATED end entity |
The controller resent an enrollment the server had already accepted, so its response was lost. No second certificate was issued. cert-manager enrolls again under a new transaction identifier |
The transaction Phase column tells you where a request stopped. Enrolling means the enrollment is unanswered and will be resent unchanged. Polling means the server asked for pollReq. Confirming means the certificate is issued and recorded, and only the confirmation is outstanding. Issued means the certificate is confirmed and recorded, and no further CMP traffic will be sent.
EJBCA client mode¶
| Symptom | Likely cause | Action |
|---|---|---|
| Refused after first success | End entity in GENERATED state | Reset end entity status to NEW before repeat enrollment |
| Subject mismatch | Signature mode enrolls bootstrap DN only | Match Certificate SAN/CN to registered end entity or use PBM with correct profile |
| Wrong protection | Alias authentication modules | Confirm alias uses HMAC for PBM and EndEntityCertificate for signature |
Nokia NCM¶
| Symptom | Likely cause | Action |
|---|---|---|
| Header protection failed with vendor client | Missing end-entity cert in P10CR extraCerts | Use cmp-issuer layout or fix client |
| pkiConf verification failed before fix | Signer not retained | Upgrade to a build with confirmation signer retention |
| Pinning certReqId -1 | NCM returns 0 | Omit pin or set p10crResponseCertReqId: 0 |
Requests stay pending and nothing happens¶
A CertificateRequest that never gains an Approved condition, with no error anywhere and no CMP
traffic, means cert-manager is not permitted to approve requests for this issuer type. Its built-in
approver acts only on issuer types it holds explicit permission for, and reports nothing when it lacks
that permission.
kubectl get certificaterequest -n <namespace>
An APPROVED column that is empty rather than True confirms it.
The installation grants this permission by default, so reaching this state usually means one of:
- cert-manager runs under a different ServiceAccount or namespace than the binding names. Check the
subject against your installation and set
certManagerApproval.serviceAccountNameandcertManagerApproval.namespace. certManagerApproval.createwas set tofalse, or the ClusterRole was removed from the manifest.- Approval is delegated to approver-policy and no
CertificateRequestPolicycovers these requests.
Inspect what is bound:
kubectl get clusterrolebinding -o wide | grep cert-manager-approver
To confirm the diagnosis before changing RBAC, approve one request by hand with
cmctl approve <name> -n <namespace> and watch it proceed.
Installation covers the settings.
Denied or unapproved requests¶
cmp-issuer sends no CMP message for unapproved or denied CertificateRequest resources. Verify cert-manager approval policies and CertificateRequest conditions.
A denied CertificateRequest keeps its Denied condition but never gains a Ready=False condition
with reason Denied, and no failure event is recorded on it. The Certificate that owns it still
fails promptly, because cert-manager acts on the Denied condition itself, so the denial is visible on
the Certificate rather than on the request. Read the Denied condition with
kubectl describe certificaterequest <name> when you need the reason.
Invalid protection tests¶
Wrong PSK or invalid response signer fail closed: no certificate accepted and no TLS Secret written.
HTTP warning¶
An HTTP issuer emits a one-time Ready warning about missing transport confidentiality. This is expected, not an error.