In the previous article we deployed an internal certificate authority using Step-CA running inside Kubernetes. That CA is now capable of issuing certificates for internal infrastructure.
The next step is enabling automatic certificate issuance for Kubernetes services.
In modern Kubernetes environments this task is usually handled by cert-manager, which integrates with certificate authorities through protocols such as ACME.
In this setup we configure cert-manager to request certificates from our internal Step-CA instance.
Current Infrastructure Overview
Before configuring certificate automation, it is useful to understand the current our lab infrastructure layout.
VM - gitlab 172.21.5.153
docker - registry 172.21.5.75
VM - jenkins 172.21.5.154
docker - ldapman 172.21.5.76
VM - devpi 172.21.5.160
VM - vault 172.21.5.157
VM - postgresql 172.21.5.158
K8s - nexus 172.21.5.76
Some services run on virtual machines while others run inside Kubernetes. Kubernetes services will obtain their certificates automatically using cert-manager.
Installing cert-manager
cert-manager is installed using the official Helm chart.
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set installCRDs=true
This installation creates the controllers responsible for managing certificate resources inside the cluster.
Checking Available Ingress Classes
Before configuring certificate issuance we must identify the ingress class used by the cluster.
kubectl get ingressclass
Example output:
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 173d
public k8s.io/ingress-nginx <none> 173d
In this cluster the following ingress classes exist:
IngressClass
├─ nginx
└─ public (default)
To determine which ingress class is the default, inspect the annotations.
kubectl get ingressclass public -o yaml
kubectl get ingressclass nginx -o yaml
Look for the annotation:
ingressclass.kubernetes.io/is-default-class: "true"
Updating the Step-CA Deployment
Next, the Step-CA deployment is updated with the configuration that enables ACME support. https://github.com/faustobranco/devops-db/blob/master/SmallStep-CA/values-step3.yaml
helm upgrade --install step-ca smallstep/step-certificates --values values-step3.yaml -n step-ca --create-namespace
At this stage ACME is configured but cannot yet issue certificates because the CA endpoint itself still requires a valid TLS certificate.
Creating a Temporary Certificate for the CA Endpoint
Since ACME is not yet functional, a temporary certificate must be created manually for the Step-CA ingress endpoint.
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: step-ca-tls
namespace: step-ca
spec:
secretName: step-ca-tls
issuerRef:
name: step-ca-issuer
kind: ClusterIssuer
dnsNames:
- ca.devops-db.internal
EOF
This creates a Certificate resource that will eventually store the TLS certificate used by the CA service.
Checking the Step-CA Ingress
Verify that the ingress resource exists and exposes the CA service.
kubectl get ingress -n step-ca
Example output:
NAME CLASS HOSTS ADDRESS PORTS AGE
step-ca-step-certificates public ca.devops-db.internal 127.0.0.1 80, 443 20m
Generating a TLS Certificate for the CA Service
Because ACME is not yet active, we generate a certificate manually for the CA service.
Create the OpenSSL configuration file:
vi ca-cert.cnfConfiguration:
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = ca.devops-db.internal
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = ca.devops-db.internal
DNS.2 = step-ca-step-certificates.step-ca.svc.cluster.localGenerate the private key:
openssl genrsa -out ca.devops-db.internal.key 2048
Create the certificate request:
openssl req -new \
-key ca.devops-db.internal.key \
-out ca.devops-db.internal.csr \
-config ca-cert.cnf
Sign the certificate using the intermediate CA.
openssl x509 -req \
-in ca.devops-db.internal.csr \
-CA intermediate-ca.crt \
-CAkey intermediate-ca.key \
-CAcreateserial \
-out ca.devops-db.internal.crt \
-days 365 \
-sha256 \
-extfile ca-cert.cnf \
-extensions v3_req
Creating the Full Certificate Chain
The TLS secret used by Kubernetes must contain the full certificate chain.
cat ca.devops-db.internal.crt intermediate-ca.crt > fullchain.crt
Creating the TLS Secret in Kubernetes
Create the Kubernetes TLS secret that will be used by the Step-CA ingress.
kubectl create secret tls step-ca-tls \
--cert=fullchain.crt \
--key=ca.devops-db.internal.key \
-n step-ca \
--dry-run=client -o yaml | kubectl apply -f -
Restarting the Ingress Controller
Restart the ingress controller to ensure the new certificate is loaded.
kubectl rollout restart daemonset nginx-ingress-microk8s-controller -n ingress
Configuring cert-manager to Use Step-CA
Next we configure cert-manager to use Step-CA as an ACME certificate issuer.
First convert the root CA certificate to base64.
cat root-ca.crt | base64 -w0
Example:
cat root-ca.crt | base64 -w0
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZiVENDQTFXZ0F3SUJBZ0lVZjRPblgrZnIzaVZOT2RrMS9iaGJDNmVzM3Jvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRUxNQWtHQTFVRUJoTUNVRlF4RWpBUUJnTlZCQW9NQ1VSbGRrOXdjeTFFUWpFak1DRUdBMVVFQXd3YQpSR1YyVDNCekxVUkNJRWx1ZEdWeWJtRnNJRkp2YjNRZ1EwRXdIaGNOTWpZd016RXdNakl5T1RNd1doY05Nell3Ck16QTNNakl5T1RNd1dqQkdNUXN3Q1FZRFZRUUdFd0pRVkRFU01CQUdBMVVFQ2d3SlJHVjJUM0J6TFVSQ01TTXcKSVFZRFZRUUREQnBFWlhaUGNITXRSRUlnU1c1MFpYSnVZV3dnVW05dmRDQkRRVENDQWlJd0RRWUpLb1pJaHZjTgpBUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBTGFFNlE1SmQwTlh3NCt5L013MTNWbjBXL2J4SFdha00yMEUxTlMrCndOOXp3QkZoSTA0QVNtZVFVOENaOXdIRE1WVXJTa3lJRmJDRldHU0lIRUxJUGdYR3dmY3poY3hDbXBZNVlIRm4Kb1ZBcVNOU2VWU2tlVTRYSUtDbkVNNzdKY0ZlMys5NnlyOFVlYUdHU3lhdVBkczJDTDlQNkdicTJ4anZ4MmtmMwpFZzNQRzVOQ1kwbWt6cE5FVG0xZVBpU3h5RmVXSDFoanRENXBuakdGNWZYRlRiclpIQm1aOVpqQnprNEhkYnNjClpFSkV6Mk1HTGF6SEh1ZWMyV1BkdGtKdlF4VnhkR1lTTWJLS05YWlMxWDhlNHJOT0lzeGhLQlBPMFFPT29FYmQKcDc2L28vUVNQWFZ6b0NNK0RhL2xtK3A5b09uNW5zZm9xSEpmTU5Yb29KRDdMMjJYWG5adXVScHVhdmpBczhYcQpIUExoYXh1N0VJRXV6K0pyNU5yQkkyY0E3RzhvVE1WWUZSaVdXbGxQb2NiekJCN1NrYXV4RnRrbFRzZ00rdEF1Ckc3Y2haWGg3eFZhV1ArOVQ5Vk5IWHJieUdmY2JuclF4d3NwTFVFMkRYbmJEeXR6L0xZbW4wTlBvdHhYN1k4cTkKTWQvTGFteFkxd1dUR0dRQ1RtRnFBN0ZUb01Fbmp3WFZBOXAwd2lQL1AwajFtaG1SWUo3bmRHS05vNXFzSmcwQgpxZlhQMnlEQkFPRHRCWXQ3T0d1SVI1czQzU2ExQ0hOVzhPR210c0F1ejlCR3I3dWtGbmZ2VzVhdk55bytZQ3cyClJObkZpalQ0ZkppOEhhRTRVVkxOUzZEeWVERXF0Z0hOd1pTR0lUL0cvd2pqRGJrdE1CTTJSNUI3ZXpVOEtPOW0KdEIxdkFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCUzBBaWdBV2MrTnJRei9nOGlkMU0vRzNEdng1REFmQmdOVgpIU01FR0RBV2dCUzBBaWdBV2MrTnJRei9nOGlkMU0vRzNEdng1REFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHCkNTcUdTSWIzRFFFQkN3VUFBNElDQVFDa1kxNlVwL3ZabXozRmQ1SDI1ZWtDaXhyZW56YWEvT0lNNkgzWGZjejUKbnlDWE92bVN0alBYVXlweVJCYTd4MXp3b3JpYno3bEhDTHJYS0JVbzdMTXNBRnR0eGo5YWdtUlAycGExa1JPNwpXQjl4ZWNXemt6UDY5bFBFVytIVERsT2loZUxmZnBCeFVSQ0Z3VENjcU02T3dZQmp6YW0vRTFkdVI4QUVEVXlGClNZdTNSYXRBbmlnS3gvaHYrMGQwNFpoZHZINmJOUHU5VkNDZlJnOTFaZTVkMmpsZjJDTlhCQjVaVHRIVllXcXoKRGdjWiszZmJjMXZyN1ZZQks1Nm9SZDNIMVl1YkI4cFBEWHc0WDRvVVg1cE4xUC9DSGUxcGRZSFlBYVI2YjdGego3cW1yOHlMblVGcFRGeFZ1QUh6Zjd6SXVKTWlFTkc3REhSMmZBb2hPN1Z3QTdObGl0MVpoM0VuRlZaRFZPbCt6CkFvWWhVT3ExUlBZSmlvSzhkU242dUNtbWhCbE1qZXQ4QkdVVkd0Q1pxVlpwZ3FHMTc5TnJQQ3RlYXRHQXdlQVUKaFl2L0NTdXhxMko4Rnp4VGJVRU9uUjA3Mk1sQlA5ZThGb3Njc3lObW5xMmZyMGRKZTErdDFOVkRCdlZ0Q0NpZwpPNitRNWtpSVVyS29KMUJRZ3dDN05RSGhjSGpoTEgwcHRDTCsyS1FhdkhMcEhMb1NiT25ZNDcwZmp2UU1hMUZlCmRKenl3TUQ4T3pzNjFCdk5qY2orV1N0SEVSc25pTlE4YVgvTGNtR2c5TCtiWWVVeGtoOVM1RGZSckpoemwrcEIKZGlBeTVadUNWNnVQa1lVNTdwM0ZlYWkwbmNnS1AxbUxOT2Ztdmh5OXJzTWVTZ3hZTEN4ZERTTWNlb2crNVRybgplUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KThis value will be used as the CA bundle for the ACME configuration.
Creating the ClusterIssuer
The ClusterIssuer resource defines how cert-manager communicates with the internal certificate authority.
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: step-ca-issuer
spec:
acme:
server: https://ca.devops-db.internal/acme/acme/directory
email: admin@devops-db.internal
privateKeySecretRef:
name: step-ca-account-key
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZiVENDQTFXZ0F3SUJBZ0lVZjRPblgrZnIzaVZOT2RrMS9iaGJDNmVzM3Jvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRUxNQWtHQTFVRUJoTUNVRlF4RWpBUUJnTlZCQW9NQ1VSbGRrOXdjeTFFUWpFak1DRUdBMVVFQXd3YQpSR1YyVDNCekxVUkNJRWx1ZEdWeWJtRnNJRkp2YjNRZ1EwRXdIaGNOTWpZd016RXdNakl5T1RNd1doY05Nell3Ck16QTNNakl5T1RNd1dqQkdNUXN3Q1FZRFZRUUdFd0pRVkRFU01CQUdBMVVFQ2d3SlJHVjJUM0J6TFVSQ01TTXcKSVFZRFZRUUREQnBFWlhaUGNITXRSRUlnU1c1MFpYSnVZV3dnVW05dmRDQkRRVENDQWlJd0RRWUpLb1pJaHZjTgpBUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBTGFFNlE1SmQwTlh3NCt5L013MTNWbjBXL2J4SFdha00yMEUxTlMrCndOOXp3QkZoSTA0QVNtZVFVOENaOXdIRE1WVXJTa3lJRmJDRldHU0lIRUxJUGdYR3dmY3poY3hDbXBZNVlIRm4Kb1ZBcVNOU2VWU2tlVTRYSUtDbkVNNzdKY0ZlMys5NnlyOFVlYUdHU3lhdVBkczJDTDlQNkdicTJ4anZ4MmtmMwpFZzNQRzVOQ1kwbWt6cE5FVG0xZVBpU3h5RmVXSDFoanRENXBuakdGNWZYRlRiclpIQm1aOVpqQnprNEhkYnNjClpFSkV6Mk1HTGF6SEh1ZWMyV1BkdGtKdlF4VnhkR1lTTWJLS05YWlMxWDhlNHJOT0lzeGhLQlBPMFFPT29FYmQKcDc2L28vUVNQWFZ6b0NNK0RhL2xtK3A5b09uNW5zZm9xSEpmTU5Yb29KRDdMMjJYWG5adXVScHVhdmpBczhYcQpIUExoYXh1N0VJRXV6K0pyNU5yQkkyY0E3RzhvVE1WWUZSaVdXbGxQb2NiekJCN1NrYXV4RnRrbFRzZ00rdEF1Ckc3Y2haWGg3eFZhV1ArOVQ5Vk5IWHJieUdmY2JuclF4d3NwTFVFMkRYbmJEeXR6L0xZbW4wTlBvdHhYN1k4cTkKTWQvTGFteFkxd1dUR0dRQ1RtRnFBN0ZUb01Fbmp3WFZBOXAwd2lQL1AwajFtaG1SWUo3bmRHS05vNXFzSmcwQgpxZlhQMnlEQkFPRHRCWXQ3T0d1SVI1czQzU2ExQ0hOVzhPR210c0F1ejlCR3I3dWtGbmZ2VzVhdk55bytZQ3cyClJObkZpalQ0ZkppOEhhRTRVVkxOUzZEeWVERXF0Z0hOd1pTR0lUL0cvd2pqRGJrdE1CTTJSNUI3ZXpVOEtPOW0KdEIxdkFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCUzBBaWdBV2MrTnJRei9nOGlkMU0vRzNEdng1REFmQmdOVgpIU01FR0RBV2dCUzBBaWdBV2MrTnJRei9nOGlkMU0vRzNEdng1REFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHCkNTcUdTSWIzRFFFQkN3VUFBNElDQVFDa1kxNlVwL3ZabXozRmQ1SDI1ZWtDaXhyZW56YWEvT0lNNkgzWGZjejUKbnlDWE92bVN0alBYVXlweVJCYTd4MXp3b3JpYno3bEhDTHJYS0JVbzdMTXNBRnR0eGo5YWdtUlAycGExa1JPNwpXQjl4ZWNXemt6UDY5bFBFVytIVERsT2loZUxmZnBCeFVSQ0Z3VENjcU02T3dZQmp6YW0vRTFkdVI4QUVEVXlGClNZdTNSYXRBbmlnS3gvaHYrMGQwNFpoZHZINmJOUHU5VkNDZlJnOTFaZTVkMmpsZjJDTlhCQjVaVHRIVllXcXoKRGdjWiszZmJjMXZyN1ZZQks1Nm9SZDNIMVl1YkI4cFBEWHc0WDRvVVg1cE4xUC9DSGUxcGRZSFlBYVI2YjdGego3cW1yOHlMblVGcFRGeFZ1QUh6Zjd6SXVKTWlFTkc3REhSMmZBb2hPN1Z3QTdObGl0MVpoM0VuRlZaRFZPbCt6CkFvWWhVT3ExUlBZSmlvSzhkU242dUNtbWhCbE1qZXQ4QkdVVkd0Q1pxVlpwZ3FHMTc5TnJQQ3RlYXRHQXdlQVUKaFl2L0NTdXhxMko4Rnp4VGJVRU9uUjA3Mk1sQlA5ZThGb3Njc3lObW5xMmZyMGRKZTErdDFOVkRCdlZ0Q0NpZwpPNitRNWtpSVVyS29KMUJRZ3dDN05RSGhjSGpoTEgwcHRDTCsyS1FhdkhMcEhMb1NiT25ZNDcwZmp2UU1hMUZlCmRKenl3TUQ4T3pzNjFCdk5qY2orV1N0SEVSc25pTlE4YVgvTGNtR2c5TCtiWWVVeGtoOVM1RGZSckpoemwrcEIKZGlBeTVadUNWNnVQa1lVNTdwM0ZlYWkwbmNnS1AxbUxOT2Ztdmh5OXJzTWVTZ3hZTEN4ZERTTWNlb2crNVRybgplUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
solvers:
- http01:
ingress:
class: public
EOFAutomatic vs Manual Certificate Issuance
At this point the internal certificate authority is fully operational and integrated with Kubernetes through cert-manager.
There are two different approaches that can be used to issue certificates for services running inside the cluster.
Both approaches are valid and useful depending on the operational model and infrastructure requirements.
The following sections demonstrate both methods.
Creating a TLS Certificate for Nexus
In this example, I’m going to create a certificate for one of our services, which is hosted on Kubernetes, called Nexus.
But the process is the same for any service.
Now that cert-manager is configured, certificates can be issued automatically for Kubernetes services.
The following ingress definition exposes the Nexus service and instructs cert-manager to generate a certificate automatically.
kubectl apply -n default -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nexus-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: step-ca-issuer
spec:
ingressClassName: public
rules:
- host: nexus.devops-db.internal
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nexus
port:
number: 8081
tls:
- hosts:
- nexus.devops-db.internal
secretName: nexus-tls
EOFVerifying the Certificate
cert-manager will generate the certificate automatically.
kubectl get certificateCertificate issuance may take a short amount of time.
When a Certificate resource is created, cert-manager performs several internal steps including creating an Order and Challenge before requesting the certificate from the CA.
Example output:
NAME READY SECRET AGE
nexus-tls True nexus-tls 115sInspect the generated certificate:
kubectl get secret nexus-tls \
-o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -issuer -subjectExample result:
issuer=C=PT, O=DevOps-DB, CN=DevOps-DB Intermediate CA
subject=CN=nexus.devops-db.internalThis confirms that the certificate was issued by the internal certificate authority.
What Happens Automatically in the Cluster
Once an ingress resource is created with the cert-manager annotation, the entire certificate issuance workflow becomes automatic.
Ingress created
↓
cert-manager detects annotation
↓
Certificate resource created
↓
ACME request generated
↓
Step-CA issues certificate
↓
TLS secret created
↓
Ingress begins serving HTTPS
This automation allows Kubernetes services to obtain trusted TLS certificates without manual intervention.
Manual certificate issuance can be useful when:
- certificates must be versioned or stored in repositories
- infrastructure automation tools such as Terraform, Ansible, or CI pipelines generate certificates
- certificates need to be distributed outside Kubernetes
- the same certificate must be used across multiple environments
In this section we generate a certificate manually using the Step CLI, then inject it into Kubernetes as a TLS secret used by the Ingress controller.
Bootstrap the Step CLI
Before requesting certificates from the internal certificate authority, the Step CLI must be bootstrapped so it can trust the CA.
This step downloads the root certificate and stores the CA configuration locally.
step ca bootstrap \
--ca-url https://ca.devops-db.internal \
--fingerprint b8d4d77badb5aeca4d23a67269846aa3dbf3271c94c0cf97e82e482aab6e7732
This command initializes the Step CLI configuration inside:
~/.step/
The CLI can now securely communicate with the internal certificate authority.
Generate a Certificate Token
The internal CA uses provisioners to control how certificates are issued. In this environment a JWK provisioner named vm-hosts is used.
A token must first be generated to authorize the certificate request.
TOKEN=$(step ca token nexus.devops-db.internal \
--provisioner vm-hosts \
--key jwk-provisioner-pub.json)
This command generates a signed JWT token that authorizes the certificate request for the hostname nexus.devops-db.internal.
Request the Certificate from Step-CA
Using the generated token, the Step CLI can now request a certificate from the internal CA.
step ca certificate \
nexus.devops-db.internal \
nexus.devops-db.internal.crt \
nexus.devops-db.internal.key \
--token $TOKEN
This command performs several operations automatically:
- generates a private key
- creates a certificate signing request
- sends the request to Step-CA
- receives the signed certificate
At this point two files are generated:
nexus.devops-db.internal.crt
nexus.devops-db.internal.key
Create the Certificate Chain
The Kubernetes ingress controller expects the certificate chain to include both the service certificate and the intermediate CA certificate.
Create the full chain by concatenating the certificates.
cat nexus.devops-db.internal.crt intermediate-ca.crt > fullchain.crt
This file now contains:
service certificate
intermediate certificate
Create the TLS Secret in Kubernetes
The certificate and private key must now be stored inside Kubernetes as a TLS secret.
kubectl create secret tls nexus-tls \
--cert=fullchain.crt \
--key=nexus.devops-db.internal.key \
-n default
Verify that the secret was created successfully.
kubectl get secret nexus-tls -n default
Example output:
NAME TYPE DATA AGE
nexus-tls kubernetes.io/tls 2 17s
Configure the Ingress to Use the TLS Secret
The Ingress resource must reference the TLS secret that was just created.
The TLS section should include:
tls:
- hosts:
- nexus.devops-db.internal
secretName: nexus-tls
Apply the Ingress configuration.
kubectl apply -n default -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nexus-ingress
spec:
ingressClassName: public
rules:
- host: nexus.devops-db.internal
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nexus
port:
number: 8081
tls:
- hosts:
- nexus.devops-db.internal
secretName: nexus-tls
EOF
Once applied, the ingress controller automatically loads the certificate from the TLS secret and begins serving HTTPS traffic.
Verify the Certificate
The final step is verifying that the correct certificate is being served by the ingress endpoint.
echo | openssl s_client -connect nexus.devops-db.internal:443 -servername nexus.devops-db.internal 2>/dev/null | openssl x509 -noout -issuer -subject
Example result:
issuer=C=PT, O=DevOps-DB, CN=DevOps-DB Intermediate CA
subject=CN=nexus.devops-db.internal
This confirms that the certificate presented by the Nexus service was successfully issued by the internal certificate authority.
Manual Certificate Workflow
The complete workflow for manual certificate issuance can be summarized as follows:
Generate token
↓
Request certificate from Step-CA
↓
Create certificate chain
↓
Create Kubernetes TLS secret
↓
Ingress serves HTTPS
This method provides full control over certificate creation and distribution, making it particularly useful when certificates must be versioned, stored in artifact repositories, or distributed across multiple infrastructure environments.
Automatic vs Manual Certificate Issuance
Both approaches demonstrated in this guide are valid and useful depending on the infrastructure requirements.
Choosing between automatic and manual certificate issuance depends mainly on how certificates are managed and distributed across the environment.
Automatic Certificate Issuance (cert-manager)
The automated approach uses cert-manager integrated with Step-CA through ACME.
In this model, certificates are created and renewed automatically whenever a Kubernetes Ingress resource requests TLS.
Typical workflow:

This approach is ideal for:
- Kubernetes-native applications
- services that require automatic certificate renewal
- environments where certificates should be managed entirely by the cluster
Because cert-manager automatically handles renewals, this method minimizes operational overhead.
Manual Certificate Issuance
The manual approach uses the Step CLI to generate certificates directly from the internal certificate authority.
The certificate is then injected into Kubernetes as a TLS secret and referenced by the Ingress resource.
Typical workflow:

This approach is useful when:
- certificates must be versioned
- certificates are distributed through artifact repositories
- infrastructure automation tools generate certificates
- the same certificate is used both inside and outside Kubernetes
It is also commonly used for services running on virtual machines, load balancers, or external infrastructure components.