Python
Security
Slack
ChatOps
errbot
OriginalLIFULLDay 8

Errbotを安全に使うために

LIFULL Advent Calendar 2017の8日目の記事です。

普段は海外向けの不動産ポータルサイトの開発に携わっていますが
本記事ではそれとは関係のない個人的な話をします。

:computer: はじめに

背景

サービスがスケールすると運用業務が増えますが、時間は有限です。
効率化の一手段として、定型的な仕事をチャットボットに任せることを考えました。

導入に際して

今やチャットボットはどこでも導入されている仕組みですが
チャットボット自体の作成や運用にもそれなりに手間がかかります。

  • ソースコードの管理(情報の秘匿)
  • 使用するユーザの権限の管理
  • チャットボット実行サーバの運用・監視・権限の管理

など。最初から一気に考えるとやる気とスピードを損ねそうです。

そこで

  • ソースコードは自身のPCの中だけに秘めて
  • 自分のタスクだけを実行するために
  • ローカルでボットを動かす

スモールスタートで始めよう と思い立ちました。

導入

今回は Python製のErrbotを使用します。
(自分しか触らないので、選択の自由度は非常に高いです)

Setupに従ってチャットボット「benriya」さんを作成し
Slackの設定をするといい感じにやりとりができるようになりました。

完全に個人的な用途で言えば
togglで管理しているタスクをサマライズして表示させたりしています。

mockmock-bot___mockmock_Slack.png

togglのダッシュボードではかゆいところに手が届かなかったりするので
APIの返り値を加工して体裁を整えて出力するようにしました。

なんとなく便利になりましたが、ここで マッチポンプ的な 問題が発生します。

:warning: 問題

他の人に絡まれる。

other.png

私しか使わないつもりで APIキーはばっちりハードコーディングしたので
他の人が勝手に私のタスクを読み取ったり、サーバを操作したりできます。

(今回は極端な実装をしていますが)真面目な話で言うと
そもそもボット自身が強い権限を持つ可能性もあるので、場合によっては危険です。

我が子が変な人に声をかけられてもついていかないようにする必要があります。

:no_pedestrians: アクセス制限

(ここからが本題)

Errbot には、各コマンドを実行できるユーザを制限する機能があります。

参照: Administration#Restricting access

BOT_ADMINS

Errbotの設定ファイルであるconfig.pyにおいて
BOT_ADMINS変数にはチャットボットの管理者のユーザ名を登録します。
なお、Slackの場合はこちらのページに正式なアカウント名が表示されます。
https://xxx.slack.com/account/settings#username

admin_onlyなコマンドの設定

ビルトインのコマンドの中では!restart!shutdownなど
管理者しか実行することができません。
ソースコードでは下記のようにadmin用コマンドとしての設定がされています。

!restart
@botcmd(admin_only=True)
def restart(self, msg, args):
!shutdown
@arg_botcmd('--confirm', dest="confirmed", action="store_true",
            help="confirm you want to shut down", admin_only=True)
@arg_botcmd('--kill', dest="kill", action="store_true",
            help="kill the bot instantly, don't shut down gracefully", admin_only=True)
def shutdown(self, msg, confirmed, kill):

!shutdownはコマンドのオプションがあるため複雑に見えますが
コマンド作成のための@botcmd@arg_botcmdというデコレータにおいて
引数としてadmin_only=Trueが指定されているのがわかります。

!taskも同様に設定してみると、adminの私以外は使用できなくなります。

!task
@botcmd(admin_only=True)  
def task(self, msg, args):

benriya___mockmock_Slack.png

管理者への通知

アクセス制限から少し話がそれますが
BotPluginクラスに実装されたwarn_admins()を使用すると
BOT_ADMINSに記載したユーザ全員にメッセージを通知します。

危険なコマンドや怪しい挙動を検知した際の通知に活用できそうです。

ACCESS_CONTROLS

今回の件はadmin_onlyのコマンドを増やしていけば解決しますが
後の運用のためにその他のアクセス制限について確認します。

細やかなACLが必要な場合は、config.pyにおいて
ACCESS_CONTROLSACCESS_CONTROLS_DEFAULTを設定します。
詳細はconfig.pyのサンプルファイルに記載されています。

これらの項目では、特定のユーザや部屋(チャンネル?)に対して
コマンドの実行を許可する/拒否する ための設定を行います。

例えば、下記のサンプルではデフォルトですべてのアクセスを許しつつ
statusaboutuptimeなどのコマンドには個別の設定がされています。

config.pyのサンプルファイルから抜粋
# Example:

ACCESS_CONTROLS_DEFAULT = {} # デフォルトですべてのアクセスを許可する
ACCESS_CONTROLS = {'status': {'allowrooms': ('someroom@conference.localhost',)},
                   'about': {'denyusers': ('*@evilhost',), 'allowrooms': ('room1@conference.localhost', 'room2@conference.localhost')},
                   'uptime': {'allowusers': BOT_ADMINS},
                   'help': {'allowmuc': False},
                   'ChatRoom:*': {'allowusers': BOT_ADMINS},
                  }

設定箇所の違い

名称 説明
ACCESS_CONTROLS_DEFAULT すべてのコマンドにデフォルトで適用するルールを記載
ACCESS_CONTROLS コマンドごとの詳細なルールを記載

ソースを読むと、ACCESS_CONTROLS_DEFAULTを取得後に
コマンドの制限がACCESS_CONTROLSに存在すれば上書きするようになっています。

errbot.core_plugins.acls
acl = self.bot_config.ACCESS_CONTROLS_DEFAULT.copy()
for pattern, acls in self.bot_config.ACCESS_CONTROLS.items():
    if ':' not in pattern:
        pattern = '*:{command}'.format(command=pattern)
    if ciglob(cmd_str, (pattern,)):
        acl.update(acls)
        break

使用可能なフィルタ

設定するルールは下記のフィルタで定義されます。

名称 説明
allowusers 使用可能なユーザを定義(ホワイトリスト)
denyusers 使用不可能なユーザを定義(ブラックリスト)
allowrooms 使用可能な部屋を定義(ホワイトリスト)
denyrooms 使用不可能な部屋を定義(ブラックリスト)
allowprivate ダイレクトメッセージでのアクセスを許可(Bool)
allowmuc 部屋の中でのコマンド実行を許可(Bool)

Slackでは「部屋」は「チャンネル」にあたるため
{'allowrooms': ('#general',)} のようなかたちで記載するようです。

上記のフィルタはソース中では下記のようなロジックで評価されています。
allow_xxxdeny_xxxよりも先に評価されているため
両方指定がある場合はホワイトリスト方式の指定が優先されます。

errbot.core_plugins.acls
if 'allowusers' in acl and not glob(usr, acl['allowusers']):
    return self.access_denied(msg, "You're not allowed to access this command from this user", dry_run)
if 'denyusers' in acl and glob(usr, acl['denyusers']):
    return self.access_denied(msg, "You're not allowed to access this command from this user", dry_run)
if msg.is_group:
    if not isinstance(msg.frm, RoomOccupant):
        raise Exception('msg.frm is not a RoomOccupant. Class of frm: %s' % msg.frm.__class__)
    room = str(msg.frm.room)
    if 'allowmuc' in acl and acl['allowmuc'] is False:
        return self.access_denied(msg, "You're not allowed to access this command from a chatroom", dry_run)

    if 'allowrooms' in acl and not glob(room, acl['allowrooms']):
        return self.access_denied(msg, "You're not allowed to access this command from this room", dry_run)

    if 'denyrooms' in acl and glob(room, acl['denyrooms']):
        return self.access_denied(msg, "You're not allowed to access this command from this room", dry_run)
elif 'allowprivate' in acl and acl['allowprivate'] is False:
    return self.access_denied(
        msg,
        "You're not allowed to access this command via private message to me",
        dry_run
    )

その他の設定

コマンドの存在自体も隠したいような場合は下記の設定でTrueを設定します。

名称 説明
HIDE_RESTRICTED_COMMANDS 使用が制限されているコマンドをhelpで隠す(Bool)
HIDE_RESTRICTED_ACCESS 使用が制限されているコマンドの呼出におけるエラーメッセージを出さない(Bool)

Command filters

既存のACL機能でも物足りない場合は
コマンドフィルタで独自のアクセス制限を実装します。

errbotは各コマンドの実行直前にこの関数を呼びます。
(前述のBOT_ADMINSACCESS_CONTROLSでの制限もこれで書かれています。)

ドキュメントによると下記のような構造でコマンドフィルタを定義します。

@cmdfilter
def some_filter(self, msg, cmd, args, dry_run):
    # 実行をブロックしたい場合はすべてNoneを返す
    return None, None, None
    # それ以外は次のフィルタ(コマンド)用の引数を返す
    return msg, cmd, args

時刻によってアクセスを制限するような
ユーザベースでの制限以外のコントロールに使用できそうです。

:end: 終わりに

Errbotを安全に使うために、アクセスコントロールの話をしました。

ちなみに、チャットボットの私物化も
やってみると意外とメリットがあります。

  • 好きな言語や環境で取り組めて自由に事故れる(勉強になる)
  • 多少の不便は自分で吸収するため、開発のスピード感がある
  • 自分にしか必要ない機能も迷わず組み込める
  • ただ定形のスクリプトを書くよりもレールに乗れて、愛着も湧く

安定したら、共用できるところはメンバーにも公開しようと思います。