Skip to content

Installation

cmp-issuer runs as a controller alongside cert-manager. Install cert-manager first, then the controller, naming the namespaces your issuers will live in.

If you are installing for the first time, follow Getting started instead. It covers these steps in order and ends with an issued certificate.

Prerequisites

Requirement Notes
Kubernetes Verified on v1.34, v1.35 and v1.36
cert-manager with external issuer support Verified on v1.19, v1.20 and v1.21
Helm v3 Only for the chart installation path

Install with Helm

The chart is published as a Helm repository:

helm repo add cmp-issuer https://misiektoja.github.io/cmp-issuer/charts
helm repo update
helm install cmp-issuer cmp-issuer/cmp-issuer \
  --namespace cmp-issuer-system \
  --create-namespace

Add --set 'credentialNamespaces={team-a,team-b}' to that command, naming the namespaces your CMPIssuer resources live in. The controller has no cluster-wide Secret access, so without it their credential Secrets are unreadable and the issuers stay Not Ready. See Namespace access for CMPIssuer.

List the available chart versions with helm search repo cmp-issuer -l.

From a packaged chart

Every release attaches a packaged chart, which is useful offline or behind a mirror. Its version has no leading v, because a Helm chart version has to be bare SemVer:

helm install cmp-issuer ./cmp-issuer-<chart version>.tgz \
  --namespace cmp-issuer-system \
  --create-namespace

From a clone

Use this when you are modifying the chart itself:

helm install cmp-issuer ./charts/cmp-issuer \
  --namespace cmp-issuer-system \
  --create-namespace

Common values

Value Purpose
manager.enabled Install the controller and supporting resources, default true. Set false to render only enabled CRDs
manager.image.repository and manager.image.tag Controller image and tag. Defaults to ghcr.io/misiektoja/cmp-issuer at the chart's appVersion. Set the repository to a name@sha256:... reference to pin a digest, in which case the tag is ignored
manager.replicas Controller replica count
manager.args Extra controller flags
manager.clusterResourceNamespace Namespace CMPClusterIssuer credential Secrets are read from. Defaults to the release namespace, and a different value also gets a credential reader RoleBinding
rbac.namespaced Watch only the release namespace with Role-based RBAC. This disables CMPClusterIssuer. Changing it on an existing release needs the existing bindings deleted first, since Kubernetes does not allow a roleRef to change
rbac.helpers.enabled Install the admin, editor and viewer roles for the issuer kinds, default true. They bind to nobody until you create a binding, and the manifest path always installs them
crd.enabled Install the CRDs with the chart, default true
crd.keep Keep the CRDs when the release is uninstalled, default true
certManagerApproval.create Let cert-manager approve requests for this issuer type, default true
certManagerApproval.serviceAccountName and .namespace Where cert-manager runs, default cert-manager in cert-manager
credentialNamespaces Namespaces to pre-authorize for CMPIssuer credential reads, default empty
serviceAccount.enabled and .name Create the default ServiceAccount or use the required existing name when creation is disabled
logging.level, .stacktraceLevel, .encoder Controller log verbosity, stack traces and format

Install with the manifest

Every release also attaches a self-contained cmp-issuer-<version>-install.yaml:

kubectl apply -f cmp-issuer-<version>-install.yaml

It installs the same CRDs, RBAC, cert-manager approval permission and controller Deployment in cmp-issuer-system, plus a RoleBinding that lets the controller read Secrets only in cmp-issuer-system. Neither installation path grants Secret access in workload namespaces on its own, and the manifest has no values to set, so a CMPIssuer namespace is authorized by applying the RoleBinding yourself.

The metrics endpoint serves HTTPS and authorizes every scrape, the same as the chart. It is the one place where the two paths differ in what they offer: the chart can have cert-manager issue the metrics serving certificate through metrics.tls.certManager.enabled, and the released manifest is built without it, so the endpoint presents a certificate the controller generates for localhost and a scraper has to skip verification. To get the issued certificate on this path, build the manifest from a checkout and uncomment the sections marked CERTMANAGER, METRICS-WITH-CERTS and PROMETHEUS-WITH-CERTS in config/default/kustomization.yaml and config/prometheus/kustomization.yaml, following the notes there, then run make build-installer.

Install without registry access

Every release attaches cmp-issuer-<version>-airgap.tar.gz, which carries everything an air-gapped cluster needs: the manager image as a multi-architecture OCI archive, the packaged chart, the installer manifest, the bill of materials, the licence and notices, and an INSTALL.txt repeating the two commands below. The bill of materials travels with the bundle so a cluster with no route to the release page can still answer what the image was built from.

It unpacks into cmp-issuer-<version>-airgap/:

cmp-issuer-<version>-airgap/
  images/cmp-issuer-<version>-image.tar    manager image as an OCI archive
  charts/cmp-issuer-<chart version>.tgz    packaged Helm chart
  cmp-issuer-<version>-install.yaml        self-contained manifest install
  cmp-issuer-<version>-sbom.cdx.json       CycloneDX bill of materials
  INSTALL.txt  README.md  RELEASE_NOTES.md  LICENSE  THIRD_PARTY_NOTICES.md

