Углубляясь в тему DevOps в своей домашней лаборатории, я начал замечать, что зачастую проще задействовать TLS/mTLS, чем настраивать и отлаживать способы обойтись без него.

Задумавшись о надежном хостинге для приватного CA, обнаружил, что среди всего моего электрооборудования только у двух приборов аптайм близок к 100%: у холодильника и интернет-роутера.

Идея получать из холодильника не только напитки, но и SSL-сертификаты так грела душу, что я почти начал искать, где купить умный холодильник. Потом немного остыл и решил сначала попробовать роутер с прошивкой OpenWRT.

Уверен, в комментариях подскажут много классных решений для приватных CA, я же остановил свой выбор на step-ca. Step-CA представляет собой PKI-ядро и различные подключаемые модули, выдающие сертификаты, если пройдена аутентификация. В этом туториале мы будем получать X.509 сертификаты в обмен на прохождение проверок ACME и в обмен на JWK токены, сгенерированные CLI клиентом step.

Дисклеймер: приведенные ниже шаги довольно инвазивны и не гарантируют успех; возможно, сначала стоит их протестировать на виртуальном OpenWrt роутере. Я начинаю сразу после настройки динамических обновлений DNS зон в OpenWRT, но этот гайд самодостаточен, если у вас уже есть контроль над разрешением имен в вашей сети.

План:

  • Установка step-ca в OpenWRT

  • Генерация ключей для PKI

  • Настройка и тестирование JWK модуля

  • Конфигурируем step-ca как сервис

  • Настройка и тестирование ACME модуля

  • Корректировка параметров сертификатов.

Установка step-ca в OpenWRT

Процесс установки несложен, но имейте в виду, что понадобится ~ 37 МБ свободного места на основном разделе роутера:

$ 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/

Далее нам нужно загрузить step-cli, который понадобится на этапе настройки, но в дальнейшем будет использоваться редко. Оставим его в /tmp и просто сделаем мягкую ссылку в /usr/bin/. Исполняемый файл будет удален после перезагрузки, но его всегда можно восстановить, повторив команды:

$ 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

Нам понадобится юзер и группа для запуска step-ca в качестве сервиса. В OpenWRT создать юзера можно двумя способами:

Устанавливаем пакет управления пользователями, создаём пользователя, группу и домашний каталог.

$ opkg update
$ opkg install shadow-useradd

$ useradd --user-group --system --create-home \
--home-dir /etc/step-ca \
--shell /bin/false step
Второй способ, если жалко места на дополнительные пакеты:

Давайте "просто" воспользуемся функциями из /lib/functions.sh.
Создаем файл useradd.sh

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

со следующим содержимым:

#!/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

Создаем юзера и группу:

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

Инициализируем PKI

Для начала инициализируем PKI без конфигурации самого CA. Это позволит нам сначала подготовить и забэкапить ключи, а уже потом заняться настройкой.

ПРИМЕЧАНИЕ: step-ca применит один и тот же пароль для ключей корневого CA и промежуточного CA. Мы же установим разные пароли для этих ключей. Во время инициализации и на последующих этапах step-ca предложит на выбор сгенерировать пароль или ввести ваш.

$ 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

Корневой ключ будет использоваться очень редко. На самом деле, мы вообще не будем оставлять его на OpenWRT. Промежуточный CA будет использоваться чаще, поэтому мы сменим пароль для закрытого ключа промежуточного CA:

$ 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.

Сейчас подходящий момент забэкапить root_ca_key и root_ca.crt в каком-нибудь надежном месте на другом устройстве.

Настраиваем и тестируем JWK модуль

Для тестирования и для выдачи сертификатов вручную нам потребуется активировать модуль JWK. На данном этапе самый простой способ — это снова запустить команду step ca init с дополнительными параметрами, а затем заменить сгенерированные сертификаты и ключи на те, которые у нас уже есть.

ВНИМАНИЕ: Пароль, который вы выберете на этом этапе, будет также и паролем к JWK модулю. Не используйте пароли от приватных ключей корневого или промежуточного CA.

Мы будем использовать порт 8443, поскольку порт 433 уже используется uhttpd для LuCi WebUI. В приведенной ниже команде предполагается, что IP-адрес роутера на стороне локальной сети — 192.168.1.1, а его полное доменное имя — openwrt.lan. Также на этот раз мы сгенерируем базу и файл конфигурации в каталоге /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'.

Удалим сгенерированные только что сертификаты и ключи и заменим на те, которые мы сгенерировали на первом шаге:

$ 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/

Также удалим ключ корневого CA, который вы уже должны были сохранить где-нибудь в безопасном месте за пределами OpenWRT.

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

Каждый раз, когда step-ca стартует, нам нужно будет вводить пароль для приватного ключа промежуточного CA. Эту проблему можно решить, поместив пароль в файл, из которого step-ca прочитает пароль при загрузке.
Чтобы не оставлять следов в хистори, считаем пароль из stdin (после вставки пароля введите ctrl+d на новой строке):

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

