In the previous sections we demonstrated how Kubernetes services can obtain certificates automatically using cert-manager and ACME, or manually using the Step CLI.
However, many infrastructure services still run outside Kubernetes on virtual machines. Examples include CI/CD servers, artifact repositories, identity providers, and databases.
In these environments certificates must be generated directly on the host system.
This section demonstrates how to configure a virtual machine to trust the internal certificate authority and obtain certificates using the Step CLI.
Installing the Step CLI on the Virtual Machine
The first step is installing the Step CLI on the host. In this example we configure a Jenkins server running on Linux.
curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o /etc/apt/keyrings/smallstep.asc
cat << EOF > /etc/apt/sources.list.d/smallstep.sources
Types: deb
URIs: https://packages.smallstep.com/stable/debian
Suites: debs
Components: main
Signed-By: /etc/apt/keyrings/smallstep.asc
EOF
apt update
apt install step-cli
This installs the step command line tool used to interact with the internal certificate authority.
Installing the Root Certificate Authority
Because this is an internal certificate authority, the root certificate is not trusted by default on operating systems.
The root certificate must therefore be installed manually so the system trusts certificates issued by the internal CA.
The Step-CA server publishes the root certificate at the /roots.pem endpoint.
curl -O https://ca.devops-db.internal/roots.pem
Inspect the certificate:
openssl x509 -in roots.pem -text -noout
Install the certificate into the system trust store.
sudo mkdir -p /usr/local/share/ca-certificates/devops-db
sudo cp roots.pem /usr/local/share/ca-certificates/devops-db/devops-db-root.crt
sudo update-ca-certificates
Obtaining the Root Fingerprint
The root fingerprint is required to bootstrap the Step CLI configuration.
step certificate fingerprint roots.pem
Example result:
b8d4d77badb5aeca4d23a67269846aa3dbf3271c94c0cf97e82e482aab6e7732
Verify that the CA endpoint is reachable:
curl https://ca.devops-db.internal/health
Expected result:
{"status":"ok"}
Bootstrapping the Step CLI
Next the Step CLI must be connected to the certificate authority.
step ca bootstrap \
--ca-url https://ca.devops-db.internal \
--fingerprint b8d4d77badb5aeca4d23a67269846aa3dbf3271c94c0cf97e82e482aab6e7732
This creates the local configuration:
~/.step/config
~/.step/certs/root_ca.crt
The CLI is now configured to communicate securely with the internal CA.
Infrastructure Certificate Issuance Model
At this point the internal PKI operates using two distinct issuance models.
Step-CA
│
├─ ACME → cert-manager → Kubernetes
│
└─ Token provisioner → VMs
Kubernetes workloads typically use ACME, while virtual machines use token-based certificate requests.
Important Note about Certificate Generation on Virtual Machines
The examples shown below generate the certificate directly on the target virtual machine using the Step CLI. While this approach is simple and useful for demonstration purposes, it is not the only possible workflow.
In many production environments certificates are generated centrally and then distributed to systems through configuration management tools, CI/CD pipelines, or artifact repositories.
Typical distribution models include:
- infrastructure automation tools such as Terraform or Ansible
- CI/CD pipelines that generate certificates during deployment
- storing certificates in artifact repositories such as Nexus
- retrieving certificates from secret management systems such as Vault
In these cases the certificate is generated once and then securely copied to the target system, rather than being generated locally on each virtual machine.
ACME Standalone Mode Considerations
The ACME example below uses the Standalone HTTP challenge. This method temporarily starts a small web server on the machine requesting the certificate in order to validate domain ownership.
Because of this requirement, the service normally running on port 80 must be stopped during the validation process.

