📮

SPFレコードをフラット化してルックアップ数制限を乗り越える

2022/07/20に公開2

SPFレコードのルックアップ数制限とは

FirebaseやSendGridなど、様々なSaaSを活用してウェブアプリケーションを作っていると、色々な場所からメールを送ることになります。

サービスで使用するドメインにSPFレコードを記述していく訳ですが、送信のサービスが増える度、includeが増えていきます。

v=spf1 include:_spf.firebasemail.com 
  include:_spf.google.com 
  include:ex1.example.com 
  (....and more) -all

ところが、SPFレコードにはDNSの参照回数に制限があり最大で10回までになっていて、これを越えると正しく動作しません。

うまくいかない状態で、バリデーターで確認すると下記のようなエラーが出力されていました。

PermError SPF Permanent Error: Too many DNS lookups

includeを使うことによって、includeした先のレコードもさらに確認することになり、SPFレコードが示したいIPアドレスに辿り付く前に10回の制限を越えてしまいます。

対処方法

  1. 余計なSPFレコードのエントリを削除する
    使っていないエントリが放置されているならどちらにせよ削除したいです。

  2. SPFレコードをフラット化する
    includeによってネストしてしまうことで余計なDNS参照が加わってしまうので、あらかじめ展開することで参照回数を削減します。

includeで指定されたレコードを展開していく例:

# 元々のレコード
v=spf1 include:_spf.firebasemail.com 
  include:_spf.google.com -all

# 展開
v=spf1 include:sendgrid.net 
  include:_spf.google.com 
  include:_netblocks.google.com 
  include:_netblocks2.google.com 
  include:_netblocks3.google.com ~all

# さらに展開(一部)
v=spf1 
  ip4:167.89.0.0/17 ip4:208.117.48.0/20 ip4:50.31.32.0/19 ip4:198.37.144.0/20 ip4:198.21.0.0/21 ip4:192.254.112.0/20 ip4:168.245.0.0/17 ip4:149.72.0.0/16 ip4:159.183.0.0/16 
  include:ab.sendgrid.net 
  include:_netblocks.google.com 
  include:_netblocks2.google.com 
  include:_netblocks3.google.com ~all

展開してみると以下のことがわかりました。

  • 実は同じメール送信サービスを使用していて重複する場合がある。
  • IPアドレス(or 範囲)は変更される可能性があるので、深く展開する必要はない。
  • 展開しすぎると、トップレベルのレコード自体が長くなりすぎる可能性がある。
  • 随時変更される可能性がある。

自動化

状況がわかりましたので、自動化していきます。
GCPのCloud DNSで管理していたのでAPIで操作することができそうです。

Go言語で書かれた、github.com/StackExchange/dnscontrolパッケージに含まれる、pkg/spflibというサブパッケージを使うと、簡単にフラット化することができそうです。

https://pkg.go.dev/github.com/StackExchange/dnscontrol/pkg/spflib#SPFRecord.Flatten

また、SPFレコードを単にフラット化するだけでなく、自前のサーバに設定するための分割したデータまで作ることができる便利なライブラリです。

流れ

ほとんどの処理はライブラリが面倒を見てくれるので、GCPのCloud DNSと元データとの繋ぎこみを行うだけでよさそうです。

以下の流れで実装します。

  1. あらかじめテンプレートになるレコードをサブドメインに作っておいてそこに素直に(フラット化せず)レコードを登録しておく(GCPのコンソールでやります)
  2. テンプレートの内容を取得
  3. テンプレートを元にフラット化する
  4. 想定レコードを生成して、現在のレコードとの差分が無くなるようにCloud DNS側を更新

https://github.com/ka2n/ka2n.dev/blob/e001bd8fdb9f938dc9df2c3462842703dc8c7c77/docs/examples/spf-flatter/main.go#L1-L44

テンプレートの内容を取得

普通にCLIオプションなどで渡しても良いのですが、基本的にドメインの管理をGCPのコンソールから行っているため、_spf.example.comのようなサブドメインのTXTレコードに置いておき、参照しています。

https://github.com/ka2n/ka2n.dev/blob/e001bd8fdb9f938dc9df2c3462842703dc8c7c77/docs/examples/spf-flatter/main.go#L92-L96

テンプレートを元にフラット化する

フラット化の処理自体はspflib#SPFRecord.Flattenをそのまま使うだけです。
また、長くなりすぎたSPFレコード自体を複数に分割(@.example.com, spf1.example.com, spf2.example.com...)する機能もあるのでそのまま利用します。

https://github.com/ka2n/ka2n.dev/blob/e001bd8fdb9f938dc9df2c3462842703dc8c7c77/docs/examples/spf-flatter/main.go#L46-L73

想定レコードを生成して、現在のレコードとの差分が無くなるようにCloud DNS側を更新

https://github.com/ka2n/ka2n.dev/blob/e001bd8fdb9f938dc9df2c3462842703dc8c7c77/docs/examples/spf-flatter/main.go#L98-L229

完成

私の環境では、以上のコマンドにCLIとしてオプション等を渡せるようにした上で、バイナリをGitHub Releasesにアップロードしておき、GitHub Actionsで定期的に実行して変更を適用しています。

GitHub Actionsのワークフローサンプル
name: SPF Flatter

on:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

env:
  TEMPLATE_OR_DOMAIN: _spf_tmpl.<FQDN>
  PROJECT_ID: <GCP Project ID>
  ZONE_NAME: <ZONE NAME>
  FQDN: "<FQDN>."

jobs:
  run:
    permissions:
      id-token: write
      contents: read

    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Fetch latest binary
        uses: dsaltares/fetch-gh-release-asset@0.0.7
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          file: "spf_flatter_Linux_x86_64.zip"
      - name: Unzip toolkit
        run: |
          7z x spf_flatter_Linux_x86_64.zip

      - id: 'auth'
        uses: 'google-github-actions/auth@v0'
        with:
          workload_identity_provider: ...
          service_account:  ...

      - name: Run
        run: |
          ./spf-flatter $TEMPLATE_OR_DOMAIN -y

終わりに

以上、SPFレコードで遭遇する参照回数の制限とフラット化による対策をGo言語でのサンプルとして紹介しました。

本文中のコードは以下のリポジトリに置いてあります。

https://github.com/ka2n/ka2n.dev/tree/e001bd8fdb9f938dc9df2c3462842703dc8c7c77/docs/examples/spf-flatter

株式会社モニクル

Discussion

@iktakahiro / Takahiro Ikeuchi@iktakahiro / Takahiro Ikeuchi

素晴らしい記事です。今日さまざま検証をしていてこの記事にたどり着きました。

今回はこのサービスを使って解決してみることにしたのですが、有料ですし、外部サービスに依存してていいのかということもありますので、実装も検討しています

https://www.autospf.com/

ka2nka2n

お役にたててよかったです!
定期的にチェックしてほぼ存在を忘れてしまうくらいの方が良いので外部サービスの利用も良い手段だと思ってます。