14 Октябрь 2016

Автоматическая ротация ключей DKIM

Automating DKIM keys rotation

Использование механизма DKIM в системе оборота электронной почты в последние годы получило широкое распространение.

Вкратце, суть его состоит в использовании цифровой подписи (пара открытый и закрытый ключи) к набору заголовков и тексту сообщения, которая позволяет подтвердить легитимность его происхождения. Благодаря этому DKIM в связке с механизмом SPF в надстройке в виде политик DMARC, в настоящее время являются одними из популярных способов борьбы с несанкционированными рассылками (спамом).

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

В этой связи, актуальным становится вопрос ротации ключей DKIM и автоматизации её процесса.

1. Схема

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

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

При этом следует учесть тот факт, что сообщения электронной почты по стандарту не являются сообщениями мгновенной доставки. Она может производится, в зависимости от сетевой доступности сервера адресата и настроек сервера отправителя от нескольких секунд, до нескольких дней. Как правило, этот срок не превышает 7 дней. Таким образом, если предположить срок отправки письма, например в последние часы апреля (чётный месяц), которое будет подписано первым ключом, то следует, чтобы сервер получателя мог произвести проверку этой подписи этим ключом, который в следующем месяце уже не будет использоваться для подписи (май месяц нечётный), как минимум ещё в течении недели. То есть производить замену этого ключа на новый следует ближе к средине месяца. Тогда будет обеспечны как возможность такой проверки для задержавшихся в пути сообщений, так и время для синхронизации зон DNS серверов, поддерживающих использующий DKIM домен.

2. Реализация

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

Для начала, надо определится с обозначением ключей для чётных и нечётных месяцев. Пусть для первых они будут называться key0, а для вторых key1.

Затем, коль уж автоматизировать, так автоматизировать всё. У автора в работе в рамках одной системы находится некоторое количество доменов, помимо kostikov.co, которые с различной степенью интенсивности используются в различного рода деятельности различных организаций и персон. Поэтому создаваемый механизм ротации будет обеспечивать все домены новыми ключами в едином режиме. При этом для упрощения и облегчения конфигурирования DNS записей, через которые, как известно, и происходит обмен открытыми ключами, можно применить следующую схему.

Пусть все записи DKIM будут прописываться в зоне одного, назовём его корневым, домена. Как преимущество, это даст единую точку доступа к записями открытых и закрытых ключей, а также необходимость к прямому доступу для DNS записей зоны только для одного домена. Для всех прочих доменов ссылки на принадлежащие им ключи можно будет прописать в виде записи CNAME на соответствующую запись корневого домена всего один раз. При этом очевидно, что не обязательно будет иметь прямой программный доступ к зоне таких доменов.

Например, имеем корневой домен my.server. Тогда запись для домена kostikov.co примет следующий вид.

...
key0._domainkey.kostikov.co.    IN CNAME key0._domainkey.kostikov.co.my.server.
key1._domainkey.kostikov.co.    IN CNAME key1._domainkey.kostikov.co.my.server.
...

И, соответственно, сама запись на my.domain будет выглядеть так.

...
key0._domainkey.kostikov.co.my.server.    IN TXT "v=DKIM1; k=rsa; p=..."
key1._domainkey.kostikov.co.my.server.    IN TXT "v=DKIM1; k=rsa; p=..."
...

Итак, имеем систему на базе FreeBSD, SMTP-сервер Exim и первичный DNS сервер для зоны my.server на базе NSD, первоначальная настройка которого ранее была описана в отдельной статье.

root@beta:~ # uname -v
FreeBSD 11.0-RELEASE-p1 #0 r306420: Thu Sep 29 01:43:23 UTC 2016     root@releng2.nyi.freebsd.org:/usr/obj/usr/src/sys/GENERIC
root@beta:~ # exim --version
Exim version 4.87 #0 (FreeBSD 11.0) built 27-Sep-2016 17:15:33
...
root@beta:~ # nsd -v
NSD version 4.1.13
...

Универсальный shell-скрипт для ротации ключей DKIM, который будет пересоздавать их набор, сохраняя (и заменяя им старый) закрытый ключ в рабочий каталог Exim, а открытый прописывать в DNS запись корневого домена my.server на NSD также взамен предыдущего. Для универсальности данный скрипт автоматически создаёт и второй, который используется в данном месяце, набор в случае его отсутствия. Это полезно для автоматизации при добавлении в систему новых доменов.

root@beta:~ # cd /usr/local/etc/exim
root@beta:/usr/local/etc/exim # cat rotatedkim.sh
#!/bin/sh

