2022/12/27(火)メールサーバの中規模改修と基礎知識(5)~ OpenDKIM

OpenDKIM は、SPFと並んで、電子メール発信元の正当性を保証する技術のひとつです。
公式公開バージョンは、記事公開日時点では、2.10.3 です。一部のLinux ディストリビューションに 2.11.x が提供されているようですが、これは、RC版(Release Candidate:リリース候補の意)の位置づけの状態です。

OpenDKIM の概要

OpenDKIM は、電子メールメッセージを基に検証鍵を電子メールヘッダに埋め込み、DNSに登録した公開鍵を使って検証することで、電子メール発信元の正当性を確認し、電子メール改ざんがされていないことも確認する一助になります。

具体的には、予め送信元DNS情報に下記2つの TXTレコードを公開する形で登録しておき、
20221227_1_opendkim_2022.png

20221227_2_opendkim_2022.png


電子メール送信の際は、下記のように「DKIM-Signature:」というヘッダ行にて、一定のルールで計算された暗号キーを追加する処理を行います。
20221227_3_opendkim_2022.png

受信の際は、このDNS情報を基にメールヘッダに添付されてきたヘッダ暗号キーの検証を行い、この暗号キーに問題が無ければ「メール改ざんが無かった」と見做す、という処理を行うのが OpenDKIM の役割です。

送信の際の「DKIM-Signature:」ヘッダ行追加・受信の際の「DKIM-Signature:」ヘッダ行検証処理は、postfix にて、milter インタフェースを介して行うことになります。

OpenDKIM のインストール準備(1)

この先は、root アカウントにて、一連の作業を行います。
OpenDKIM は、公開鍵の管理にPython と Perlを使用しているため、この2つは必須です。
OpenDKIM をインストールする場合、FreeBSD13 では、下記のモジュールが事前に必要です。(但し、2022/12/24 現在)
これらの多くは、Ports や Package でインストールしても、管理上特段問題にはなりません。
 ・perl5-5.36.0 	(Ports から カテゴリ:lang)
 ・python39-3.9.16	(Ports から カテゴリ:lang)
 ・lua54-5.4.4 	(Ports から カテゴリ:lang)
 ・autoconf-2.71	(Ports から カテゴリ:devel)
 ・automake-1.16	(Ports から カテゴリ:devel)
 ・libltdl-2.4.7	(Ports から カテゴリ:devel)
 ・libtool-2.4.7	(Ports から カテゴリ:devel)
 ・gnutls-3.7.8_1	(Ports から カテゴリ:security)
 ・db5-5.3.28_9	(Ports から カテゴリ:databases)  ※ これは BerkeleyDB 5.3.28 です
OpenDKIM 2.10.3 は、OpenSSL 1.0 系までは対応しているものの、OpenSSL 1.1 系には対応していません。
昨今の FreeBSD にて採用されている OpenSSL は1.1 系のため、代替選択として、gnutls のインストールが必要になります。OpenSSL 1.1系対応なら、素直に OpenSSL を使うことで事が足りるのですが。。

更に、インストールに先立ち、OpenDKIM のセキュリティポリシーに従うため、ユーザ opendkim を、vipw や useradd コマンドで追加します:

vipw コマンドの場合は、下記の1行を編集画面で追加します。
opendkim::2006:3000::0:0:OpenDKIM execute user:/var/empty:/usr/sbin/nologin
vipw でユーザ追加した場合、/etc/group に下記の1行を例示のように変更しておきます:
mailuser:*:3000:opendkim
更に vipw でユーザ追加した場合は、つまらないセキュリティホールを作らないために、下記コマンドも一応実行しておきます:
# passwd opendkim

OpenDKIM のインストール準備(2)

