Привет, Хабр! Недавно делал одну интересную задачу, связанную с актуализацией просроченных сертификатов (открытых ключей), используемых для подписания и шифрования файлов при помощи Криптопро 5. Решил поделиться опытом. Возможно, это поможет сэкономить время при решении похожей задачи или будет просто интересным чтивом для вас.

Дисклеймер

Я буду очень рад вашим замечаниям и предложениям по оптимизации bash скрипта, генерирующего сервисный json с информацией о сроке годности сертификатов. Так же буду рад услышать советы по оптимизации Dockerfile. Заранее спасибо.

Что будет в статье

  • подготовка скрипта для сбора данных о состоянии сертификатов

  • сборка Docker образа с Криптопро 5 на основе Fedora 38

  • загрузка сертификатов в контейнер

Подготовка скрипта для сбора данных о состоянии сертификатов

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

Чтобы решить эту проблему, было принято решение разработать небольшое дополнение к Криптопро 5, которое помогло актуализировать информацию об имеющихся сертификатах.

#!/usr/bin/env bash

CERTS_CHECK="$(certmgr -list | grep -E "Subject ")"

# check if certs exists
if [[ $CERTS_CHECK == *"Subject "* ]]; then
	touch ./result.json
else
	echo "[]" > ./result.json
    exit 0
fi

CERTS_QTY="$(certmgr -list | grep -E "Subject " | wc -l)"
COUNTER=1

# prepare data from certmgr
certmgr -list | grep -E "Subject " | awk -F'CN=' '{ print $2 }' | tr -d '"' > ./names.txt
certmgr -list | grep -E "Subject " | awk -F'E=' '{ print $2 }' | cut -d "," -f 1 | tr -d '"' > ./emails.txt
certmgr -list | grep -E "Not valid after" | cut -d ":" -f 2 | cut -d " " -f 2 | tr -d '"' > ./valid_to.txt

# prepare result json
echo "[" > ./result.json
while (($COUNTER <= $CERTS_QTY ))
do
	echo '	{' >> ./result.json
	echo '		"name": "'"$(cat ./names.txt | tail -n $COUNTER | head -n 1)"'",'>> ./result.json
	echo '		"e-mail": "'"$(cat ./emails.txt | tail -n $COUNTER | head -n 1)"'",' >> ./result.json
	echo '		"valid-to": "'"$(cat ./valid_to.txt | tail -n $COUNTER | head -n 1)"'"' >> ./result.json
	echo '	}' >> ./result.json
	echo "	," >> ./result.json
	COUNTER=$((COUNTER + 1))
done
sed -i '$ d' ./result.json
echo "]" >> ./result.json

# remove tmp files
rm -f ./names.txt
rm -f ./emails.txt
rm -f ./valid_to.txt

# print result
if [[ $CERTS_CHECK == *"Subject "* ]]; then
	cat ./result.json
fi

Результатом работы скрипта является файл result.json, содержащий информацию о сроке действия сертификатов, электронной почте владельцев сертификатов и CN.

Пример вывода содержимого файла result.json
Пример вывода содержимого файла result.json

Также скрипт выводит в консоль содержание файла result.json, если в Криптопро установлены сертификаты или предупреждение об отсутствии сертификатов, если их нет.

Пример вывода предупреждение об отсутствии сертификатов
Пример вывода предупреждение об отсутствии сертификатов

Сборка Docker образа с Криптопро 5 на основе fedora 38

Здесь все просто, как пять копеек. Качаем rpm дистрибутив, делаем Dockerfile и собираем образ. Дистрибутив доступен по этой ссылке - https://www.cryptopro.ru/products/csp.

Обратите внимание, что есть возможность выбрать и deb и rpm дистрибутив.

Варианты дистрибутивов Криптопро 5
Варианты дистрибутивов Криптопро 5

Содержимое Dockerfile:

FROM fedora:38

WORKDIR /usr/src/cryptopro

COPY ./distr ./distr

RUN yes Y | ./distr/install.sh kc1
RUN ln -s /opt/cprocsp/sbin/amd64/cpconfig /usr/bin/cpconfig
RUN ln -s /opt/cprocsp/bin/amd64/certmgr /usr/bin/certmgr
RUN ln -s /opt/cprocsp/bin/amd64/cryptcp /usr/bin/cryptcp

COPY ./src/certs_info /usr/bin/certs_info

CMD [ "tail", "-f", "/dev/null" ]

Папка distr содержит распакованный архив с дистрибутивом Криптопро 5. Папка src содержит исполняемый скрипт certs_info, который был описан ранее.

Обратите внимание, что после установки нужно сделать soft link на исполняемые файлы, чтобы не указывать полный путь до них каждый раз, когда запускаете ту или иную команду. В этом примере используются только три исполняемые команды: cpconfig, certmgr и cryptcp. Если вам нужны другие команды, не забудьте их добавить в этот перечень.

Чтобы собрать образ, нужно выполнить команду docker build.

docker build -t cryptopro5 .

Чтобы запустить контейнер, нужно выполнить команду docker run.

docker run -v ./cer:/cer -d --name cryptopro5 cryptopro5

Обратите внимание на расшареную директорию сer. Она понадобится для установки сертификатов.

Чтобы проверить статус лицензии, нужно выполнить команду в запущенном контейнере.

docker exec cryptopro5 cpconfig -license -view
Результат проверки состояния лицензии
Результат проверки состояния лицензии

Загрузка сертификатов в контейнер