# Create and rotate DKIM keys for domains with Exim and NSD
# (c)2016 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co
#
# cat /etc/crontab | grep rotatedkim
# 0 6 15 * 0 root /usr/local/etc/exim/rotatedkim.sh >/dev/null 2>&1

# Dependent domains must have CNAME record to root domain TXT where
# DKIM keys will be created and rotated e.g.
# key0._domainkey.depended.domain IN CNAME key0._domainkey.depended.domain.root.domain
dom="my.server"                                   # root domain

dkimdir="/usr/local/etc/exim/dkim"              # path to keys storage
zonedir="/usr/local/etc/nsd/zones/${dom}"       # path to root domain zone location
log="${dkimdir}/rotatedkim.log"                 # log name
keypref="key"                                   # key prefix name
bits="2048"                                     # key length

# domains list to rotate DKIM keys (may contain root domain)
list="my.server kostikov.co"

cur=`expr $(date '+%m') % 2`                    # current index in use
ind=`expr \( $cur + 1 \) % 2`                   # index to rotate

echo "`date '+%Y-%m-%d %H:%M:%S'` --- Starting DKIM key rotation (root domain - ${dom}, index to rotate - ${ind})..." >>$log

# generate DKIM keys with given index as argument
genkeys ()
{
        pref="${keypref}$1._domainkey`if [ $2 != $dom ]; then echo .$2; fi`.${dom}."
        sed -i.bak "/^${pref}/,/)/d" ${zonedir}/${dom}.zone
        openssl genrsa ${bits} > ${keyfile}$1
        echo "${pref}   IN TXT (\"v=DKIM1; k=rsa; p=\"" >>${zonedir}/${dom}.zone
        echo "`openssl rsa -in ${keyfile}$1 -pubout -outform der | openssl enc -base64 | sed 's/.*/     "&"/'`)" >>${zonedir}/${dom}.zone
}

# rotate DKIM keys
for j in $list;
do
        keyfile="${dkimdir}/${j}.${keypref}"    # file with path w/o index

        # create current key file if doesn't exist
        if [ ! -f ${keyfile}${cur} ];
        then
                genkeys $cur
                echo "`date '+%Y-%m-%d %H:%M:%S'`     Current keys set ${cur} for ${j} created" >>$log
        fi

        genkeys $ind $j
        echo "`date '+%Y-%m-%d %H:%M:%S'`     Keys set $ind for ${j} updated" >>$log
done

# change root domain zone serial
newser=`date '+%Y%m%d'`
curser=`grep "serial" ${zonedir}/${dom}.zone | tr -cd "[:digit:]"`
if [ `expr $curser : $newser` = 8 ]; then newser=`expr $curser + 1`; else newser=${newser}00; fi
sed -i.bak "s/.*serial/         ${newser}       \; serial/" ${zonedir}/${dom}.zone

# reload root domain zone
nsd-control reload ${dom}
echo "`date '+%Y-%m-%d %H:%M:%S'`     Serial for ${dom} zone updated and reloaded" >>$log

echo "`date '+%Y-%m-%d %H:%M:%S'` --- DKIM key rotation finished! " >>$log

В принципе, все параметры его работы прозрачны и откомментированы в тексте. Обращу лишь внимание, что зона корневого домена хранится в рабочем каталоге NSD под именем my.server.zone, а закрытые ключи сохраняются с именами типа kostikov.co.key0 или kostikov.co.key1 в зависимости от чётности набора.

Замечание. Для корректной работы механизма обновления серийного номера зоны важно, чтобы он был прописан в файле зоны отдельной строкой с комментарием, содержащим ключевое слово "serial".

Закрытые ключи, а также протокол ротации, будут сохраняться в каталоге /usr/local/etc/exim/dkim, поэтому до первого запуска скрипта создадим его вручную.

root@beta:/usr/local/etc/exim # mkdir dkim

Выполним теперь первый запуск скрипта и пронаблюдаем его работу.

root@beta:/usr/local/etc/exim # /bin/sh rotatedkim.sh
Generating RSA private key, 2048 bit long modulus
.......+++
........+++
e is 65537 (0x10001)
writing RSA key
Generating RSA private key, 2048 bit long modulus
...........................................+++
.....+++
e is 65537 (0x10001)
writing RSA key
Generating RSA private key, 2048 bit long modulus
..........+++
............................................................................+++
e is 65537 (0x10001)
writing RSA key
Generating RSA private key, 2048 bit long modulus
......................................................................................................+++
....................................................................................+++
e is 65537 (0x10001)
writing RSA key
ok