The chart is the one file named without the leading v, because a Helm chart version has to be bare SemVer. It is otherwise the same version as everything else in the bundle.

Copy the image into a registry your cluster can reach:

skopeo copy --all oci-archive:images/cmp-issuer-<version>-image.tar docker://<registry>/cmp-issuer:<version>

Import it straight into each node's container runtime instead when you have no registry at all:

ctr --namespace k8s.io images import images/cmp-issuer-<version>-image.tar

Then install from the bundled chart, pointing it at wherever the image now lives:

helm install cmp-issuer charts/cmp-issuer-<chart version>.tgz --namespace cmp-issuer-system --create-namespace --set manager.image.repository=<registry>/cmp-issuer

The archive holds the image that was published, exported from the same build rather than rebuilt, so its digest matches the one covered by the release provenance attestation described in Provenance and supply chain.

Custom resource definitions

The CRDs ship inside the chart rather than in Helm's separate crds/ directory, so helm upgrade updates them along with everything else. Helm never upgrades or removes anything placed in crds/, which would leave a new controller running against an old schema.

crd.keep defaults to true, which marks the CRDs with helm.sh/resource-policy: keep. The CRDs therefore survive helm uninstall on purpose. Deleting a CRD deletes every object of that kind, so an uninstall would otherwise destroy all of your CMPIssuer, CMPClusterIssuer and CMPTransaction resources, including issuers that are in use.

Remove them deliberately when you are certain, after uninstalling the release:

kubectl delete crd \
  cmpissuers.certmanager.misiektoja.github.io \
  cmpclusterissuers.certmanager.misiektoja.github.io \
  cmptransactions.certmanager.misiektoja.github.io

Set crd.enabled=false if you manage the CRDs separately, for example when a cluster administrator applies them ahead of the release.

cert-manager approval

cert-manager's built-in approver acts only on issuer types it holds explicit permission for, and it reports nothing when it lacks that permission, so a CertificateRequest would simply stay pending forever. Both installation paths grant that permission for you, by creating a ClusterRole with approve on signers for cmpissuers and cmpclusterissuers, bound to the cert-manager controller.

The binding assumes the upstream default, the cert-manager ServiceAccount in the cert-manager namespace. Point it elsewhere when your installation differs:

helm install cmp-issuer cmp-issuer/cmp-issuer \
  --namespace cmp-issuer-system --create-namespace \
  --set certManagerApproval.serviceAccountName=<name> \
  --set certManagerApproval.namespace=<namespace>

Turn it off entirely when approver-policy makes the decision instead, and express the same rule in a CertificateRequestPolicy:

helm install cmp-issuer cmp-issuer/cmp-issuer \
  --namespace cmp-issuer-system --create-namespace \
  --set certManagerApproval.create=false

For the manifest path, edit or remove the cmp-issuer-cert-manager-approver ClusterRole and its binding. Both installation paths name it the same way.

Namespace access for CMPIssuer

The controller has no cluster-wide Secret access. Every namespace hosting a CMPIssuer needs a RoleBinding granting it the credential reader role there. Without it the issuer stays Not Ready and names the missing authorization. Both routes below create the same grant, described in Credential Secret access.

Let the chart create the binding

List the namespaces in credentialNamespaces, at install time or in a later upgrade:

helm install cmp-issuer cmp-issuer/cmp-issuer \
  --namespace cmp-issuer-system --create-namespace \
  --set 'credentialNamespaces={team-a,team-b}'
helm upgrade cmp-issuer cmp-issuer/cmp-issuer \
  --namespace cmp-issuer-system \
  --reuse-values \
  --set 'credentialNamespaces={team-a,team-b}'

Each listed namespace must exist before the release is installed or upgraded, and each entry widens the boundary this project is built around, so name only the namespaces you have decided on. An upgrade has to carry the whole list, since --set replaces the value rather than appending to it. This setting requires rbac.namespaced=false, because a RoleBinding in one namespace cannot reference a Role that lives in another.

Apply the binding yourself

Applying the RoleBinding directly suits a namespace you do not want recorded in the release values, one created long after the install, and the manifest installation, which has no values to set. The manifest is in Credential Secret access.

The chart names its binding cmp-issuer-credential-reader-rolebinding, so a hand-applied binding under a different name is not adopted or replaced by a later upgrade. Delete yours if you move the same namespace into credentialNamespaces, or the namespace ends up with two identical grants.

A CMPClusterIssuer reads its credentials only from the controller's cluster resource namespace and needs no per-namespace binding.

Verify the installation

kubectl -n cmp-issuer-system get deploy,pods
kubectl get crd | grep certmanager.misiektoja.github.io

The manager names its own build on its first log line, so this is also how you confirm which release, commit and image are actually running:

kubectl -n cmp-issuer-system logs deploy/cmp-issuer-controller-manager | head -1

A Helm install adds the chart and release name to that line. The same output is available from /manager --version inside the container. See Which build is running.

Next

Create Secrets and an issuer, then request a certificate. Getting started shows the whole sequence, and the CMPIssuer reference documents every field. Samples live under config/samples/.