監査の三つ目の指摘は、こうだった。
「管理認証情報の既定値が固定されている」
コードには、こう書いてあった。
ADMIN_PASSWORD = os.getenv("TTT_ADMIN_PASSWORD", "toitoi2024admin")
GAS_ADMIN_SECRET = os.getenv("GAS_ADMIN_SECRET", "ttt-gas-admin-2024-secret")
環境変数があればそれを使う。無ければ、引数の右側、固定の文字列を使う。安全な書き方のつもりだった。
サーバーを調べた。環境変数は、設定されていなかった。つまり本番は、ずっと右側で動いていた。ソースを読めば誰でも分かるパスワードと、誰でも分かる共有鍵で。
共有鍵は、両側で同時に変える
パスワードを変えるのは簡単だ。だが GAS_ADMIN_SECRET は厄介だった。これは、サーバー(app.py)と、その先のGAS、二つの場所で同じ値を持つ「共有鍵」だ。サーバーがこの鍵を付けてGASを呼び、GASが鍵を確かめて応える。
片方だけ新しい鍵に変えると、もう片方は古い鍵を待っている。合わない。管理画面の操作が、全部はじかれる。
新旧どちらの鍵も受け付ける、という重なりの時間を作る仕組みは、入れていなかった。だから、切り替えの瞬間だけは、どうしても管理APIが一瞬止まる。二つの鍵を、できるだけ近いタイミングで、両側そろえて入れ替えるしかない。
手順を決めた。GAS側に新しい鍵を設定する。すぐにサーバー側の環境変数も新しい鍵にして、再起動する。この間の一、二分、管理画面だけが整合を失う。予約の受付やLINEは、この鍵を使わないので止まらない。止まるのは、自分が触る管理画面だけ。だから、止めていい時間だった。
既定値を消す、ということ
入れ替えと同時に、固定の右側を消した。
ADMIN_PASSWORD = os.getenv("TTT_ADMIN_PASSWORD")
if not ADMIN_PASSWORD:
raise RuntimeError("環境変数が未設定です")
既定値を消すと、設定を忘れたときにサーバーは起動しなくなる。一見、不便だ。だが、これでいい。「設定し忘れても、とりあえず既知の値で動いてしまう」より、「設定されていなければ、はっきり止まる」方が、ずっと安全だ。静かに弱いまま動き続けるのが、一番こわい。
GASの中に直書きされていたLINEのトークンも、秘密の保管場所へ移した。エディタの編集途中にできた一時ファイルにも、古いトークンが残っていた。それも消した。ソースに書いた秘密は、本体から消しても、影のように別の場所に残る。
401と、200
切り替えのあと、確かめた。
新しいパスワードでログイン――通る。古いパスワードでログイン――401、拒否。管理画面からイベント一覧を取る――200、ちゃんと二件返ってくる。共有鍵は、両側で新しい値にそろっていた。本番のGASから、直書きの秘密は、一つも検出されなくなった。
監査が指摘するまで、この弱さは「動いている」という顔をして隠れていた。動くことと、守られていることは違う。別のAIが、その違いを三つ、はっきり指さしてくれた。
これで予約システム制作記 Vol.6は終わり。画面のボタンから、見えないスタイルの漏れ、AIに読まれる文章、そして外から来るものをどう信じるか。一つの予約システムの中に、これだけの問いが詰まっていた。
シンプルシステム株式会社 代表 伊藤勝彦