Ввиду того, что это был первых запуск для двух ранее (как бы) не использовавших DKIM подписи доменов (см. список list в скрипте), то было сгенерировано оба комплекта ключей — и для чётных и для нечётных месяцев.

root@beta:/usr/local/etc/exim # ls dkim/
kostikov.co.key0        my.server.key0          kostikov.co.key1        my.server.key1            rotatedkim.log

Также открытые ключи были добавлены в файл зоны my.server.

root@beta:/usr/local/etc/exim # cat ../nsd/zones/my.server/my.server.zone
$ORIGIN my.server.
$TTL 86400
@       IN SOA  my.server. hostmaster.my.server. (
                2016101400      ; serial
                10800           ; refresh
                1800            ; retry
                604800          ; expire
                14400   )       ; minimum
; NS
        IN NS   my.server.
        IN NS   ns1.he.net.
        IN NS   ns2.he.net.
        IN NS   ns3.he.net.
        IN NS   ns4.he.net.
        IN NS   ns5.he.net.
...
key0._domainkey.my.server.        IN TXT ("v=DKIM1; k=rsa; p="
        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwn99GclJvZBYtZAU3wDc"
        "Nr86CeFJPZzJ1vwMGrU3/X7w7oGD7H336SgjH5u6CnhN/jk6MXM+eDBe/VFIex76"
        "0TsZ1Dfskl422FNO+Fkj/RSU6a8kx2trpFYAguUvhSrFcqPZHGWwvf172VwGVGNt"
        "I19D6At3CUEDWcAD6XW2sTuRHqt9uNGapKx9XPI6GY+PXoMAZjuWh9/Dkjzj13I/"
        "Jp+b0kZFByj/5PUyRoIAVD1eO6YzTZyQPt3e+5uXqZPNq79qsUrGDXW6VAKBs/VK"
        "SWcwduqnp7HdDQ5IvEjJiRiMzNyQjqyoCa5gOxtsbR+ndXqRa4yUOS3SQoPEz52y"
        "XwIDAQAB")
key0._domainkey.kostikov.co.my.server.    IN TXT ("v=DKIM1; k=rsa; p="
        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3SYy2frHcT0UCpkkWzo"
        "GMmSnRL8zUsrRdVeKsu5Hsd437Y6DVw4dZJP1f1CCb9cBnZHnC6Ck1Gjc1iv0qk3"
        "pb4gVbszMti65IK4jPPOcdnaprUQVNdXpPOh5fFSYB+InuF9XX1z7vATGNN8WeB0"
        "PnEFg0oGkFK/TNLfdCgDA7GA82T0LDqMsupAKbk9mLX3h1JqvY2w06zJQMBY0Css"
        "ttkIEMUlR4jpf7ihZr1Pae2gRnV3Omexoi7p5dfQHwDmIcalqVhPE4QdRR0CmYdX"
        "hixztuJaJkhfUgnz05EkrZOAEnmjgCbc3k1WfGBIRvD7Pd+Zp98RLrScf7KE8SyZ"
        "VwIDAQAB")
key1._domainkey.my.server.        IN TXT ("v=DKIM1; k=rsa; p="
        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8MCcPBGbvo8UjYX0z4iZ"
        "10bBZAuzgtoQyaf6jcEsM6F4zWMys2/sJZ3r9gPmlBWQTqsi+9aVXj5B6F0ZiwJ8"
        "jOWsTdOuj085HwIsr4AHaT5ZR0jr6AowdWXw17SU/jBxc6IGu6ybA3qdxaLJb/2+"
        "cD/+D4kcFfo7lIsOkmwbbG5biUI6hfZhvfGjRoFsNlQ66e8p7Pd2XXjdinbsSyye"
        "flX+6511dh+aFa3977WWHYcmkp8W/j4tvimNyXE6NAvM3myXAR21omkM/tpMqV3E"
        "DzI5KcKqDgqUlJHKqY3e+VFyHabQfDWxZrynIN4jIQb9d51XNnnQlr8RsifxXZN3"
        "lwIDAQAB")
