pikesaku’s blog

個人的なプログラム勉強メモです。記載内容について一切の責任は持ちません。

PostfixのSMTP認証に認証ユーザー単位の接続元NW制限を追加する方法

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=には、Postfixaccessテーブルで利用可能なアクションが設定できる。
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が登録されている必要がある。

最後に

上記のプログラムは簡単に動作確認するだけのもので、細かい点はあまり考慮してません。