投稿者: atmark

  • Debian 9 Stretch で連続接続を Fail2ban ではじく

    Debian 9 Stretch で連続接続を Fail2ban ではじく

    Fail2ban は、ログを参照して認証エラーを繰返しているホストを ban するための Python 製のツールです。iptables で接続制御をするのは ufw と同じですが、Fail2ban はより柔軟にログを分析して接続を制御します。

    インストール

    $ sudo apt install fail2ban
    $ sudo fail2ban-client status
    Status
    |- Number of jail: 1
    `- Jail list: sshd

    設定

    /etc/fail2ban/jail.conf の中には次のような記述があります。

    # YOU SHOULD NOT MODIFY THIS FILE.
    #
    # It will probably be overwritten or improved in a distribution update.
    #
    # Provide customizations in a jail.local file or a jail.d/customisation.local.
    # For example to change the default bantime for all jails and to enable the
    # ssh-iptables jail the following (uncommented) would appear in the .local file.
    # See man 5 jail.conf for details.

    jail.cof は更新時に上書きされる可能性があるので、jail.local 等の別名ファイルを作成して差分を記述するようにしましょう、ということです。

    /etc/fail2ban/jail.conf 次のように書きます。ufw と組合せているので、ufw の limit を考慮しないとずっと ban されないことになります。

    [DEFAULT]
    bantime = 1800
    findtime = 600
    maxretry = 5
    
    [postfix-sasl]
    enabled = true
    
    [dovecot]
    enabled = true

    設定を再読込したら完了です。

    $ sudo systemctl reload fail2ban
    $ sudo fail2ban-client status
    Status
    |- Number of jail: 3
    `- Jail list: dovecot, postfix-sasl, sshd
    $ sudo fail2ban-client status sshd
    Status for the jail: sshd
    |- Filter
    | |- Currently failed: 1
    | |- Total failed: 7
    | `- File list: /var/log/auth.log
    `- Actions
     |- Currently banned: 0
     |- Total banned: 0
     `- Banned IP list:

    自分でフィルタやアクションを定義することもできますが、とりあえずはこれで様子を見てその都度変更していこうと思います。

    bantime

    接続を拒否する時間を秒で指定します。

    # "bantime" is the number of seconds that a host is banned.
    bantime = 600

    findtime と maxtretry

    findtime 秒間の間に maxretry 回の認証失敗があったときに拒否対象となります。標準の findtime = 600, maxretry = 5 であれば 600秒間に 5 回の試行で拒否対象となります。

    # A host is banned if it has generated "maxretry" during the last "findtime"
    # seconds.
    findtime = 600
    
    # "maxretry" is the number of failures before a host get banned.
    maxretry = 5

    参考

  • Debian 9 Stretch で ufw による簡単ファイアウォール設定

    Debian 9 Stretch で ufw による簡単ファイアウォール設定

    Linux 環境でファイアウォールを構築するお話です。

    Linux にはカーネルレベルでパケットフィルタや NAT を行う netfilter という仕組みがあり、それを設定するのが iptables です。iptables をそのまま扱えれば一番良いのですが、柔軟さ故に学習コストは高めです。そのため、firewalld や ufw といった iptables のフロントエンドが存在しています。今回は ufw を使います。

    ufw は Uncomplicated Firewall の略で、Python で書かれた iptables のフロントエンドです。

    以下、インストールと簡単な使い方です。

    インストールと有効化

    $ sudo apt install ufw
    $ sudo ufw enable
    Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
    Firewall is active and enabled on system startup
    $ sudo ufw status
    Status: active

    SSH で作業しましたが、有効化しても切断されることはありませんでした。絶対に切断されないとは言い切れないので、念のため切断されたら八方塞りになる状況では止めておきましょう。

    全てを deny して SSH のみ allow する

    $ sudo ufw default deny
    Default incoming policy changed to 'deny'
    (be sure to update your rules accordingly)
    $ sudo ufw allow ssh
    Rule updated
    Rule updated (v6)
    atmark@dev:~$ sudo ufw status
    Status: active
    
    To Action From
    -- ------ ----
    22/tcp ALLOW Anywhere 
    22/tcp (v6) ALLOW Anywhere (v6)

    たったこれだけです。default を deny して、ssh を allow するだけ。簡単ですね。

    アプリケーション単位で allow/deny する

    ufw app list で有効なアプリケーションの一覧が表示されます。

    $ sudo ufw app list
    Available applications:
     AIM
     Bonjour
     CIFS
     DNS
     Deluge
     Dovecot IMAP
     Dovecot Secure IMAP
     IMAP
     IMAPS
     IPP
     KTorrent
     Kerberos Admin
     Kerberos Full
     Kerberos KDC
     Kerberos Password
     LDAP
     LDAPS
     LPD
     MSN
     MSN SSL
     Mail submission
     NFS
     OpenSSH
     POP3
     POP3S
     PeopleNearby
     Postfix
     Postfix SMTPS
     Postfix Submission
     SMTP
     SSH
     Socks
     Telnet
     Transmission
     Transparent Proxy
     VNC
     WWW
     WWW Cache
     WWW Full
     WWW Secure
     XMPP
     Yahoo
     qBittorrent
     svnserve

    この名前をつかって deny/allow するだけです。

    $ sudo ufw allow SSH
    $ sudo ufw allow "WWW Full"
    $ sudo ufw status
    Status: active
    
    To Action From
    -- ------ ----
    SSH LIMIT Anywhere 
    WWW Full ALLOW Anywhere 
    SSH (v6) LIMIT Anywhere (v6) 
    WWW Full (v6) ALLOW Anywhere (v6)

    ルールの削除

    2 通りあります。まずは追加したルールをそのまま削除するやり方。

    $ sudo ufw delete allow SSH

    もう一つは現在のルール一覧を番号付けして、その番号で削除するやり方。

    $ sudo ufw status numbered
    Status: active
    
    To Action From
    -- ------ ----
    [1] SSH LIMIT Anywhere 
    [2] WWW Full ALLOW Anywhere 
    [3] SSH (v6) LIMIT Anywhere (v6) 
    [4] WWW Full (v6) ALLOW Anywhere (v6) 
    
    $ sudo ufw delete 4
    Deleting:
     allow 'WWW Full'
    Proceed with operation (y|n)?

    名前で指定するか、ポート番号で指定するか

    ちょっとややこしかったので、軽くまとめておきます。

    app list で表示される内容は /etc/ufw/applicatoin.d/ 以下に用意されています。例えば WWW 関係は次のようになっています。

    $ cat /etc/ufw/application.d/ufw-webserver 
    [WWW]
    title=Web Server
    description=Web server
    ports=80/tcp
    
    [WWW Secure]
    title=Web Server (HTTPS)
    description=Web Server (HTTPS)
    ports=443/tcp
    
    [WWW Full]
    title=Web Server (HTTP,HTTPS)
    description=Web Server (HTTP,HTTPS)
    ports=80,443/tcp
    
    [WWW Cache]
    title=Web Server (8080)
    description=Web Server (8080)
    ports=8080/tcp

    ルールを追加するときのポートの指定は 3 通りあって

    • ポート番号の直接指定 – 443/tcp
    • プロトコル名 – https
    • アプリケーション名 – WWW Secure

    このうち、ポート番号の直接指定もプロトコル名も、ルールに登録されるのは数字のポート番号そのものです。allow https としても 443/tcp でルールに登録されます。

    アプリケーション名で指定すると、指定した名前そのままで登録されます。

    ここでややこしいのが、例えばアプリケーション名とプロトコル名が同じ SSH の場合。allow SSH と書くと名前で登録されますが、allow ssh とすると 22/tcp で登録されます。

    接続制限 limit

    $ sudo ufw limit ssh

    もう一つ、limit というルールがあります。man には次のよう書かれています。

    ......
    ufw will normally allow the connection
    but will deny connections if an IP address attempts
    to initiate 6 or more connections within 30 seconds
    ......

    30 秒間に 6 回以上接続を試みた場合に接続を拒否するルールです。SSH などは総当りでログイン試行が来るので、limit をかけておいた方が良いでしょう。

    参考

  • Apache2 MPM-ITK 環境下の CGI で sudo するときの注意点

    Apache2 MPM-ITK 環境下の CGI で sudo するときの注意点

    TL; DR

    /etc/apache2/conf-available/security.conf に次を追加します。root の UID: 0, GID: 0 を範囲に含めています。

    <IfModule mpm_itk_module>
        LimitUIDRange 0 6000
        LimitGIDRange 0 6000
    </IfModule>

    (さらに…)

  • sudo でファイルへのテキスト書き込み

    sudo でファイルへのテキスト書き込み

    TL; DR

    パイプで sudo tee しましょう。

    $ cat <<EOS | sudo tee /root/script.txt
    foo
    bar
    hoge
    EOS

    (さらに…)

  • Apache2 MPM-ITK で WSGI を daemon モードで動かす

    Apache2 MPM-ITK で WSGI を daemon モードで動かす

    Apache2 MPM-ITK 環境下の情報が少なすぎて解決するのに丸一日かかってしまいました。もうこんな目に遭うのはご免なので記録します。前回の記事「Python CGI プログラマのための WSGI 移行記録」の続きから書きます。環境は同じく Apache2 MPM-ITK on Debian 9 Jessie です。


    mod_wsgi には embedded mode と daemon mode があります。

    embedded mode は呼び出される度にプロセスを起動して実行するのでほぼ CGI と同じ実行方式です。プロセス ID をしばらく観察していると、すぐには終了せずにしばらく残存してから終了するみたいです。CGI は都度終了するのでそこが違いでしょうか。

    daemon mode は文字通り、デーモンとして起動したまま待機しています。プロセス ID を観察しているとずっと同じまま動作していることがわかります。

    Apache2 の設定に AddHandler wsgi-script だけを設定すると embedded mode で実行されます。daemon mode で実行するには設定を加える必要があります。

    Apache2 の VirtualHost に次の 2 行を加えます。

    <VirtualHost *.443>
        ...
        WSGIDaemonProcess wsgi_app socket-user=USERNAME
        WSGIProcessGroup wsgi_app
        ...
    </VirtualHost>

    保存したら Apache2 の再読込を行います。

    $ sudo systemctl reload apache2

    これで完了です。気になる方は次のような WSGI スクリプトを実行してみて、PID が間を置いたリロードで PID が変化するかどうかを試してみてください。

    # coding: utf-8
    
    import os
    
    def applicatoin(e, r):
        r('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<p>PID: {}</p>'.format(os.getpid()).encode('utf-8')

    MPM-ITK 環境では socket-user オプションを指定しないと次のようなエラーを吐きます。

    503 Service Unavailable

    The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.

    このとき Apache2 の error ログには次のようなものが残ります。

    [Thu Oct 11 15:04:38.059487 2018] [wsgi:error] [pid 15574] (13)Permission denied: [client 153.194.19.27:55658] mod_wsgi (pid=15574): Unable to connect to WSGI daemon process 'wsgi_app' on '/var/run/wsgi.15557.0.1.sock' as user with uid=1001.

    これを見たら、パーミッションが不足していると思ってディレクトリのオーナーやパーミッションの変更ばかり試してハマりました。権限があるところもパーミッションが 777 のところを指定しても自体は解決しません。

    socket-user ではなく user と group オプションもありますがこれまた何も解決してくれず、おかげで丸一日悩む羽目になりました。

    以下はおまけ程度の内容です。

    実際に embedded mode と daemon mode でどれくらいの差があるのかを、Chrome の開発者モードに表示されるページの読込時間で試してみました。上記の PID を表示するだけの WSGI スクリプトをひたすら再読込するだけで、単位は ms です。

    embeddeddaemon
    初回100100
    直後3030
    間欠10070

    数回リロードして安定した数値を拾っているだけなのでかなり雑です。目安程度にしてください。直後というのは概ね数秒以内です。間欠は 10 秒以上間を空けています。

    サーバを起動した最初の 1 回目は同じですが、それ以後は daemon mode の方が有利ですね。開発時ならともかく、運用時はまず再起動することはないので daemon mode を選ばない理由はありませんね。

    複数ユーザへの対応

    MPM-ITK を選んでいるのは複数ユーザのパーミッション分離を行うためで、Apache2 もユーザ別に設定ファイルを書いています。仮に foo さんと bar さんと hoge さんがいるとすると、設定ファイルは以下のようにしておけば、それぞれで WSGI スクリプトが daemon mode で動きます。

    WSGIDaemonProcess 直後の名前は WSGIProcessGroup と揃えておけば好きな名前をつけて構いません。今回は WSGI であることがわかり、ユーザ別に分けたかったので wsgi_username の形にしました。

    foo.conf

    <VirtualHost *.443>
        ...
        ServerName foo.example.com
        ...
        WSGIDaemonProcess wsgi_foo socket-user=foo
        WSGIProcessGroup wsgi_foo
        ...
    </VirtualHost>

    bar.conf

    <VirtualHost *.443>
        ...
        ServerName bar.example.com
        ...
        WSGIDaemonProcess wsgi_bar socket-user=bar
        WSGIProcessGroup wsgi_bar
        ...
    </VirtualHost>

    hoge.conf

    <VirtualHost *.443>
        ...
        ServerName hoge.example.com
        ...
        WSGIDaemonProcess wsgi_hoge socket-user=hoge
        WSGIProcessGroup wsgi_hoge
        ...
    </VirtualHost>

    参考

  • Python CGI プログラマのための WSGI 移行記録

    Python CGI プログラマのための WSGI 移行記録

    Apache2 MPM-ITK on Debian 9 Jessie を想定しています。

    10 年近く書いてきた CGI を、昨今の WSGI に準拠したものへと移行するに当って大変苦労した記録です。技術的なことだけではなく、心情的なことも含んでいます。

    まず最初に書いてしまうと、CGI も WSGI も呼び出しと結果の受け渡しが違うだけで、プログラムの内容や動作には殆ど関係ありません(細かく言うと実行の仕方も違うはずですが、そこはどちらかというとサーバの都合だと思うので今回は考慮しません)。なので単に今まで書いた CGI を WSGI に移行するだけなら、あまりメリットは感じないどころか環境構築が煩雑過ぎて疲れるだけです。

    そもそもが CGI をどのような用途に使っているかによって、WSGI に移行すべきかどうかの判断が異なってきます。Raspberry pi やそれに近い組込み系の環境で単なる Web インターフェースとして使うのであれば、CGI の簡潔さは今なお有用です。ターミナル上で HTML の出力を確認しながらデバッグといったことも CGI なら簡単です。

    CGI の代替となる技術が登場した背景には、CGI の実行に「無駄」が多いとされたことがあります。CGI へのアクセスがある度に Web サーバはプロセス(インタープリタ等)を起動します。アクセス数が多くなるとこの手順はサーバにとって負荷になります。インタープリタは常に同じものを使うことが多いので、当然ながらインタープリタは常時起動しておけばいいのではないかという考えに至ります。Web サーバと Web アプリケーション間のやり取りには様々なインターフェースが開発されましたが、それを統一したものの一つが WSGI という規格です。Python という言語の中から生れた WSGI はその後、他の言語にも派生しました。

    さて、いよいよ Apache2 で WSGI を使って Python スクリプトを動かす準備に入ります。既に Apache2 で CGI が動いているなら実は変更することは 2 つです。

    • mod_wsgi のインストールと設定
    • AddHandler に wsgi-script を加える

    mod_wsgi は Debian 環境で apt 一発インストールです。このとき、Python 2.x を使うのか Python 3.x を使うのかによってパッケージが異なるので注意しましょう。今回は Python 3.x を使う予定なので Python 3.x に対応した “libapache2-mod-wsgi-py3” をインストールします。パッケージがあるか確認してインストールします。

    $ sudo apt search mod-wsgi
    Sorting... Done
    Full Text Search... Done
    libapache2-mod-wsgi/stable,now 4.5.11-1 amd64
     Python WSGI adapter module for Apache
    
    libapache2-mod-wsgi-py3/stable,now 4.5.11-1 amd64
     Python 3 WSGI adapter module for Apache
    
    $ sudo apt install libapache2-mod-wsgi-py3

    “sudo a2enmod wsgi” としなくても自動で有効化されていました。

    Apache2 の設定ファイル(*.conf)に加えるのは次の一行だけ。<Directory /var/www> </Directory> の中などに書き加えましょう。

    AddHandler wsgi-script .wsgi .py

    WSGI はインターフェースの名前であって、スクリプト自体は Python だから拡張子を変えるのは間違っている気もしますが、それを言ったらどんな言語でも拡張子を .cgi とするのもおかしな話になるので、気にしないでおきましょう。

    わかりにくいと思うので 000-default.conf を書き換えた例を示します。今回は実験用の独立したサーバなので全て許可した設定していますが、本番環境ではくれぐれも必要のないものまで許可しないようにしましょう。

    ...
    DocumentRoot /var/www
    <Directory /var/www>
        AllowOverride all
        Options ExecCGI
        AddHandler wsgi-script .wsgi .py
        Require all granted
    </Directory>
    ...

    設定を書き換えたら Apache2 を restart または reload します。

    $ sudo systemctl reload apache2

    または

    $ sudo systemctl restart apache2

    動作を確認するために簡単な WSGI スクリプトを書いてみます。基本の Hello, world. で。

    def application(environ, start_response):
        start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<p>Hello, world!</p>'.encode('utf-8')

    これを /var/www/hello.wsgi として保存して http://[サーバのIPアドレス等]/hello.wsgi にアクセスして表示されたら完了です。CGI と違って chmod +x で実行権限を与える必要もありません。

    ついでに調べた情報についても記しておきます。

    def application() は呼び出し時にこの名前が呼ばれるので必須です。面倒だからと def app() にすると “404 Not Found” と怒られます。

    引数の environ, start_response は別名にしても問題ありません。つまり、引数名使ったキーワード呼び出しは使わず、引数の順番しか定義されていません。これは仕様書でもはっきり次のように書かれています。

    The application object must accept two positional arguments. For the sake of illustration, we have named them environ and start_response, but they are not required to have these names. A server or gateway must invoke the application object using positional (not keyword) arguments. (E.g. by calling result = application(environ, start_response) as shown above.)

    PEP 333 — Python Web Server Gateway Interface v1.0 | Python.org

    なので、例えば次のように書いても良いわけです。殆どが environ, start_response で書かれているようなので、そんなところで要らない個性を発揮する必要はないと思いますが、さっと動作確認したいときには省略したくなりますからね。

    def application(e, r):
        r('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<p>Hello, world!</p>'.encode('utf-8')

    application() を呼び出したときの戻り値は Iteratable である必要があるので、例えば複数の出力をしたい場合は次の 2 通りのパターンで行えます。結果はどちらも同じです。普通に return バイト列 とやると 500 Internal Server Error でハマります。

    yield バイト列

    def application(environ, start_response):
        start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<title>Hello, WSGI!</title>'.encode('utf-8')
        yield '<p>This is a WSGI sample.</p>'.encode('utf-8')

    return バイト列のリスト

    def application(environ, start_response):
        start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        return ['<title>Hello, WSGI!</title>'.encode('utf-8'), <p>Hello, world!</p>'.encode('utf-8')]

    謝辞

    この記事を書くに当って、れお氏(@reoreo125)には多大なる貢献を頂戴したこと、誠に感謝申し上げます。

    参考

  • Python3 の CGI で日本語が出力できない!

    TL; DR

    print 文で UnicodeEncodeError が出ているなら次のコードで stdout をラップしましょう。

    import io, sys
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding = 'utf-8')

    (さらに…)