native Let's Encrypt in OpenWRT

As I delved deeper into DevOps in my home lab, I began to notice that it was often easier to enable TLS/mTLS than to configure and debug ways to do without it.

Thinking about reliable hosting for a private CA, I discovered that among all my electrical equipment, only two devices have uptime close to 100%: the refrigerator and the Internet router.

The idea of ​​getting not only drinks but also SSL certificates from the fridge warmed my soul so much that I almost started looking for where to buy a smart fridge. Then I cooled down a bit and decided to try a router with OpenWRT firmware first.

I'm sure the comments will suggest many cool solutions for private CAs, but I settled on step-ca. Step-CA is a PKI core and various plug-ins modulesissuing certificates if authentication is passed. In this tutorial, we will obtain X.509 certificates in exchange for passing ACME checks and in exchange for JWK tokens generated by the CLI client. step.

Disclaimer: The steps below are quite invasive and not guaranteed to work; you might want to test them on a virtual OpenWrt router first. I start right after setting up dynamic DNS zone updates in OpenWRT, but this guide is self-contained if you already have control over name resolution on your network.

Plan:

  • Installation step-ca in OpenWRT

  • Generating keys for PKI

  • Setting up and testing the JWK module

  • Configuring step-ca as a service

  • Setting up and testing the ACME module

  • Correction of certificate parameters.

Installing step-ca in OpenWRT

The installation process is simple, but keep in mind that you will need ~37 MB of free space on the main partition of the router:

$ opkg update
$ opkg install curl

$ mkdir -p /tmp/step-ca
$ cd /tmp/step-ca

$ curl -LO https://dl.smallstep.com/certificates/docs-ca-install/latest/step-ca_linux_arm64.tar.gz

$ tar -zxf step-ca_linux_arm64.tar.gz && rm step-ca_linux_arm64.tar.gz
$ mv step-ca_linux_arm64/step-ca /usr/bin
$ rm -rf step-ca_linux_arm64/

Next we need to download step-cliwhich will be needed at the setup stage, but will be used rarely in the future. Let's leave it in /tmp and we'll just make a soft link in /usr/bin/. The executable file will be deleted after reboot, but it can always be restored by repeating the commands:

$ mkdir -p /tmp/step-ca
$ cd /tmp/step-ca

$ curl -LO https://dl.smallstep.com/cli/docs-ca-install/latest/step_linux_arm64.tar.gz

$ tar -zxf step_linux_arm64.tar.gz && rm step_linux_arm64.tar.gz
$ ln -s /tmp/step-ca/step_linux_arm64/bin/step /usr/bin/step

We will need a user and a group to run step-ca as a service. In OpenWRT, you can create a user in two ways:

We install the user management package, create a user, group and home directory.

$ opkg update
$ opkg install shadow-useradd

$ useradd --user-group --system --create-home \
--home-dir /etc/step-ca \
--shell /bin/false step
The second way, if you feel sorry for space for additional packages:

Let's “just” use the functions from /lib/functions.sh.
Create a file useradd.sh

$ touch useradd.sh
$ chmod +x useradd.sh

with the following contents:

#!/bin/sh

# Source OpenWRT functions file
. /lib/functions.sh

usage() {
    echo "Usage: $0 <username> <home_directory>"
    exit 1
}
# Two non empty strings are required arguments
if [ "$#" -ne 2 ] || [ -z "$1" ] || [ -z "$2" ]; then
    usage
fi

USERNAME="$1"
HOME_DIR="$2"
SHELL="/bin/false"
# Script will choose IDs in this range
BASE_ID=900
MAX_ID=999
ID_PAIR=""

fail_fast() {
    issues_found=0
    if user_exists "$USERNAME"; then
        echo "User $USERNAME already exists with UID $(grep "^${USERNAME}:" /etc/passwd | cut -d: -f3)"
        issues_found=1
    fi

    if group_exists "$USERNAME"; then
        echo "Group $USERNAME already exists with GID $(grep "^${USERNAME}:" /etc/group | cut -d: -f3)"
        issues_found=1
    fi

    if [ -d "$HOME_DIR" ]; then
        echo "Home directory $HOME_DIR already exists."
        issues_found=1
    fi

    if [ "$issues_found" -gt 0 ]; then
        echo "Please fix the above. Exiting without changes."
        exit 1
    fi
}

# Find the first pair of unused UID == GID
find_available_id() {
    id="$BASE_ID"
    group_ids=$(cut -d: -f3 /etc/group)
    passwd_ids=$(cut -d: -f3 /etc/passwd)
    # Loop through IDs from BASE_ID to MAX_ID
    while [ "$id" -le "$MAX_ID" ]; do
        # Check if ID is not in /etc/group
        if ! echo "$group_ids" | grep -qw "$id"; then
            # Check if ID is also not in /etc/passwd
            if ! echo "$passwd_ids" | grep -qw "$id"; then
                # ID confirmed
                ID_PAIR="$id"
                break
            fi
        fi
        # Increment the ID
        id=$((id + 1))
    done
    # ID not found
    if [ -z "$ID_PAIR" ]; then
        echo "No available ID found in range $BASE_ID-$MAX_ID" >&2
        exit 1
    fi
}

create_group() {
        echo "Creating group $USERNAME with GID $ID_PAIR"
        group_add "$USERNAME" "$ID_PAIR"
}

create_user() {
        echo "Creating user $USERNAME with UID $ID_PAIR"
        user_add "$USERNAME" "$ID_PAIR" "$ID_PAIR" "$USERNAME" "$HOME_DIR" "$SHELL"
}

