Develop a Custom Signer

Third-party signers

You can also introduce your own custom signer, which should have a similar prefixed name but using your own domain name. For example, if you represent an open source project that uses the domain open-fictional.example then you might use issuer.open-fictional.example/service-mesh as a signer name.

To implement your custom signer, you need to provide a set of controllers that use the Kubernetes API to interact with CertificateSigningRequests, PodCertificateRequests, and ClusterTrustBundles that are linked to your signer's name.

Useful ClusterRoles

CertificateSigningRequests

CertificateSigningRequests have three roles — requesters, approvers, and signers. Depending on the signer's logic around approvals, the approver and signer role may be shared by the same controller.

Requesters need to be able to create and read CertificateSigningRequests. Allowing broad read access to CSRs is not a security issue, because the CSRs only contain public (not private) keys.

  • Verbs: create, get, list, watch, group: certificates.k8s.io, resource: certificatesigningrequests

If your cluster uses RBAC, here's an example ClusterRole for a CSR requester:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: csr-creator
rules:
- apiGroups:
  - certificates.k8s.io
  resources:
  - certificatesigningrequests
  verbs:
  - create
  - get
  - list
  - watch

Approvers need to be able to watch CSRs, and approve or deny CSRs addressed to their signer name:

  • Verbs: get, list, watch, group: certificates.k8s.io, resource: certificatesigningrequests
  • Verbs: update, group: certificates.k8s.io, resource: certificatesigningrequests/approval
  • Verbs: approve, group: certificates.k8s.io, resource: signers, resourceName: <signerNameDomain>/<signerNamePath> or <signerNameDomain>/*

If your cluster uses RBAC, here's an example ClusterRole for a CSR approver:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: csr-approver
rules:
- apiGroups:
  - certificates.k8s.io
  resources:
  - certificatesigningrequests
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - certificates.k8s.io
  resources:
  - certificatesigningrequests/approval
  verbs:
  - update
- apiGroups:
  - certificates.k8s.io
  resources:
  - signers
  resourceNames:
  - example.com/my-signer-name # example.com/* can be used to authorize for all signers in the 'example.com' domain
  verbs:
  - approve

Issuers need to be able to watch CSRs, and issue or fail CSRs addressed to their signer name:

  • Verbs: get, list, watch, group: certificates.k8s.io, resource: certificatesigningrequests
  • Verbs: update, group: certificates.k8s.io, resource: certificatesigningrequests/status
  • Verbs: sign, group: certificates.k8s.io, resource: signers, resourceName: <signerNameDomain>/<signerNamePath> or <signerNameDomain>/*

If your cluster uses RBAC, here's an example ClusterRole for a CSR issuer:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: csr-signer
rules:
- apiGroups:
  - certificates.k8s.io
  resources:
  - certificatesigningrequests
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - certificates.k8s.io
  resources:
  - certificatesigningrequests/status
  verbs:
  - update
- apiGroups:
  - certificates.k8s.io
  resources:
  - signers
  resourceNames:
  - example.com/my-signer-name # example.com/* can be used to authorize for all signers in the 'example.com' domain
  verbs:
  - sign

PodCertificateRequests

For PodCertificateRequests, the requester role is almost always filled by kubelet, which automatically has the necessary permissions to create and read PCRs. There is no approver role.

Issuers need to be able to watch CSRs, and issue or fail CSRs addressed to their signer name:

  • Verbs: get, list, watch, group: certificates.k8s.io, resource: podcertificaterequests
  • Verbs: update, group: certificates.k8s.io, resource: podcertificaterequests/status
  • Verbs: sign, group: certificates.k8s.io, resource: signers, resourceName: <signerNameDomain>/<signerNamePath> or <signerNameDomain>/*

If your cluster uses RBAC, here's an example ClusterRole for a PCR issuer:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pcr-signer
rules:
- apiGroups:
  - certificates.k8s.io
  resources:
  - podcertificaterequests
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - certificates.k8s.io
  resources:
  - podcertificaterequests/status
  verbs:
  - update
- apiGroups:
  - certificates.k8s.io
  resources:
  - signers
  resourceNames:
  - example.com/my-signer-name # example.com/* can be used to authorize for all signers in the 'example.com' domain
  verbs:
  - sign

ClusterTrustBundles

ClusterTrustBundles have two broad roles: consumers and attesters.

Consumers need to be able to read ClusterTrustBundles. In most scenarios, the consumer will be kubelet (automatic permission via the node authorizer, if enabled) or a service account (automatic permission via an RBAC bootstrap ClusterRole, if enabled).

  • Verbs: get, list, watch, group certificates.k8s.io, resource: clustertrustbundles

Attesters are typically a signer controller, and will need permission to create and maintain specific signer-linked ClusterTrustBundles

  • Verbs: create, get, list, watch, group: certificates.k8s.io, resource: clustertrustbundles
  • Verbs: attest, group: certificates.k8s.io, resource: signers, resourceName: <signerNameDomain>/<signerNamePath> or <signerNameDomain>/*

Writing a PodCertificateRequest controller

Under the hood, kubelet runs the state machine depicted in Figure 1 for each podCertificate projection. The actions your signer takes on the PodCertificateRequests that kubelet generates control the state transitions.

stateDiagram-v2 direction LR Initial --> Wait Wait --> Fresh Wait --> Failed Wait --> Denied Fresh --> WaitRefresh WaitRefresh --> Failed WaitRefresh --> Denied
Figure 1. Kubelet podCertificate lifecycle

  1. The projection starts out in Initial state.
  2. Kubelet generates a private key and holds it in memory.
  3. Kubelet creates a PodCertificateRequest addressed to the requested signer. Kubelet then moves the projection into the Wait state.
  4. If the PodCertificateRequest is marked "Denied", move to the Denied state. This is a permanent error state, and the container(s) that mount this projection will fail to start.
  5. If the PodCertificateRequest is marked "Failed", move to the Failed state. This is a permanent error state, and the container(s) that mount this projection will fail to start.
  6. If the PodCertificate is marked "Issued", move to the Fresh state. Kubelet holds the private key and certificate chain in memory, and will periodically write them to the filesystem at the requested location. The container that mounts this projection will start up and run (assuming nothing else blocks its execution).
  7. The signer indicated an appropriate time to begin refreshing the certificate when it issued the PodCertificateRequest. Once that time has passed Kubelet will generate a new private key, create a new PodCertificateRequest, and move the projection into WaitRefresh state.
  8. If the PodCertificateRequest is marked "Denied", move to the Denied state. This is a permanent error state, and the container(s) will begin to get Kubelet volume remount errors.
  9. If the PodCertificateRequest is marked "Failed", move to the Failed state. This is a permanent error state, and the container(s) will begin to get Kubelet volume remount errors.
  10. If the PodCertificate is marked "Issued", move back to the Fresh state. The container(s) will continue to run, with the new private key and certificate chain written to the filesystem.

What's next

Last modified February 27, 2025 at 4:10 PM PST: Refactor Certificates Documentation (8ba16fc3e5)