先ず、ソースコードの展開を行います。
# cp opendkim-2.10.3.tar.gz /usr/local/src
# cd /usr/local/src
# tar xvzf opendkim-2.10.3.tar.gz
# cd opendkim-2.10.3
実は、実際にソースコードのコンパイルを実施したところ、FreeBSDにおいては、素のソースコードでは、GnuTLS 使用環境での構築も上手く出来ません。
具体的には、configure スクリプト、libopendkim/tests/Makefile.in、libopendkim/dkim-canon.c、libopendkim/dkim.c、miltertest/miltertest.c、opendkim/tests/Makefile.in、opendkim/opendkim-crypto.c、 opendkim/opendkim-lua.c の8本の変更が必要なようです。

これらは、patch コマンドを使用するか、手入力修正して、作業実施前にソースコードの修正を行う必要があります。
FreeBSD patch コマンド向けの差分ファイルは、https://ctrl.basekernel.ne.jp/rebase/download.html からダウンロードできるようにしてありますので、『opendkim-2.3.10用 FreeBSD 向け Patch ファイル群』をダウンロードしてみて、使えるようであれば、使ってみてください。

おそらく、以下のように実行すると良いと思います(当方では、この手法で出来ることの確認をしていません)
# cp opendkim-2.10.3.patch.gz /usr/local/src/opendkim-2.10.3
# cd /usr/local/src/opendkim-2.10.3
# tar xvzf opendkim-2.10.3.patch.gz
# patch < patch-configure.ac
# patch < patch-libopendkim__tests__Makefile.in
      ・
      ・
# patch < patch-opendkim_opendkim-lua.c

OpenDKIM のインストール

以下の手順で実行していきます:
# cd /usr/local/src/opendkim-2.10.3
# setenv CPPFLAGS '-I/usr/local/include -I/usr/include' 	  (configure が上手くライブラリを探せないため)
# setenv LDFLAGS '-L/usr/local/lib -L/usr/lib -L/usr/local/lib/db5'(configure が上手くライブラリを探せないため)
# unsetenv LD_LIBRARY_PATH	 

※以下、説明のために改行していますが、 ./configure の部分は、改行せずに、半角スペース区切りで、一気に入力。
# ./configure --localstatedir=/var 	(unix ソケットなどを格納するディレクトリ)
       --with-gnutls 		(openssl 1.1.x は未サポートのため、gnutls を使用する)
       --with-milter 		(milter インタフェースを構築する)
       --with-db 		(BerkeleyDBをサポートする)
       --with-domain 		(domain 設定をサポートする)
# make
# make install
ここまで、エラー無く出来たら、インストール不具合が1つあるので、下記の要領でファイル修正を行います。
# vi /usr/local/sbin/opendkim-genkey
1行目を下記の要領で変更
#!/usr/bin/perl
    ↓
#!/usr/local/bin/perl
次に設定作業を行います。
# mkdir /usr/local/etc/opendkim
# cd /usr/local/etc/opendkim
# mkdir example.com.keys 	(メールサーバ収容ドメイン毎にディレクトリを作成)
# cd example.com.keys
# /usr/local/sbin/opendkim-genkey --bits=2048 --domain=example.com --selector=sel
 	(--domain:収容ドメイン --selector:任意)
	※ --selector の文字列は公開鍵識別子になるので、適当にせずに何等かの規則を作るとよいでしょう。
	  pop1 とか mx1 とか、メールサーバの付番に対応すると良いと思います。
# cd ../
# chown -R opendkim:mailuser example.com.key
# vi KeyTable		※ 以下のように編集をする(複数ドメインになる場合は、行追加)
sel._example.com example.com:sel:/usr/local/etc/opendkim/example.com.keys/sel.private

# vi SigningTable	※ 以下のように編集をする(複数ドメインになる場合は、行追加)
*@example.com sel._domainkey.example.com

# chown opendkim:mailuser KeyTable
# chown opendkim:mailuser SigningTable
ここで、KeyTable ファイルへの記述方法ですが、
①._②:①:/usr/local/etc/opendkim/②.keys/①.private
(①= --selector への設定文字列 ②= --domain への設定文字列)

