Zero Touch PQC PKI Deployment with Kubernetes, Helm, and PQC Composite Certificates
ENTERPRISE
In this guide, you will learn how to deploy a PQC-ready PKI in Kubernetes using automation and real HSMs. The outcome is a PQC-enabled PKI with a root CA and subordinate CAs, deployed in separate Kubernetes namespaces, backed by SecuroSys and Fortanix HSMs, and initialized entirely through Helm and EJBCA ConfigDump.
The guide demonstrates all the steps of the configuration: generating keys on HSMs, creating and subordinating CAs, issuing certificates, and finalizing the environment through automated Helm upgrades, all without manual intervention.
To conclude, the guide demonstrates real-world use cases of the deployed PQC PKI using off-the-shelf software:
Hybrid and fully quantum-safe TLS connections using standard Apache, curl, and OpenSSL
PQC CMS signing and verification using SignServer and OpenSSL
About PQC Composite Signatures and Certificates
Video
Prerequisites
For this guide, a special build of EJBCA including PQC composite certificates was used. Please get in touch with your sales representative if you are interested in testing PQC composite certificates.
Before you begin, you need:
Kubernetes deployed, version 1.30 or later. MicroK8s was used for this tutorial
Internet access
Helm installed
An available load balancer. Metal LB was used for this tutorial
Step 1 - Create namespaces for the deployments
Check that the environment is fresh and that nothing is there:
kubectl get all -A
Create namespaces for the TechMeetup deployment
kubectl create namespace forseti
kubectl create namespace jarls
kubectl create namespace karls
Step 2 - Deploy Database
Add the
groundhog2khelm repo:
helm repo add groundhog2k https://groundhog2k.github.io/helm-charts
Create a secret for
MARIADB_ROOT_PASSWORD:
kubectl -n forseti create secret generic mariadb-secret --from-literal=MARIADB_ROOT_PASSWORD=foo123
kubectl -n forseti create secret generic mariadb-user-secret --from-literal=MARIADB_DATABASE=jarl --from-literal=MARIADB_USER=jarl --from-literal=MARIADB_PASSWORD=foo123
Create a Directory tree for the K8s configuration:
mkdir -p ~/configs/mariadb ~/configs/root ~/configs/sub
Create the MariaDB helm
mariadb-override.yamlfile:
cat > ~/configs/mariadb/mariadb-override.yaml << EOF
## Default values for MariaDB deployment
## MariaDB docker image
image:
registry: "docker.io"
repository: "mariadb"
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "latest"
## Optional service account
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# Resource limits and requests
resources:
limits:
cpu: 2
memory: 2048Mi
requests:
cpu: 1000m
memory: 1024Mi
## Use Kubernetes Deployment instead of StatefulSet
useDeployment: false
## Database configuration
settings:
## Arguments for the container entrypoint process
arguments:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
## Optional existing secret for the MariaDB root password
existingSecret: mariadb-secret
## Set true to skip loading timezone data during init
skipTZInfo: false
## Storage parameters
storage:
## Alternative set requestedSize to define a size for a dynamically created PVC
requestedSize: 10Gi
userDatabase:
existingSecret: mariadb-user-secret
name:
secretKey: MARIADB_DATABASE
user:
secretKey: MARIADB_USER
password:
secretKey: MARIADB_PASSWORD
EOF
Deploy MariaDB using the Helm chart with override:
helm install -n forseti forseti-mariadb groundhog2k/mariadb -f ~/configs/mariadb/mariadb-override.yaml
# Uninstall
helm uninstall -n forseti forseti-mariadb
Exec into the mariadb container:
kubectl -n forseti exec -it pod/forseti-mariadb-0 -c mariadb -- /bin/bash
Access the database SQL:
mariadb -u root -p
Type in the password for the DB root account
Create the additional databases:
CREATE DATABASE karl CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON karl.* TO 'karl'@'%' IDENTIFIED BY 'foo123';
CREATE DATABASE thrall01 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON tharll01.* TO 'thrall01'@'%' IDENTIFIED BY 'foo123';
CREATE DATABASE thrall02 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON tharll02.* TO 'thrall02'@'%' IDENTIFIED BY 'foo123';
Enter
exitto exit the SQL shellEnter
exitto exit the container exec session
Step 3 - View DB logs
Use the following command to view the pod logs for the database:
kubectl -n forseti logs -f pod/forseti-mariadb-0 -c mariadb
Step 4 - Deploy Root CA
Create the
after-deployed-pre.shconfigmap file:
cat > ~/configs/root/after-deployed-pre.yaml << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: after-deployed-pre
data:
after-deployed-pre.sh: |
#!/bin/bash
if /opt/keyfactor/bin/ejbca.sh cryptotoken list | grep "RootCa"; then
return 0
else
/opt/keyfactor/bin/ejbca.sh cryptotoken create --token RootCa --autoactivate true --pin "\${HSM_TOKEN_PIN}" --type SecurosysCryptoToken --securosysauthenticationtype token --securosysauthtoken "" --securosysrestapi "https://primusdev.cloudshsm.com" --securosysapprovalkey 30 --securosysauthcert "" --securosysmanagementkey "" --securosysoperationkey "" --securosysservicekey ""
# Generate keys on HSM
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias signKeyEc01 --keyspec secp521r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias defaultKeyEc01 --keyspec 4096 --key-usage SIGN_ENCRYPT
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias signKeyPq01 --keyspec ML-DSA-87 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias defaultKeyPq01 --keyspec 4096 --key-usage SIGN_ENCRYPT
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias signKeyHyPq01 --keyspec ML-DSA-87 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias signKeyHyEc01 --keyspec secp521r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias defaultKeyHy01 --keyspec 4096 --key-usage SIGN_ENCRYPT
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias testKey-Ec --keyspec secp256r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCa --alias testKey-Ml-Dsa --keyspec ML-DSA-87 --key-usage SIGN
fi
# Create Soft Crypto Token for Composite keys
/opt/keyfactor/bin/ejbca.sh cryptotoken create --token RootCompositeCa --autoactivate true --type SoftCryptoToken --pin foo123
# Generate keys on soft token
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCompositeCa --alias signKey001 --keyspec MLDSA87-ECDSA-P521-SHA512
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCompositeCa --alias defaultKey001 --keyspec 4096
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token RootCompositeCa --alias testKey --keyspec MLDSA87-ECDSA-P521-SHA512
EOF
Create the configmap for the
after-deployed-pre.shfile:
kubectl -n jarls apply -f ~/configs/root/after-deployed-pre.yaml
Create a secret for the Securosys HSM Token PIN:
kubectl -n jarls create secret generic configdump-secrets --from-literal=HSM_TOKEN_PIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.ey
Create the Root configdump configmap:
Create the configmap for the Root configdump:
kubectl -n jarls create configmap ca-init-configmap --from-file=configdump.json=configs/root/configdump.json
Create the database secret:
kubectl -n jarls create secret generic root-db-credentials \
--from-literal=DATABASE_USER=jarl \
--from-literal=DATABASE_PASSWORD=foo123
Create the Password Encryption Key (PEK) secret:
kubectl -n jarls create secret generic root-pek --from-literal=PASSWORD_ENCRYPTION_KEY=Rkjir6RVtRp6rPF1rpzhnbedVV01DYsJM1Ol94
Create the
root-override.yamlvalues file:
Deploy the Root CA
helm install -n jarls root -f ~/configs/root/root-override.yaml oci://repo.keyfactor.com/charts/ejbca
# Upgrade
helm upgrade -n jarls root -f configs/root/root-override.yaml oci://repo.keyfactor.com/charts/ejbca
Export the Root CA certs from the Root instance:
kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- /opt/keyfactor/bin/ejbca.sh ca getcacert --caname Ecdsa-Root-G1 -f /var/tmp/Ecdsa-Root-G1.crt
kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- /opt/keyfactor/bin/ejbca.sh ca getcacert --caname Pqc-Root-G1 -f /var/tmp/Pqc-Root-G1.crt
kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- /opt/keyfactor/bin/ejbca.sh ca getcacert --caname Composite-Root-G1 -f /var/tmp/Composite-Root-G1.crt
kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- /opt/keyfactor/bin/ejbca.sh ca getcacert --caname Hybrid-Root-G1 -f /var/tmp/Hybrid-Root-G1.crt
EcdsaRootG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Ecdsa-Root-G1.crt)"
PqcRootG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Pqc-Root-G1.crt)"
CompositeRootG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Composite-Root-G1.crt)"
HybridRootG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Hybrid-Root-G1.crt)"
echo "$EcdsaRootG1" > ~/configs/root/Ecdsa-Root-G1.crt
echo "$PqcRootG1" > ~/configs/root/Pqc-Root-G1.crt
echo "$CompositeRootG1" > ~/configs/root/Composite-Root-G1.crt
echo "$HybridRootG1" > ~/configs/root/Hybrid-Root-G1.crt
Create a configmap file of the root CA certificates:
kubectl -n karls create configmap root-ca-files --from-file=configs/root/Ecdsa-Root-G1.crt --from-file=configs/root/Pqc-Root-G1.crt --from-file=configs/root/Composite-Root-G1.crt --from-file=configs/root/Hybrid-Root-G1.crt
Step 5 - Deploy Sub CA
Create the
after-deployed-pre.shconfigmap file:
cat > ~/configs/sub/after-deployed-pre.yaml << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: after-deployed-pre
data:
after-deployed-pre.sh: |
#!/bin/bash
if /opt/keyfactor/bin/ejbca.sh cryptotoken list | grep "SubCA"; then
return 0
else
/opt/keyfactor/bin/ejbca.sh cryptotoken create --token SubCA --autoactivate true --pin "\${HSM_TOKEN_PIN}" --type FortanixCryptoToken
# Generate keys on HSM
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias signKeyEc001 --keyspec secp384r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias defaultKeyEc001 --keyspec 4096 --key-usage SIGN_ENCRYPT
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias signKeyPq001 --keyspec ML-DSA-65 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias defaultKeyPq001 --keyspec 4096 --key-usage SIGN_ENCRYPT
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias signKeyHyPq001 --keyspec ML-DSA-65 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias signKeyHyEc001 --keyspec secp384r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias defaultKeyHy001 --keyspec 4096 --key-usage SIGN_ENCRYPT
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias testKey-ec --keyspec secp256r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias testKey-ml-dsa --keyspec ML-DSA-65 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias signKeyMgmt001 --keyspec secp384r1 --key-usage SIGN
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCA --alias defaultKeyMgmt001 --keyspec 4096 --key-usage SIGN_ENCRYPT
fi
if /opt/keyfactor/bin/ejbca.sh cryptotoken list | grep "SubCompositeCa"; then
return 0
else
# Create Soft Crypto Token for Composite keys
/opt/keyfactor/bin/ejbca.sh cryptotoken create --token SubCompositeCa --autoactivate true --type SoftCryptoToken --pin foo123
# Generate keys on soft token
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCompositeCa --alias signKey001 --keyspec MLDSA87-ECDSA-P521-SHA512
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCompositeCa --alias defaultKey001 --keyspec 4096
/opt/keyfactor/bin/ejbca.sh cryptotoken generatekey --token SubCompositeCa --alias testKey --keyspec MLDSA87-ECDSA-P521-SHA512
fi
EOF
Create the configmap in K8s for the
after-deployed-pre.shfile:
kubectl -n karls apply -f ~/configs/sub/after-deployed-pre.yaml
# Replace
kubectl -n karls replace -f ~/configs/sub/after-deployed-pre.yaml
# Delete
kubectl -n karls delete cm/after-deployed-pre
Create the
after-init.postfile:
cat > ~/configs/sub/after-deployed-post.yaml << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: after-deployed-post
data:
after-deployed-post.sh: |
#!/bin/bash
if /opt/keyfactor/bin/ejbca.sh ca listcas | grep "Ecdsa-Sub-G1"; then
return 0
else
/opt/keyfactor/bin/ejbca.sh ca init --caname Ecdsa-Sub-G1 --dn "CN=Kefactor TechMeetup Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
-s SHA384withECDSA --tokenName SubCA --tokenprop /opt/keyfactor/tokenprops/Ecdsa-Sub-G1.properties --signedby External -externalcachain /opt/keyfactor/chain/Ecdsa-Root-G1.crt \
--policy null -v 1825 --tokenPass "\$HSM_TOKEN_PIN"
base64 Ecdsa-Sub-G1_csr.der > /var/tmp/Ecdsa-Sub-G1.pem
fi
if /opt/keyfactor/bin/ejbca.sh ca listcas | grep "Pqc-Sub-G1"; then
return 0
else
/opt/keyfactor/bin/ejbca.sh ca init --caname Pqc-Sub-G1 --dn "CN=Kefactor TechMeetup PQC Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
-s ML-DSA-65 --tokenName SubCA --tokenprop /opt/keyfactor/tokenprops/Pqc-Sub-G1.properties --signedby External -externalcachain /opt/keyfactor/chain/Pqc-Root-G1.crt \
--policy null -v 1825 --tokenPass "\$HSM_TOKEN_PIN"
base64 Pqc-Sub-G1_csr.der > /var/tmp/Pqc-Sub-G1.pem
fi
if /opt/keyfactor/bin/ejbca.sh ca listcas | grep "Composite-Sub-G1"; then
return 0
else
/opt/keyfactor/bin/ejbca.sh ca init --caname Composite-Sub-G1 --dn "CN=Kefactor TechMeetup Composite Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
-s MLDSA87-ECDSA-P521-SHA512 --tokenName SubCompositeCa --tokenprop /opt/keyfactor/tokenprops/Composite-Sub-G1.properties --signedby External -externalcachain /opt/keyfactor/chain/Composite-Root-G1.crt \
--policy null -v 1825 --tokenPass "\$HSM_TOKEN_PIN"
base64 Composite-Sub-G1_csr.der > /var/tmp/Composite-Sub-G1.pem
fi
if /opt/keyfactor/bin/ejbca.sh ca listcas | grep "Hybrid-Sub-G1"; then
return 0
else
/opt/keyfactor/bin/ejbca.sh ca init --caname Hybrid-Sub-G1 --dn "CN=Kefactor TechMeetup Hybrid Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
-s SHA384withECDSA --tokenName SubCA --tokenprop /opt/keyfactor/tokenprops/Hybrid-Sub-G1.properties --signedby External -externalcachain /opt/keyfactor/chain/Hybrid-Root-G1.crt \
--policy null -v 1825 --altsigalg ML-DSA-65 --tokenPass "\$HSM_TOKEN_PIN"
base64 Hybrid-Sub-G1_csr.der > /var/tmp/Hybrid-Sub-G1.pem
fi
EOF
Create a configmap in K8s for the
after-init.postfile:
kubectl -n karls apply -f ~/configs/sub/after-deployed-post.yaml
# Replace
kubectl -n karls replace -f ~/configs/sub/after-deployed-post.yaml
# Delete
kubectl -n karls delete cm/after-deployed-post
# View
kubectl -n karls get cm/after-deployed-post -o yaml
Create token property files for the Sub CAs:
cat > ~/configs/sub/Ecdsa-Sub-G1.properties << EOF
certSignKey signKeyEc001
crlSignKey signKeyEc001
keyEncryptKey defaultKeyEc001
testKey testKey-ec
defaultKey defaultKeyEc001
EOF
cat > ~/configs/sub/Pqc-Sub-G1.properties << EOF
certSignKey signKeyPq001
crlSignKey signKeyPq001
keyEncryptKey defaultKeyPq001
testKey testKey-ml-dsa
defaultKey defaultKeyEc001
EOF
cat > ~/configs/sub/Composite-Sub-G1.properties << EOF
certSignKey signKey001
crlSignKey signKey001
keyEncryptKey defaultKey001
testKey testKey
defaultKey defaultKey001
EOF
cat > ~/configs/sub/Hybrid-Sub-G1.properties << EOF
certSignKey signKeyHyEc001
crlSignKey signKeyHyEc001
keyEncryptKey defaultKeyHy001
testKey testKey-ec
defaultKey defaultKeyHy001
alternativeCertSignKey signKeyHyPq001
EOF
Create a configmap in K8s for the Sub CA token properties:
kubectl -n karls create configmap ca-token-properties --from-file=configs/sub/Ecdsa-Sub-G1.properties --from-file=configs/sub/Pqc-Sub-G1.properties --from-file=configs/sub/Composite-Sub-G1.properties --from-file=configs/sub/Hybrid-Sub-G1.properties
# Delete
kubectl -n karls delete cm/ca-token-properties
Create a secret for the Fortanix HSM Token PIN:
kubectl -n karls create secret generic configdump-secrets --from-literal=HSM_TOKEN_PIN=YWVhN2Q4YjctZGM3My00ZD
Create a configmap file for the management CA:
Create the configmap for the SubCA configdump:
kubectl -n karls create configmap ca-init-configmap --from-file=configdump.json=configs/sub/configdump.json
# Delete
kubectl -n karls delete cm/ca-init-configmap
# View the contents
kubectl -n karls get cm/ca-init-configmap -o yaml
Create the database secret:
kubectl -n karls create secret generic sub-db-credentials \
--from-literal=DATABASE_USER=karl \
--from-literal=DATABASE_PASSWORD=foo123
Create the Password Encryption Key (PEK) secret:
kubectl -n karls create secret generic sub-pek --from-literal=PASSWORD_ENCRYPTION_KEY=Rkjir6RVtRp6rPF1rpzhdfejwRV01DYsJM1Ol94
Create the
sub-override.yamlvalues file:
Deploy the Issuing CA
helm install -n karls sub -f ~/configs/sub/sub-override.yaml oci://repo.keyfactor.com/charts/ejbca
# Tails logs
kubectl -n karls logs -f pod/sub-ejbca-0 -c ejbca
Export CSRs to the host machine
EcdsasubG1="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/Ecdsa-Sub-G1.pem)"
PqcsubG1="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/Pqc-Sub-G1.pem)"
CompositesubG1="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/Composite-Sub-G1.pem)"
HybridsubG1="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/Hybrid-Sub-G1.pem)"
echo "-----BEGIN CERTIFICATE REQUEST-----
$EcdsasubG1
-----END CERTIFICATE REQUEST-----" > ~/configs/sub/Ecdsa-Sub-G1.csr
echo "-----BEGIN CERTIFICATE REQUEST-----
$PqcsubG1
-----END CERTIFICATE REQUEST-----" > ~/configs/sub/Pqc-Sub-G1.csr
echo "-----BEGIN CERTIFICATE REQUEST-----
$CompositesubG1
-----END CERTIFICATE REQUEST-----" > ~/configs/sub/Composite-Sub-G1.csr
echo "-----BEGIN CERTIFICATE REQUEST-----
$HybridsubG1
-----END CERTIFICATE REQUEST-----" > ~/configs/sub/Hybrid-Sub-G1.csr
Create a configmap of the Sub CA CSRs for the Root CA:
kubectl -n jarls create configmap sub-ca-csrs --from-file=configs/sub/Ecdsa-Sub-G1.csr --from-file=configs/sub/Pqc-Sub-G1.csr --from-file=configs/sub/Composite-Sub-G1.csr --from-file=configs/sub/Hybrid-Sub-G1.csr
# Delete
kubectl -n jarls delete cm/sub-ca-csrs
# View
kubectl -n jarls get cm/sub-ca-csrs -o yaml
Create the
root-after-deployed-post.yamlfile:
cat > ~/configs/root/after-deployed-post.yaml << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: after-deployed-post
data:
after-deployed-post.sh: |
#!/bin/bash
/opt/keyfactor/bin/ejbca.sh ra addendentity --username Ecdsa-Sub-G1 --dn "CN=Kefactor TechMeetup Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
--caname Ecdsa-Root-G1 --type 1 --token USERGENERATED --password foo123 \
--certprofile SubCa-Ec-G1 --eeprofile subCa
/opt/keyfactor/bin/ejbca.sh ra addendentity --username Pqc-Sub-G1 --dn "CN=Kefactor TechMeetup PQC Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
--caname Pqc-Root-G1 --type 1 --token USERGENERATED --password foo123 \
--certprofile SubCa-MlDsa-G1 --eeprofile subCa
/opt/keyfactor/bin/ejbca.sh ra addendentity --username Composite-Sub-G1 --dn "CN=Kefactor TechMeetup Composite Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
--caname Composite-Root-G1 --type 1 --token USERGENERATED --password foo123 \
--certprofile SubCaComposite-EcMlDsa-G1 --eeprofile subCa
/opt/keyfactor/bin/ejbca.sh ra addendentity --username Hybrid-Sub-G1 --dn "CN=Kefactor TechMeetup Hybrid Sub CA G1,OU=Certification Authorities,O=Keyfactor TechMeetup,C=SE" \
--caname Hybrid-Root-G1 --type 1 --token USERGENERATED --password foo123 \
--certprofile SubCaHybrid-EcMlDsa-G1 --eeprofile subCa
/opt/keyfactor/bin/ejbca.sh createcert --username Ecdsa-Sub-G1 --password foo123 -c /opt/keyfactor/subcsrs/Ecdsa-Sub-G1.csr -f /var/tmp/Ecdsa-Sub-G1.crt
/opt/keyfactor/bin/ejbca.sh createcert --username Pqc-Sub-G1 --password foo123 -c /opt/keyfactor/subcsrs/Pqc-Sub-G1.csr -f /var/tmp/Pqc-Sub-G1.crt
/opt/keyfactor/bin/ejbca.sh createcert --username Composite-Sub-G1 --password foo123 -c /opt/keyfactor/subcsrs/Composite-Sub-G1.csr -f /var/tmp/Composite-Sub-G1.crt
/opt/keyfactor/bin/ejbca.sh createcert --username Hybrid-Sub-G1 --password foo123 -c /opt/keyfactor/subcsrs/Hybrid-Sub-G1.csr -f /var/tmp/Hybrid-Sub-G1.crt
EOF
Create k8's configmap of the Root
after-deployed-post.yamlfile:
kubectl -n jarls apply -f ~/configs/root/after-deployed-post.yaml
# Replace
kubectl -n jarls replace -f ~/configs/root/after-deployed-post.yaml
# Delete
kubectl -n jarls delete cm/after-deployed-post
# View
kubectl -n jarls get cm/after-deployed-post -o yaml
Create a new override values
root-override-p2.yamlfile:
Use Helm to upgrade the deployment for the Root CA
helm upgrade -n jarls root -f configs/root/root-override-p2.yaml oci://repo.keyfactor.com/charts/ejbca
#Install
helm install -n jarls root -f configs/root/root-override-p2.yaml oci://repo.keyfactor.com/charts/ejbca
# Tails logs
kubectl -n jarls logs -f pod/root-ejbca-0 -c ejbca
# Exec
kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- /bin/bash
Export signed CA certificates to host machine
EcdsaSubG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Ecdsa-Sub-G1.crt)"
PqcSubG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Pqc-Sub-G1.crt)"
CompositeSubG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Composite-Sub-G1.crt)"
HybridSubG1="$(kubectl -n jarls exec -it pod/root-ejbca-0 -c ejbca -- cat /var/tmp/Hybrid-Sub-G1.crt)"
echo "$EcdsaSubG1" > ~/configs/sub/Ecdsa-Sub-G1.crt
echo "$PqcSubG1" > ~/configs/sub/Pqc-Sub-G1.crt
echo "$CompositeSubG1" > ~/configs/sub/Composite-Sub-G1.crt
echo "$HybridSubG1" > ~/configs/sub/Hybrid-Sub-G1.crt
Create a configmap file of the signed Sub CA certificates:
kubectl -n karls create configmap sub-ca-crts --from-file=configs/sub/Ecdsa-Sub-G1.crt --from-file=configs/sub/Pqc-Sub-G1.crt --from-file=configs/sub/Composite-Sub-G1.crt --from-file=configs/sub/Hybrid-Sub-G1.crt
# Delete
kubectl -n karls delete cm/sub-ca-crts
# View
kubectl -n karls get cm/sub-ca-crts -o yaml
Create the
after-deployed-pre-p2.yamlfile:
cat > ~/configs/sub/after-deployed-pre-p2.yaml << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: after-deployed-pre-p2
data:
after-deployed-pre.sh: |
#!/bin/bash
baseDir="\$1"
tempDir="\$2"
ID="\$3"
/opt/keyfactor/bin/ejbca.sh ca importcacert --caname Ecdsa-Sub-G1 -f /opt/keyfactor/importcacrt/Ecdsa-Sub-G1.crt
/opt/keyfactor/bin/ejbca.sh ca importcacert --caname Pqc-Sub-G1 -f /opt/keyfactor/importcacrt/Pqc-Sub-G1.crt
/opt/keyfactor/bin/ejbca.sh ca importcacert --caname Composite-Sub-G1 -f /opt/keyfactor/importcacrt/Composite-Sub-G1.crt
/opt/keyfactor/bin/ejbca.sh ca importcacert --caname Hybrid-Sub-G1 -f /opt/keyfactor/importcacrt/Hybrid-Sub-G1.crt
\${baseDir}/bin/configdump.sh import -l "\${baseDir}/configdump/stage.d/configdump.json" \
--overwrite update --ignore-errors --non-interactive continue --resolve-reference default --expand-variables
/opt/keyfactor/bin/ejbca.sh ra addendentity --username \$HTTPSERVER_HOSTNAME --dn "CN=\$HTTPSERVER_HOSTNAME,OU=TLS Servers,O=Keyfactor TechMeetup,C=SE" \
--caname ManagementCA --type 1 --token PEM --altname dNSName=\$HTTPSERVER_HOSTNAME \
--certprofile tlsServer-Ec-30d --eeprofile tlsServer --password NOTUSED
/opt/keyfactor/bin/ejbca.shra setendentitystatus --username \$HTTPSERVER_HOSTNAME -S 10
/opt/keyfactor/bin/ejbca.sh ra setclearpwd \$HTTPSERVER_HOSTNAME NOTUSED
/opt/keyfactor/bin/ejbca.sh batch \$HTTPSERVER_HOSTNAME -dir /var/tmp/ --keyalg ECDSA --keyspec secp384r1
/opt/keyfactor/bin/ejbca.sh ra addendentity --username \$ADMIN_USERNAME --dn "CN=\$ADMIN_USERNAME,OU=User Authentication,O=Keyfactor TechMeetup,C=SE" \
--caname ManagementCA --type 1 --token P12 --certprofile userAuth-1yr --eeprofile userAuth --password foo123
/opt/keyfactor/bin/ejbca.sh ra setendentitystatus --username \$ADMIN_USERNAME -S 10
EOF
Create the configmap in K8s for the
after-deployed-pre-p2.yamlfile:
kubectl -n karls apply -f ~/configs/sub/after-deployed-pre-p2.yaml
# Replace
kubectl -n karls replace -f ~/configs/sub/after-deployed-pre-p2.yaml
# Delete
kubectl -n karls delete cm/after-deployed-pre-p2
# View
kubectl -n karls get cm/after-deployed-pre -o yaml
Create the configdump of CA settings for post configuration (enrollment aliases, CP, EEPs, services):
Create the configmap in K8s for the configdump part 2:
kubectl -n karls create configmap ca-post-configmap --from-file=post-configdump.json=configs/sub/post-configdump.json
# Delete
kubectl -n karls delete cm/ca-post-configmap
# View the contents
kubectl -n karls get cm/ca-post-configmap -o yaml
Create new deployment override values:
Update Helm using the new deployment override values:
helm upgrade -n karls sub -f configs/sub/sub-override-p2.yaml oci://repo.keyfactor.com/charts/ejbca
#Install
helm install -n karls sub -f configs/sub/sub-override-p2.yaml oci://repo.keyfactor.com/charts/ejbca
# Tails logs
kubectl -n karls logs -f pod/sub-ejbca-0 -c ejbca
# Exec
kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- /bin/bash
Step 6 - Stage TLS certificate
Copy the certificate, key, and CA cert out of the container:
subTlsCert="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/pem/enroll.techmeetup.test.pem)"
subTlsKey="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/pem/enroll.techmeetup.test-Key.pem)"
subTlsCa="$(kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- cat /var/tmp/pem/enroll.techmeetup.test-CA.pem)"
echo "$subTlsCert" > ~/configs/sub/enroll.techmeetup.test.pem
echo "$subTlsKey" > ~/configs/sub/enroll.techmeetup.test-Key.pem
echo "$subTlsCa" > ~/configs/sub/enroll.techmeetup.test-CA.pem
Create a secret for the nginx TLS cert, key, and CA chain:
kubectl -n karls create secret generic tls-enroll-techmeetup-test \
--from-file=enroll.techmeetup.test.pem=configs/sub/enroll.techmeetup.test.pem \
--from-file=enroll.techmeetup.test-Key.pem=configs/sub/enroll.techmeetup.test-Key.pem \
--from-file=enroll.techmeetup.test-CA.pem=configs/sub/enroll.techmeetup.test-CA.pem
Create Helm override file for the finale:
Upgrade helm deployment with updated values file:
helm upgrade -n karls sub -f configs/sub/sub-override-final.yaml oci://repo.keyfactor.com/charts/ejbca
#Install
helm install -n karls sub -f configs/sub/sub-override-final.yaml oci://repo.keyfactor.com/charts/ejbca
# Tails logs
kubectl -n karls logs -f pod/sub-ejbca-0 -c ejbca
# Exec
kubectl -n karls exec -it pod/sub-ejbca-0 -c ejbca -- /bin/bash
Next steps
In this guide, you learned how to deploy a fully automated, zero-touch PQC-ready PKI using composite certificates in Kubernetes using Helm, configuration-as-code, and real HSMs, and how to validate and use it today with off-the-shelf TLS and CMS tooling.
Here are some next steps we recommend:
Learn more about the current state of PQC interoperability by reading this solution area: https://docs.keyfactor.com/solution-areas/latest/interoperability-and-future-ready-cryptography
Learn more about EJBCA deployment using Helm and ConfigDump https://docs.keyfactor.com/solution-areas/latest/ejbca-helm-chart-building-blocks and https://docs.keyfactor.com/container/latest/ejbca/ejbca-configdump-in-kubernetes
If you are interested in EJBCA Enterprise, read more on Keyfactor EJBCA Enterprise.
If you are interested in EJBCA Community, check out EJBCA Community vs Enterprise or read more on ejbca.org.
If you are an EJBCA Enterprise customer and need support, visit the Keyfactor Support Portal.
Discuss with the EJBCA Community on GitHub Discussions.
Contact us
Request a live demo with one of our experts — whether you want to explore workflows hands-on or discuss your specific needs.