В целях упрощения тестирования на сайте Криптопро есть возможность загрузить тестовые сертификаты. Они доступны по этой ссылке - http://testca2012.cryptopro.ru/ui/.

Пункты 3 и 4
Пункты 3 и 4

Чтобы установить корневой сертификат, нужно воспользоваться параметром -store со значением root. Также обратите внимание, что в процессе установки сертификата утилита ожидает ответа от пользователя, ответ не буква Y а буква o.

docker exec cryptopro5 bash -c "yes o | certmgr -install -store root -file /cer/rootca.cer"

Чтобы установить сторонний сертификат, параметр -store не нужен.

docker exec cryptopro5 certmgr -install -file /cer/subca.cer

Теперь можно проверить состояние сторонних сертификатов при помощи написанного ранее скрипта.

docker exec cryptopro5 certs_info

Заключение

Дальнейшие шаги по автоматизации процесса обновления сертификатов выполнялись при помощи SOAR, они включали в себя периодический опрос контейнера с Криптопро на предмет устаревающих сертификатов (здесь как раз использовался json сформированный скриптом), формирование предупреждающих уведомлений до того как сертификат просрочиться и отправку инструкций по замене сертификата на электронную почту ответственных лиц.

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


  1. Wakeonlan
    09.09.2023 04:53

    А выпускающую организацию нельзя заставить присылать уведомление о выпуске сертификата на корпоративную почту? Я бы хотел чтобы Контур по моей конторе просто присылал алерт что сертификат получен пользователем, но ещё не смотрел в эту строну


    1. andrewfromtver Автор
      09.09.2023 04:53

      Здравствуйте, к сожалению тоже пока не изучил этот вопрос, уточню у коллег на неделе, отвечу вам как только что нибудь узнаю.


  1. RumataEstora
    09.09.2023 04:53
    +4

    предложениям по оптимизации bash скрипта

    1

    Сразу скажу, что #!/bin/sh и конструкции вида [[ ... ]] или (( ... <= ... )) (которые назыаются башизмами) - несовместимые вещи. Такой шебанг в начале скрипта предполагает, что это будет классифеский шелл, который не поддерживает удобные, расширенные возможности баша.

    Поэтому: или #!/usr/bin/env bash и используйте всю мощь баша или откажитесь от башизмов.

    2

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

    3

    Надо оптимизировать скрипт: я насчитал 5 вызовов команды certmgr -list. При этом в трех местах используется grep -E "Subject ". Понятно, что в каждый момент вы собираете разную информацию, но это очень не рационально.

    4

    Ваш скрипт построен по принципу

    команда1 > FILE
    команда2 >> FILE
    cat FILE
    

    Вероятно нет смысла вначале все сохранять в файл, а потом этот файл выводить. надо сразу писать в STDOUT. Если промежуточный результат надо еще как-то дополнительноо обработать, то можно сделать примерно так:

    что-то-писать() {
    	команда1
    	команда2
    }
    
    что-то-писать | как-то-обрабатывать
    

    5

    Простите, но cat FILE | tail -n $COUNTER | head -n 1 - это ужасно. Определенно, требуется оптимизация в части пунктов 3 и 4.

    Я понимаю, что оно уже работает и чего его трогать. Но неоптимально написанный код в будущем порождает "неоптимальную оптимизацию".

    6. Резюме

    Я не знаю, как работает certmgr и каков его выхлоп, но смею предположить, что однократный вызов certmgr -list | grep -e 'Subject ' -e 'Not valid after' > "$TMP/certmgr-$$.out" уже будет существенной оптимизацией. А дальше работать с файлом "$TMP/certmgr-$$.out".

    Выразительных средств awk/perl будет достаточно, чтобы оптимизировать скрипт, поддерживать его в дальнейшем и даже, при необходимости, с легкостью улучшать.


    1. andrewfromtver Автор
      09.09.2023 04:53

      Огромное спасибо вам за рекомендации, учту все ваши замечания и попробую оптимизировать, постараюсь к следующим выходным обновить скрипт в статье на основе ваших замечаний, спасибо еще раз!


  1. RumataEstora
    09.09.2023 04:53
    +2

    Забыл сказать. Хранить временные файлы в текущем каталоге - не есть хорошо. Если вы рут, то вы просто засоряете ФС. Если рядовой - у вас может не быть прав на текущий каталог и результат вашего скрипта - не предсказуем.

    Почему это важно. Скрипт можно завершить разными способами: клавишами CTRL-C, командой kill. В результате - команды rm FILE в конце скрипта могут быть и не выполнены. А в ФС вы оставите следы выполнения скрипта в произвольном месте.

    Лучшее решение для этого (но этого не гарантирует от полной очистки ФС и, тем более, от авадакедавры типа kill -9) - в начале скрипта описать что-то вида:

    # Какой-то временный файл, который нужен только на время выполнения скрипта
    # $$ - PID текущего процесса в имени файла, обеспечивает уникальность файла
    TMPFILE="${TMP:-/tmp}/certmgr-list-$$.out"
    
    # ловушка - код, который будет выполнен по выходу (`EXIT`) из скрипта
    # может быть внешней или внутренней командой или шелл-функцией
    trap 'rm -f "$TMPFILE"' EXIT
    

    И вам уже не надо писать отдельные команды для удаления временных файлов.


    1. sa2304
      09.09.2023 04:53

      Можно ведь обойтись без временных файлов - склеивать строки в памяти мы умеем https://stackoverflow.com/questions/2250131/how-do-you-append-to-an-already-existing-string