Kubernetes cert-manager Tutorial: How to Set Up Custom CA post provides a quick overview of using cert-manager on Kubernetes, showing how to easily set up a CA with the simple CA Issuer.
Certificate management is not just a security sensitive task dealing with issuing security sensitive objects: it is a cumbersome activity including taking care of handling the reissuing of expiring certificates before they reach their end of life, delivering them to the consuming services.
In Kubernetes cert-manager Tutorial: How to Set Up Custom CA post we discuss how to simplify this process using cert-manager, automating the issuing and renewal of issued certificates.
What Is Cert-Manager
cert-manager is a Kubernetes-native tool that automates the issuance, renewal, and management of TLS certificates for workloads running in Kubernetes clusters. It supports multiple certificate authorities, integrates seamlessly with Kubernetes APIs, and securely stores certificates, simplifying the entire certificate lifecycle management.
Designed to handle the dynamic and ephemeral nature of Kubernetes workloads, cert-manager automates certificate lifecycle management at scale without manual intervention. This facilitates smooth DevSecOps integration by embedding security into the development lifecycle through automated certificate provisioning and renewal.
It supports various issuers such as:
-
ACME Issuer: Uses the ACME protocol (e.g., Let's Encrypt) to automate certificate issuance and renewal via HTTP-01 or DNS-01 challenges.
-
Simple CA Issuer: Signs certificate signing requests (CSRs) using a provided Certificate Authority (CA) bundle for internal or self-managed PKI.
-
Vault Issuer: Integrates with HashiCorp Vault to issue certificates from Vault's PKI secrets engine.
-
Venafi Issuer: Connects to Venafi Trust Protection Platform (TPP) or Venafi as a Service (VaaS) for enterprise certificate management.
-
SelfSigned Issuer: Generates self-signed certificates, useful for testing or bootstrapping a CA chain.
-
External Issuer: Enables custom certificate issuance through an external system via a validating and issuing webhook.
These issuers enable you to request certificates to both private as well as globally available CAs. This means that:
- when dealing with certificates for public domains, you can use the ACME Issuer with Let's Encrypt
- when dealing with certificates for local domains (which canot be issued by glabal CAs), you can use the ACME Issuer with an on premises Step CA instance running your own private CA.
If your corporate is running Hashicorp's Vault or Venafi TPP or VaaS, you can use their specific issuer.
You can even handle corner cases such as any other CA manageable by API or scripting, such as Cloudflare's CFSSL PKI and TLS Toolkit: in such a scenario, you can integrate cert-manager to those CAs using the External issuer, despite this is a cumbersome integration, since it requires you to write the code to implement the necessary webhooks.
Last but no least, if your needs are quite basic, such as when dealing with labs or testing environments, you can use the SelfSigned issuer to generate a self-signed ROOT CA certificate in conjunction with the Simple CA Issuer to sign certificate requests.
The SimpleCA Issuer does not support extensions such as CRL and OCSP: if you are dealing with production security sensitive environments requiring to be able to quickly revoke the issued certificates, consider using the ACME issuer or Vault or Venafi, or the External issuer with a CA supporting CRL and or OCSP.
In Kubernetes-based scenarios, cert-manager is preferred over traditional tools due to its deep Kubernetes integration, flexibility in issuer options, and native automation capabilities. These features make it ideal for dynamic, containerized environments where manual certificate management is complex and error-prone.
Moreover, mind that, despite very often cert-manager is used to generate certificates consumed by applications running inside Kubernetes, it is possible to use it also to sign certificates for applications running outside it.
This post is meant only at showing how to set up cert-manager and at providing an overview of how to setup an automated CA: to maximize compatibility, it illustrates how to use the Simple CA issuer, since it does not introduce any external dependency on third part products. To keep the post short and focused, we also don't delve in security hardening (which I may put in a dedicated post later on).
Deploy The Cert-Manager Suite
Let's start the workshop by showing how to set up cert-manager: it can be easily deployed using its official Helm chart, which is available in Jetstack's Helm repository.
if you still don't have the helm command line utility installed, you can install it as follows:
sudo mkdir -m 755 -p /opt/helm /opt/helm/bin
curl -sL https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz \
| sudo tar xvz --strip-components=1 -C /opt/helm/bin
the above statements create the /opt/helm/bin directory and download and installs helm v3.17.3 for x86_64 systems beneath it.
Of course, adjust the second command to match the Helm version and architecture you want to install (if you are using an AARM, replace amd64 with arm64).
Let's add to the system-wide PATH as follows:
echo PATH=\${PATH}:/opt/helm/bin | sudo tee -a /etc/profile.d/helm
We must then disconnect and reconnect, or source the /etc/profile.d/helm file to import it in the current session:
source /etc/profile.d/helm
We are now able to run the helm command line tool, so as our first command, let's add the Jetstack Helm repository by running:
helm repo add jetstack https://charts.jetstack.io
Let's verify the outcome:
helm repo list
If everything is properly working, the jetstack repository must be in the returned list.
Install cert-manager Via Helm
The first component to provision is cert-manager itself. Just run:
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.16.2 \
--set crds.enabled=true
In this example we deploy version v1.16.2 it in the cert-manager namespace.
We also specify the crds.enable option so to deploy the Custom Resource Definitions (CRD) as part of the Helm installation.
If everything works as expected, you must get a message like the below one:
NAME: cert-manager
LAST DEPLOYED: Sat Oct 4 14:07:29 2025
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.16.2 has been deployed successfully!
Install trust-manager Via Helm
The cert-manager suite provides also trust-manager, which is a component providing a Kubernetes operator which automates the aggregation, generation, and lifecycle management of trust-stores.
These bundles can be output in various formats such as PEM, JKS, or PKCS#12, and are distributed as ConfigMaps for consumption by workloads and cluster components.
Trust-manager consolidates CA certificates from multiple sources, including private CAs issued and managed by cert-manager as Kubernetes Secrets, as well as globally recognized root CAs, into standardized trust bundles. This last one is particularly important for containers that do not include a comprehensive system-wide trust store by default.
The trust-manager operator continuously reconciles and updates these trust bundles to ensure that all consuming applications have access to current and consistent trust anchors, thereby facilitating secure TLS communication and certificate validation within dynamic Kubernetes environments.
In this example we install it in the cert-manager namespace, waiting until the process completes:
helm upgrade trust-manager jetstack/trust-manager \
--install \
--namespace cert-manager \
--wait
If everything works as expected, you must get a message like the below one:
NAME: trust-manager
LAST DEPLOYED: Sat Oct 4 14:09:44 2025
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
WARNING: Consider increasing the Helm value `replicaCount` to 2 if you require high availability.
WARNING: Consider setting the Helm value `podDisruptionBudget.enabled` to true if you require high availability.
trust-manager v0.19.0 has been deployed successfully!
Your installation includes a default CA package, using the following
default CA package image:
quay.io/jetstack/trust-pkg-debian-bookworm:20230311-deb12u1.0
It's imperative that you keep the default CA package image up to date.
Install cert-manager-CSI-Driver Via Helm
Another component provided by the cert-manager suite is the cert-manager’s CSI driver: this is a Container Storage Interface (CSI) driver which allows Kubernetes pods to request certificates and to consume them as files mounted directly into their container’s filesystem.
The CSI driver works alongside cert-manager to automate the certificate lifecycle by handling key and Certificate Signing Request (CSR) creation, certificate issuance, and renewal.
When certificates are renewed, the CSI driver updates the mounted certificate files inside the container’s filesystem automatically
Be wary that, whether the application uses the updated certificates without restart depends on the application’s ability to reload certificates dynamically. If the application only loads certificates at startup, the pod or container must be restarted to pick up the renewed certificates.
In this example we install the CSI driver in the cert-manager namespace, along with cert-manager.
Just run:
helm upgrade cert-manager-csi-driver jetstack/cert-manager-csi-driver \
--install \
--namespace cert-manager \
--wait
If everything works as expected, you must get a message like the below one:
NAME: cert-manager
NAME: cert-manager-csi-driver
LAST DEPLOYED: Sat Oct 4 14:13:57 2025
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
if you are curious, you can get more information on this CSI driver by running:
kubectl describe csidrivers csi.cert-manager.io
Working With The Simple CA Issuer
In this lab we have the first go with cert-manager by creating a private single-tier Certificate Authority (CA) using the simple CA issuer.
As said, this is the built-in CA issuer type, suitable for quite basic internal or self-signed certificate needs. Although it's not a full-fledged PKI system, since it does not have any third part requirements, it's ideal for basic private CA setups such as labs, testing and user acceptance environment.
Create The Self-signed Issuer
The first step for setting up a private CA is defining a selfSigned Issuer in the same namespace where we deployed cert-manager.
This component is used to generate self-signed certificates, which are the kind of certificate used by ROOT CAs.
Just create the self-issuer.yml manifest file with the following contents:
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: issuer.selfsigned
namespace: cert-manager
spec:
selfSigned: {}
then, submit it to Kubernetes by running:
kubectl create -f self-issuer.yml
Generate The CA's Self-Signed Certificate
Once done, we are ready to generate the CA's ROOT certificate.
Just create the carcano-t1-root01.yml file with the following contents:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: carcano-t1-root01
namespace: cert-manager
spec:
isCA: true
commonName: carcano-t1 ROOT01 CA
subject:
organizations:
- Carcano SA
organizationalUnits:
- Services
secretName: carcano-t1-root01-ca
privateKey:
algorithm: ECDSA
size: 521
# 10 years
duration: 87600h
# 7 years
renewBefore: 61320h
issuerRef:
name: issuer.selfsigned
kind: Issuer
group: cert-manager.io
In this example we are creating the carcano-t1 ROOT01 CA certificate with a 10 years lifetime, automatically renewed after 7 years.
It is a common practice to renew the CA's certificate early enough to avoid issued certificates overstepping the CA's certificate: renewing 3 years before the expiration is a reasonable boundary.
We can now submit this manifest to Kubernetes by running:
kubectl create -f carcano-t1-root01.yml
The cert-manager operator immediately takes care of enrolling the certificate.
We can check that the certificate was actually issued by running:
kubectl -n cert-manager get certificate
The output must look like as follows:
NAME READY SECRET AGE
carcano-t1-root01 True carcano-t1-root01-ca 7s
trust-manager True trust-manager-tls 5m36s
beside the certificate generation, cert-manager took also care of creating the carcano-t1-root01-ca secret in the cert-manager namespace.
We can view it by running:
kubectl -n cert-manager get secret carcano-t1-root01-ca
the output must look like as follows:
NAME TYPE DATA AGE
carcano-t1-root01-ca kubernetes.io/tls 3 53s
Create A ClusterCA ClusterIssuer
We must now create the carcano-t1 ROOT01 leaf certificate's issuer component. Since we want it to be able to enroll certificates also from namespaces other than the one where cert-manager is running, we define it of kind ClusterIssuer.
Create the carcano-t1-root01-issuer.yml manifest file with the following contents:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: issuer.carcano-t1-root01
spec:
ca:
secretName: carcano-t1-root01-ca
submit the manifest to Kubernetes by running:
kubectl create -f carcano-t1-root01-issuer.yml
This creates the issuer.carcano-t1-root01 ClusterIssuer.
Once invoked, this cluster issuer will always enroll certificates using the carcano-t1 ROOT01 CA's certificate.
Configure Trust-Stores
As we said, cert-manager enables also the automatic lifecycle management of trust-stores. In a typical scenario, you would need three kind of trust stores:
-
global-ca: a global CA bundle, containing only the ROOT certificates of the global available CAs. This kind of trust-store is useful when dealing with container images which not embed this kind of trust store, or when you don't want to be forced to generate a new container image version of your running containers whenever you need to update the globally available ROOT CA's certificates.
-
cluster-ca-only: a cluster CA bundle, containing only the CA's certificate of a specific private CA - you will need a distinct one for each single private CA
-
cluster-ca-extended: a cluster CA bundle, containing the CA's certificate of a specific private CA along with all the ROOT certificates of the global available CAs - you will need a distinct one for each single private CA
Create The Global CA Bundle
Let's start our experiment by creating the globalca-bundle.yml manifest file with the following contents:
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: globalca-bundle
spec:
sources:
- useDefaultCAs: true
target:
configMap:
key: "globalca-bundle.pem"
as you see, in the sources dictionary we specify only the useDefaultCA directive, which tells to add to the list the bundle with all the global CAs.
Once create the above manifest file, submit it to Kubernetes by running:
kubectl create -f globalca-bundle.yml
the outcome is the creation of the globalca-bundle Bundle, which triggers the creation of the globalca-bundle ConfigMap in every namespace.
You can verify this by checking a few ones, such as:
kubectl -n cert-manager get configmap globalca-bundle
kubectl -n default get configmap globalca-bundle
for every namespace you must get:
NAME DATA AGE
globalca-bundle 1 4m55s
If you fancy, you can easily inspect its contents by running:
kubectl -n default get configmap globalca-bundle -o jsonpath='{.data.globalca-bundle\.pem}'
in this example we are inspecting the on in the default namespace.
Mind that it is not necessary to inspect them all, since, as mentioned, the contents of the globalca-bundle ConfigMap are consistent across all namespaces.
Create The ClusterCA-Only Bundle
Let's now create a clusterca-only bundle dedicated to the carcano-t1 ROOT01 CA we just set up.
Create the carcano-t1-root01-ca-only.yml manifest file with the following contents:
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: carcano-t1-root01-ca-only
spec:
sources:
- useDefaultCAs: false
- secret:
name: "carcano-t1-root01-ca"
key: "tls.crt"
target:
configMap:
key: "clusterca.pem"
as you see, in the sources dictionary we this time the useDefaultCA directive is set to false, and we provide the coordinates to the secret containing the the carcano-t1 ROOT01 CA certificate.
Once done with the above file, submit it to Kubernetes by running:
kubectl create -f carcano-t1-root01-ca-only.yml
We can then inspect its contents by running:
kubectl -n default get configmap carcano-t1-root01-ca-only -o jsonpath='{.data.clusterca\.pem}'
Create The ClusterCA-extended Bundle
Let's finally create a clusterca-extended bundle containing the carcano-t1 ROOT01 CA we just set up along with all the globally available ROOT CAs' certificates.
Create the carcano-t1-root01-ca-extended.yml manifest with the following contents:
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: carcano-t1-root01-ca-extended
spec:
sources:
- useDefaultCAs: true
- secret:
name: "carcano-t1-root01-ca"
key: "tls.crt"
target:
configMap:
key: "clusterca.pem"
as you see, this time the sources list contains both the useDefaultCAs option to true along with the coordinates to the secret containing the the carcano-t1 ROOT01 CA certificate.
Let's submit the manifest to Kubernetes by running:
kubectl create -f carcano-t1-root01-ca-extended.yml
Let's ensure that carcano-t1-ca-extended bundle contains all the global's CAs certificates along with the carcano-t1 ROOT01 certificate - first, let's dump the carcano-t1-root01-ca-extended bundle into the ca-chain.pem file by running:
kubectl -n default get configmap carcano-t1-root01-ca-extended -o jsonpath='{.data.clusterca\.pem}' > ca-chain.pem
Then, we can lookup for the carcano-t1 ROOT01 CA as follows:
openssl crl2pkcs7 -nocrl -certfile ca-chain.pem | openssl pkcs7 -print_certs
Footnotes
In this tutorial, we successfully engineered a powerful internal PKI simulator by deploying cert-manager and trust-manager to bootstrap a custom PKI. We moved from a self-signed root authority to a cluster-wide CA Issuer, building multi-format trust stores.
While the lack of CRL and OCSP keeps it lean, this lightweight architecture is an invaluable asset for enterprise sandboxes, development environments, and QA testing, giving developers and DevOps engineers a fully functional playground to validate complex TLS setups safely and rapidly.
With this high-velocity testing foundation fully operational, you are now ready to see how workloads actually consume this trust material.
In the next post, we will bridge the gap to deployment by exploring how to generate Server and Client TLS certificates for rigid mTLS communication. We will also dive into advanced security by using the cert-manager CSI Driver to mount certificates directly into Pod memory, before wrapping up with production-ready Helm charts to automate the entire lifecycle.
As usual, if you appreciate this post and any other ones, just share this and the others on Linkedin - sharing and comments are an inexpensive way to push me into going on writing - this blog makes sense only if it gets visited.
Also concrete contributions to the maintenance of this blog are also very welcome, ... just put your tip in the below cup.