September 4, 2026
Red Hat Cert-Manager with Custom CA
In this lab, I want OpenShift to use certificates signed by my own Custom CA.

By Fabio Reis
5 min read
This runbook shows how to use cert-manager and a Custom CA to manage TLS certificates for an OpenShift lab.
The configuration covers:
- The OpenShift API
- The default application domain
- The OAuth endpoint used by
oc login - The default OpenShift Console route
- Application Routes
The endpoints are:
API: api.ocp.example.com
OAuth: oauth-openshift.apps.ocp.example.com
Console: console-openshift-console.apps.ocp.example.com
Apps: *.apps.ocp.example.comAPI: api.ocp.example.com
OAuth: oauth-openshift.apps.ocp.example.com
Console: console-openshift-console.apps.ocp.example.com
Apps: *.apps.ocp.example.comThis lab stores the Custom CA private key in an OpenShift Secret. This is acceptable for a lab or a CA dedicated to one cluster. A corporate root CA private key should never be stored in the cluster.
Prerequirements
Before starting, make sure you have:
- An OpenShift cluster.
- The Red Hat build of cert-manager installed from OperatorHub.
oclogged in as a cluster administrator.- DNS records for the API, Console, and Apps domains.
- OpenSSL available on your workstation.
Operator :
Check cert-manager:
oc get pods -n cert-manager
oc get crd | grep cert-manager.iooc get pods -n cert-manager
oc get crd | grep cert-manager.ioIf your cert-manager installation uses another namespace, replace cert-manager in the commands below.
1. Create a Custom CA
A CA Issuer needs a certificate and private key. In this lab, I will create a simple self-signed CA to simulate a site which uses a internal CA.
openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -nodes \
-key lab-ca.key \
-sha256 \
-days 3650 \
-out lab-ca.crt \
-subj "/CN=OpenShift Lab Custom CA/O=Lab"openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -nodes \
-key lab-ca.key \
-sha256 \
-days 3650 \
-out lab-ca.crt \
-subj "/CN=OpenShift Lab Custom CA/O=Lab"This creates:
lab-ca.crt
lab-ca.keylab-ca.crt
lab-ca.keyNow create a TLS Secret in the cert-manager namespace:
oc -n cert-manager create secret tls cluster-custom-ca \
--cert=lab-ca.crt \
--key=lab-ca.keyoc -n cert-manager create secret tls cluster-custom-ca \
--cert=lab-ca.crt \
--key=lab-ca.keyCheck the Secret:
oc get secret cluster-custom-ca -n cert-manageroc get secret cluster-custom-ca -n cert-manager2. Create the ClusterIssuer
The ClusterIssuer allows cert-manager to use this CA from any namespace.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: cluster-custom-ca
spec:
ca:
secretName: cluster-custom-ca
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: cluster-custom-ca
spec:
ca:
secretName: cluster-custom-ca
Apply it:
oc apply -f cluster-custom-ca-issuer.yamloc apply -f cluster-custom-ca-issuer.yamlValidate it:
oc get clusterissuer cluster-custom-caoc get clusterissuer cluster-custom-caExpected result:
NAME READY AGE
cluster-custom-ca True 1mNAME READY AGE
cluster-custom-ca True 1m3. Create the API certificate
The API Server expects the generated TLS Secret in openshift-config.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-serving-cert
namespace: openshift-config
spec:
secretName: api-serving-cert
duration: 2160h
renewBefore: 360h
issuerRef:
name: cluster-custom-ca
kind: ClusterIssuer
dnsNames:
- api.ocp.example.comapiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-serving-cert
namespace: openshift-config
spec:
secretName: api-serving-cert
duration: 2160h
renewBefore: 360h
issuerRef:
name: cluster-custom-ca
kind: ClusterIssuer
dnsNames:
- api.ocp.example.comApply it and wait for the certificate:
oc apply -f api-serving-cert.yaml
oc get certificate api-serving-cert \
-n openshift-config -woc apply -f api-serving-cert.yaml
oc get certificate api-serving-cert \
-n openshift-config -wThe expected result is:
NAME READY SECRET AGE
api-serving-cert True api-serving-cert 1mNAME READY SECRET AGE
api-serving-cert True api-serving-cert 1m4. Configure the API Server
First, check whether the cluster already has named API certificates:
oc get apiserver cluster \
-o jsonpath='{.spec.servingCerts.namedCertificates}{"\n"}'oc get apiserver cluster \
-o jsonpath='{.spec.servingCerts.namedCertificates}{"\n"}'For a lab without existing named certificates, configure the API Server with this patch:
oc patch apiserver cluster \
--type=merge \
-p '{
"spec": {
"servingCerts": {
"namedCertificates": [
{
"names": [
"api.ocp.example.com"
],
"servingCertificate": {
"name": "api-serving-cert"
}
}
]
}
}
}'oc patch apiserver cluster \
--type=merge \
-p '{
"spec": {
"servingCerts": {
"namedCertificates": [
{
"names": [
"api.ocp.example.com"
],
"servingCertificate": {
"name": "api-serving-cert"
}
}
]
}
}
}'The patch replaces the complete
namedCertificateslist. If entries already exist, useoc edit apiserver clusterand append the new certificate instead.
Check the API Server Operator:
oc get co kube-apiserveroc get co kube-apiserverValidate the certificate served by the API:
openssl s_client \
-connect api.ocp.example.com:6443 \
-servername api.ocp.example.com \
-CAfile lab-ca.crt </dev/nullopenssl s_client \
-connect api.ocp.example.com:6443 \
-servername api.ocp.example.com \
-CAfile lab-ca.crt </dev/nullExpected result:
issuer=CN=OpenShift Lab Custom CA, O=Lab
Verify return code: 0 (ok)issuer=CN=OpenShift Lab Custom CA, O=Lab
Verify return code: 0 (ok)5. Create the wildcard certificate for Apps
The default Ingress Controller expects its TLS Secret in openshift-ingress.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: apps-wildcard-cert
namespace: openshift-ingress
spec:
secretName: apps-wildcard-cert
duration: 2160h
renewBefore: 360h
issuerRef:
name: cluster-custom-ca
kind: ClusterIssuer
dnsNames:
- "*.apps.ocp.example.com"apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: apps-wildcard-cert
namespace: openshift-ingress
spec:
secretName: apps-wildcard-cert
duration: 2160h
renewBefore: 360h
issuerRef:
name: cluster-custom-ca
kind: ClusterIssuer
dnsNames:
- "*.apps.ocp.example.com"Apply it and wait for readiness:
oc apply -f apps-wildcard-cert.yaml
oc get certificate apps-wildcard-cert \
-n openshift-ingress -woc apply -f apps-wildcard-cert.yaml
oc get certificate apps-wildcard-cert \
-n openshift-ingress -wTrust the Custom CA inside OpenShift
The Authentication Operator validates the OAuth Route through the default Ingress Controller. Before configuring a Custom CA-signed certificate on the router, add the CA certificate to the OpenShift trust bundle.
Extract the Root CA created by cert-manager:
oc -n cert-manager get secret cluster-custom-ca \
-o jsonpath='{.data.tls\.crt}' \
| base64 -d > lab-ca.crtoc -n cert-manager get secret cluster-custom-ca \
-o jsonpath='{.data.tls\.crt}' \
| base64 -d > lab-ca.crtCreate the Custom CA ConfigMap:
oc -n openshift-config create configmap custom-ca \
--from-file=ca-bundle.crt=lab-ca.crtoc -n openshift-config create configmap custom-ca \
--from-file=ca-bundle.crt=lab-ca.crtAssociate it with the cluster Proxy configuration:
oc patch proxy cluster \
--type=merge \
-p '{"spec":{"trustedCA":{"name":"custom-ca"}}}'oc patch proxy cluster \
--type=merge \
-p '{"spec":{"trustedCA":{"name":"custom-ca"}}}'Validate the configuration:
oc get proxy cluster \
-o jsonpath='{.spec.trustedCA.name}{"\n"}'oc get proxy cluster \
-o jsonpath='{.spec.trustedCA.name}{"\n"}'Expected output:
custom-cacustom-ca6. Configure the default Ingress Controller
Patch only the default certificate field, preserving the existing Ingress Controller configuration:
oc patch ingresscontroller default \
-n openshift-ingress-operator \
--type=merge \
-p '{"spec":{"defaultCertificate":{"name":"apps-wildcard-cert"}}}'oc patch ingresscontroller default \
-n openshift-ingress-operator \
--type=merge \
-p '{"spec":{"defaultCertificate":{"name":"apps-wildcard-cert"}}}'Confirm the configured Secret:
oc get ingresscontroller default \
-n openshift-ingress-operator \
-o jsonpath='{.spec.defaultCertificate.name}{"\n"}'oc get ingresscontroller default \
-n openshift-ingress-operator \
-o jsonpath='{.spec.defaultCertificate.name}{"\n"}'Expected result:
apps-wildcard-certapps-wildcard-certThe router automatically observes the Secret. A manual router restart is not required.
7. Validate OAuth, Console, and application Routes
The OAuth endpoint is especially important because oc login is redirected to it after contacting the API.
Check the OAuth Route hostname:
oc get route oauth-openshift \
-n openshift-authentication \
-o jsonpath='{.spec.host}{"\n"}'oc get route oauth-openshift \
-n openshift-authentication \
-o jsonpath='{.spec.host}{"\n"}'Validate its certificate:
openssl s_client \
-connect oauth-openshift.apps.ocp.example.com:443 \
-servername oauth-openshift.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullopenssl s_client \
-connect oauth-openshift.apps.ocp.example.com:443 \
-servername oauth-openshift.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullValidate the Console:
openssl s_client \
-connect console-openshift-console.apps.ocp.example.com:443 \
-servername console-openshift-console.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullopenssl s_client \
-connect console-openshift-console.apps.ocp.example.com:443 \
-servername console-openshift-console.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullValidate an application Route:
openssl s_client \
-connect myapp.apps.ocp.example.com:443 \
-servername myapp.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullopenssl s_client \
-connect myapp.apps.ocp.example.com:443 \
-servername myapp.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullAll three checks should return:
issuer=CN=OpenShift Lab Custom CA, O=Lab
Verify return code: 0 (ok)issuer=CN=OpenShift Lab Custom CA, O=Lab
Verify return code: 0 (ok)8. Trust the Custom CA on the workstation
On RHEL, Fedora, or CentOS, add the Custom CA to the system trust store:
sudo cp -v lab-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extractsudo cp -v lab-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extractFor an explicit and reproducible login, pass the CA file directly:
oc login \
--certificate-authority="$PWD/lab-ca.crt" \
--server=https://api.ocp.example.com:6443 \
-u kubeadminoc login \
--certificate-authority="$PWD/lab-ca.crt" \
--server=https://api.ocp.example.com:6443 \
-u kubeadminThe resulting kubeconfig entry stores the CA data for this cluster.
Troubleshooting
The API certificate is valid, but oc login fails
A common error is:
tls: failed to verify certificate: x509: certificate signed by unknown authoritytls: failed to verify certificate: x509: certificate signed by unknown authorityValidate the API first:
openssl s_client \
-connect api.ocp.example.com:6443 \
-servername api.ocp.example.com \
-CAfile lab-ca.crt </dev/nullopenssl s_client \
-connect api.ocp.example.com:6443 \
-servername api.ocp.example.com \
-CAfile lab-ca.crt </dev/nullIf the API returns:
Verify return code: 0 (ok)Verify return code: 0 (ok)but oc login still fails, validate the OAuth Route:
openssl s_client \
-connect oauth-openshift.apps.ocp.example.com:443 \
-servername oauth-openshift.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullopenssl s_client \
-connect oauth-openshift.apps.ocp.example.com:443 \
-servername oauth-openshift.apps.ocp.example.com \
-CAfile lab-ca.crt </dev/nullIf the issuer is similar to this:
issuer=CN=ingress-operator@...issuer=CN=ingress-operator@...and the output contains:
Verify return code: 19 (self-signed certificate in certificate chain)Verify return code: 19 (self-signed certificate in certificate chain)the default Ingress Controller is still using the OpenShift-generated certificate.
Create the wildcard certificate and apply the Ingress Controller patch described in steps 5 and 6.
Confirm that the API certificate uses the expected CA
Extract the certificate served by the API:
openssl s_client \
-connect api.ocp.example.com:6443 \
-servername api.ocp.example.com \
-showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERTIFICATE/{p=1} p{print} /END CERTIFICATE/{exit}' \
> server-api.crtopenssl s_client \
-connect api.ocp.example.com:6443 \
-servername api.ocp.example.com \
-showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERTIFICATE/{p=1} p{print} /END CERTIFICATE/{exit}' \
> server-api.crtValidate it:
openssl verify -CAfile lab-ca.crt server-api.crtopenssl verify -CAfile lab-ca.crt server-api.crtExpected output:
server-api.crt: OKserver-api.crt: OKCheck cert-manager resources
oc get clusterissuer
oc get certificate -A
oc get certificaterequest -Aoc get clusterissuer
oc get certificate -A
oc get certificaterequest -AInspect a failed Certificate resource:
oc describe certificate api-serving-cert \
-n openshift-configoc describe certificate api-serving-cert \
-n openshift-configFinal result
cert-manager now manages the certificate lifecycle for the OpenShift API and default application domain.
Custom CA
โ
cert-manager ClusterIssuer
โ
TLS Secrets
โโโ openshift-config / api-serving-cert
โโโ openshift-ingress / apps-wildcard-cert
โ
OpenShift API, OAuth, Console, and application RoutesCustom CA
โ
cert-manager ClusterIssuer
โ
TLS Secrets
โโโ openshift-config / api-serving-cert
โโโ openshift-ingress / apps-wildcard-cert
โ
OpenShift API, OAuth, Console, and application RoutesThe API, OAuth endpoint, Console, and application Routes use certificates signed by the same Custom CA. Certificates are declared as Kubernetes resources, renewed by cert-manager, and consumed by the appropriate OpenShift Operators.