key1._domainkey.kostikov.co.my.server.    IN TXT ("v=DKIM1; k=rsa; p="
        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwn99GclJvZBYtZAU3wDc"
        "Nr86CeFJPZzJ1vwMGrU3/X7w7oGD7H336SgjH5u6CnhN/jk6MXM+eDBe/VFIex76"
        "0TsZ1Dfskl422FNO+Fkj/RSU6a8kx2trpFYAguUvhSrFcqPZHGWwvf172VwGVGNt"
        "I19D6At3CUEDWcAD6XW2sTuRHqt9uNGapKx9XPI6GY+PXoMAZjuWh9/Dkjzj13I/"
        "Jp+b0kZFByj/5PUyRoIAVD1eO6YzTZyQPt3e+5uXqZPNq79qsUrGDXW6VAKBs/VK"
        "SWcwduqnp7HdDQ5IvEjJiRiMzNyQjqyoCa5gOxtsbR+ndXqRa4yUOS3SQoPEz52y"
        "XwIDAQAB")

Обратите внимание на следующие особенности. Во-первых, ввиду того, что используются ключи длиной 2048 бит (см. переменную bits в скрипте) и наличие ограничения на стандартную длину строки DNS записи в 256 байт, строка открытого ключа разбивается на части. Во-вторых, для корневого домена нет необходимости в CNAME записях, поэтому ключи непосредственно прописываются в зону по своим именем. В-третьих, скрипт автоматически изменил серийный номер записи на новый с тем, чтобы вторичные DNS серверы зоны произвели её синхронизацию. Проверить, прошла ли она можно в файле лога NSD.

root@beta:/usr/local/etc/exim # tail ../nsd/var/log/nsd.log
[2016-10-14 15:12:04.227] nsd[739]: info: new control connection from 127.0.0.1
[2016-10-14 15:12:04.323] nsd[739]: info: remote control connection authenticated
[2016-10-14 15:12:04.430] nsd[739]: info: control cmd:  reload my.server
[2016-10-14 15:12:04.430] nsd[739]: info: remote control operation completed
[2016-10-14 15:12:04.433] nsd[744]: info: zone my.server read with success
[2016-10-14 15:12:04.433] nsd[744]: info: zone my.server written to db
[2016-10-14 15:13:55.147] nsd[63698]: info: axfr for my.server. from 216.218.133.2

После синхронизации проверим также как наш ключ отображается при запросе через DNS.

root@beta:/usr/local/etc/exim # host -t txt key0._domainkey.kostikov.co
key0._domainkey.kostikov.co is an alias for key0._domainkey.kostikov.co.my.server.
key0._domainkey.kostikov.co.my.server descriptive text "v=DKIM1; k=rsa; p=" "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3SYy2frHcT0UCpkkWzo"
"GMmSnRL8zUsrRdVeKsu5Hsd437Y6DVw4dZJP1f1CCb9cBnZHnC6Ck1Gjc1iv0qk3"
"pb4gVbszMti65IK4jPPOcdnaprUQVNdXpPOh5fFSYB+InuF9XX1z7vATGNN8WeB0"
"PnEFg0oGkFK/TNLfdCgDA7GA82T0LDqMsupAKbk9mLX3h1JqvY2w06zJQMBY0Css"
 "ttkIEMUlR4jpf7ihZr1Pae2gRnV3Omexoi7p5dfQHwDmIcalqVhPE4QdRR0CmYdX"
 "hixztuJaJkhfUgnz05EkrZOAEnmjgCbc3k1WfGBIRvD7Pd+Zp98RLrScf7KE8SyZ"
 "VwIDAQAB"

Теперь следует прописать наш скрипт rotatedkim.sh в cron для запуска в средине месяца, например, 15 числа в 6 часов утра.

root@beta:/usr/local/etc/exim # cat /etc/crontab | grep rotatedkim
0       6       15      *       *       root    /usr/local/etc/exim/rotatedkim.sh >/dev/null 2>&1

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

root@beta:/usr/local/etc/exim # cat configure
# $Cambridge: exim/exim-src/src/configure.default,v 1.14 2009/10/16 07:46:13 tom Exp $
# --- by Max Kostikov (c) 2010...2016 v.20161009
...
qualify_domain          = my.server
...
begin transports

# This transport is used for delivering messages over SMTP connections.

# -- DKIM data
  SENDER_DOMAIN         = ${if def:h_from:{${lc:${domain:${address:$h_from:}}}}{$qualify_domain}}
  KEYNAME               = key${eval10:${substr{4}{2}{$tod_logfile}}%2}
  DKIM_FILE             = /usr/local/etc/exim/dkim/SENDER_DOMAIN.KEYNAME

remote_smtp:
  driver                = smtp
  dkim_domain           = SENDER_DOMAIN
  dkim_selector         = KEYNAME
  dkim_private_key      = ${if exists{DKIM_FILE}{DKIM_FILE}{}}
...

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