create_home_directory() {
    echo "Creating home directory $HOME_DIR"
    mkdir -p "$HOME_DIR"
    chown "$USERNAME:$USERNAME" "$HOME_DIR"
    chmod 755 "$HOME_DIR"
}

main() {
    fail_fast
    find_available_id
    create_group
    create_user
    create_home_directory
}

#Execute script
main

Create a user and a group:

$ ./useradd.sh step /etc/step-ca
Creating group step with GID 900
Creating user step with UID 900
Creating home directory /etc/step-ca

Initializing PKI

First, let's initialize the PKI without configuring the CA itself. This will allow us to first prepare and backup the keys, and then proceed with the configuration.

NOTE: step-ca will use the same password for the root CA and intermediate CA keys. We will set different passwords for these keys. During initialization and in subsequent stages step-ca will offer you the choice to generate a password or enter yours.

$ export STEPPATH=/tmp/step-ca
$ step ca init --pki --name="Homelab" --deployment-type standalone

Choose a password for your CA keys.
✔ [leave empty and we'll generate one]:
✔ Password: ZbeS;.)JR`=^Jak%`)3:Xy9\NwnXTA]_

Generating root certificate... done!
Generating intermediate certificate... done!

✔ Root certificate: /tmp/step-ca/certs/root_ca.crt
✔ Root private key: /tmp/step-ca/secrets/root_ca_key
✔ Root fingerprint: cd6555dce8cfcab515223fdea99531a86d329df31d453d3d0eab4e5d185f6130
✔ Intermediate certificate: /tmp/step-ca/certs/intermediate_ca.crt
✔ Intermediate private key: /tmp/step-ca/secrets/intermediate_ca_key

The root key will be used very rarely. In fact, we won't leave it on OpenWRT at all. The intermediate CA will be used more often, so we'll change the password for the intermediate CA's private key:

$ step crypto change-pass /tmp/step-ca/secrets/intermediate_ca_key

Please enter the password to encrypt /tmp/step-ca/secrets/intermediate_ca_key:
✔ Would you like to overwrite /tmp/step-ca/secrets/intermediate_ca_key [y/n]: y
Your key has been saved in /tmp/step-ca/secrets/intermediate_ca_key.

Now is a good time to back up root_ca_key And root_ca.crt in some secure location on another device.

Setting up and testing the JWK module

For testing and for issuing certificates manually, we will need to activate the JWK module. At this point, the easiest way is to run the command again step ca init with additional parameters, and then replace the generated certificates and keys with those we already have.

NOTE: The password you choose at this stage will also be the password for the JWK module. Do not use passwords for the private keys of the root or intermediate CA.

We will use the port 8443because the port 433 already in use uhttpd for LuCi WebUI. The command below assumes that the IP address of the router on the LAN side is 192.168.1.1and its full domain name is openwrt.lan. Also this time we will generate the database and configuration file in the directory /etc/step-ca:

$ export STEPPATH=/etc/step-ca

$ step ca init \
--name="Homelab CA" \
--dns="openwrt.lan" \
--dns="192.168.1.1" \
--address=":8443" \
--provisioner="JWK@openwrt.lan" \
--deployment-type standalone