Примечание: загрузка секрета из файла - приемлемый компромисс, если вы запускаете step-ca в контейнере в безопасной инфраструктуре. Но на голом железе это лишь чуть лучше, чем задать пустую строку в качестве пароля к ключу.

Сменим владельца файлов на step:step

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

и запустим step-ca от имени пользователя step, чтобы все новые файлы имели нужные права. Поскольку OpenWRT не предоставляет sudo "из коробки", воспользуемся командой 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 ...

Как видно из X.509 Root Fingerprint:, отпечаток соответствует ключу, который мы сгенерировали в самом начале. Этот отпечаток понадобится нам на следующем шаге.

Во второй ssh сессии сгенерируем тестовый сертификат для самих себя. Сначала инициализируем 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.

Затем сгенерируем приватный ключ и сертификат с Common Name="openwrt.lan". Нам также нужно будет ввести пароль, который мы задали, когда добавляли JWK модуль:

$ 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

Взглянем на сертификат:

$ 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
....

Супер, у нас есть сертификат, валидный целый день! Нам нужно автоматизировать выдачу сертификатов, но сначала запустим step-ca как сервис.

Завершите процесс step-ca или закройте второй терминал.

Запускаем step-ca как сервис в OpenWRT

Создадим файл /etc/init.d/step-ca

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

со следующим содержимым:

#!/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
}

Во второй ssh сессии запустим мониторинг syslog

$ logread -f 

и попробуем команды:

$ /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

Добавим сервис в автозагрузку

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

и проверим, что сервис добавлен:

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

Теперь мы готовы запустить свой Let's Encrypt.

Настраиваем и тестируем ACME модуль

Дополним наш CA модулем ACME:

$ 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.

Применим обновленную конфигурацию:

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

Для тестирования нам нужно будет использовать хост, подключенный к нашему маршрутизатору со стороны локальной сети.

Первым делом проверим доступность 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"
}

Примечание: в URL часть ACME@openwrt.lan чувствительна к регистру. Она должна быть точно такой, как вводили в команде step ca provider add.

Теперь добавим в систему корневой сертификат нашего CA , чтобы TLS-соединения устанавливались без ошибок:

