正規表現ミスって一晩誰もサービスにログインできなくしてしまった話

# はじめに
この記事は、[本番環境などでやらかしちゃった人 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/yarakashi)の11日目です。

https://qiita.com/advent-calendar/2023/yarakashi

どうも、[@_tinoji](https://twitter.com/_tinoji)と申します。実に4年ぶりにアドベントカレンダーに参加しました。

正規表現で1文字消し忘れて、なんぴとたりともサービスにログインできない状態にしてしまったという話をします。正規表現にはまじで気をつけましょうという教訓になれば・・・ :pray: 


# 犠牲となったログイン画面
とあるtoBなWebサービスを開発していたときの話です。法人のユーザーが使う管理画面的なイメージです。

当然ログイン機能があって、至って普通なログインなのですが1つだけ特徴がありまして、**ログイン画面のURLをアカウントごとに変えています**。`https://example.com/<uuid>/login`みたいな感じですね。
あまり見ない形式ではありつつも、個別のUUIDを特定されない限りログイン画面に対して辞書攻撃などができないというメリットがあります。

# WAFでのIP制限
いつぞやにお客様からIP制限(つまりホワイトリストによる許可)をしたいという要望があり、「せっかくログインURLがアカウントごとに違うんだからWAFでサクッとやれるじゃん」ということで、AWS WAFのBlock actionで実現しています。

```text:WAFルール
IF
    そのアカウントのUUIDがログインURLに含まれる
AND
    NOT(IPアドレスが指定したIP setに含まれる)
THEN
    アクセスをブロック
```
ということをやっているだけです。

![FireShot Capture 046 - Add rule - WAF & Shield - Global - us-east-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130232/dc7067bb-ef3f-4f2a-990b-9deab8ba06b8.png)

これだけなら簡単なのですが、「ウチは支店ごとにアカウントを持っているけど、制限するIPレンジは全支店を含めたものにしたい」という要望も出てきました。至極当然ですね。いつもと違うオフィスで仕事をしていたらアクセスできない、ということは避けたいので。

さて、このユースケースをWAFでやろうとすると一工夫必要になります。と言ってもUUIDをチェックする部分をORにすればいいだけです。AWS WAFでは「このリストに入っている文字列を含むなら〜」というリストを使った条件は定義できないので、**正規表現が必要になります**(不穏)。

`(<uuid_1>|<uuid_2>|...)\/login`とUUIDを足していくだけです。
※カッコを付けているのは多少見やすくするためで意味はありません。

![スクリーンショット 2023-12-06 20.14.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130232/cf2d953a-ac71-7fde-4c7e-6d4283e8216a.png)


# やらかしの日
やらかしの日がやってきます。たしか冷たい雨が降っていました。

数日前にカスタマーサクセスのメンバーからIP制限の依頼が来ており、ちょうど手が空いたのでいつも通りWAFのルールを追加しました。何度もやった、なんてことのないルーチン作業。ただ、**いつもよりも対象のアカウントが多く、10以上のUUIDに対する正規表現を書く必要がありました**。

こういうとき、自分はだいたいvscodeで文字列を整形します。対象のUUIDをリストアップして、改行コードをパイプに一括置換して、カッコで囲んで、`\/login`を末尾に付けるだけ。
![スクリーンショット 2023-12-07 14.19.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130232/514a32f1-a4a1-4e28-e4e2-af18f29bc7a7.png)

もう大体予想がついたと思います。一括置換をした際に末尾にもパイプが入ってしまいました。
![スクリーンショット_2023-12-07_14_17_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130232/ef9d58ce-5c91-a8fe-cc5d-36ea09f51382.png)

もちろん確認はしたのですが、UUIDに漏れがないか、IP setが正しいか、に慎重になっていてこのミスには気づきませんでした・・・。

この正規表現はORのところで**空文字もマッチする**ので、`/login`の前がいかなる文字でも条件に一致してしまいます。つまり**全てのアカウントで条件に入り、今回設定したIP以外の場合アクセスがブロックされます**。
![スクリーンショット 2023-12-07 13.01.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130232/bff63cc6-1931-1601-2fa9-acb658430095.png)

動作確認ですか？もちろんしました。**正しくブロックされることを。**
IP制限を入れると、当然「自分の環境からはお客様のログイン画面にアクセスできない」ことが想定挙動になります。手順書にはその確認をすることが書かれていたので、しっかりと動作確認しました。そして退勤しました。


# 翌朝
8時すぎにCTOから電話がかかってきて飛び起きました。普段めったに電話なんてかかってこないので、取る前に嫌な予感がしました。

**「カスタマーサクセスが誰もログインできないらしいんだけど、見てくれない？」**

120%の確信を持ってPCの前に向い、昨晩追加したルールのRule actionをCountモードにしました。こうすればactionは実行されなくなります。こういうとき意外と頭は冴えますよね・・・。数分で暫定復旧ができました。その後正規表現を見直してミスを発見、修正して完全復旧となりました。

時間にして9時間59分。この間だれもサービスにログインすることができなかったということです。


# 不運と幸運
まず不運から。
- 夜間のアクセス数が少なく4xxのアラートのしきい値を超えなかった。
    - 法人のお客様がほとんどで営業時間外に操作することはなく、クローラーやアタック系のアクセスのノイズに埋もれてアラートが発火しなかった。
- 1日早く作業していれば夜のうちに気づける可能性が高かった。
    - 作業した日は月初(1日)。前日の月末にやっていれば、月末締め作業などでいつもより少し遅くいる社内のメンバーが気づいたはず。

そして幸運。
- 夜の作業 → 朝に検知 ということでピークタイムを避けられた。
    - 不運の裏返し。法人のお客様が夜間にログインすることはなかった。
- ユーザーからの問い合わせではなく社内で検知できた。
    - このインシデント起因で問い合わせやクレームに発展することはなかった。

幸運だったと言いたいところですが、影響の大きさも加味すると不運が勝ちそうですね・・・。
自分自身SREとして1年間監視の強化に力を入れていてモニタリング・アラートの整備状況にはある程度自信があったのですが、オフピークだとリクエストエラーのアラートがまともに動かないことがあるというのは非常に学びになりました。


# 再発防止と属人化解消
まず手順書に「他のアカウントではIP制限されていないこと」を確認するステップを追加しました。いま思うと当たり前なのですが・・・。なにか起きたらWAFルールをCountモードにして緊急対応することも追記しました。

インシデントが起きたこと自体は当然よくないのですが、これをきっかけに「この作業って属人化してない？もし翌朝詳しい人が電話出られなかったら対応遅くならない？」という議論に発展し、依頼チケットのアサインを分散したうえで何度かペアオペレーションをして誰でもできるようにしようという流れになりました。

起こした本人が言う事じゃないですが、インシデントは組織を強くしますね :innocent: 


# おわりに
特に宣伝などはありません！

明日は[waitonly](https://qiita.com/waitonly)さんの「Windowsの共有ドライブ6人目が弾かれる」です！お楽しみに！