step-ca Certificate Revocation
In previous posts we successfully deployed our own PKI with step-ca
and even configured traefik
to use ACME protocol to request certificates from our PKI server. Now we are going to add an additional feature to our PKI, which is active certificate revocation.
Please keep in mind that active revocation via CRLs (Certificate Revocation Lists) is not a perfect solution to certificate revocation - ideally, you would be issuing short-lived certificates that you will just let expire. Another issue is that it heavily relies on browsers to do the work, and since they use different methods to check validity of a certificate, results may vary. Once expired, certificate can no longer be used for authentication. Additionally, you can set the certificate to be revoked via step-ca
and, once revoked, it cannot be renewed.
For that reason, Smallstep actually recommends passive revocation through short-lived certificates. However, sometimes your services will require certificates that have CRLs included in them - one recent example for me was when I tried installing a new certificate for a test Exchange server. There was simply no way to get it to work (or I couldn't figure out how to do it) without having the extra extension in the certificate (X509v3 CRL Distribution Points). And so I went researching on how to get the CRL working with my step-ca
installation. Luckily, with the version of step-ca
I'm running, you are able to enable a minimal Certificate Revocation List server. I don't know if this is a new thing or something, as on some other parts of their documentation, they specifically state that they don't support active revocation, but I'm not going to complain about that 😄.
So let's take a look at what needs to be done. There are actually a few steps:
- modify your existing
step-ca
server'sca.json
file to enable insecure port and URL for the CRL - create a new template for your intermediate CA that is used for signing all your certificates; once that is completed, you will generate a new intermediate CA and its accompanying key
- you will also want to create a leaf template for all your new certificates to include the CRL as well
- finally, you will want to modify your
ca.json
file again to include the new leaf template when issuing certificates
Modify CA server configuration
Let's start with modifying our CA server configuration. There's a few things that need to be added at the start:
"insecureAddress": ":80",
"crl": {
"enabled": true,
"cacheDuration": "8h0m0s",
"idpURL": "http://ca.demo.networktechguy.com/1.0/crl"
},
Looking from the beginning of the file, it should be fit something like this:
$ cat config/ca.json
{
"root": "/etc/step-ca/certs/root_ca.crt",
"federatedRoots": null,
"crt": "/etc/step-ca/certs/intermediate_ca.crt",
"key": "/etc/step-ca/secrets/intermediate_ca_key",
"address": ":443",
"insecureAddress": ":80",
"commonName": "NetworkTechGuy.com Demo CA",
"crl": {
"enabled": true,
"cacheDuration": "8h0m0s",
"idpURL": "http://ca.demo.networktechguy.com/1.0/crl"
},
"dnsNames": [
"ca.demo.networktechguy.com",
"10.75.1.10"
],
---OUTPUT OMITTED---
At this point I would restart the service as it is easier to troubleshoot potential problems while we are just beginning with the process:
sudo systemctl restart step-ca
sudo systemctl status step-ca
If you get something like this for status:
Active: active (running) since Sun 2024-09-29 14:39:29 EDT; 1s ago
It means that the service has been successfully restarted. If not, check the logs with journalctl
:
sudo journalctl -u step-ca
Create new intermediate CA template file
New template file will be intermediate.tpl
and we can place it in the /etc/step-ca/templates
directory. Content should be something like this:
{
"subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 0
},
"crlDistributionPoints": ["http://ca.demo.networktechguy.com/1.0/crl"]
}
We can now generate new intermediate CA:
step certificate create \
--template /etc/step-ca/templates/intermediate.tpl \
--ca $(step path)/certs/root_ca.crt \
--ca-key $(step path)/secrets/root_ca_key \
--not-after 87660h \
"NetworkTechGuy.com CA Intermediate CA" \
$(step path)/certs/intermediate_ca.crt \
$(step path)/secrets/intermediate_ca_key
Please note that, for this command to work, you need to have completed the setup of the server as explained here, otherwise use full path (/etc/step-ca/
) instead of variables ($(step path)/
).
The whole process should look something like this:
$ sudo step certificate create \
--template /etc/step-ca/templates/intermediate.tpl \
--ca $(step path)/certs/root_ca.crt \
--ca-key $(step path)/secrets/root_ca_key \
--not-after 87660h \
"NetworkTechGuy.com CA Intermediate CA" \
$(step path)/certs/intermediate_ca.crt \
$(step path)/secrets/intermediate_ca_key
Please enter the password to decrypt /etc/step-ca/secrets/root_ca_key:
Please enter the password to encrypt the private key:
✔ Would you like to overwrite /etc/step-ca/secrets/intermediate_ca_key [y/n]: y
✔ Would you like to overwrite /etc/step-ca/certs/intermediate_ca.crt [y/n]: y
Your certificate has been saved in /etc/step-ca/certs/intermediate_ca.crt.
Your private key has been saved in /etc/step-ca/secrets/intermediate_ca_key.
As you can see, you will be asked to overwrite the existing intermediate cert and key. Since all of your certificates are authenticated against the root
CA, and we are not changing that, feel free to do so.
If we inspect the newly created certificate, we can see that it now includes the needed extension:
$ step certificate inspect /etc/step-ca/certs/intermediate_ca.crt
Certificate:
---OUTPUT OMITTED---
X509v3 CRL Distribution Points:
Full Name:
URI:http://ca.demo.networktechguy.com/1.0/crl
---OUTPUT OMITTED---
Next, we will restart the service and check the status:
$ sudo systemctl restart step-ca
$ sudo systemctl status step-ca
● step-ca.service - step-ca service
Loaded: loaded (/etc/systemd/system/step-ca.service; enabled; preset: enabled)
Active: active (running) since Sun 2024-09-29 14:36:05 EDT; 16s ago
Docs: https://smallstep.com/docs/step-ca
https://smallstep.com/docs/step-ca/certificate-authority-server-production
Main PID: 3086 (step-ca)
Tasks: 9 (limit: 4643)
Memory: 17.3M
CPU: 156ms
CGroup: /system.slice/step-ca.service
└─3086 /usr/bin/step-ca config/ca.json --password-file password.txt
Sep 29 14:36:05 deb-step-01 systemd[1]: Started step-ca.service - step-ca service.
Creating a leaf template
Next, let's create a leaf template. The content should look something like this:
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
"keyUsage": ["digitalSignature"],
{{- end }}
"extKeyUsage": ["serverAuth", "clientAuth"],
"crlDistributionPoints": ["http://ca.demo.networktechguy.com/1.0/crl"]
}
I've placed in a newly created subdirectory (x509
) of the templates directory, so the full path of the file is /etc/step-ca/templates/x509/leaf.tpl
.
Modify provisioners to use new template file
There are two ways to do it - one way is to do it via step ca provisioner
command, like this:
step ca provisioner update NTG --x509-template /etc/step-ca/templates/x509/leaf.tpl
Please keep in mind that if you want ACME certificates to use the new template file, you have to do it for that provisioner as well:
step ca provisioner update acme --x509-template /etc/step-ca/templates/x509/leaf.tpl
Now, with that said, this will modify your ca.json
file and add whatever was in the leaf.tpl
file into the ca.json
file, in-line:
{
"type": "JWK",
"name": "NTG",
"key": {
"use": "sig",
"kty": "EC",
"kid": "---OUTPUT OMITTED---",
"crv": "P-256",
"alg": "ES256",
"x": "---OUTPUT OMITTED---",
"y": "---OUTPUT OMITTED---"
},
"encryptedKey": "---OUTPUT OMITTED---",
"claims": {
"minTLSCertDuration": "5m0s",
"maxTLSCertDuration": "43830h0m0s",
"defaultTLSCertDuration": "17532h0m0s",
"enableSSHCA": true,
"disableRenewal": false,
"allowRenewalAfterExpiry": false,
"disableSmallstepExtensions": false
},
"options": {
"x509": {
"template": "{\n \"subject\": {{ toJson .Subject }},\n \"sans\": {{ toJson .SANs }},\n {{- if typeIs \"*rsa.PublicKey\" .Insecure.CR.PublicKey }}\n \"keyUsage\": [\"keyEncipherment\", \"digitalSignature\"],\n {{- else }}\n \"keyUsage\": [\"digitalSignature\"],\n {{- end }}\n \"extKeyUsage\": [\"serverAuth\", \"clientAuth\"],\n \"crlDistributionPoints\": [\"http://ca.demo.networktechguy.com/1.0/crl\"]\n}\n"
},
"ssh": {}
}
}
I don't like that, so after I did it via these commands, I modified the file to look like this instead:
{
"type": "JWK",
"name": "NTG",
"key": {
"use": "sig",
"kty": "EC",
"kid": "---OUTPUT OMITTED---",
"crv": "P-256",
"alg": "ES256",
"x": "---OUTPUT OMITTED---",
"y": "---OUTPUT OMITTED---"
},
"encryptedKey": "---OUTPUT OMITTED---",
"claims": {
"minTLSCertDuration": "5m0s",
"maxTLSCertDuration": "43830h0m0s",
"defaultTLSCertDuration": "17532h0m0s",
"enableSSHCA": true,
"disableRenewal": false,
"allowRenewalAfterExpiry": false,
"disableSmallstepExtensions": false
},
"options": {
"x509": {
"templateFile": "/etc/step-ca/templates/x509/leaf.tpl"
},
"ssh": {}
}
},
This is an output for JWK provisioner only, if you added the new leaf template to ACME provisioner, you will have to fix it there as well.
For this to work, make sure that your template file has correct ownership and permissions:
$ ls -alh /etc/step-ca/templates/x509/leaf.tpl
-rw-r--r-- 1 step step 367 Sep 29 13:39 templates/x509/leaf.tpl
Finally, restart the service and check the status:
sudo systemctl restart step-ca
sudo systemctl status step-ca
If everything is working after all these changes, you can check the status of your CRL server - I'm doing this now on the workstation, as all the changes on the server are complete:
$ step crl inspect http://ca.demo.networktechguy.com/1.0/crl --insecure
Results should be something like this:
$ step crl inspect http://ca.demo.networktechguy.com/1.0/crl --insecure
Certificate Revocation List (CRL):
Data:
Valid: false
Version: 17 (0x11)
Signature algorithm: ECDSA-SHA256
Issuer:
Last Update: 2024-09-29 20:57:38 +0000 UTC
Next Update: 2024-09-30 04:57:38 +0000 UTC
CRL Extensions:
X509v3 Authority Key Identifier:
keyid:1B:20:FA:72:A8:A4:26:10:D9:B2:8D:AC:29:88:DB:13:B9:1D:7E:7F
X509v3 CRL Number:
17
X509v3 Issuing Distribution Point: critical
Full Name:
URI:http://ca.demo.networktechguy.com/1.0/crl
Only User Certificates
Revoked Certificates:
Serial Number: 14901852086807387906094692013751768567 (0x0B35FE086D3BF2E8D33BB60BED8595F7)
Revocation Date: 2024-09-29 19:59:04 +0000 UTC
Serial Number: 165745321111098419965161888279534387093 (0x7CB16340C3FCA91B5F07181E28C63F95)
Revocation Date: 2024-09-29 19:45:44 +0000 UTC
Serial Number: 265544075792166084197075778072633340760 (0xC7C5EDC016778C94A2B743E32F86F358)
Revocation Date: 2024-09-29 18:56:15 +0000 UTC
Serial Number: 287599246715617662997638596379242195474 (0xD85D99A1ADEDF37E0FDDB0A31EC8EA12)
Revocation Date: 2024-09-29 19:32:29 +0000 UTC
Signature Algorithm: ECDSA-SHA256
30:45:02:20:0b:c8:a8:4a:8a:0e:08:5d:83:cb:54:d9:
15:7a:c7:4d:24:17:f6:62:18:36:5b:d7:55:e0:b8:75:
83:8f:3e:e1:02:21:00:99:7c:06:0d:95:7b:2f:ba:d2:
31:63:71:b1:88:75:89:67:a1:24:ed:4e:5a:e3:6c:b1:
d7:a0:3c:d2:b3:b4:66
You will notice that I already have a few revoked certificates in my list. Also, notice that updates are done in 8-hour intervals, as configured here:
"cacheDuration": "8h0m0s",
If you don't want to wait this long, you can always change the value to something that fits your needs better. Also, when you issue a service restart command, it will also clear the cache.
So let's try and create a new certificate with this new setup. One thing to note is that I changed the minimum TLS certificate duration parameter from the time I posted previous article, so it is now set to 5 minutes:
"minTLSCertDuration": "5m0s",
New certificate request:
$ step ca certificate test-06.demo.networktechguy.com test-06.demo.networktechguy.com.crt test-06.demo.networktechguy.com.key --not-after=60m
✔ Provisioner: NTG (JWK) [kid: 6zkvMKLGZxf0qa4n-cleOyn2seI04fweoAuOjBlcr4A]
Please enter the password to decrypt the provisioner key:
✔ CA: https://ca.demo.networktechguy.com
✔ Certificate: test-06.demo.networktechguy.com.crt
✔ Private Key: test-06.demo.networktechguy.com.key
Let's inspect the new certificate:
step certificate inspect test-06.demo.networktechguy.com.crt
Certificate:
---OUTPUT OMITTED---
Issuer: CN=NetworkTechGuy.com CA Intermediate CA
Validity
Not Before: Sep 29 21:40:32 2024 UTC
Not After : Sep 29 22:41:32 2024 UTC
Subject: CN=test-06.demo.networktechguy.com
---OUTPUT OMITTED---
X509v3 Subject Alternative Name:
DNS:test-06.demo.networktechguy.com
X509v3 CRL Distribution Points:
Full Name:
URI:http://ca.demo.networktechguy.com/1.0/crl
---OUTPUT OMITTED---
What we can see from the output is that the new certificate has a one hour validity and that the CRL distribution point is included in the extensions. From this point on, any service that requires CRL extension to be included in the certificate, should be happy!
As for the revocation, it's quite easy to do. When you inspect the certificate, you will notice that there is a serial number of the certificate at the beginning:
$ step certificate inspect test-06.demo.networktechguy.com.crt
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 65623237680889282882253127865884846522 (0x315e936ae33eec9d05dca6a17c99a9ba)
We will use this serial number to revoke the certificate:
$ step ca revoke 65623237680889282882253127865884846522 --reason="No longer needed"
✔ Provisioner: NTG (JWK) [kid: 6zkvMKLGZxf0qa4n-cleOyn2seI04fweoAuOjBlcr4A]
Please enter the password to decrypt the provisioner key:
✔ CA: https://ca.demo.networktechguy.com
Certificate with Serial Number 65623237680889282882253127865884846522 has been revoked.
If we were to check the status of the CRL right now, it would not reflect the change as it's reading the list from the cache:
$ step crl inspect http://ca.demo.networktechguy.com/1.0/crl --insecure
---OUTPUT OMITTED---
Revoked Certificates:
Serial Number: 14901852086807387906094692013751768567 (0x0B35FE086D3BF2E8D33BB60BED8595F7)
Revocation Date: 2024-09-29 19:59:04 +0000 UTC
Serial Number: 165745321111098419965161888279534387093 (0x7CB16340C3FCA91B5F07181E28C63F95)
Revocation Date: 2024-09-29 19:45:44 +0000 UTC
Serial Number: 265544075792166084197075778072633340760 (0xC7C5EDC016778C94A2B743E32F86F358)
Revocation Date: 2024-09-29 18:56:15 +0000 UTC
Serial Number: 287599246715617662997638596379242195474 (0xD85D99A1ADEDF37E0FDDB0A31EC8EA12)
Revocation Date: 2024-09-29 19:32:29 +0000 UTC
As you can see, our certificate is not on the list of revoked certificates (yet). To manually refresh the CRL, restart the service on the server and then try again:
step crl inspect http://ca.demo.networktechguy.com/1.0/crl --insecure
---OUTPUT OMITTED---
Serial Number: 65623237680889282882253127865884846522 (0x315E936AE33EEC9D05DCA6A17C99A9BA)
Revocation Date: 2024-09-29 21:50:41 +0000 UTC
---OUTPUT OMITTED---
As you can see, this certificate is now successfully revoked and any service that utilises CRLs will see that it's been revoked.
Conclusion
Unfortunately, status of browser CRL checks is not that great at the moment. Even though certificate has been revoked, and it is so stated in the inspection, I have yet to figure out how to get my browsers to actually check the CRL stated in the certificate itself. That's why consider this blog post a half-done - if a service requires a CRL extension to be present in the certificate - this will do. If you require CRL checks to be performed against issued certificates, this does not (yet) work. I will continue looking into this to see how (and if) I can make it work, but for now, consider passive revocation - issue personal certificates with validity that you are comfortable with. If you want someone to temporarily access your service, issue them a certificate with validity of one day for example and that will work without an issue. Hopefully there's a solution for this that I'm just not aware of.
Member discussion