docker-postfixでDKIM対応送信専用SMTPリレーを立てる

# 概要

[docker-postfix](https://github.com/panubo/docker-postfix)を使用してDockerのコンテナ内リンク（またはローカルネットワークやVPN）からの送信専用SMTPリレー（SMTPサーバ）を構築した際のメモ。

https://github.com/panubo/docker-postfix

[作者公式Dockerイメージ](https://hub.docker.com/r/panubo/postfix)をdocker-composeでハンドリングしてDKIMに対応させる。自身の用途としては[docker-composeで構築したオンプレアプリ類](https://qiita.com/macropygia/items/dc6e716e786ad098803a)からユーザーへのメール配信用。

なお、実績のないドメインからのメールはSPFもDKIMも関係なく問答無用で迷惑メール扱いされるのが普通で、アカウント云々の文言が含まれる場合は尚更である。ユーザーへの念押しは必須。

※RHEL8互換環境を想定、それ以外の場合は必要に応じて読み替えのこと

# DKIM

docker-postfixではDKIMはoptionalのため、鍵は自分で用意する必要がある。今回はOpenDKIMを使用し、ホスト上で鍵を作成してvolumesでマウントする。

## パッケージのインストール

DKIM鍵を生成するためにホストにOpenDKIMと依存パッケージをインストールする。

```bash:shell
dnf install -y opendkim opendkim-tools
```

- OpenDKIMは[EPEL](https://fedoraproject.org/wiki/EPEL)に収録されている

## 鍵の生成

```bash:shell
mkdir -p /etc/opendkim/keys/<DOMAIN>
opendkim-genkey -b 2048 -d <DOMAIN> -D /etc/opendkim/keys/<DOMAIN> -s <SELECTOR> -v
```

- `<DOMAIN>` は使用するドメイン名（ `exmaple.com` 等）
- `<SELECTOR>` はDKIM鍵の識別名のようなもの（例えば `mail` 等）
    - 指定しないと `default` になる
- 実行すると秘密鍵が `/etc/opendkim/keys/<DOMAIN>/<SELECTOR>.private` に生成され、TXTレコードの内容が `/etc/opendkim/keys/<DOMAIN>/<SELECTOR>.txt` に出力される

# DNS

## DKIMレコード

出力された `/etc/opendkim/keys/<DOMAIN>/<SELECTOR>.txt` を元にDNSにDKIMレコードを追加する。OpenDKIMが出力するテキストはzoneファイルの書式のためbindならそのまま使える。

```plaintext
<SELECTOR>._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIB......"
```

- Type: TXT
- Name: `<SELECTOR>._domainkey`
- Value: `v=DKIM1; k=rsa; p=MIIB......`
    - `/etc/opendkim/keys/<DOMAIN>/<SELECTOR>.txt` の二重引用符の中身を連結したもの
- TTL: 適当

<details><summary>LuaDNSにおける設定例</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63934/d31fe940-c333-ff40-4cf0-2f1b1dfb93a4.png)
</details>

## SPFレコード

わかりやすい解説が大量にあるため検索して参照のこと。

```plaintext
<DOMAIN>. IN TXT "v=spf1 +ip4:<GLOBAL_IP>/32 -all"
```

- 修飾子 `+` `-` `~` `?` はその条件に一致した場合の扱いを規定する
    - `+` : 認証する（省略可）/ `-` : 認証しない/ `~` : 認証しないが受信推奨/ `?` : 認証の有無を評価しない
- よって設定した条件以外で送る可能性が全くない場合、allディレクティブの修飾子は `~` ではなく厳格な `-` の方がより安全

<details><summary>LuaDNSにおける設定例</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63934/c322a128-d9b4-8075-0265-ba03bd2b0282.png)
</details>

# Composeファイル

environmentの項目は[docker-postfixのreadme](https://github.com/panubo/docker-postfix/blob/master/README.md)を参照のこと。

## コンテナ間リンク用設定例

```yaml:docker-compose.yml
version: '3.8'

services:
  smtp:
    image: panubo/postfix:latest
    container_name: smtp
    hostname: smtp-server
    environment:
      - MAILNAME=<DOMAIN>
      - TZ=Asia/Toyo
      - USE_TLS=no
      - USE_DKIM=yes
      - DKIM_KEYFILE=/etc/opendkim/keys/<DOMAIN>/<SELECTOR>.private
      - DKIM_SELECTOR=<SELECTOR>
    expose:
       #- '25'
       - '587'
    volumes:
      - /etc/opendkim:/etc/opendkim
    networks:
      - container-link
    logging:
      options:
        max-size: "10m"
        max-file: "3"

networks:
  default:
    external:
      name: bridge
  container-link:
    name: docker.internal
```

- 各コンテナで指定するSMTPサーバのアドレスはdocker-postfixコンテナのhostnameと待ち受けポートになる
    - この設定例では `smtp-server:587` を指定することになる
- 許可するIP範囲は明示的に指定しないと `127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16` に設定される
    - コンテナ間リンクで使用する場合はそのままでOKと思われる
- TLSは標準でON（ `USE_TLS=yes` ）のため、不要なら明示的に切る必要がある
    - コンテナ間リンク内で閉じているなら不要と思われる
- DKIMを有効化し、先の手順で作成したDKIM鍵をマウントして使用する
- コンテナ間リンクに明示的に命名したbridgeネットワークを使用している
    - 立てたSMTPサーバを使用するコンテナはこのネットワークに繋げること
- 当然ながらメールにはSMTPのチェーンの始点となる送信元コンテナのhostnameとIPが記載される
- 内部アクセス専用のため認証はない

## その他設定例

ローカルIP `10.0.0.1` のアダプタ上でIPアドレス範囲 `10.0.0.0/24` からのアクセスのみ待ち受ける

```yaml:docker-compose.yml
    environment:
      - MYNETWORKS=10.0.0.0/24
    ports:
      - '10.0.0.1:587:587'
```

TLSの使用を強制し、Let's Encryptの証明書と鍵を使用（使用するドメインで繋がるようにしておく必要がある）

```yaml:docker-compose.yml
    environment:
      - USE_TLS=yes
      - TLS_SECURITY_LEVEL=encrypt
      - TLS_KEY=/etc/letsencrypt/live/<DOMAIN>/privkey.pem
      - TLS_CRT=/etc/letsencrypt/live/<DOMAIN>/fullchain.pem
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
```

# 実際のヘッダー

Gmailに送ったもの。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63934/0dcc133e-90eb-cce2-046d-37ffe2fb5935.png)

- `s=mail` は `<SELECTOR>` のこと
- `Received` に送信元コンテナのホスト名とIPが入っている