SlackのWebhookをプロキシする仕組みを作る

Slackはさまざまなカスタマイズ機能を持っているのが魅力のツールです。例えばBotを作ったり、カスタムのslash commandを作ったりすることで、プラットフォームの拡張ができます。

Slack Botの作り方はいくつかあるのですが、Slackのリッチな機能を最大限に引き出すには、SlackからのWebhookを受けることが必要になってきます。すなわち、ボタンなどが付いたリッチなメッセージの投稿は難しくないのですが、投稿したメッセージのボタンやメニュー操作は、SlackからWebhookの形で通知される仕組みになっています。

※ この辺りの仕組みの詳細については、まとまっている記事がいくつもあるので省略します。

- [Slack Botの種類と大まかな作り方](https://qiita.com/namutaka/items/233a83100c94af033575)
- [slackで単純なボタン付きメッセージを送る](https://qiita.com/masakabe/items/c47fc98e939475594842)

Slackは当然publicなサービスなので、インターネット経由でWebhookが飛んできます。これを受けて処理するためには、Bot用の公開APIサーバを書くことになるでしょう。

## 公開サーバ？ 何か問題でも？

それじゃあ、単にサーバを立てればいいじゃないかというのはもっともですが、サーバを公開するのはいくつか面倒な点があります。

- 公開サーバはセキュリティリスクがある
  - もちろん、ちゃんと管理すればいいのですが、publicに露出するものが増えると相応にリスクが増加してしまうのはどうしようもないことです。できればやりたくありません
- とりわけ会社業務で使う場合、イントラネットにある社内システムや社内ネットワークとの接続性を考慮する必要が出てくる
  - 当然、Botであるからには、社内のいろいろなものと連携させたくなります。

SlackのRTM APIというWebSocketベースのAPIでchatメッセージを受信する場合には、こういった悩みは発生しませんでした（メッセージをpullするAPIを使うので、基本的にインターネットへ接続できるBot実行環境がありさえすればよかった）。できる限りそのお手軽さを保ったまま、セキュリティなどの煩雑な問題をコントロールする方法がないか、ということを考えたくなります。

## そうだプロキシしよう

というわけで、次のようなシステムを考えることにしました。

- Webhookを直接Botサーバに流すのではなく、プロキシ（リバースプロキシ）を経由させる
- プロキシは単にリクエストを横流しするのではなく、リクエストの検証を行い、不正なリクエストを弾く
  - Slackから事前に払い出された秘密鍵を元に、指定の手順でHMACを計算することで、リクエストの正当性の検証が可能です
    - [Verifying requests from Slack](https://api.slack.com/docs/verifying-requests-from-slack)
  - [SDKがある](https://github.com/slackapi/node-slack-events-api)ので、Slack社オフィシャルの実装をそのまま引っこ抜いてくることができます

具体的には、AWSのAPI GatewayとLambdaを組み合わせた、簡単なプロキシを書きます。

![slack-to-intranet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64680/51fc62b2-7402-4d47-4986-11d9f78608ef.png)

パターンとして変則的ではありますが、結果的に[AWSのDDoS対策ホワイトペーパー](https://d1.awsstatic.com/whitepapers/Security/DDoS_White_Paper.pdf)で述べられている、API Gatewayに公開箇所を絞るというプラクティスに沿っているように思います。

> Typically, when you must expose an API to the public, there is a risk that the API frontend could be targeted by a DDoS attack. To help reduce the risk, you can use Amazon API Gateway as a “front door” to applications running on Amazon EC2, AWS Lambda, or elsewhere.
> By using Amazon API Gateway, you don’t need your own servers for the API frontend and you can obfuscate other components of your application. By making it harder to detect your application’s components, you can help prevent those AWS resources from being targeted by a DDoS attack.

## 実装例

今回は（ちょっと慣れない言語なのですが）Node.jsでlambdaを書いてみることにします。
フレームワークとして[serverless framework](https://serverless.com/framework/)を使うとお手軽にAPI Gateway + Lambdaの構成を作ることができました。

<https://github.com/saka1/slack-webhook-gatekeeper>

バックエンドのSlack App名に対応したURLパスに対してWebhookが飛んでくると、秘密鍵を使ってWebhookの検証をします。あらかじめ、Slack App名と秘密鍵の対応関係はparameter storeに入れておきます。正当なWebhookだった場合にはupstreamにリクエストを飛ばしてプロキシ動作をします。URLパスは複数登録することができるように作りました。


## 学んだこと

- AWSのparameter storeはあんまり高速ではなさそう（getに数百msかかる？）だったので、lambdaでキャッシュしたほうが無難そうでした
- プロキシとBotは1:Nの関係にあるため、プロキシシステムは全てのBot Appの秘密情報を管理しないといけなくなります（そうしなければリクエストの検証ができません）
  - 理想的ではないので、何かうまい回避方法があればなあと思っています

API Gatewayからlambdaを呼ぶには「lambda統合」「lambdaプロキシ統合」の2つがあるらしいのですが、前者を使ったほうがよさそうでした。というか飛んでくるWebhookの検証でHMAC計算が入るため、1バイトでもゴミが入ると正しく動きません。そもそも前者は用途に対して複雑すぎました。

## まとめ
この記事では、Slack Botの前段に置くプロキシシステムを検討し、その実装までをやってみました。ざっくり机上で検討 → ざっと実装した割には案外使えそうなものが考案できて、個人的には満足しています。