10 min read

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.

Smallstep’s step-ca as CA with ACME support
In this blog post, we will go through the basic installation process and install both the step-ca and step-cli tool that will help us manage our CA and certificates issued by the CA.
Using traefik with local CA with ACME support
How to use custom PKI through ACME calls with traefik.

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's ca.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.