In the example below the Jenkins service is temporarily stopped so the Step CLI can bind to port 80.
While this approach works well for manual testing, it may not be suitable for production environments where services cannot be interrupted.
In automated environments it is often preferable to generate certificates using token-based authentication (JWK provisioners) or through centralized certificate management workflows, avoiding the need to stop running services.
If the ACME provisioner is enabled, the Step CLI can automatically perform an HTTP challenge to validate the hostname.
Stop Jenkins temporarily so the ACME challenge can bind to port 80.
sudo systemctl stop jenkins
Request the certificate:
step ca certificate \
jenkins.devops-db.internal \
jenkins.crt \
jenkins.key
Example output:
✔ Provisioner: acme (ACME)
Using Standalone Mode HTTP challenge to validate jenkins.devops-db.internal . done!
Waiting for Order to be 'ready' for finalization .. done!
Finalizing Order .. done!
✔ Certificate: jenkins.crt
✔ Private Key: jenkins.key
Verify the certificate issuer:
openssl x509 -in jenkins.crt -text -noout | grep Issuer
Example output:
Issuer: C = PT, O = DevOps-DB, CN = DevOps-DB Intermediate CA
Restart Jenkins:
sudo systemctl start jenkins
Converting the Certificate to PKCS12
Java applications such as Jenkins require certificates in PKCS12 keystore format.
Convert the certificate:
openssl pkcs12 -export \
-in jenkins.crt \
-inkey jenkins.key \
-out jenkins.p12 \
-name jenkins \
-CAfile ~/.step/certs/root_ca.crt \
-caname root
Example password:
1234qwer
An alternative method uses the JWK provisioner, which relies on signed tokens to authorize certificate requests.
Example provisioner key:
cat jwk-provisioner-pub.json
Example output:
{
"use": "sig",
"kty": "EC",
"kid": "PvKMc182_erIthyh2DXdAp39eg1MvB3eMuTrl4cSq4I",
"crv": "P-256",
"alg": "ES256",
"x": "nlQ1XzR0rUOaPXPNZOFvnLPXXhrXFeX9hYgRSHrz70o",
"y": "m1oydTO_j3nTKYAaEvYih-VkrgjyOueFL5kU_Z6-1eQ",
"d": "lAeYP-q420sSYfTQxRZpP9JuSTjwpZGiqF583tbQW2Q"
}
Generate a signed token:
step ca token jenkins.devops-db.internal \
--provisioner vm-hosts \
--key jwk-provisioner-pub.json
This produces a signed JWT token authorizing the certificate request.
Use the token to request the certificate:
step ca certificate \
jenkins.devops-db.internal \
jenkins.crt \
jenkins.key \
--token <TOKEN>
Convert the certificate to PKCS12 format:
openssl pkcs12 -export \
-in jenkins.crt \
-inkey jenkins.key \
-out jenkins.p12 \
-name jenkins \
-CAfile ~/.step/certs/root_ca.crt \
-caname root
Example password:
1234qwer
Configuring Jenkins to Use the Certificate
Move the keystore into the Jenkins home directory.
sudo mv jenkins.p12 /var/lib/jenkins/
sudo chown jenkins:jenkins /var/lib/jenkins/jenkins.p12
sudo chmod 600 /var/lib/jenkins/jenkins.p12
Update the Jenkins configuration.
sudo vi /etc/default/jenkins
Example configuration:
JENKINS_ARGS="--webroot=/var/cache/$NAME/war --httpPort=-1 --httpsPort=8443 --httpsKeyStore=/var/lib/jenkins/jenkins.p12 --httpsKeyStorePassword=1234qwer"
Update the systemd service configuration.
sudo vi /lib/systemd/system/jenkins.service
Example configuration:
[Service]
Environment="JENKINS_PORT=-1"
Environment="JENKINS_HTTPS_PORT=443"
Environment="JENKINS_HTTPS_KEYSTORE=/var/lib/jenkins/jenkins.p12"
Environment="JENKINS_HTTPS_KEYSTORE_PASSWORD=1234qwer"
Once Jenkins is restarted, it will serve HTTPS using a certificate issued by the internal Step-CA.