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.