Choose a password for your CA keys and first provisioner.
✔ [leave empty and we'll generate one]:XXXXX

Generating root certificate... done!
Generating intermediate certificate... done!

✔ Root certificate: /etc/step-ca/certs/root_ca.crt
✔ Root private key: /etc/step-ca/secrets/root_ca_key
✔ Root fingerprint: 5e4390f0581c479b7e36db4a3642d128f1160d745708e64f588c5f3dabaabd2f
✔ Intermediate certificate: /etc/step-ca/certs/intermediate_ca.crt
✔ Intermediate private key: /etc/step-ca/secrets/intermediate_ca_key
✔ Database folder: /etc/step-ca/db
✔ Default configuration: /etc/step-ca/config/defaults.json
✔ Certificate Authority configuration: /etc/step-ca/config/ca.json

Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.

Let's delete the certificates and keys we just generated and replace them with those we generated in the first step:

$ rm /etc/step-ca/certs/root_ca.crt
$ rm /etc/step-ca/secrets/root_ca_key
$ rm /etc/step-ca/certs/intermediate_ca.crt
$ rm /etc/step-ca/secrets/intermediate_ca_key

$ mv /tmp/step-ca/secrets/intermediate_ca_key /etc/step-ca/secrets/
$ mv /tmp/step-ca/certs/intermediate_ca.crt /etc/step-ca/certs/
$ mv /tmp/step-ca/certs/root_ca.crt  /etc/step-ca/certs/

Also delete the root CA key, which you should already have saved somewhere safe outside of OpenWRT.

$ rm /tmp/step-ca/secrets/root_ca_key

Every time when step-ca starts, we will need to enter the password for the private key of the intermediate CA. This problem can be solved by placing the password in a file from which step-ca will read the password on boot.
In order not to leave traces in the history, we read the password from stdin (after inserting the password, enter ctrl+d on a new line):

$ cat -> /etc/step-ca/secrets/intermediate_ca_key_pass

Note: loading the secret from a file is an acceptable compromise if you are running step-ca in a container in a secure infrastructure. But on bare metal, it's only slightly better than setting the key password to an empty string.

Change the owner of the files to step:step

$ chown -R step:step /etc/step-ca

and let's launch step-ca on behalf of the user stepso that all new files have the correct permissions. Since OpenWRT does not provide sudo “out of the box”, we will use the command start-stop-daemon :

$ start-stop-daemon -S \
-c step:step \
-x /usr/bin/step-ca -- \
/etc/step-ca/config/ca.json \
--password-file /etc/step-ca/secrets/intermediate_ca_key_pass

badger 2024/07/02 11:03:14 INFO: All 0 tables opened in 13ms
2024/07/02 11:03:14 Building new tls configuration using step-ca x509 Signer Interface
2024/07/02 11:03:16 Starting Smallstep CA/0.26.2 (linux/arm64)
2024/07/02 11:03:16 Documentation: https://u.step.sm/docs/ca
2024/07/02 11:03:16 Community Discord: https://u.step.sm/discord
2024/07/02 11:03:16 Config file: /etc/step-ca/config/ca.json
2024/07/02 11:03:16 The primary server URL is https://openwrt.lan:8443
2024/07/02 11:03:16 Root certificates are available at https://openwrt.lan:8443/roots.pem
2024/07/02 11:03:16 Additional configured hostnames: 192.168.1.1
2024/07/02 11:03:16 X.509 Root Fingerprint: cd6555dce8cfcab515223fdea99531a86d329df31d453d3d0eab4e5d185f6130
2024/07/02 11:03:16 Serving HTTPS on :8443 ...

As can be seen from X.509 Root Fingerprint:the fingerprint matches the key we generated at the very beginning. We will need this fingerprint in the next step.

In the second ssh session, we will generate a test certificate for ourselves. First, we initialize the CLI:

$ unset STEPPATH
$ step ca bootstrap \
--ca-url "https://openwrt.lan:8443" \
--fingerprint cd6555dce8cfcab515223fdea99531a86d329df31d453d3d0eab4e5d185f6130
The root certificate has been saved in /root/.step/certs/root_ca.crt.
The authority configuration has been saved in /root/.step/config/defaults.json.

Then we will generate a private key and certificate with Common Name="openwrt.lan". We will also need to enter the password that we set when adding the JWK module:

$ step ca certificate \
"openwrt.lan" \
localhost.crt localhost.key \
--san="openwrt.lan" \
--san="192.168.1.1"

✔ Provisioner: JWK@openwrt.lan (JWK) [kid: sNXCP0f2uaMH3Nvj9wFHPwzQiSxQfSKVwLqO_73kstE]
Please enter the password to decrypt the provisioner key:
✔ CA: https://openwrt.lan:8443
✔ Certificate: localhost.crt
✔ Private Key: localhost.key

Let's take a look at the certificate:

$ step certificate inspect localhost.crt --format=text

Certificate:
    Data:
...
    Signature Algorithm: ECDSA-SHA256
        Issuer: O=Homelab,CN=Homelab Intermediate CA
        Validity
            Not Before: Jul 2 11:09:15 2024 UTC
            Not After : Jul 3 11:10:15 2024 UTC
        Subject: CN=openwrt.lan
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                Server Authentication, Client Authentication
...
            X509v3 Subject Alternative Name:
                DNS:openwrt.lan
                IP Address:192.168.1.1
            X509v3 Step Provisioner:
                Type: JWK
                Name: JWK@openwrt.lan
                CredentialID: sNXCP0f2uaMH3Nvj9wFHPwzQiSxQfSKVwLqO_73kstE
....

Great, we have a certificate that is valid all day long! We need to automate the issuance of certificates, but first let's run step-ca as a service.

Complete the process step-ca or close the second terminal.

Launch step-ca as a service in OpenWRT

Let's create a file /etc/init.d/step-ca

$ touch /etc/init.d/step-ca
$ chmod +x /etc/init.d/step-ca

with the following contents:

#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
SERVICE_COMMAND='/usr/bin/step-ca'
SERVICE_CONFIG='/etc/step-ca/config/ca.json'
SERVICE_ARGS='--password-file /etc/step-ca/secrets/intermediate_ca_key_pass'
SERVICE_PIDFILE=/var/run/step-ca.pid
SERVICE_USER=step
SERVICE_GROUP=step
start_service() {
    procd_open_instance
    procd_set_param command $SERVICE_COMMAND
    procd_append_param command $SERVICE_CONFIG
    procd_append_param command $SERVICE_ARGS
    procd_set_param user $SERVICE_USER
    procd_set_param group $SERVICE_GROUP
    procd_set_param pidfile $SERVICE_PIDFILE
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_set_param file $SERVICE_CONFIG
    procd_set_param respawn
    procd_close_instance
}
reload_service() {
        procd_send_signal step-ca
}

In the second ssh session, we will start syslog monitoring

$ logread -f 

and let's try the commands:

$ /etc/init.d/step-ca start
$ /etc/init.d/step-ca stop
$ /etc/init.d/step-ca restart
$ /etc/init.d/step-ca status
$ /etc/init.d/step-ca reload

Let's add the service to startup

$ /etc/init.d/step-ca enable

and check that the service has been added:

$ ls -l /etc/rc.d/ | grep step-ca
lrwxrwxrwx  1  root root  17 Jul  2  11:20  S99step-ca -> ../init.d/step-ca

Now we are ready to launch our Let's Encrypt.

Setting up and testing the ACME module

Let's supplement our CA with the ACME module:

$ export STEPPATH=/etc/step-ca

$ step ca provisioner add ACME@openwrt.lan --type ACME
✔ CA Configuration: /etc/step-ca/config/ca.json

Success! Your `step-ca` config has been updated. 
To pick up the new configuration SIGHUP (kill -1 <pid>) or restart the step-ca process.

Let's apply the updated configuration:

$ /etc/init.d/step-ca reload

For testing we will need to use a host connected to our router from the LAN side.

First, let's check the availability of the API:

[rocky@test ~]$ curl -s --insecure https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory | jq .

{
  "newNonce": "https://openwrt.lan:8443/acme/ACME@openwrt.lan/new-nonce",
  "newAccount": "https://openwrt.lan:8443/acme/ACME@openwrt.lan/new-account",
  "newOrder": "https://openwrt.lan:8443/acme/ACME@openwrt.lan/new-order",
  "revokeCert": "https://openwrt.lan:8443/acme/ACME@openwrt.lan/revoke-cert",
  "keyChange": "https://openwrt.lan:8443/acme/ACME@openwrt.lan/key-change"
}

Note: in the URL part ACME@openwrt.lan is case sensitive. It must be exactly as entered in the command step ca provider add.

Now let's add our CA's root certificate to the system so that TLS connections are established without errors:

[rocky@test ~]$ curl -s --insecure https://openwrt.lan:8443/roots.pem | tee - openwrt.crt

[rocky@test ~]$ sudo mv openwrt.crt /etc/pki/ca-trust/source/anchors/openwrt.crt

[rocky@test ~]$ sudo update-ca-trust extract

Note: A reference guide to commands for installing root certificates on various Linux operating systems and distributions

Let's make sure we trust our CA:

[rocky@test ~]$ curl https://openwrt.lan:8443/roots

There should be no errors on Linux machines at this stage.

Now let's load it acme.sh

[rocky@test ~]$ curl -LO https://raw.githubusercontent.com/acmesh-official/acme.sh/master/acme.sh

[rocky@test ~]$ chmod +x acme.sh

and we will sequentially test the checks that the ACME module supports in step-ca:

  • http-01 — ACME offers the client to place a given random value in /.well-known/acme-challenge at the port 80. ACME then sends an HTTP GET request to this URL.

  • tls-alpn-01 — ACME prompts the client to generate a self-signed X.509 certificate with the specified string in the X509v3 extension. ACME then establishes a TLS connection on the port with the requested IP and/or domain name 443 using ALPN and checks for the presence of a given string in the certificate extension.

  • dns-01 — ACME prompts the client to create a TXT resource record in DNS for the requested domain name. ACME then sends a request to read this record in DNS.

Testing http-01 ACME check

To pass the type check http-01 we will need to respond to an http GET request on the port 80. The easiest way would be to give the opportunity acme.sh do everything for us by installing socat :

[rocky@test ~]$ sudo dnf install socat -y

You may also need to open a port. 80 in the firewall:

[rocky@test ~]$ sudo firewall-cmd --permanent --add-service=http
[rocky@test ~]$ sudo firewall-cmd --reload

Now we will request a certificate with the fully qualified hostname and IP address as subject alternative names (SAN). Since the port is involved 80the easiest way to run the command is with sudo:

[rocky@test ~]$ sudo ./acme.sh --force --issue --standalone \
--server https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory \
-d $(ip -4 addr show eth0 | awk '/inet/ {split($2, a, "/"); print a[1]}') \
-d $(hostname -f)

[] Using CA: https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory
[] Standalone mode.
[] Standalone mode.
[] Create account key ok.
[] Registering account: https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory
[] Registered
[] ACCOUNT_THUMBPRINT='NYf5qbz9bWo_lBNytbgDaaFCv8MyUHpukWncBvZY7_E'
[] Creating domain key
[] The domain key is here: /root/.acme.sh/192.168.1.179_ecc/192.168.1.179.key
[] Multi domain='IP:192.168.1.179,DNS:test.lan'
[] Getting webroot for domain='192.168.1.179'
[] Getting webroot for domain='test.lan'
[] Verifying: 192.168.1.179
[] Standalone mode server
[] Success
[] Verifying: test.lan
[] Standalone mode server
[] Success
[] Verify finished, start to sign.
[] Lets finalize the order.
[] Le_OrderFinalize="https://openwrt.lan:8443/acme/ACME@openwrt.lan/order/zYqWGaDfUJX3EaIxitPCFnGxvnKu9aJ6/finalize"
[] Downloading cert.
[] Le_LinkCert="https://openwrt.lan:8443/acme/ACME@openwrt.lan/certificate/X1jfcvk9jxso0KBJWDRYYceNE3TkKM3J"
[] Cert success.
-----BEGIN CERTIFICATE-----
MIIB7jCCAZWgAwIBAgIRAMd9th9qDVmO3W1592vVCDEwCgYIKoZIzj0EAwIwNDEQ
MA4GA1UEChMHSG9tZWxhYjEgMB4GA1UEAxMXSG9tZWxhYiBJbnRlcm1lZGlhdGUg
Q0EwHhcNMjQwNzAyMTIyOTIyWhcNMjQwNzAzMTIzMDIyWjAAMFkwEwYHKoZIzj0C
AQYIKoZIzj0DAQcDQgAEiW9mGrhg7ENqXP2c1xFRQLzBEFiiKk8hYD8nQ89Yv8Lp
pOjLQCZtE0hwtyx1bquxUjToO9J5jCef9A+Bwy8luqOBuzCBuDAOBgNVHQ8BAf8E
BAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSr
Alsat1jF0nWnyyy5BrGNJJV0VDAfBgNVHSMEGDAWgBQ/JWh0aHYV//iBsDVMo8jN
1kWpeDAcBgNVHREBAf8EEjAQggh0ZXN0LmxhbocEwKgBszApBgwrBgEEAYKkZMYo
QAEEGTAXAgEGBBBBQ01FQG9wZW53cnQubGFuBAAwCgYIKoZIzj0EAwIDRwAwRAIg
atUHqsKBI0X331v4raTjMGD4yqW2DNFaMOjb455zMmYCIGi9402P30gb+8uIdZtk
y+OK2HMpRjoivujbaIG45qVi
-----END CERTIFICATE-----
[] Your cert is in: /root/.acme.sh/192.168.1.179_ecc/192.168.1.179.cer
[] Your cert key is in: /root/.acme.sh/192.168.1.179_ecc/192.168.1.179.key
[] The intermediate CA cert is in: /root/.acme.sh/192.168.1.179_ecc/ca.cer
[] And the full chain certs is there: /root/.acme.sh/192.168.1.179_ecc/fullchain.cer

Let's take a look at the certificate details:

[rocky@test ~]$ sudo openssl x509 --noout --text -in /root/.acme.sh/192.168.1.179_ecc/192.168.1.179.cer

Certificate:
    Data:
...
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: O = Homelab, CN = Homelab Intermediate CA
        Validity
            Not Before: Jul  2 12:29:22 2024 GMT
            Not After : Jul  3 12:30:22 2024 GMT
        Subject:
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
...
            X509v3 Subject Alternative Name: critical
                DNS:test.lan, IP Address:192.168.1.179
            1.3.6.1.4.1.37476.9000.64.1:
                0......ACME@openwrt.lan..
...

Great! Now let's test the check tls-alpn-01.

Testing tls-alpn-01 ACME check

You may need to open a port. 443 in the firewall:

[rocky@test ~]$ sudo firewall-cmd --permanent --add-service=https
[rocky@test ~]$ sudo firewall-cmd --reload

Let's request a certificate with the same SANs as before, but this time using the option --alpn:

[rocky@test ~]$ sudo ./acme.sh --force --issue --alpn \
--server https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory \
-d $(ip -4 addr show eth0 | awk '/inet/ {split($2, a, "/"); print a[1]}') \
-d $(hostname -f)

[] Using CA: https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory
[] Standalone alpn mode.
[] Standalone alpn mode.
[] Multi domain='IP:192.168.1.179,DNS:test.lan'
[] Getting webroot for domain='192.168.1.179'
[] Getting webroot for domain='test.lan'
[] Verifying: 192.168.1.179
[] Starting tls server.
[] Success
[] Verifying: test.lan
[] Starting tls server.
[] Success
[] Verify finished, start to sign.
[] Lets finalize the order.
[] Le_OrderFinalize="https://openwrt.lan:8443/acme/ACME@openwrt.lan/order/AShNFavQaGD6NUQzAloClC25U7FpiaI5/finalize"
[] Downloading cert.
[] Le_LinkCert="https://openwrt.lan:8443/acme/ACME@openwrt.lan/certificate/bSqEfweMCa8Zrm0ACa9RvBR8m1EXYv72"
[] Cert success.
-----BEGIN CERTIFICATE-----
MIIB7jCCAZSgAwIBAgIQCGxsU1/Od7+w9pOhGC4stTAKBggqhkjOPQQDAjA0MRAw
DgYDVQQKEwdIb21lbGFiMSAwHgYDVQQDExdIb21lbGFiIEludGVybWVkaWF0ZSBD
QTAeFw0yNDA3MDIxMjUyNTRaFw0yNDA3MDMxMjUzNTRaMAAwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAASJb2YauGDsQ2pc/ZzXEVFAvMEQWKIqTyFgPydDz1i/wumk
6MtAJm0TSHC3LHVuq7FSNOg70nmMJ5/0D4HDLyW6o4G7MIG4MA4GA1UdDwEB/wQE
AwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFKsC
Wxq3WMXSdafLLLkGsY0klXRUMB8GA1UdIwQYMBaAFD8laHRodhX/+IGwNUyjyM3W
Ral4MBwGA1UdEQEB/wQSMBCCCHRlc3QubGFuhwTAqAGzMCkGDCsGAQQBgqRkxihA
AQQZMBcCAQYEEEFDTUVAb3BlbndydC5sYW4EADAKBggqhkjOPQQDAgNIADBFAiEA
ubZ2PGzXa/s3QNCalEzPBP90yChTz68WOKDcBOWRaz8CIHiUZijOd4Z9oiR1/nLq
L4CspCrIZjTSalfjoGRJ2Jhp
-----END CERTIFICATE-----
[] Your cert is in: /root/.acme.sh/192.168.1.179_ecc/192.168.1.179.cer
[] Your cert key is in: /root/.acme.sh/192.168.1.179_ecc/192.168.1.179.key
[] The intermediate CA cert is in: /root/.acme.sh/192.168.1.179_ecc/ca.cer
[] And the full chain certs is there: /root/.acme.sh/192.168.1.179_ecc/fullchain.cer

It worked!

Testing dns-01 ACME check

Disclaimer: Since we have configured our own private DNS zone with dynamic updates in OpenWRT, we will use a DNS hook dns_nsupdate For acme.sh. If you are using a public DNS provider, take a look at integrations acme.sh for relevant instructions.

Download the DNS hook:

[rocky@test ~]$ curl -L --create-dirs -o dnsapi/dns_nsupdate.sh \
https://raw.githubusercontent.com/acmesh-official/acme.sh/master/dnsapi/dns_nsupdate.sh

For this test we will use the existing TSIG key:

[rocky@test ~]$ cat <<EOK> keys.conf
key "tsig-key" {
        algorithm hmac-sha256;
        secret "HAyLN66//YxVF2lrZ6kSZK4TZEpV7WMvzYnNUQ0BvEo=";
};
EOK

In general, it is necessary through environment variables $NSUPDATE_SERVER, $NSUPDATE_SERVER_PORT, $NSUPDATE_KEY And $NSUPDATE_ZONE pass on to dns_nsupdate.sh information about the DNS server parameters. In our case, it is enough to specify two of them:

[rocky@test ~]$ export NSUPDATE_SERVER="openwrt.lan"
[rocky@test ~]$ export NSUPDATE_KEY="/home/rocky/keys.conf"

Default acme.sh tries to check for the appearance of the requested TXT record by querying the major public DNS providers. Since we have a private DNS zone, we need to run acme.sh with option --dnssleep=<int>which will make him wait <int> seconds before querying the default DNS server instead of the public ones.

With this type of check we won't be able to use IP as SAN, so let's ask for a wildcard instead (*) certificate:

[rocky@test ~]$ ./acme.sh --force --issue --dns dns_nsupdate --dnssleep 0 \
--server https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory \
-d $(hostname -f) \
-d *.$(hostname -f)

[] Using CA: https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory
[] Multi domain='DNS:test.lan,DNS:*.test.lan'
[] Getting webroot for domain='test.lan'
[] Getting webroot for domain='*.test.lan'
[] Adding txt value: klj5Ewi9WBRJe3qDw6MMYuHcOScy7WJxhRFj-NAlQbE for domain:  _acme-challenge.test.lan
[] adding _acme-challenge.test.lan. 60 in txt "klj5Ewi9WBRJe3qDw6MMYuHcOScy7WJxhRFj-NAlQbE"
[] The txt record is added: Success.
[] Adding txt value: J-Td2g5phdsjapXEMdIKlD-kJdPbzJmKlqhYxri35V0 for domain:  _acme-challenge.test.lan
[] adding _acme-challenge.test.lan. 60 in txt "J-Td2g5phdsjapXEMdIKlD-kJdPbzJmKlqhYxri35V0"
[] The txt record is added: Success.
[] Sleep 0 seconds for the txt records to take effect
[] Verifying: test.lan
[] Success
[] Verifying: *.test.lan
[] Success
[] Removing DNS records.
[] Removing txt: klj5Ewi9WBRJe3qDw6MMYuHcOScy7WJxhRFj-NAlQbE for domain: _acme-challenge.test.lan
[] removing _acme-challenge.test.lan. txt
[] Removed: Success
[] Removing txt: J-Td2g5phdsjapXEMdIKlD-kJdPbzJmKlqhYxri35V0 for domain: _acme-challenge.test.lan
[] removing _acme-challenge.test.lan. txt
[] Removed: Success
[] Verify finished, start to sign.
[] Lets finalize the order.
[] Le_OrderFinalize="https://openwrt.lan:8443/acme/ACME@openwrt.lan/order/FO5Jl80UWqfNI3oQkapt7wv2U2rOP3F8/finalize"
[] Downloading cert.
[] Le_LinkCert="https://openwrt.lan:8443/acme/ACME@openwrt.lan/certificate/PcJd1MPhm1quJPCYrYm8m5UOJVVjDPGF"
[] Cert success.
-----BEGIN CERTIFICATE-----
MIICBTCCAaqgAwIBAgIQH+jtwcPGB55FOSejBblXnDAKBggqhkjOPQQDAjA0MRAw
DgYDVQQKEwdIb21lbGFiMSAwHgYDVQQDExdIb21lbGFiIEludGVybWVkaWF0ZSBD
QTAeFw0yNDA3MDIxNTUwMjRaFw0yNDA3MDMxNTUxMjRaMBMxETAPBgNVBAMTCHRl
c3QubGFuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiGQ65642wyOn5kdzlGTM
kwi7/9KnRl6wLGOJ1hPGC0CE5FG4G9MpnyfuFfndL+4H3UZzIP0oN1D4DCoPj5kj
gaOBvjCBuzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
AQUFBwMCMB0GA1UdDgQWBBQqnem/GMhwtJVGRY6wn8ALxAYswjAfBgNVHSMEGDAW
gBQ/JWh0aHYV//iBsDVMo8jN1kWpeDAfBgNVHREEGDAWggoqLnRlc3QubGFuggh0
ZXN0LmxhbjApBgwrBgEEAYKkZMYoQAEEGTAXAgEGBBBBQ01FQG9wZW53cnQubGFu
BAAwCgYIKoZIzj0EAwIDSQAwRgIhAIp5TAMCK1RxiZKBELENjMPRwP+5kRJDl3dD
aDk1gxbxAiEA34ARTiq8HXB+XAqpgKk++YP4ZjpKWOYr+2+sGt5gro4=
-----END CERTIFICATE-----
[] Your cert is in: /home/rocky/.acme.sh/test.lan_ecc/test.lan.cer
[] Your cert key is in: /home/rocky/.acme.sh/test.lan_ecc/test.lan.key
[] The intermediate CA cert is in: /home/rocky/.acme.sh/test.lan_ecc/ca.cer
[] And the full chain certs is there: /home/rocky/.acme.sh/test.lan_ecc/fullchain.cer

Let's take a look at the certificate details:

[rocky@test ~]$ openssl x509 --noout --text -in /home/rocky/.acme.sh/test.lan_ecc/test.lan.cer

Certificate:
    Data:
...
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: O = Homelab, CN = Homelab Intermediate CA
        Validity
            Not Before: Jul  2 15:50:24 2024 GMT
            Not After : Jul  3 15:51:24 2024 GMT
        Subject: CN = test.lan
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
...
            X509v3 Subject Alternative Name:
                DNS:*.test.lan, DNS:test.lan
            1.3.6.1.4.1.37476.9000.64.1:
                0......ACME@openwrt.lan..
...

Out of curiosity, let's take a look at the DNS server logs:

tsig-key: updating zone 'lan/IN': adding an RR at '_acme-challenge.test.lan' TXT "klj5Ewi9WBRJe3qDw6MMYuHcOScy7WJxhRFj-NAlQbE"
...
tsig-key: updating zone 'lan/IN': adding an RR at '_acme-challenge.test.lan' TXT "J-Td2g5phdsjapXEMdIKlD-kJdPbzJmKlqhYxri35V0"
...
tsig-key: updating zone 'lan/IN': deleting rrset at '_acme-challenge.test.lan' TXT
...
tsig-key: updating zone 'lan/IN': deleting rrset at '_acme-challenge.test.lan' TXT

Well, the ACME module works, but there are still a few issues left to be resolved.

Configuring certificate parameters

At the moment our CA configuration file /etc/step-ca/config/ca.json should look something like this:

{
    "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": ":8443",
    "insecureAddress": "",
    "dnsNames": [
            "openwrt.lan",
            "192.168.1.1"
    ],
    "logger": {
            "format": "text"
    },
    "db": {
            "type": "badgerv2",
            "dataSource": "/etc/step-ca/db",
            "badgerFileLoadingMode": ""
    },
    "authority": {
            "provisioners": [
                    {
                            "type": "JWK",
                            "name": "JWK@openwrt.lan",
                            "key": { ... },
                            "encryptedKey": " ... "
                    },
                    {
                            "type": "ACME",
                            "name": "ACME@openwrt.lan",
                            "claims": {
                                    "enableSSHCA": true,
                                    "disableRenewal": false,
                                    "allowRenewalAfterExpiry": false,
                                    "disableSmallstepExtensions": false
                            },
                            "options": {
                                    "x509": {},
                                    "ssh": {}
                            }
                    }
            ],
            "template": {},
            "backdate": "1m0s"
    },
    "tls": { ... },
    "commonName": "Step Online CA"
}

Now let's tweak a few things:

  • Let's add more details to the field Subject:;

  • Let's set the validity period of certificates;

  • Let's enable the extension CRLDistributionPointsto overcome the error curl in Windows.

Setting up Subject:

At the moment in Subject: certificate is present only Common Name. In order for something else to appear there, we need to fill in authority.template:

...
  "authority": {
...
            "template":{
                "Country": "AQ",
                "Province": "South Pole",
                "Locality": "Amundsen-Scott South Pole Station",
                "Organization": "Homelab",
                "OrganizationalUnit": "Homelab cluster"
                },
}
...

and reload the configuration:

$ /etc/init.d/step-ca reload

Note: technically it shouldn't matter what we have written in the field "Country":but some systems for some reason perform a check for compliance with codes ISO 3166-1 alpha-2. Therefore, it is recommended to choose something from this just in case. list

Setting up the validity period of certificates

Default step-ca does not allow creating certificates with a validity period longer than 24 hours. For example, this request will not work:

[rocky@test ~]$ ./acme.sh --force --issue --dns dns_nsupdate --dnssleep 0 \
--server https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory \
-d $(hostname -f) \
--valid-to  "+25h"

...
[] Sign failed, finalize code is not 200.
...

Such a narrow time window can be a problem for automation tools that have a minimum certificate renewal frequency of one day.

For each module in the list authority.provisioners[] and in the very authority you can specify a dictionary claimswhich controls the verification of the requested certificate parameters. A full list of options is available Herebut for our task we are interested in:

  • maxTLSCertDuration — maximum validity period of the certificate,

  • defaultTLSCertDuration — the default certificate validity period.

Let's configure our CA so that the module for manually issuing certificates JWK@openwrt.lan could create certificates with a validity period of up to a year (8765h):

"provisioners": [
    {
     "type": "JWK",
     "name": "JWK@openwrt.lan",
     "claims": {
         "maxTLSCertDuration": "8765h"
     },
....

and a module for automatic issuance of certificates ACME@openwrt.lan issued certificates valid for two days:

"provisioners": [
...
   {
    "type": "ACME",
    "name": "ACME@openwrt.lan",
    "claims": {
      ...
      "maxTLSCertDuration": "48h",
      "defaultTLSCertDuration": "48h",
      ...
    },
...

Now the configuration file /etc/step-ca/config/ca.json looks something like this:

{
    "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": ":8443",
    "insecureAddress": "",
    "dnsNames": [
            "openwrt.lan",
            "192.168.1.1"
    ],
    "logger": {
            "format": "text"
    },
    "db": {
            "type": "badgerv2",
            "dataSource": "/etc/step-ca/db",
            "badgerFileLoadingMode": ""
    },
    "authority": {
            "provisioners": [
                    {
                            "type": "JWK",
                            "name": "JWK@openwrt.lan",
                            "claims": {
                                "maxTLSCertDuration": "8765h"
                            },
                            "key": {...},
                            "encryptedKey": "..."
                    },
                    {
                            "type": "ACME",
                            "name": "ACME@openwrt.lan",
                            "claims": {
                                    "enableSSHCA": true,
                                    "disableRenewal": false,
                                    "allowRenewalAfterExpiry": false,
                                    "disableSmallstepExtensions": false,
                                    "maxTLSCertDuration": "48h",
                                    "defaultTLSCertDuration": "48h"
                            },
                            "options": {
                                    "x509": {},
                                    "ssh": {}
                            }
                    }
            ],
            "template": {
                "Country": "AQ",
                "Province": "South Pole",
                "Locality": "Amundsen-Scott South Pole Station",
                "Organization": "Homelab",
                "OrganizationalUnit": "Homelab cluster"
                },
            "backdate": "1m0s"
    },
    "tls": {...},
    "commonName": "Step Online CA"
}

Reloading the settings

 $ /etc/init.d/step-ca reload

and we ask ACME to issue a certificate for 2 days:

[rocky@test ~]$ ./acme.sh --force --issue --dns dns_nsupdate --dnssleep 0 \
--server https://openwrt.lan:8443/acme/ACME@openwrt.lan/directory \
-d $(hostname -f) \
--valid-to  "+2d"

...
[] Cert success.
...

Enable the CRLDistributionPoints extension

In Windows, even if we add our CA to the trusted certificate store using the following commands

> curl -k -LO https://openwrt.lan:8443/roots.pem
> certutil -addstore -enterprise -f "Root" roots.pem

some versions curl at https A request to any URL that returns a certificate issued by our CA may return an error:
curl: (35) schannel: next InitializeSecurityContext failed: CRYPT_E_NO_REVOCATION_CHECK (0x80092012)- The revocation function was unable to check revocation for the certificate.

To fix this behavior, we need to enable the CRL (Certificate Revocation Lists) feature. To do this, in step-ca we will raise an unprotected http endpoint crl at the port 8080 (usually this is a port 80but it is already used by the router's web interface) and configure the CA so that it adds the corresponding X509v3 extension to the certificates generated by our modules.

IN /etc/step-ca/config/ca.json we fill in "insecureAddress" and add a dictionary "crl":

{
...
    "insecureAddress": ":8080",
    "crl": {
        "enabled": true,
        "generateOnRevoke": true,
        "idpURL": "http://openwrt.lan:8080/crl"
    },
...
}

Let's create a template file for our certificates:

$ mkdir -p /etc/step-ca/templates/x509
$ touch /etc/step-ca/templates/x509/leaf-crl.tpl
$ chown -R step:step /etc/step-ca/templates/x509

and just add in basic template field "crlDistributionPoints": ["http://openwrt.lan:8080/crl"]

$ cat <<EOF> /etc/step-ca/templates/x509/leaf-crl.tpl
{

    "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://openwrt.lan:8080/crl"]

}
EOF

Now in the file /etc/step-ca/config/ca.json for each module in the list authority.provisioners[] we need to refer to options.x509 on the way to the template "templateFile": "/etc/step-ca/templates/x509/leaf-crl.tpl":

...
"options": {
    "x509": {
        "templateFile": "/etc/step-ca/templates/x509/leaf-crl.tpl"
    },
...
}
...

Ultimately the configuration file /etc/step-ca/config/ca.json will look something like this:

{
    "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": ":8443",
    "insecureAddress": ":8080",
    "crl": {
        "enabled": true,
        "generateOnRevoke": true,
        "idpURL": "http://openwrt.lan:8080/crl"
    },
    "dnsNames": [
            "openwrt.lan",
            "192.168.1.1"
    ],
    "logger": {
            "format": "text"
    },
    "db": {
            "type": "badgerv2",
            "dataSource": "/etc/step-ca/db",
            "badgerFileLoadingMode": ""
    },
    "authority": {
            "provisioners": [
                    {
                            "type": "JWK",
                            "name": "JWK@openwrt.lan",
                            "claims": {
                                "maxTLSCertDuration": "8765h"
                            },
                            "key": {...},
                            "encryptedKey": "...",
                            "options": {
                                    "x509": {
                                        "templateFile": "/etc/step-ca/templates/x509/leaf-crl.tpl"
                                    }
                                }
                    },
                    {
                            "type": "ACME",
                            "name": "ACME@openwrt.lan",
                            "claims": {
                                    "enableSSHCA": true,
                                    "disableRenewal": false,
                                    "allowRenewalAfterExpiry": false,
                                    "disableSmallstepExtensions": false,
                                    "maxTLSCertDuration": "48h",
                                    "defaultTLSCertDuration": "48h"
                            },
                            "options": {
                                    "x509": {
                                        "templateFile": "/etc/step-ca/templates/x509/leaf-crl.tpl"
                                    },
                                    "ssh": {}
                            }
                    }
            ],
            "template": {
                "Country": "AQ",
                "Province": "South Pole",
                "Locality": "Amundsen-Scott South Pole Station",
                "Organization": "Homelab",
                "OrganizationalUnit": "Homelab cluster"
                },
            "backdate": "1m0s"
    },
    "tls": {...},
    "commonName": "Step Online CA"
}

This time you will have to restart the service itself:

$ /etc/init.d/step-ca restart

The fastest way to check if your certificates have an extension X509v3 CRL Distribution Points: – this is to create a certificate for the webUI of the router itself:

$ step ca certificate openwrt.lan \
/etc/uhttpd.crt /etc/uhttpd.key \
--san openwrt.lan \
--not-after=8765h

✔ Provisioner: JWK@openwrt.lan (JWK) [kid: sNXCP0f2uaMH3Nvj9wFHPwzQiSxQfSKVwLqO_73kstE]

Please enter the password to decrypt the provisioner key:
✔ CA: https://openwrt.lan:8443
✔ Would you like to overwrite /etc/uhttpd.crt [y/n]: y
✔ Would you like to overwrite /etc/uhttpd.key [y/n]: y
✔ Certificate: /etc/uhttpd.crt
✔ Private Key: /etc/uhttpd.key

Restart the service uhttpd

$ /etc/init.d/uhttpd restart

and check if the problem is gone:

C:\>curl -I https://openwrt.lan:443
HTTP/1.1 200 OK

Conclusion

If you've made it this far, you have a tool in your hands that can greatly simplify the use of TLS in your home lab.

Good luck with your experiments!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *