fail2banが起動しない

サーバに再起動を掛けてから立ち上がらなくなった。(fail2ban-0.9.2-1.el6.noarch)
詳しいエラーを出すために fail2ban-client -xvd start してみると

ERROR  Failed during configuration: 'NoneType' object has no attribute 'startswith'
Traceback (most recent call last):
  File "/usr/bin/fail2ban-client", line 435, in <module>
    if client.start(sys.argv):
  File "/usr/bin/fail2ban-client", line 368, in start
    self.dumpConfig(self.__stream)
  File "/usr/bin/fail2ban-client", line 424, in dumpConfig
    for c in cmd:
TypeError: 'NoneType' object is not iterable

と出る*1。たどっていった結果、直接の原因は

# /usr/lib/python2.6/site-packages/fail2ban/client/jailreader.py (el6)
# /usr/lib/python2.7/site-packages/fail2ban/client/jailreader.py (el7)
    def extractOptions(option):
        match = JailReader.optionCRE.match(option)
        if not match:
            # TODO proper error handling
            return None, None
# ...
    def getOptions(self):
# ...
            if self.__opts["filter"]:
                filterName, filterOpt = JailReader.extractOptions(
                    self.__opts["filter"])

この部分だった。jail.local の filter の設定値が optionCRE にマッチしなくなっていたらしい。
filterにはfilter.d以下のファイル名の拡張子抜きの値を指定する。当該サーバではここに独自のファイル群の入ったディレクトリのシンボリックリンクを置いているため、filter = custom/filter-name のようにしていた。
ソースでは optionCRE = re.compile("^((?:\w|-|_|\.)+)(?:\[(.*)\])?$") となっていて、スラッシュを含むとマッチしない。続くエラー処理はTODOのため、別の場所で返り値の None の属性を取ろうとして落ちたようだ。
サブディレクトリを掘れないというのは不便なので、姑息に (?:\w|-|_|\.)+/を足して [\w_./-]+ として対処した。バージョンアップ時のことはまた今度考えようと思う。

*1:-d(ダンプ)しないと別の場所で落ちる。同じ種類のエラーだがバックトレースが出ない。