[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

Примечание: справочник по командам для установки корневых сертификатов в различных операционных системах и дистрибутивах Linux

Убедимся, что мы доверяем нашему CA:

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

На этом этапе не должно возникать ошибок на Linux машинах.

Теперь загрузим acme.sh

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

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

и последовательно протестируем проверки, которые поддерживает ACME модуль в step-ca:

  • http-01 — ACME предлагает клиенту разместить заданное случайное значение в /.well-known/acme-challenge на порту 80. Затем ACME отправляет HTTP GET запрос на этот URL.

  • tls-alpn-01 — ACME предлагает клиенту сгенерировать самоподписанный X.509 сертификат с заданной строкой в X509v3 расширении. Затем ACME устанавливает с запрошенным IP и/или доменным именем TLS соединение на порту 443 с использованием ALPN и проверяет наличие заданной строки в расширении сертификата.

  • dns-01 — ACME предлагает клиенту сделать ресурсную TXT запись в DNS для запрошенного доменного имени. Затем ACME отправляет запрос на чтение этой записи в DNS.

Тестируем http-01 проверку ACME

Для прохождения проверки типа http-01 нам понадобится ответить на http GET запрос на порту 80. Самым простым способом будет дать возможность acme.sh сделать все за нас, установив socat :

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

Также, возможно, нужно будет открыть порт 80 в файрволе:

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

Теперь запросим сертификат с полным именем хоста и IP-адресом в качестве альтернативных имен субъекта (SAN). Поскольку задействован порт 80, проще всего запустить команду с помощью 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

Взглянем на детали сертификата:

[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..
...

Прекрасно! Теперь протестируем проверку tls-alpn-01.

Тестируем tls-alpn-01 проверку ACME

Возможно, понадобится открыть порт 443 в файрволе:

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

Запросим сертификат с теми же SAN, что и ранее, но на этот раз с использованием опции --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

Сработало!

Тестируем dns-01 проверку ACME

Дисклеймер: поскольку мы настроили в OpenWRT свою приватную зону DNS с динамическими обновлениями, то будем использовать DNS-хук dns_nsupdate для acme.sh. Если вы используете публичный DNS провайдер, загляните в интеграции acme.sh для соответствующих инструкций.

Скачиваем DNS-хук:

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

Для этого теста воспользуемся уже имеющимся TSIG ключом:

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

В общем случае, нужно через переменные среды $NSUPDATE_SERVER, $NSUPDATE_SERVER_PORT, $NSUPDATE_KEY и $NSUPDATE_ZONE передать в dns_nsupdate.sh информацию о параметрах DNS-сервера. В нашем же случае достаточно задать две из них:

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

По умолчанию acme.sh пытается проверить появление запрошенной записи TXT, опрашивая основных публичных DNS провайдеров. Поскольку у нас приватная зона DNS, нам нужно запустить acme.sh с опцией --dnssleep=<int>, которая заставит его ждать <int> секунд перед опросом DNS-сервера по умолчанию вместо публичных.

С этим типом проверки мы не сможем использовать IP в качестве SAN, поэтому давайте вместо этого попросим вайлдкард (*) сертификат:

[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

Взглянем на детали сертификата:

[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..
...

Из любопытства заглянем в логи DNS сервера:

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

Что ж, ACME модуль работает, однако осталось решить еще несколько вопросов.

Настраиваем параметры сертификатов

В данный момент файл конфигурации нашего CA /etc/step-ca/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": ":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"
}

Теперь давайте подправим несколько вещей:

  • Добавим больше деталей в поле Subject:;

  • Настроим срок действия сертификатов;

  • Включим расширение CRLDistributionPoints, чтобы побороть ошибку curl в Windows.

Настраиваем Subject:

На данный момент в Subject: сертификата присутствует только Common Name. Чтобы там появилось еще что-то, нам нужно заполнить authority.template:

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

и перезагрузить конфигурацию:

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

Примечание: технически не должно иметь значения, что у нас записано в поле "Country":, но некоторые системы зачем-то выполняют проверку на соответствие кодам ISO 3166-1 alpha-2. Поэтому на всякий случай рекомендуется выбрать что-нибудь из этого списка

Настраиваем срок действия сертификатов

По умолчанию step-ca не позволяет создавать сертификаты со сроком действия более чем 24 часа. Например, такой запрос не пройдет:

[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.
...

Такой узкий временной интервал может стать проблемой для инструментов автоматизации, у которых минимальная частота обновления сертификатов равна одному дню.

Для каждого модуля в списке authority.provisioners[] и в самом authority можно задать словарь claims, который управляет проверкой запрошенных параметров сертификата. Полный список опций доступен здесь, но для нашей задачи нас интересуют:

  • maxTLSCertDuration — максимальный срок действия сертификата,

  • defaultTLSCertDuration — срок действия сертификата по умолчанию.

Давайте настроим наш CA так, чтобы модуль для ручной выдачи сертификатов JWK@openwrt.lan мог создавать сертификаты со сроком действия до года (8765h):

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

а модуль для автоматической выдачи сертификатов ACME@openwrt.lan выдавал сертификаты со сроком действия два дня:

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

Теперь файл конфигурации /etc/step-ca/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": ":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"
}

Перезагружаем настройки

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

и просим ACME выдать сертификат на 2 дня:

[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.
...

Включаем расширение CRLDistributionPoints

В Windows, даже если мы добавим наш CA в хранилище доверенных сертификатов с помощью следующих команд

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

некоторые версии curl при https запросе на любой URL, который возвращает сертификат, выданный нашим CA, могут выдать ошибку:
curl: (35) schannel: next InitializeSecurityContext failed: CRYPT_E_NO_REVOCATION_CHECK (0x80092012)- The revocation function was unable to check revocation for the certificate.

Чтобы исправить такое поведение, нам нужно включить функцию CRL (списки отзыва сертификатов). Для этого в step-ca мы подымем незащищенный http эндпоинт crl на порту 8080 (как правило, это порт 80, но он уже используется веб-интерфейсом роутера) и настроим CA так, чтобы он добавлял соответствующее расширение X509v3 в сертификаты, генерируемые нашими модулями.

В /etc/step-ca/config/ca.json заполняем "insecureAddress" и добавляем словарь "crl":

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

Создаем файл шаблона для наших сертификатов:

$ 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

и просто добавляем в базовый шаблон поле "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

Теперь в файле /etc/step-ca/config/ca.json для каждого модуля в списке authority.provisioners[]  нам нужно сослаться в options.x509 на путь к шаблону "templateFile": "/etc/step-ca/templates/x509/leaf-crl.tpl":

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

В конечном итоге файл конфигурации /etc/step-ca/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": ":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"
}

На этот раз придется перезагрузить сам сервис:

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

Самый быстрый способ проверить, присутствует ли в сертификатах расширение X509v3 CRL Distribution Points: — это создать сертификат для webUI самого роутера:

$ 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

Перезагружаем сервис uhttpd

$ /etc/init.d/uhttpd restart

и проверяем, ушла ли проблема:

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

Заключение

Если вы дошли до этого места — в ваших руках находится инструмент, который может значительно упростить использование TLS в домашней лаборатории.

Удачи в экспериментах!

Комментарии (1)


  1. Loggus66
    07.07.2024 22:23
    +1

    Статья отличная, но OpenWRT - ненужная специфика. Я поднимал PoC step-ca с демо для одной компании, бегло посмотрел - вроде ничего не изменилось с тех пор. Только с меня ещё и мониторинг требовали, сколько и кому сертов выдали, и вот тут нетривиально, потому что step-ca хранит JSON внутри mysql строками. Посмотрел на это, плюнул, поднял Postgres, mysql_fdw и оперировал строками оттуда как ::json внутри pg, с приведением типов.

    Специфику не помню, но данные внутри БД step-ca разнесены по двум таблицам по каждому сертификату и проще сначала было вырезать JSON в нормальные таблицы, потом сделать full join, а потом городить view из набора колонок.