Kubernetes CSR
A Mesh Node running in your cluster can act as an outbound-only custom signer for the stable certificates.k8s.io/v1 CertificateSigningRequest (CSR) API. Workloads create CSRs with your custom signerName; after an independent approval, the node issues them through Sectigo Edge policy and writes the certificate into status.certificate.
The node is a signer, not an approver. It never adds the Approved=True condition and never reads Kubernetes Secrets.
How it works
- A workload creates a CSR with the configured
spec.signerNameand the exact managed label. - An administrator or a policy controller validates the request and approves it (
Approved=True). - The node polls the API with a projected, automatically rotated service-account token and validates the object, the API-supplied requester identity, the namespace allowlist, signer, label, usages, lifetime, PKCS#10 signature, DNS SANs, subject and CA constraints.
- It derives a requester such as
spiffe://acme-corp.example/kubernetes/prod-ca/payments/api(trust domain, cluster ID, namespace, service account), evaluates your signed local policy, and submits the transaction through the normal approval quorum and protected signer. - Before writing
status.certificate, it checks that the leaf has the CSR public key, the exact DNS SAN set, no CA capability, no extra SAN types and no validity expansion. - One replica holds a pre-created coordination Lease and signs; others are hot standbys and take over when the lease expires.
- The issued result is persisted (mode
0600) before the status update, so a conflict or restart replays the same certificate instead of issuing another.
Invalid approved requests get a Failed=True condition with a short machine reason. Transient API, authorization, quorum or signer failures stay retryable and mark the integration Degraded.
Requirements
- The Mesh Node deployed with the Helm chart in the cluster whose CSRs it signs.
- A custom signer name in
domain/nameform; the domain must not bekubernetes.ioor end in.kubernetes.io. - An independent approval process (an administrator or a controller).
spiffe://<workload trust domain>/kubernetes/<cluster id>/must be covered byallowed_spiffe_prefixes— the node refuses to start otherwise.
Enable the signer with Helm
helm upgrade mesh ./edgepki-node --namespace edgepki --reuse-values \
--set kubernetesCsr.enabled=true \
--set kubernetesCsr.clusterId=prod-ca \
--set kubernetesCsr.signerName=issuer.sharppki.com/workload \
--set 'kubernetesCsr.allowedNamespaces={payments,orders}'
| Value | Default | config.json key and limits |
|---|---|---|
kubernetesCsr.enabled | false | kubernetes_csr_enabled |
kubernetesCsr.adapterReference | local/kubernetes-incluster | kubernetes_adapter_reference (local/…) |
kubernetesCsr.apiServerUrl | https://kubernetes.default.svc | kubernetes_api_server_url (HTTPS) |
kubernetesCsr.signerName | issuer.sharppki.com/workload | kubernetes_signer_name |
kubernetesCsr.managedLabel | sharppki.com/managed=true | kubernetes_managed_label (one key=value) |
kubernetesCsr.clusterId | replace-me | kubernetes_cluster_id (DNS label) |
kubernetesCsr.allowedNamespaces | ["default"] | kubernetes_allowed_namespaces (at least one) |
kubernetesCsr.allowedUsages | all four | kubernetes_allowed_usages: digital signature, key encipherment, server auth, client auth |
kubernetesCsr.maxRequestsPerPoll | 20 | kubernetes_max_requests_per_poll (1–100) |
kubernetesCsr.requestTimeoutSeconds | 10 | kubernetes_request_timeout_seconds (2–30) |
kubernetesCsr.leaseDurationSeconds | 30 | kubernetes_lease_duration_seconds (15–120) |
The chart also sets the CA and token files from a projected volume (one-hour token), the lease namespace to the release namespace and the lease name to <release>-edgepki-node-csr-signer.
What the chart grants
A dedicated ServiceAccount, a pre-created Lease and an exact-signer ClusterRole allowing: list CSRs, update the CSR status subresource, the sign verb on your custom signer name only, and get/update on that one Lease. It cannot create leases, approve CSRs or read Secrets.
CSR objects are cluster-scoped, so Kubernetes RBAC cannot restrict list to a namespace or signer. The node enforces the namespace allowlist itself before issuance. Use a dedicated Mesh Node release for this adapter and review this boundary.
Configure in the console
- Open ConsoleIntegrations and select Configure on Kubernetes CSR.
- Select Enable this integration and the node(s) of that release.
- Select the Certificate / identity profile used for CSR issuance (required; an empty profile reports
profile_not_configured). - Keep the Local adapter reference
local/kubernetes-inclusteror enter the value ofkubernetesCsr.adapterReference. - Select Save desired state.
The cloud schema never accepts a bearer token, CA private key or PKCS#11 PIN.
Policy requirements
Issuance is evaluated against the selected profile: the derived requester must fall inside the profile's SPIFFE prefixes, every DNS SAN inside its DNS suffixes, and algorithm and lifetime inside its limits. The node's own allowed_spiffe_prefixes, allowed_dns_suffixes and max_validity_seconds also apply. Kubernetes manages renewal: workloads submit a new CSR before expiry; there is no dual-slot activation for this adapter.
Request a certificate (workload side)
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: payments-api
labels:
sharppki.com/managed: "true"
spec:
signerName: issuer.sharppki.com/workload
expirationSeconds: 3600
request: <base64-pem-pkcs10>
usages:
- digital signature
- key encipherment
- server auth
kubectl apply -f payments-api-csr.yaml
certificatesigningrequest.certificates.k8s.io/payments-api created
kubectl certificate approve payments-api
certificatesigningrequest.certificates.k8s.io/payments-api approved
kubectl get csr payments-api
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
payments-api 41s issuer.sharppki.com/workload system:serviceaccount:payments:api 1h Approved,Issued
kubectl get csr payments-api -o jsonpath='{.status.certificate}' | base64 -d > payments-api.pem
Trust anchors are distributed separately; they are never embedded in the CSR.
Verify
kubectl -n edgepki port-forward pod/mesh-edgepki-node-0 9443:9443 &
sectigo-edge -ca local-api-ca.pem -cert workload-svid.pem -key workload-svid-key.pem integrations
{
"integrations": [
{
"kind": "kubernetes_csr",
"adapter_reference": "local/kubernetes-incluster",
"signer_name": "issuer.sharppki.com/workload",
"health": "healthy",
"last_reconciled_at": "2026-10-01T15:40:02Z",
"observed": 3,
"issued": 2,
"rejected": 1,
"pending": 0,
"role": "leader"
}
],
"node_id": "node_7c1e4b2a-93d5-4f61-8a0e-2b6d9f3c5e17"
}
Example output. The same information is available to agents through the read-only MCP tool edge.integrations. Before production, qualify in a real cluster: RBAC denial, token rotation, API conflicts, lease failover, replica races, signer outage, policy denial and restart recovery.
Troubleshooting
| Code | Meaning | What to do |
|---|---|---|
kubernetes_csr_not_configured | The assigned node does not have the signer enabled | Enable it in Helm |
adapter_reference_invalid | Console reference differs from kubernetesCsr.adapterReference | Make them identical |
profile_not_configured | No profile selected in the console | Select a profile |
kubernetes_authorization_failed | The API returned 401/403 | Check the chart's RBAC objects and the projected token |
kubernetes_resource_conflict | The API returned 409 while updating status | Usually transient; the node replays the persisted certificate |
kubernetes_api_unavailable | The API server could not be reached | Check apiServerUrl and network policy |
kubernetes_lease_lost | This replica lost the coordination lease during work | Another replica takes over; investigate if it repeats |
issuer_unavailable | Quorum or signer temporarily unavailable | Retries automatically; check node health and quorum |
issuer_response_invalid | The issued certificate failed the post-issuance checks | Contact support with the transaction ID |
state_persistence_failed | The node could not persist the result to its volume | Check the PVC |
CSR shows Failed | The approved request violated signer rules (namespace, usages, SANs, lifetime, CA flag, signature) | Fix the request and submit a new CSR |