Step-by-step tutorial, written from an actual deployment of Certificate Manager 8.15.1-1 (CF server image 8.15.1-1, PGW image 8.15.1-1) on a 2-node kubeadm cluster.
Status of this document (2026-11-12) Steps 1-8 were executed and verified. Step 9 (license) is pending: CF will not start without a Nexus-issued
*.licensefile, and none was available. Step 9 now contains the full procedure to obtain and apply it. Steps 10-12 (verification, bootstrap, protocol enablement) cannot be completed until then and are written from the chart contents and vendor guidance, not from a tested run. They are marked (untested). See the Completion checklist near the end for what is done and what remains.
0. Environment used
| Item | Value |
|---|---|
| Kubernetes | v1.35.10 (kubeadm), containerd 2.2.1, Calico v3.31 |
| Nodes | 1 control-plane (16.4.37.44), 1 worker (13.204.63.105), Ubuntu 24.04, 2 vCPU / 8 GB each |
| Access | SSH as ubuntu with sudo; kubeconfig at /etc/kubernetes/admin.conf (root only) |
| Namespace | nexus (CM, PGWY and the database share it) |
| Package | certificate-manager-8.15.1-1.zip from the Nexus Support Portal |
Package contents that matter:
certificate-manager-8.15.1-1/
images/cf-server-image-8.15.1-1.tar # Certificate Factory (CM server) image
images/pgw-image-8.15.1-1.tar # Protocol Gateway image
deployment/helm/cf-8.15.1-SNAPSHOT-1.tgz # base chart
deployment/helm/cf-example-8.15.1-SNAPSHOT-1.tgz # configurable parent chart (use this for CM)
deployment/helm/pgw-8.15.1-SNAPSHOT-1.tgz # base chart
deployment/helm/pgw-example-8.15.1-SNAPSHOT-1.tgz # configurable parent chart (use this for PGWY)
install/PostgreSQL/{create_db_and_role.sql,create_cmdb.sql,add_bootca.sql}
Code language: PHP (php)
Run kubectl/helm on the control-plane node as root (
sudo -i) or prefix withsudo, andexport KUBECONFIG=/etc/kubernetes/admin.conf.
1. Prerequisites checklist
- A working cluster:ย
kubectl get nodesย shows all nodesยReady. - A ReadWriteMany (RWX) StorageClass.ย CF’s shared volumes are RWX. A cluster with onlyย
local-pathย is not enough (see step 3). - A PostgreSQL databaseย reachable from the cluster (step 4).
- Container images available to every nodeย (step 5). The images are not public.
- A CM license fileย (
*.license) from Nexus (step 9). - Helm 3.6.2+ (step 2).
- Working pod-to-pod networking between nodesย (see Troubleshooting #1). Check it now:
# from the control-plane: ping a pod that runs on the worker kubectl get pods -A -o wide | grep <worker-node-name> # pick any pod IP ping -c2 <that-pod-ip>
2. Install Helm (if missing)
curl -sSL https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz | tar xz -C /tmp
sudo install /tmp/linux-amd64/helm /usr/local/bin/helm
helm version --short
Code language: JavaScript (javascript)
3. RWX storage (NFS) – lab setup
Skip this if you already have an RWX CSI class (EFS, Azure Files, CephFS…). For a lab, an NFS export on the control-plane node plus the NFS subdir provisioner is enough.
On the control-plane node:
sudo apt-get install -y nfs-kernel-server
sudo mkdir -p /srv/nfs/k8s && sudo chmod 777 /srv/nfs/k8s
echo "/srv/nfs/k8s 172.31.0.0/16(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
sudo exportfs -ra && sudo systemctl enable --now nfs-kernel-server
Code language: PHP (php)
Adjust 172.31.0.0/16 to your node subnet.
On every other node:
sudo apt-get install -y nfs-common
Code language: JavaScript (javascript)
Install the provisioner (use the control-plane node’s private IP for nfs.server):
helm repo add nfs-subdir https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm repo update
helm install nfs-provisioner nfs-subdir/nfs-subdir-external-provisioner \
-n nfs-provisioner --create-namespace \
--set nfs.server=172.31.10.122 \
--set nfs.path=/srv/nfs/k8s \
--set storageClass.name=nfs-rwx \
--set storageClass.defaultClass=false \
--set storageClass.reclaimPolicy=Retain \
--wait
kubectl get sc nfs-rwx
Code language: JavaScript (javascript)
NFS on a cluster node is for labs only. Use a managed RWX service in production.
Create the namespace:
kubectl create namespace nexus
Code language: PHP (php)
4. PostgreSQL
CM needs a database cmdb and a user lcmreq. For a lab, run PostgreSQL in the cluster. For production use a managed/HA database and skip the StatefulSet below (run only the SQL).
4.1 Prepare the SQL
Copy the three scripts from install/PostgreSQL/ to the control-plane node. Change the default password in create_db_and_role.sql (REQreq01#):
LCM_PW=$(openssl rand -hex 10) <em># keep this; it goes into the CM Helm values later</em>
sed -i "s/REQreq01#/$LCM_PW/" create_db_and_role.sql
Code language: PHP (php)
4.2 Deploy PostgreSQL
postgres.yaml (the Service is named cmdb, matching the chart’s default JDBC host):
apiVersion: v1
kind: Secret
metadata: {name: postgres-auth, namespace: nexus}
stringData:
POSTGRES_PASSWORD: "<superuser-password>"
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: cmdb, namespace: nexus}
spec:
serviceName: cmdb
replicas: 1
selector: {matchLabels: {app: cmdb}}
template:
metadata: {labels: {app: cmdb}}
spec:
containers:
- name: postgres
image: postgres:16
envFrom: [{secretRef: {name: postgres-auth}}]
env: [{name: PGDATA, value: /var/lib/postgresql/data/pgdata}]
ports: [{containerPort: 5432}]
readinessProbe: {exec: {command: ["pg_isready","-U","postgres"]}, periodSeconds: 5}
volumeMounts: [{name: data, mountPath: /var/lib/postgresql/data}]
resources: {requests: {cpu: 100m, memory: 256Mi}}
volumeClaimTemplates:
- metadata: {name: data}
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources: {requests: {storage: 5Gi}}
---
apiVersion: v1
kind: Service
metadata: {name: cmdb, namespace: nexus}
spec:
selector: {app: cmdb}
ports: [{port: 5432, targetPort: 5432}]
Code language: JavaScript (javascript)
kubectl apply -f postgres.yaml
kubectl -n nexus rollout status statefulset/cmdb
4.3 Load the schema
K="kubectl -n nexus"
$K exec -i cmdb-0 -- psql -X -v ON_ERROR_STOP=1 -U postgres -d postgres < create_db_and_role.sql
$K exec -i cmdb-0 -- psql -X -v ON_ERROR_STOP=1 -q -U postgres -d cmdb < create_cmdb.sql
$K exec -i cmdb-0 -- psql -X -v ON_ERROR_STOP=1 -q -U postgres -d cmdb < add_bootca.sql
Code language: PHP (php)
create_cmdb.sqlย creates the tables and grants access toยlcmreq.add_bootca.sqlย loads theย vendor Boot CAย used for bootstrapping. It matchesยbootca.cerย andยboot_vro.p12ย shipped in the example charts. It is a well-known sample CA: it must be replaced during CM bootstrap before production use (step 11).
Verify:
kubectl -n nexus exec cmdb-0 -- psql -U postgres -d cmdb -c "\dt" | tail -3 <em># ~46 tables</em>
kubectl -n nexus exec cmdb-0 -- psql -U postgres -d cmdb -tc "select objectid from adminstore"
<em># expected: c-BootCA, k-BootCAkey</em>
Code language: HTML, XML (xml)
5. Load the container images on every node
The images are in the package as tar files, and the Helm values use pullPolicy: IfNotPresent, so importing them into containerd’s k8s.io namespace on every node is enough (no registry needed for a lab).
<em># on each node</em>
sudo ctr -n k8s.io images import cf-server-image-8.15.1-1.tar
sudo ctr -n k8s.io images import pgw-image-8.15.1-1.tar
sudo ctr -n k8s.io images ls -q | grep smartid
<em># docker.io/smartid/certificatemanager/cf-server:8.15.1-1</em>
<em># docker.io/smartid/certificatemanager/pgw:8.15.1-1</em>
Code language: HTML, XML (xml)
Tips:
- The tars are ~650 MB. Over a slow link,ย
scpย them toย oneย node, thenยscpย from that node to the others over the private network. - For production, push to a private registry and setย
image.repositoryย plusยimagePullSecretsย instead. Nexus does not allow publishing CM images publicly.
6. Extract the Helm charts
mkdir -p ~/nexus-helm && cd ~/nexus-helm
for f in /path/to/deployment/helm/*.tgz; do tar xzf "$f"; done
ls <em># cf cf-example pgw pgw-example</em>
Code language: HTML, XML (xml)
cf-example and pgw-example are the charts you configure. They already contain the cf / pgw base charts under charts/ and the example config files under files/.
7. Install Certificate Manager
7.1 Values file (cm-values.yaml)
config:
database:
name: "jdbc:postgresql://cmdb.nexus.svc.cluster.local:5432/cmdb"
user: "lcmreq"
password: "<LCM_PW from step 4.1>"
connections: "20"
cf:
replicaCount: 1 <em># chart default is 2; 1 suits small lab nodes</em>
image:
repository: smartid/certificatemanager/cf-server
tag: 8.15.1-1
pullPolicy: IfNotPresent
pvc:
cf:
storageClassName: nfs-rwx <em># must support ReadWriteMany</em>
size: 200Mi
cis:
storageClassName: nfs-rwx
size: 100Mi
configStatePvc:
storageClassName: local-path <em># chart default is "default"; this cluster has no default class</em>
Code language: HTML, XML (xml)
Notes on what we changed from the chart defaults:
database.name/password: point at our PostgreSQL and use the non-default password.pvc.*.storageClassName: chart default isยazurefile-csiย (Azure only).configStatePvc.storageClassName: chart default isยdefault; if your cluster has no default StorageClass the PVC staysยPending. Use an RWO class that exists.- The password is passed as a plain Helm value. It is stored in the Helm release secret. For production inject it more securely.
7.2 Place the license (required, see step 9)
cp /path/to/<your>.license ~/nexus-helm/cf-example/files/license/
Code language: HTML, XML (xml)
The chart builds the secret nexus-cf-license-secret from files/license/*.license. The file must end in .license.
7.3 Install
cd ~/nexus-helm/cf-example
helm dependency build
helm template cm . -n nexus -f ../cm-values.yaml > /tmp/cm-rendered.yaml <em># optional dry check</em>
helm install cm . -n nexus -f ../cm-values.yaml --wait --timeout 15m
Code language: HTML, XML (xml)
Release name cm produces the StatefulSet and Service cm-cf (port 5009). PGWY’s default cmhost: cm-cf relies on this name, so keep the release name cm.
7.4 Check
kubectl -n nexus get pods,pvc,svc
kubectl -n nexus logs cm-cf-0 -c cf --tail=50
Code language: JavaScript (javascript)
Expected once licensed: cm-cf-0 is 1/1 Running; PVCs cf and cis-keystore are Bound (RWX), configstate-cm-cf-0 is Bound (RWO).
8. Install Protocol Gateway
8.1 Create a TLS keystore for Tomcat (required, not in the package)
PGW’s server.xml expects /opt/tomcat/conf/tomcat-tls.p12 (password 1234), delivered through the files/tomcat/ directory of the chart. Without it, Tomcat fails with FileNotFoundException: /opt/tomcat/conf/tomcat-tls.p12.
Lab self-signed certificate:
cd ~/nexus-helm/pgw-example
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
-keyout /tmp/tls.key -out /tmp/tls.crt \
-subj "/CN=pgw.nexus.svc.cluster.local/O=Nexus Lab" \
-addext "subjectAltName=DNS:pgw,DNS:pgw.nexus.svc.cluster.local,DNS:localhost"
openssl pkcs12 -export -in /tmp/tls.crt -inkey /tmp/tls.key -name tomcat \
-out files/tomcat/tomcat-tls.p12 -passout pass:1234
rm -f /tmp/tls.key /tmp/tls.crt
Code language: JavaScript (javascript)
For production use a certificate from your CA with the real DNS names, and change the keystore password in server.xml as well.
8.2 Values file (pgw-values.yaml)
cmhost: cm-cf <em># service name of CF; the chart writes this into cm-gateway.properties</em>
pgw:
replicaCount: 1
image:
repository: smartid/certificatemanager/pgw
tag: 8.15.1-1
pullPolicy: IfNotPresent
Code language: HTML, XML (xml)
8.3 Install
cd ~/nexus-helm/pgw-example
helm dependency build
helm install pgw . -n nexus -f ../pgw-values.yaml
kubectl -n nexus get pods,svc
Code language: JavaScript (javascript)
The Service pgw exposes 8080 (HTTP / health, do not publish), 8443 (TLS) and 8444 (mutual TLS).
PGW needs CF running. At start-up it connects to CM (
cm-cf). If CF is down or the name does not resolve, PGW exits withStart failed: Protocol Gateway/Ping bad configuration. Always bring CM up first. After CF is healthy, restart PGW:kubectl -n nexus delete pod pgw-0.
If you change anything under files/ and run helm upgrade, delete the pod afterwards so it re-reads the ConfigMaps.
9. License – PENDING
CF logs on every start:
license critical No license files were found in directory /opt/cm/server/license.
oper critical Startup Failure: License check failed ... License directory contains no .license files
CF checks the license directory at every start and refuses to run without a valid file. The license is issued by Nexus and is tied to your organisation and the products/modules you bought. It is not included in the install package and cannot be generated locally.
9.1 Obtain the license
- Sign in to the Nexus Support Portal (https://support.nexusgroup.com/) with your customer account, or ask your Nexus account manager / support contact.
- Request (or download) theย Certificate Manager 8.15ย license for your installation. Give them what they ask for (typically customer name and the environment: test or production).
- You receive one or more files ending inย
.license. Keep the original safe. - Confirm it covers what you will use: the CM server, and the PGWY protocols you plan to enable (for example ACME, EST, SCEP). A missing module shows up later as a disabled servlet, not at start-up.
9.2 Apply it with Helm (recommended)
The chart builds the secret nexus-cf-license-secret from cf-example/files/license/*.license. The file name must end in .license.
<em># on the control-plane node</em>
cp /path/to/<your>.license ~/nexus-helm/cf-example/files/license/
ls -l ~/nexus-helm/cf-example/files/license/ <em># readme.md + your .license</em>
cd ~/nexus-helm/cf-example
helm upgrade cm . -n nexus -f ../cm-values.yaml
<em># confirm the secret now contains the file</em>
kubectl -n nexus get secret nexus-cf-license-secret -o jsonpath='{.data}' | grep -o '"[^"]*\.license"'
<em># restart so the pod re-mounts the secret</em>
kubectl -n nexus delete pod cm-cf-0
kubectl -n nexus logs -f cm-cf-0 -c cf
Code language: PHP (php)
Healthy start-up: the License check failed / Startup Failure lines disappear, CF reports that the license was loaded, connects to the database, and the pod becomes 1/1 Running.
9.3 Alternative: create the secret directly (keeps the file out of the chart directory)
Helm always renders the secret from files/license/, so if you prefer not to store the license in the chart folder, copy the file in only for the helm upgrade, then remove it:
cp /path/to/<your>.license ~/nexus-helm/cf-example/files/license/
helm upgrade cm . -n nexus -f ../cm-values.yaml
rm ~/nexus-helm/cf-example/files/license/*.license
Code language: HTML, XML (xml)
(Later upgrades re-render the secret from the folder, so the file must be present again for every helm upgrade, otherwise the secret is emptied and CF fails on its next restart.)
9.4 Renewal or change of license
Repeat 9.2 with the new file (remove the old one first if it should be replaced rather than added). Always restart the CF pod afterwards. Check expiry dates with Nexus support; there is no warning if a license is about to lapse other than CF log messages.
9.5 License troubleshooting
| Log message / symptom | Meaning | Fix |
|---|---|---|
No license files were found in directory /opt/cm/server/license | Secret is empty or the file doesn’t end in .license | Rename, re-run helm upgrade, restart pod |
Same message after helm upgrade | Old pod still running with the old secret | kubectl -n nexus delete pod cm-cf-0 |
| License rejected / invalid / expired | File is for another product/version or has expired | Request a new file from Nexus |
| CF runs but a PGWY protocol does nothing | License lacks that module | Ask Nexus to extend the license |
Never share the license file publicly or commit it to a repository.
10. Verification (untested – needs the license)
kubectl -n nexus get pods <em># cmdb-0, cm-cf-0, pgw-0 all Running/Ready</em>
helm list -n nexus
kubectl -n nexus logs cm-cf-0 -c cf --tail=100
kubectl -n nexus logs pgw-0 --tail=100
Code language: HTML, XML (xml)
Reach PGW from your workstation:
kubectl -n nexus port-forward svc/pgw 8080:8080 8443:8443 8444:8444
curl -i http://localhost:8080/health
curl -ik https://localhost:8443/ <em># -k: lab certificate</em>
Code language: HTML, XML (xml)
Reach CF with Nexus CM client tools (controlled testing only):
kubectl -n nexus port-forward svc/cm-cf 5009:5009
11. Bootstrap CM (untested)
A fresh install trusts the vendor sample Boot CA. Before production, follow Nexus’s “Bootstrap Certificate Manager” procedure for 8.15 (Nexus documentation site): create your own Issuing CA and officers, replace the bootstrap identity, and revoke the Boot CA once the replacement is verified. Helm rollback does not undo database changes or PKI key state, so take a database backup first.
12. Enable PGWY protocols (untested)
The protocol servlets (ACME, EST, SCEP, CMP, REST…) are installed but off by default. A running pod does not mean enrollment works. Edit the matching file in pgw-example/files/conf/ (for example acme.properties: start = true), helm upgrade pgw, and delete the pod. Each protocol also needs a matching procedure/profile in CM.
For production access, expose PGW through a layer-4 (TCP) load balancer if you need TLS client-certificate authentication, because an HTTP-terminating ingress breaks mutual TLS.
Out of scope here: Nexus Hermod (authentication service for the CM Web UI) and the CM Web UI (a separate WAR/Tomcat install per the vendor) were not part of this task.
Completion checklist
Tick these off in order. Items 1-6 were completed on the lab cluster; 7 onward need the license.
- [x] 1. Cluster healthy, cross-node pod networking and DNS working (Troubleshooting #1)
- [x] 2. Helm installed;ย
nfs-rwxย StorageClass created; namespaceยnexusย exists - [x] 3. PostgreSQL running;ย
cmdbย schema and Boot CA loaded - [x] 4. CF and PGW images imported onย everyย node
- [x] 5.ย
cmย andยpgwย Helm releases installed (helm list -n nexusย showsยdeployed) - [x] 6.ย
tomcat-tls.p12ย created for PGW - [ ] 7.ย License file obtained and applied (step 9)
- [ ] 8.ย
cm-cf-0ย isย1/1 Running - [ ] 9.ย
pgw-0ย restarted andย1/1 Running;ยcurl http://localhost:8080/healthย returns 200 via port-forward - [ ] 10. CM bootstrap done, Boot CA replaced (step 11)
- [ ] 11. At least one PGWY protocol enabled and a test certificate issued (step 12)
- [ ] 12. Passwords rotated, database backup scheduled, monitoring in place
Troubleshooting (all hit during this deployment)
| # | Symptom | Cause | Fix |
|---|---|---|---|
| 1 | PGW: UnknownHostException: cm-cf / Temporary failure in name resolution; pods on one node cannot reach CoreDNS; ping to a pod on the other node gets 100% loss | Cross-node pod traffic dropped. Calico was routing without encapsulation (ipipMode: Never, vxlanMode: Never), which on AWS requires source/destination check disabled on every EC2 node | Disable source/dest check on all nodes (AWS), or enable IPIP/VXLAN on the Calico IPPool. After the fix, ping a pod on the other node and nslookup cm-cf.nexus.svc.cluster.local from a pod on each node |
| 2 | cm-cf-0 CrashLoopBackOff: License directory contains no .license files | No license file in cf-example/files/license/ | Step 9 |
| 3 | pgw-0 fails: FileNotFoundException: /opt/tomcat/conf/tomcat-tls.p12 | Keystore not shipped in the package | Step 8.1 |
| 4 | PVC Pending for cf / cis-keystore | StorageClass has no RWX (e.g. local-path) | Step 3 / set cf.pvc.*.storageClassName |
| 5 | PVC configstate-cm-cf-0 Pending | configStatePvc.storageClassName: default but no default class | Set it to an existing RWO class |
| 6 | ImagePullBackOff / ErrImagePull | Image not on that node, or tag mismatch | Import on every node (step 5); names must be smartid/certificatemanager/{cf-server,pgw}:8.15.1-1 |
| 7 | scp of the image tars takes forever | Slow uplink | Copy once, relay node-to-node |
| 8 | CF restarts but ignores a new license | Pod still has the old secret | kubectl -n nexus delete pod cm-cf-0 after helm upgrade |
Useful commands:
kubectl -n nexus describe pod <pod>
kubectl -n nexus logs <pod> --previous
kubectl -n nexus get events --sort-by=.lastTimestamp | tail -20
helm history cm -n nexus ; helm history pgw -n nexus
Code language: HTML, XML (xml)
Uninstall
helm uninstall pgw -n nexus
helm uninstall cm -n nexus
kubectl -n nexus delete statefulset cmdb svc/cmdb <em># database</em>
kubectl -n nexus delete pvc --all <em># destroys data</em>
Code language: HTML, XML (xml)
Security notes
- Do not leave SSH passwords in scripts or chat. Use SSH keys and rotate any shared password.
- Change every default:ย
lcmreqย password, Tomcat keystore password (1234), the sample Boot CA, truststore password (123456ย inยserver.xml). - Helm values containing passwords end up in Helm release secrets; restrict access to theย
nexusย namespace. - The NFS export usesย
no_root_squashย and open permissions: lab only.
I’m Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms.
I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.
Find Trusted Cardiac Hospitals
Compare heart hospitals by city and services โ all in one place.
Explore Hospitals