SigningTable ファイルへの記述方法は、
*@② ①._domainkey.②
(①= --selector への設定文字列 ②= --domain への設定文字列) のようにします。

次に、上記で /usr/local/sbin/opendkim-genkey を実行した時に生成された、sel.txt の内容と、取り扱いポリシーをDNSへ登録します。(--selector の指定により、拡張子 .txt は変わりませんが、ファイル名は変わります。)

生成された、sel.txt の内容を見ると、テキスト形式にて、
sel._domainkey  IN      TXT     ( "v=DKIM1; k=rsa; "         "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAulH2mMsm8eP7bpAmlXI9EE5TQ8tjWBaMDaSP34zB+0+LLjJhbi9dhGV9T5reppaxaU7lzokGaHJX+l9X4sA53G9PlpzQ0JwsGO5Z1E9LaN1CDijqtGNQWr7bdLpy4AwzfhDThY25rwzMg4Wt4LK74Ba7Q5a2M9m0d/gEqs6ZWrK/6DiQ1qnzU7JTm13SZRBvnNhmLBMnSu1sz3"
          "Hc8nE+02oAJC04xCjI8XUowu+sxyJQm7OY97MOvOt9HAXt/jbBVyG1scJTggjYsH1JcfC1xbE1VnNiWSkpid2P+4bXy/GIG90nZ7We4KNMshXEby/FbUZPPERHW1ntCyga58rPJQIDAQAB" )  ;
----- DKIM key sel0 for example.jp
のような内容になっています。この例に沿うと、example.jp のDNSゾーンに、
 ・name : sel._domainkey.example.jp.
 ・type : TXT
 ・content:"v=DKIM1; .... 以下 .... yga58rPJQIDAQAB" まで
を登録します。もう一つ、example.jp のDNSゾーンに
 ・name : _adsp._domainkey.example.jp.
 ・type : TXT
 ・content:"dkim=unknown"
を登録します。このパラメータの意味は下記のとおりです:
unknown該当ドメインからの送信メールには、メール作成者署名(DKIM)がある場合と無い場合がある
all該当ドメインからの送信メールには、必ずメール作成者署名(DKIM)がある
もしメール作成者署名(DKIM)が得られない場合の処置は受信側の任意。(通常は配送する)
discardable該当ドメインからの送信メールには、必ずメール作成者署名(DKIM)がある。
もしメール作成者署名(DKIM)が得られない場合は、受信者はそのメールを破棄することが望まれる
日本国内の電子メール環境を鑑みた場合、ここは「unknown」が無難と思います。

起動・停止スクリプトの設置(FreeBSD に特化している項目)

このスクリプトは一度作っておくと、バージョンアップの際に再作成の必要はありません。

○ OpenDKIM の起動スクリプト
/usr/local/etc/rc.d/ ディレクトリ配下に、milter-opendkim のファイル名で下記内容を作成します。
実行権限を与えることを忘れないようにしてください:
#!/bin/sh

# PROVIDE: milter-opendkim
# REQUIRE: DAEMON
# BEFORE: mail
# KEYWORD: shutdown

# Define these milteropendkim_* variables in one of these files:
#       /etc/rc.conf
#       /etc/rc.conf.local
#       /etc/rc.conf.d/milteropendkim
#
# milteropendkim_enable (bool):   Set to "NO" by default.
#                             Set it to "YES" to enable dkim-milter
# milteropendkim_uid (str):       Set username to run milter.
# milteropendkim_gid (str):       Set group to run milter.
# milteropendkim_profiles (list): Set to "" by default.
#                             Define your profiles here.
# milteropendkim_cfgfile (str):   Configuration file. See opendkim.conf(5)
#
# milteropendkim_${profile}_* :   Variables per profile.
#                             Sockets must be different from each other.
#
# milteropendkim_socket_perms (str):
#                                 Permissions for local|unix socket.
#
#  all parameters below now can be set in opendkim.conf(5).
# milteropendkim_socket (str):    Path to the milter socket.
# milteropendkim_domain (str):    Domainpart of From: in mails to sign.
# milteropendkim_key (str):       Path to the private key file to sign with.
# milteropendkim_selector (str):  Selector to use when signing
# milteropendkim_alg (str):       Algorithm to use when signing
# milteropendkim_flags (str):     Flags passed to start command.

