はじめに
サーバー A からサーバー B の API を叩く。このとき認証をどうするか、という話をする。
- バッチサーバーから社内 API を叩く
- 外部 SaaS から Webhook を受け取る
- アプリサーバーから決済サービスを呼ぶ
どれも「相手が本物か」を確かめる必要がある。しかし手段は API キーから mTLS まで幅があり、どれを選べばいいのか迷う。
この記事では認証方式を 5段のはしごとして並べ、下から順に「何が破れるのか」「だから次の段に登る」という形で整理する。
結論を先に書くとこうなる。
はしごを登るとは「強い暗号を使うこと」ではない。
秘密を回線に流さないことと、漏れたときの有効期限を短くすることである。
そして各段を、次の3つの問いで評価していく。
| 問い | 見るもの |
|---|---|
| ① 何を確かめているか | 経路 / 知識(共有秘密)/ 所持(秘密鍵)/ 発行元 |
| ② 秘密は回線に流れるか | 毎回のリクエストに秘密そのものが載るか |
| ③ 漏れたら、いつまで有効か | 無期限か、期限つきか |
👉 以降、5段すべてをこの3問で見る。方式を覚えるのではなく、この3問で自分で評価できるようになるのがゴールである。
まずクライアント-サーバーと何が違うのか
サーバー間の認証を考える前に、ブラウザ相手の認証との違いをはっきりさせておく。ここを混ぜると、対策が過剰にも過少にもなる。
| ブラウザ ⇔ サーバー | サーバー ⇔ サーバー | |
|---|---|---|
| 人間 | いる | いない |
| 認証のきっかけ | ログイン画面・MFA | なし(起動時から動く) |
| 秘密の置き場 | 都度入力される | ファイル・環境変数に置きっぱなし |
| Cookie / セッション | 使う | 基本使わない |
| 同一オリジン / CORS | 効く | 関係しない |
| 相手 | 不特定多数 | 少数・固定 |
| 頻度 | 人間のペース | 自動・大量 |
とくに効いてくるのは次の3点である。
1. 対話的な確認が一切使えない
パスワード入力も MFA も、人間がいることが前提の仕組みである。深夜に動くバッチサーバーには誰もいない。
2. 秘密が長命になりやすい
ブラウザのパスワードは人間が都度入力し、送られるのはログイン時だけ。対してサーバー間の API キーは、設定ファイルや環境変数に置かれたまま何年も生き続ける。
❌ 「ログイン画面がないから認証は軽くていい」は逆である。人間が介在しない分、秘密が長命になりやすいのがサーバー間の弱点になる。
3. CORS や同一オリジンポリシーは関係しない
これらはブラウザが自分に課している制約であって、サーバーを守る仕組みではない。curl にも自作の HTTP クライアントにも同一オリジンポリシーは無い。
👉 「CORS を設定したから外部から叩かれない」は成り立たない。サーバー間通信では最初から存在しない防御である。
一方で、サーバー間には有利な点もある。
👉 相手が少数・固定なので、ブラウザ相手では非現実的な手段が使える。全ユーザーにクライアント証明書を配るのは無理だが、連携先が3社なら配れる。後で出てくる mTLS が現実的な選択肢になるのはこのためである。
はしごの全体像
これから登る5段はこうなる。
上に行くほど「回線に流れる秘密が短命になり」「漏れたときの有効期間が短くなる」。
ただし、最初に断っておくことがある。
❌ 段0(IP制限)は正確には「はしごの一段」ではない。
IP制限は認証ではなく絞り込みであり、段1〜4 のどれとでも併用する直交した軸である。「段1 を選んだから IP 制限は要らない」という関係ではない。
それでも最初に扱うのは、「まず IP で絞ればいいのでは」という発想が実際によく出てくるからである。出発点として見ておく。
段0:IP制限だけ
送信元 IP アドレスが許可リストに入っていれば通す。ファイアウォールやセキュリティグループ、あるいはリバースプロキシの設定で実現する。
| 問い | 答え |
|---|---|
| ① 何を確かめているか | 経路(どこから来たか) |
| ② 秘密は回線に流れるか | そもそも秘密が無い |
| ③ 漏れたら、いつまで有効か | — |
ポイントは①である。IP アドレスが示すのは「どこを通ってきたか」であって、「誰か」ではない。
ここから3つの破れ方が出てくる。
1. IP は共有される
NAT やプロキシの背後にいる相手を許可すると、同じ出口を使う無関係なサーバーもまとめて通る。クラウドの NAT ゲートウェイや、社内から外へ出ていく共有プロキシがこれにあたる。
2. SSRF で「正しい送信元」から叩かれる
許可された内部サーバーに細工したリクエストを送り、そのサーバーに代わりに叩かせる攻撃がある(SSRF)。このとき送信元 IP は正規の許可済み IP になる。IP制限は素通しされる。
3. アプリ層で見る IP は詐称できる
ロードバランサやプロキシを挟むと、アプリから見た接続元は LB になる。そこで X-Forwarded-For を見て判定することになるが、このヘッダはリクエストを送る側が自由に書ける。信頼できる LB が付けた部分だけを見る構成(末尾から数える等)にしていないと、簡単に詐称される。
👉 IP制限の役割は「攻撃対象面を減らすこと」である。有効だが、これは認証ではない。次の段に進む。
段1:静的APIキー
あらかじめ共有した長い文字列を、リクエストごとにヘッダへ載せる。
curl -H "Authorization: Bearer sk_live_9f2c8a1e..." https://api.example.com/orders
サーバー側は受け取った文字列が登録済みのものと一致するかを見るだけ。実装は最も簡単で、広く使われている。
| 問い | 答え |
|---|---|
| ① 何を確かめているか | 知識(共有秘密を知っていること) |
| ② 秘密は回線に流れるか | 毎回流れる |
| ③ 漏れたら、いつまで有効か | 無期限(気づいて無効化するまで) |
②と③がそのまま弱点になる。
秘密が毎回流れるので、あちこちに残る
- アクセスログ、プロキシログ、監視ツールのトレース
- URL のクエリ文字列に入れた場合は特に危ない(ログにフル URL が残る)
- TLS を終端したロードバランサから内側は平文で流れる
気づかないと無期限に有効
漏洩した API キーが公開リポジトリに何年も置かれていた、という話はここに由来する。キー自体に期限が無いので、誰も無効化しなければずっと使える。
ローテーションが辛い
キーを差し替えるには、発行側と利用側を同時に切り替えるか、新旧2本を並行して受理する仕組みを自前で用意する必要がある。「面倒だから替えない」が起きやすい。
👉 それでも段1 が消えないのは、運用コストが圧倒的に低いからである。同一 VPC 内の内部サービス間で、経路も絞ってあるなら、これで足りる場面は多い。
登る動機になるのはこうだ。
秘密そのものを毎回送るのを、やめられないか。
段2:HMAC署名 + タイムスタンプ / nonce
共有鍵は送らない。代わりに「共有鍵を使って計算した署名」を送る。
受信側は同じ材料と同じ共有鍵で署名を計算し直し、送られてきた署名と一致するかを見る。一致すれば「共有鍵を知っている相手が、この内容で送った」と分かる。
| 問い | 答え |
|---|---|
| ① 何を確かめているか | 知識(ただし秘密そのものは渡さない) |
| ② 秘密は回線に流れるか | 流れない |
| ③ 漏れたら、いつまで有効か | 無期限(鍵自体に期限が無い) |
段1 から改善した点は3つある。
- 秘密そのものが回線にもログにも載らない — 流れるのは署名(計算結果)だけ
- 改ざんを検知できる — ボディを1バイト変えれば署名が合わなくなる
- リプレイを防げる — タイムスタンプを署名対象に含め、受信側で古いものを拒否する。nonce を併用すれば同じリクエストの再送も弾ける
Webhook の受信でこの方式がよく使われるのはこのためである。受信側は公開エンドポイントを晒すことになるので、「送信元が本物か」「中身が送信時のままか」を自力で確かめる必要がある。AWS の SigV4 も HMAC ベースの署名方式である。
一方で、残る問題と落とし穴がある。
鍵の配布と保管の問題はそのまま残る
共有鍵は結局どこかのファイルか環境変数にある。漏れれば無期限に有効という③は、段1 から変わっていない。
署名対象の抜け
これが最も多い実装ミスである。ボディだけを署名してメソッドやパスを署名に含めないと、同じボディを別のエンドポイントに流用できてしまう。「何を署名するか」は仕様として明記し、送信側と受信側で一致させる。
時刻ずれ
タイムスタンプを検証するなら、送信側と受信側の時刻が合っている必要がある。許容ウィンドウは、狭すぎると正常な通信が落ち、広すぎるとリプレイの窓が開く。
署名の比較
署名の照合には時間一定比較(constant-time comparison)を使う。多くの言語に専用の関数が用意されている。
👉 段2 は「通信路の問題」をほぼ解いた。残っているのは鍵そのものの問題である。
段3:mTLS(クライアント証明書)
TLS ハンドシェイクの中で、クライアント側も証明書を提示する。通常の HTTPS はサーバーだけが証明書を出すが、mTLS(相互 TLS)では双方が出す。
| 問い | 答え |
|---|---|
| ① 何を確かめているか | 所持(証明書に対応する秘密鍵を持っていること) |
| ② 秘密は回線に流れるか | 流れない(秘密鍵は署名にしか使わない) |
| ③ 漏れたら、いつまで有効か | 証明書の有効期限まで、または失効させるまで |
段2 から変わったのは③である。
期限と取り消しが仕組みとして存在する
証明書には有効期限があり、期限が来れば使えなくなる。さらに CRL / OCSP という失効の手段がある。共有秘密には無かった「取り消せる」が入ってきた。
信頼を CA に集約できる
連携先が増えたとき、「この CA が発行した証明書なら通す」という一本の設定で済む(実際にはさらに Subject / SAN で相手を特定する)。相手ごとに鍵を配って管理する段2 とはスケールの仕方が違う。
ここで使うのはクライアント証明書である。サーバー証明書(DV / OV / EV)とは役割が別で、証明する対象が「このドメインのサーバーである」ではなく「この主体である」になる。サーバー証明書の側は別記事で扱った。
コストも大きい。
期限管理が事故になる
証明書は必ず切れる。切れれば通信が止まる。「更新を忘れて本番が止まった」は mTLS でよく聞く障害である。
失効の運用は実際には重い
CRL の配布や OCSP の参照を、通信の相手すべてに対して回し続ける必要がある。短い有効期限で回すほうが現実的、という判断も採られる。
LB で TLS を終端すると「誰か」が消える
ロードバランサで TLS を終端すると、アプリケーションに届くのは平文の HTTP である。クライアント証明書の情報は、LB が付けたヘッダとして渡ってくる。
👉 このとき、外部から同じヘッダを送られたら区別できない構成になっていないかを確認する必要がある。LB が必ずヘッダを上書きし、LB を迂回してアプリに直接届く経路が無いこと。段0 の X-Forwarded-For と同じ落とし穴がここにも出る。
登る動機はこうなる。
証明書の配布と期限管理を、自分でやらずに済ませられないか。
段4:短命トークン
長命な秘密で直接 API を叩くのをやめる。まず発行元から短命なトークンを受け取り、そのトークンで API を叩く。
| 問い | 答え |
|---|---|
| ① 何を確かめているか | 発行元(信頼する発行元が出したか)+所持(トークンそのもの) |
| ② 秘密は回線に流れるか | 流れる(ただし短命なトークン) |
| ③ 漏れたら、いつまで有効か | 数分〜1時間程度(サービスと設定による) |
ここで発想が変わる。
段3 までは「相手が持っている秘密」を信頼していた。
段4 では相手ではなく、発行元を信頼する。
受け取る側は相手ごとの鍵や証明書を持たない。「この発行元が出したトークンで、このスコープを持つなら通す」という判断をする。
代表的な形は次のとおり。
- OAuth2 の client_credentials — クライアント ID とシークレットでアクセストークンを取り、それで API を叩く
- クラウドのマシン ID — AWS の EC2 インスタンスプロファイルや EKS の IRSA など、実行環境そのものに権限を紐づける
- CI からの OIDC — GitHub Actions が発行する OIDC トークンをクラウド側に渡し、一時的な認証情報を受け取る
最上段である理由は「短い」ことではない。
👉 そもそも秘密を配らなくて済むからである。
OAuth2 の client_credentials には、まだ長命なクライアントシークレットが残っている。しかしクラウドのマシン ID や CI の OIDC では、開発者が長命なキーをどこにも置かない。置いていない秘密は漏れない。
GitHub Actions の OIDC については別記事で詳しく扱った。
残るコストもある。
発行元への依存
発行元が落ちれば、トークンを取れないすべての通信が止まる。単一障害点が増える。
スコープ設計
「発行元を信頼する」ということは、発行されたトークンに何ができるかが権限設計そのものになる。広すぎるスコープを配ると、短命であっても被害は出る。
相手が対応しているとは限らない
連携先が対応していなければ選べない。段2 や段3 が現役なのは、このためでもある。
早見表
5段を3つの問いで並べるとこうなる。
| 段 | ①確かめるもの | ②秘密は流れるか | ③漏れたら | 運用コスト |
|---|---|---|---|---|
| 0 IP制限 | 経路 | (秘密が無い) | — | 低 |
| 1 静的APIキー | 知識 | 毎回流れる | 無期限 | 低 |
| 2 HMAC署名 | 知識 | 流れない | 無期限 | 中 |
| 3 mTLS | 所持 | 流れない | 証明書の期限まで | 高 |
| 4 短命トークン | 発行元+所持 | 流れる(短命) | 数分〜1時間 | 中 |
👉 ②が「流れない」に変わるのが段1→段2、③が「期限つき」に変わるのが段2→段3、そして「秘密を配らない」に届くのが段4 である。
どこまで登るべきか
❌ 「常に段4 まで登れ」ではない。
段が上がるほど運用コストも上がる。期限切れで止まる事故も増える。釣り合う段を選ぶのが結論になる。
| 状況 | 現実的な選択 |
|---|---|
| 同一 VPC 内の内部サービス間 | 段1 + 段0(セキュリティグループや IP で絞る) |
| クラウドの managed サービスを呼ぶ | 段4(SDK が自動でやるので、自分で書くことは少ない) |
| 外部 SaaS からの Webhook を受ける | 段2(署名の検証) |
| 監査要件のある社外接続 | 段3(相手から mTLS を要求されることが多い) |
判断に迷ったら、3つの問いに戻る。
- その秘密は、どこに置かれ、誰が見られるか(②)
- 漏れたことに気づけるか。気づいたとき、止められるか(③)
- そもそも秘密を置かずに済む選択肢はあるか(段4)
👉 とくに③が効く。「漏れても1時間で切れる」と「漏れたら誰かが気づくまで有効」の差は、暗号の強さの差より大きい。
まとめ
- 認証方式は3つの問いで評価できる。①何を確かめているか ②秘密は回線に流れるか ③漏れたらいつまで有効か
- 段が上がるとは、②で流れるものが「秘密そのもの」から「短命なトークン」に変わり、③が「短い」に近づくこと。強い暗号を使うことではない
- 段1(静的APIキー)は②③ともに最も弱いが、運用コストが低く、条件が揃えば現役の選択肢である
- 段2(HMAC署名)は②を解く。段3(mTLS)は③に期限と失効を持ち込む。段4(短命トークン)は③を数分〜1時間にし、環境によっては秘密を配ること自体をやめられる
- ❌ 段0(IP制限)は認証ではない。攻撃対象面を減らす絞り込みであり、他の段と併用するもの
- ブラウザ相手の認証との最大の違いは、人間がいないことそのものではなく、その結果として秘密が長命になりやすいこと
👉 どこまで登るかは、運用コストと釣り合う段を選べばよい。迷ったら「漏れたとき、いつまで有効か」から考える。