root@beta:/usr/local/etc/exim # service exim restart
Stopping exim.
Waiting for PIDS: 2207.
Starting exim.
root@beta:/usr/local/etc/exim # tail -f 1000 /var/log/maillog
...
Oct 14 15:39:23 beta exim[64023]: 1bv2hn-000Ged-BW <= max@kostikov.co H=(mail.peek.ru) [127.0.0.1] I=[127.0.0.1]:25 P=esmtpa A=auth_plain:max@kostikov.co S=645 id=5f4d3cda12ec99f09f7db4c31c59c5e5@kostikov.co from <max@kostikov.co> for somesmartguy@gmail.com
Oct 14 15:39:24 beta exim[64024]: 1bv2hn-000Ged-BW => somesmartguy@gmail.com R=dnslookup T=remote_smtp H=gmail-smtp-in.l.google.com [64.233.162.26] I=[10.10.10.10] X=TLSv1.2:ECDHE-RSA-AES128-GCM-SHA256:128 CV=yes DN="/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mx.google.com" C="250 2.0.0 OK 1476452364 42si11857482lfv.409 - gsmtp"
Oct 14 15:39:24 beta exim[64024]: 1bv2hn-000Ged-BW Completed
...

Судя по заголовкам полученного письма на Gmail всё корректно — выбран ключ для текущего чётного месяца с именем key0 (см. s= в заголовке DKIM-Signature:), с его помощью подписан набор заголовоков и тело письма (bh= и b=), а проверка на корректность пройдена успешно (dkim=pass в заголовке Authentication-Results:).

Delivered-To: somesmartguy@gmail.com
Received: by 10.25.72.210 with SMTP id v201csp333503lfa;
        Fri, 14 Oct 2016 06:39:24 -0700 (PDT)
X-Received: by 10.25.210.5 with SMTP id j5mr3483424lfg.14.1476452364487;
        Fri, 14 Oct 2016 06:39:24 -0700 (PDT)
Return-Path: <prvs=0095f6b524=max@kostikov.co>
Received: from my.server (my.server. [10.10.10.10])
        by mx.google.com with ESMTPS id 42si11857482lfv.409.2016.10.14.06.39.23
        for <somesmartguy@gmail.com>
        (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
        Fri, 14 Oct 2016 06:39:24 -0700 (PDT)
Received-SPF: pass (google.com: domain of prvs=0095f6b524=max@kostikov.co designates 10.10.10.10 as permitted sender) client-ip=10.10.10.10;
Authentication-Results: mx.google.com;
       dkim=pass header.i=@kostikov.co;
       spf=pass (google.com: domain of prvs=0095f6b524=max@kostikov.co designates 10.10.10.10 as permitted sender) smtp.mailfrom=prvs=0095f6b524=max@kostikov.co;
       dmarc=pass (p=QUARANTINE dis=NONE) header.from=kostikov.co
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=kostikov.co ; s=key0; h=Message-ID:Subject:To:From:Date:Content-Transfer-Encoding: Content-Type:MIME-Version:Sender:Reply-To:Cc:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=1zhQLR2s53tKwd9WmtV74Z9gXWJl14pUNErS8K6rgyA=; b=PNDg89Ne7stzfsXgdw8VAZv3xu 0wHGizBoSbo8j/k4guDmi6c1Cv5e6PEgd3KhFC5ciNn+mT2UwuuBHsWQwmk3oIKEnUKRqvVrwBcTy Gcz29kGyWer1rfkCDAzoAvzl7YIUET6meQMyWgtvfc0lY9CYgw8xAhJvdDfCM+w36rGc=;
Received: from [127.0.0.1] (helo=mail.my.server) by my.server with esmtpa (Exim 4.87 (FreeBSD)) (envelope-from <max@kostikov.co>) id 1bv2hn-000Ged-BW for somesmartguy@gmail.com; Fri, 14 Oct 2016 15:39:23 +0200
MIME-Version: 1.0
Content-Type: text/plain; charset=US-ASCII; format=flowed
Content-Transfer-Encoding: 7bit
Date: Fri, 14 Oct 2016 15:39:23 +0200
From: Max Kostikov <max@kostikov.co>
To: somesmartguy@gmail.com
Subject: DKIM test
Message-ID: <5f4d3cda12ec99f09f7db4c31c59c5e5@kostikov.co>
X-Sender: max@kostikov.co
User-Agent: Roundcube Webmail/1.2.2

3. PROFIT!

Статья была полезной? Тогда прошу не стесняться и поддерживать деньгами через PayPal или Яндекс.Деньги.


exim  dev  shell  DNS