. /etc/rc.subr

name="milteropendkim"
rcvar=milteropendkim_enable

extra_commands="reload"
start_precmd="dkim_prepcmd"
start_postcmd="dkim_start_postcmd"
stop_postcmd="dkim_postcmd"
command="/usr/local/sbin/opendkim"
_piddir="/var/run/milteropendkim"
pidfile="${_piddir}/pid"
sig_reload="USR1"

load_rc_config $name

#
# DO NOT CHANGE THESE DEFAULT VALUES HERE
#
: ${milteropendkim_enable:="NO"}
: ${milteropendkim_uid:="opendkim"}
: ${milteropendkim_gid:="mailuser"}
: ${milteropendkim_cfgfile:="/usr/local/etc/opendkim/opendkim.conf"}
: ${milteropendkim_socket_perms:="0775"}

# Options other than above can be set with $milteropendkim_flags.
# see dkim-milter documentation for detail.

extra_commands="reload"
start_precmd="dkim_prepcmd"
start_postcmd="dkim_start_postcmd"
stop_postcmd="dkim_cleansockets"
command="/usr/local/sbin/opendkim"
sig_reload="USR1"

dkim_cleansockets()
{
    case ${milteropendkim_socket%:*} in
    local|unix)
        rm -f "${milteropendkim_socket#*:}"
        ;;
    esac
}

