PostfixのPolicyサービスを使う
Polixyサービスとは?
独自プログラムを呼び出して細かいアクセス制御を実現する機能
独自プログラムは、Postfixが呼び出し時に提供する属性情報(SASL認証ユーザー名等)を利用してリレー判定結果を返すだけ
→入力データはPostfix経由時に精査されて、プログラムに渡される為、簡単&安心に使える感じ!
Postfixパッケージにサンプルプログラムがあり。
メールデータをいじらない為、メールデータロストのリスクは少ない(?)
今回実装したPolicyサービスの動き
①クライアントがRCPTコマンドを実行
②Postfixが標準入力で以下の情報をプログラムに渡す
例)
request=smtpd_access_policy protocol_state=RCPT protocol_name=SMTP helo_name=some.domain.tld queue_id=8045F2AB23 sender=foo@bar.tld recipient=bar@foo.tld client_address=1.2.3.4 client_name=another.domain.tld instance=123.456.7 sasl_method=plain sasl_username=you sasl_sender= ccert_subject=solaris9.porcupine.org ccert_issuer=Wietse Venema ccert_fingerprint=C2:9D:F4:87:71:73:73:D9:18:E7:C2:F3:C1:DA:6E:04 size=12345 [empty line]
③プログラムはPostfixから渡される以下情報を利用してリレー判定をする。
接続元IP(client_address)
SMTP認証ユーザー情報(sasl_username)
接続元IPの情報を、DBに問い合わせて接続を許可するNWか判定する。
④プログラムは判定結果を標準出力に以下のデータを返す。
リレー拒否する場合
action=reject ERROR MESSAGE [empty line]
リレー拒否しない場合
actions=dunno [empty line]
actions=には、Postfixのaccessテーブルで利用可能なアクションが設定できる。
Postfix manual - access(5)
dunnoは、テーブルにマッチするものがない場合と同じ動き。
こうすれば、smtpd_recipient_restrictionsで定義した以降のフィルタも評価される。
前提
ユーザー毎に接続を許可する接続元NWをDBに入れておく。
テスト環境構築
main.cf
smtpd_recipient_restrictions = check_policy_service unix:private/policy, permit_sasl_authenticated, reject_unauth_destination smtpd_sasl_auth_enable = yes smtpd_sasl_security_options = noanonymous broken_sasl_auth_clients = yes smtpd_sasl_local_domain = $myhostname smtp_sasl_path = smtpd
master.cf
policy unix - n n - - spawn user=nobody argv=/bin/python /usr/libexec/postfix/smtpd-policy-chk.py
/usr/libexec/postfix/smtpd-policy-chk.py
#!/usr/bin/python # -*- coding: utf-8 -*- import sys import MySQLdb import ipaddress import syslog import time def output_log(mes): syslog.openlog() syslog.syslog("DEBUG: " + mes) syslog.closelog() def get_attr(): attr = dict() while True: ent = raw_input() if ent: if "=" in ent: k = ent.split("=")[0] v = ent.split("=")[1] attr[k] = v output_log("Input: " + ent) else: output_log("Invalid entry: " + ent) else: output_log("Fin input" + ent) break return attr def chk_relay(client_address, sasl_username): connector = MySQLdb.connect(host="localhost", db="testdb", user="testuser", passwd="password", charset="utf8") cursor = connector.cursor() sql = "select network from access where user = '" + sasl_username + "'" cursor.execute(sql) result = cursor.fetchall() cursor.close() connector.close() if result: network = result[0][0] nw = ipaddress.ip_network(network) ip = ipaddress.ip_address(unicode(client_address, "utf-8")) if ip in nw: return "relay" else: return "no_relay" else: return "no_relay" def main(): attr = get_attr() if attr.has_key("client_address") and attr.has_key("sasl_username"): client_address = attr["client_address"] sasl_username = attr["sasl_username"].split("@")[0] if client_address and sasl_username: ret = chk_relay(client_address, sasl_username) if ret == "relay": output_log("Result 1") sys.stdout.write("action=dunno\n\n") else: output_log("Result 2") sys.stdout.write("action=reject Dameyo\n\n") else: output_log("Result 3") sys.stdout.write("action=dunno\n\n") else: output_log("Result 4") sys.stdout.write("action=dunno\n\n") exit() if __name__ == '__main__': main()
テストデータ
mysql > create database testdb; > grant all on testdb.* to testuser@localhost; > flush privileges; > set password for testuser@localhost=password('password'); > use testdb > create table access (user VARCHAR(32), network VARCHAR(64)); > insert into access (user, network) values("test1", "127.0.0.1/32"); > insert into access (user, network) values("test2", "172.31.27.95/32"); > create table user (user VARCHAR(32), password VARCHAR(64), maildir VARCHAR(64)); > insert into user (user, password) values("test1", "password"); > insert into user (user, password) values("test2", "password"); > select * from user; +-------+----------+---------+ | user | password | maildir | +-------+----------+---------+ | test1 | password | NULL | | test2 | password | NULL | +-------+----------+---------+ 2 rows in set (0.00 sec) > select * from access; +-------+-----------------+ | user | network | +-------+-----------------+ | test1 | 127.0.0.1/32 | | test2 | 172.31.27.95/32 | +-------+-----------------+ 2 rows in set (0.00 sec)
動作結果
SMTP認証に成功しただけでは、リレー許可されない。
DBに接続元NWが登録されている必要がある。
最後に
上記のプログラムは簡単に動作確認するだけのもので、細かい点はあまり考慮してません。