This how-to describes one method of supporting SSH service accounts on Smallstep Certificate Manager using JWK Provisioners. In Certificate Manager, authorities are x509 only by default. An SSH authority must be enabled before proceeding.


Service Accounts are accounts for machines, automations, and other non-person entities that need to SSH between systems non-interactively. In order to SSH, these entities need:

  • An SSH certificate
  • A POSIX user account on each host they need to SSH into

JWK Provisioner

The JWK provisioner is extremely flexible. A public key is registered with the CA (as a JSON Web Key, or JWK, hence the name). The corresponding private key can be used to sign short-lived one-time tokens that are used to authenticate certificate requests. By default, the CA will also store (or “escrow”) the private key, encrypted with a password (the CA never sees the decrypted private key).


In this default configuration, the JWK provisioner reduces to a password-based authentication mechanism. Anyone who knows the JWK provisioner password can generate a one-time token and obtain a certificate with any name. If you run step ssh login, step ca certificate, or any other certificate workflow using the step CLI, it will prompt for a password and automatically generate an authentication token for you:


$ step ca certificate foo foo.crt foo.key
✔ Provisioner: jwk1 (JWK) [kid: Z529mfLDiITdY_11tJ4ub5WZCldd2sQpBKycC_ZqMZQ]
✔ Please enter the password to decrypt the provisioner key: 

Service Accounts using JWK Provisioners

We can implement simple password-based authentication for Service Accounts by creating a unique JWK Provisioner for each Service Account. Assuming some existing mechanism is in place to deliver the JWK Provisioner password to the Service Account, obtaining an SSH certificate is a simple one-line command:


step ssh certificate <service-account-posix-username> id_ecdsa

The Service Account can use the generated certificate to SSH by running:


ssh -i <id_ecdsa-cert.pub> <target-host>

This is a simple method that works well for a small number of Service Accounts that avoids the complexities of managing Service Accounts and credentials using an Identity Provider like Okta.


? Note: since we’re not managing Service Accounts at the Identity Provider, Smallstep won’t automatically create and remove posix accounts for the Service Account. We’re assuming that posix accounts are created and removed via some other mechanism (e.g., via config management, startup scripts, or in base instance images).


Constraining the Provisioner

For each Service Account we need to create a unique JWK Provisioner. By default, a JWK Provisioner can be used to obtain a certificate for any SSH principal. To eliminate this risk, we’ll create a template file to hardcode the Service Account’s posix username(s) onto all certificates issued from this provisioner:


{
"type": {{ toJson .Type }},
"keyId": "<ServiceAcctName>",
"principals": ["<ServiceAcctName>","<serviceAcctName2>"],
"extensions": {{ toJson .Extensions }},
"criticalOptions": {{ toJson .CriticalOptions }}
}

Use this template to create a new provisioner and create a strong passcode when prompted and save the passcode for later use:


step beta ca provisioner add <ServiceAccountName> --create \\
  --type JWK \\
  --ssh-template="ssh-template.tpl" \\
  --ssh

Then create a certificate using that provisioner specifically for that user providing the provisioner password (saved from the step above), and a password to encrypt the private key when prompted:


step ssh certificate <ServiceAccountName> id_edcsa

To avoid all prompts, add flags referencing file locations for the provisioner passcode, a password to encrypt the private key, and the force command to overwrite prior cert data.


step ssh certificate <ServiceAccountName> id_edcsa \
  --provisioner <new_provisioner_name> \
  --provisioner-password-file <jwk_provisioner_password_file> \
  --password-file <ssh_private_key_password_file> --force