dkim_get_pidfile()
{
        if get_pidfile_from_conf PidFile ${milteropendkim_cfgfile#-x }; then
                pidfile="$_pidfile_from_conf"
        else
                pidfile="/var/run/milteropendkim/${profile:-pid}"
        fi
}

dkim_prepcmd()
{
    dkim_cleansockets
    dkim_get_pidfile
    if [ ! -d "$(dirname "$pidfile")" ]; then
        mkdir "$(dirname "$pidfile")"
    fi
    case ${milteropendkim_socket%:*} in
    local|unix)
        socketfile=${milteropendkim_socket#*:}
        install -d -o ${milteropendkim_uid%:*} -g $milteropendkim_gid \
            -m ${milteropendkim_socket_perms} \
               ${pidfile%/*} ${socketfile%/*}
        ;;
    esac
}

dkim_start_postcmd()
{
    case ${milteropendkim_socket%:*} in
    local|unix)
        # postcmd is executed too fast and socket is not created before checking...
        sleep 1
        chmod -f ${milteropendkim_socket_perms} ${milteropendkim_socket#*:}
        ;;
    esac
}

if [ -n "$2" ]; then
    profile="$2"
    if [ -n "${milteropendkim_profiles}" ]; then
        pidfile="${_piddir}/${profile}.pid"
        eval milteropendkim_enable="\${milteropendkim_${profile}_enable:-${milteropendkim_enable}}"
        eval milteropendkim_socket="\${milteropendkim_${profile}_socket:-}"
        eval milteropendkim_socket_perms="\${milteropendkim_${profile}_socket_perms:-}"
        if [ -z "${milteropendkim_socket}" ];then
            echo "You must define a socket (milteropendkim_${profile}_socket)"
            exit 1
        fi
        eval milteropendkim_cfgfile="\${milteropendkim_${profile}_cfgfile:-${milteropendkim_cfgfile}}"
        eval milteropendkim_domain="\${milteropendkim_${profile}_domain:-${milteropendkim_domain}}"
        eval milteropendkim_key="\${milteropendkim_${profile}_key:-${milteropendkim_key}}"
        eval milteropendkim_selector="\${milteropendkim_${profile}_selector:-${milteropendkim_selector}}"
        eval milteropendkim_alg="\${milteropendkim_${profile}_alg:-${milteropendkim_alg}}"
        eval milteropendkim_flags="\${milteropendkim_${profile}_flags:-${milteropendkim_flags}}"
        if [ -f "${milteropendkim_cfgfile}" ];then
            milteropendkim_cfgfile="-x ${milteropendkim_cfgfile}"
        else
            milteropendkim_cfgfile=""
        fi
        if [ -n "${milteropendkim_socket}" ];then
            _socket_prefix="-p"
        fi
        if [ -n "${milteropendkim_uid}" ];then
            _uid_prefix="-u"
            if [ -n "${milteropendkim_gid}" ];then
                milteropendkim_uid=${milteropendkim_uid}:${milteropendkim_gid}
            fi
        fi
        if [ -n "${milteropendkim_domain}" ];then
            milteropendkim_domain="-d ${milteropendkim_domain}"
        fi
        if [ -n "${milteropendkim_key}" ];then
            milteropendkim_key="-k ${milteropendkim_key}"
        fi
        if [ -n "${milteropendkim_selector}" ];then
            milteropendkim_selector="-s ${milteropendkim_selector}"
        fi
        if [ -n "${milteropendkim_alg}" ];then
            milteropendkim_alg="-S ${milteropendkim_alg}"
        fi
        dkim_get_pidfile
        command_args="-l ${_socket_prefix} ${milteropendkim_socket} ${_uid_prefix} ${milteropendkim_uid} -P ${pidfile} ${milteropendkim_cfgfile} 
${milteropendkim_domain} ${milteropendkim_key} ${milteropendkim_selector} ${milteropendkim_alg}"
    else
        echo "$0: extra argument ignored"
    fi
else
    if [ -n "${milteropendkim_profiles}" ] && [ -n "$1" ]; then
        if [ "$1" != "restart" ]; then
            for profile in ${milteropendkim_profiles}; do
                echo "===> milteropendkim profile: ${profile}"
                /usr/local/etc/rc.d/milter-opendkim $1 ${profile}
                retcode="$?"
                if [ "${retcode}" -ne 0 ]; then
                    failed="${profile} (${retcode}) ${failed:-}"
                else
                    success="${profile} ${success:-}"
                fi
            done
            exit 0
        else
            restart_precmd=""
        fi
    else
        if [ -f "${milteropendkim_cfgfile}" ];then
            milteropendkim_cfgfile="-x ${milteropendkim_cfgfile}"
        else
            milteropendkim_cfgfile=""
        fi
        if [ -n "${milteropendkim_socket}" ];then
            _socket_prefix="-p"
        fi
        if [ -n "${milteropendkim_uid}" ];then
            _uid_prefix="-u"
            if [ -n "${milteropendkim_gid}" ];then
                milteropendkim_uid=${milteropendkim_uid}:${milteropendkim_gid}
            fi
        fi
        if [ -n "${milteropendkim_domain}" ];then
            milteropendkim_domain="-d ${milteropendkim_domain}"
        fi
        if [ -n "${milteropendkim_key}" ];then
            milteropendkim_key="-k ${milteropendkim_key}"
        fi
        if [ -n "${milteropendkim_selector}" ];then
            milteropendkim_selector="-s ${milteropendkim_selector}"
        fi
        if [ -n "${milteropendkim_alg}" ];then
            milteropendkim_alg="-S ${milteropendkim_alg}"
        fi
        dkim_get_pidfile
        command_args="-l ${_socket_prefix} ${milteropendkim_socket} ${_uid_prefix} ${milteropendkim_uid} -P ${pidfile} ${milteropendkim_cfgfile} 
${milteropendkim_domain} ${milteropendkim_key} ${milteropendkim_selector} ${milteropendkim_alg}"
    fi
fi
run_rc_command "$1"
OpenDKIM の設定は、これで終わりです。