#!/usr/bin/env python3 """ Route 53 DNS 設定スクリプト: SPF / DKIM / DMARC 使い方(サーバー上で実行): pip3 install boto3 export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... export HOSTED_ZONE_ID=Z... # Route 53 ホストゾーン ID (yyamashita.com) export SERVER_IP= # 自動検出不可の場合のみ python3 /app/infra/mail/dns/setup-route53.py domains.txt に記載されたサービスドメインと hetzner.yyamashita.com に対して SPF / DKIM / DMARC TXT レコードを UPSERT する。 """ import os import re import sys import urllib.request try: import boto3 except ImportError: print("pip3 install boto3 を実行してください", file=sys.stderr) sys.exit(1) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) DOMAINS_FILE = os.environ.get("DOMAINS_FILE", os.path.join(SCRIPT_DIR, "..", "domains.txt")) HOSTED_ZONE_ID = os.environ.get("HOSTED_ZONE_ID", "") AWS_REGION = os.environ.get("AWS_REGION", "us-east-1") SELECTOR = "mail" PRIMARY = "hetzner.yyamashita.com" def get_server_ip(): if ip := os.environ.get("SERVER_IP", ""): return ip for url in ["https://api.ipify.org", "https://ifconfig.me/ip"]: try: with urllib.request.urlopen(url, timeout=5) as r: return r.read().decode().strip() except Exception: continue return "" def read_domains(path): domains = [] with open(path) as f: for line in f: line = line.strip() if line and not line.startswith("#"): domains.append(line) return domains def read_dkim_pubkey(path): with open(path) as f: content = f.read() parts = re.findall(r'"([^"]*)"', content) return "".join(parts) def txt_record(fqdn, raw_value): chunks = [raw_value[i:i + 255] for i in range(0, len(raw_value), 255)] txt_value = " ".join(f'"{c}"' for c in chunks) name = fqdn if fqdn.endswith(".") else fqdn + "." return { "Action": "UPSERT", "ResourceRecordSet": { "Name": name, "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": txt_value}], }, } def main(): if not HOSTED_ZONE_ID: print("ERROR: HOSTED_ZONE_ID が未設定です", file=sys.stderr) print(" export HOSTED_ZONE_ID=Z...", file=sys.stderr) sys.exit(1) server_ip = get_server_ip() if not server_ip: print("ERROR: SERVER_IP を自動取得できませんでした", file=sys.stderr) print(" export SERVER_IP=", file=sys.stderr) sys.exit(1) dkim_key_file = "/etc/opendkim/keys/mail.txt" try: dkim_value = read_dkim_pubkey(dkim_key_file) except FileNotFoundError: print(f"ERROR: DKIM 公開鍵ファイルが見つかりません: {dkim_key_file}", file=sys.stderr) print("先に mail/install.sh を実行してください", file=sys.stderr) sys.exit(1) service_domains = read_domains(DOMAINS_FILE) print(f"サーバー IP : {server_ip}") print(f"ホストゾーン: {HOSTED_ZONE_ID}") print(f"ドメイン : {[PRIMARY] + service_domains}") print() changes = [] # プライマリドメイン (IP 直指定 SPF) changes.append(txt_record(PRIMARY, f"v=spf1 ip4:{server_ip} -all")) changes.append(txt_record(f"{SELECTOR}._domainkey.{PRIMARY}", dkim_value)) changes.append(txt_record(f"_dmarc.{PRIMARY}", "v=DMARC1; p=none")) # サービスドメイン (include SPF) for domain in service_domains: changes.append(txt_record(domain, f"v=spf1 include:{PRIMARY} -all")) changes.append(txt_record(f"{SELECTOR}._domainkey.{domain}", dkim_value)) changes.append(txt_record(f"_dmarc.{domain}", "v=DMARC1; p=none")) print(f"{len(changes)} 件の DNS レコードを UPSERT します...") for c in changes: rrs = c["ResourceRecordSet"] print(f" {rrs['Type']} {rrs['Name']}") client = boto3.client("route53", region_name=AWS_REGION) resp = client.change_resource_record_sets( HostedZoneId=HOSTED_ZONE_ID, ChangeBatch={ "Comment": "Mail auth records (SPF/DKIM/DMARC) via hetzner-infra", "Changes": changes, }, ) info = resp["ChangeInfo"] print() print(f"Change ID : {info['Id']}") print(f"Status : {info['Status']}") print() print("確認コマンド (数分後):") for domain in [PRIMARY] + service_domains: print(f" dig TXT {SELECTOR}._domainkey.{domain} +short") if __name__ == "__main__": main()