Web サイトや社内ツールを公開していると、どれだけ気をつけても、いつか Web サーバーの不具合を突かれて入り込まれる可能性は残ります。使っている部品のどこかに、まだ誰も気づいていない不具合があるかもしれないからです。そこで考えておきたいのが、入り込まれたあとに相手が何を手にできるかです。Web から侵入されたとき、被害をサーバー全体に広げないためには、どう設計すればよいのか。この記事では、そのための設計を説明します。
侵入されることを前提に、入り込んだ相手が手にできるものを最小にする設計にします。インターネットに見せるのは小さなフロントの Web サーバー1台だけにし、データベースと業務の処理を担うバックエンドは内側に置きます。バックエンドはそのフロントからの API の呼び出しだけを受け付け、データベースはバックエンドからの接続だけを受け付けます。どのプログラムも root(サーバーの全権を持つ管理者)ではない専用のユーザーで動かし、データベースの合言葉のような秘密情報はフロントに置きません。こうしておけば、フロントを乗っ取られても、相手が触れられるのはフロントに許した範囲に限られます。
この記事の調べ方
最小権限の定義は米国国立標準技術研究所(NIST)の用語集から、root の扱いは Linux の公式マニュアルから引きました。Web サーバーのユーザー設定は nginx の公式文書、コンテナの設定は Docker と Kubernetes の公式文書、通信の許可の書き方は AWS の公式文書、sudo の不具合は sudo の開発元の発表、iPhone の仕組みは Apple の「Apple Platform Security」、Windows の仕組みは Microsoft の公式文書を根拠にしています。記事中の設定は、この記事のために当社で動かして試してはいません。
侵入される前提で、取られる範囲を先に決めておく
セキュリティ対策というと、入り込まれないための対策を思い浮かべる方が多いはずです。部品を新しく保つこと、使わない機能を外すこと、管理画面に二要素認証を付けることは、どれも欠かせません。ただ、入口の対策がどれだけ厚くても、公開した直後には誰も知らなかった不具合が、あとから見つかることがあります。AI を使って不具合を探す動きについては、このシリーズの1本目で扱っています。
そこで、入口の対策と並べて、入り込まれたあとに被害がどこまで広がるかも考えておきます。同じ不具合を突かれても、Web サーバーが何でもできる権限で動いていれば、サーバーの中のファイルも、同じ機械で動くデータベースも、まとめて相手の手に渡ります。反対に、Web サーバーに許した範囲が狭ければ、相手はその範囲の外へ出るために、別の不具合をもう一つ見つけなければなりません。
この記事で扱うのは、後者の状態を作るための設計です。中心になる考え方は二つあります。一つは、どのプログラムにも root の権限を持たせないこと。もう一つは、インターネットに見せる機械を1台に絞り、ほかの機械はその1台からの通信しか受け付けないようにすることです。どちらも昔から推奨されてきた組み方で、新しい技術は要りません。
root を取られると、サーバーの中身はすべて相手の手に渡る
Linux のサーバーには、という特別なユーザーがいます。Linux の公式マニュアルは、root として動くプログラムについて、OS が行う権限の確認をすべて素通りすると説明しています。普通のユーザーには「このファイルは読めない」「この設定は変えられない」という制限がかかりますが、root にはかかりません。
そのため、Web から入り込んだ相手が root の権限を得ると、できることに上限がなくなります。サーバーに置いたどのファイルも読め、どのファイルも書き換えられます。好きなプログラムを常駐させて外部と通信させることも、操作の記録を書き換えて入り込んだ跡を消すことも、root なら止めるものがありません。同じサーバーで動く別のプログラムの設定ファイルに書かれた合言葉も読めるので、被害はそのサーバーの外にまで広がります。

図の左側では、相手はサーバーの中のすべてに手が届きます。右側では、相手が読めるファイルも書き換えられる場所も、Web サーバーのユーザーに許した分だけです。データベースへ直接つなぐ道もなく、できるのはバックエンドに決められた API を頼むことだけです。どちらの側も最初に突かれた不具合は同じで、違うのは設計だけです。
右側でも被害がゼロになるわけではありません。Web サーバーが表示している情報は見られますし、画面を書き換えられるおそれもあります。それでも、顧客の情報がまとめて入ったデータベースや、ほかのシステムの合言葉まで一度に取られる状態と比べれば、被害が出たあとの対応にかかる手間は大きく違います。
最小権限は、仕事に要る分だけを渡すという原則
この考え方はと呼ばれます。NIST の用語集は、これを「システムは、利用者(または利用者の代わりに動くプロセス)のアクセス権限を、割り当てられた仕事を果たすのに必要な最小限に制限すべきだ」というセキュリティの原則として定義しています。
この定義には「利用者の代わりに動くプロセス」も含まれています。人のアカウントに加えて、Web サーバーやデータベースのような、裏で動き続けるプログラムも対象です。Web サーバーの仕事は、届いた依頼に画面を返すことです。その仕事に、ほかのユーザーのファイルを読む権限や、OS の設定を変える権限は要りません。要らない権限を渡さないでおけば、乗っ取られても、相手がその権限を使うことはできません。
身近な端末でも、この原則は使われています。iPhone が乗っ取られにくい理由として「中のコードが公開されていないから」と言われることがありますが、Apple の説明は違います。Apple の「Apple Platform Security」によると、他社が作ったアプリはすべての中で動き、OS の多くの部分とすべての他社製アプリは、特別な権限を持たない「mobile」というユーザーで動きます。アプリごとに専用の保存場所が割り当てられ、ほかのアプリの情報が要るときは iOS が用意した窓口を通すしかありません。連絡先、写真、カメラ、マイクなどへのアクセスは、利用者が許したアプリにだけ認められ、設定画面からあとで取り消せます。コードを隠しているかどうかで守りの強さが決まらないことは、このシリーズの2本目で詳しく書いています。
Windows も同じ方向に変わってきました。Windows Vista で入ったについて、Microsoft の当時の説明)は、管理者がサインインすると権限の異なる二つのアクセストークン(その利用者に何を許すかを記した情報)が作られ、普段のアプリは権限を絞ったほうで動くとしています。現在の公式文書でも、標準のユーザーも管理者も、既定では標準ユーザーの権限でアプリを動かし、管理者の権限が要る操作のときだけ確認の画面が出ると書かれています。Windows は管理者の権限のまま動いている、という理解は、Vista 以降の既定の動き方には当てはまりません。
インターネットに見せるのは、小さなフロントの Web サーバー1台だけ
次の図は、通信できる相手を機械ごとに絞った構成です。インターネットから届く通信を受けるのは、フロントの Web サーバー1台だけにします。業務の処理を担うバックエンドと、データを入れておくデータベースは、インターネットから直接は届かない内側に置きます。

図で矢印が引かれているのは三つの経路だけです。インターネットからフロントへの画面の通信、フロントからバックエンドへの API の呼び出し、バックエンドからデータベースへの接続です。灰色の線で示した二つの経路、つまりインターネットやフロントからデータベースへ直接つなぐ経路は、ネットワークの設定でつなげないようにしておきます。
フロントに載せる機能や部品も少なくします。インターネットのどこからでも通信が届く唯一の機械なので、不具合を突かれる機会がいちばん多いのはフロントの Web サーバーです。そこに載せる機能や部品が少ないほど、不具合が入り込む余地も減ります。Web サーバーを動かす OS そのものを小さくする考え方は、このシリーズの4本目で扱っています。
Next.js や React のような既存の仕組みで画面を作っても、この構成は組めます。守りの強さは、画面をどの仕組みで作ったかでは決まりません。どの機械を外に出し、どの機械がどこと通信できて、各プログラムがどの権限で動くかで決まります。Next.js のようにフロント側のサーバーでもプログラムが動く作りの場合は、そのサーバーも「外に見せる1台」として扱い、そこに置く秘密情報と権限を同じ基準で絞ります。
データベースは、決まったサーバーからの通信しか受け付けない
図の構成をネットワークで設定するときは、許可を「どこからの通信か」で書きます。データベースなら、受け付ける相手はバックエンドの機械だけです。クラウドの例として AWS の公式文書を見ると、通信を許可する規則の送り元に、IP アドレスの範囲のほかに、別のサーバーの集まりを指定できると書かれています。同じ文書の構成例では、ロードバランサー(通信の受付を担う機械)はインターネットからの通信を受け、Web サーバーはロードバランサーからの通信だけを受け、データベースは Web サーバーからの接続だけを受けるように規則が分けてあり、この記事の図とほぼ同じ形です。
バックエンドの API も同じ考え方で、受け付ける相手をフロントの Web サーバーに限ります。そのうえで、バックエンドは届いた依頼をそのまま受け付けず、ログインした利用者がその処理をしてよいかを自分で確かめます。フロントが乗っ取られた場合、相手はフロントの立場で API を呼べてしまうからです。API で頼める処理を業務に要るものだけに絞り、「データベースの中身を全部返す」ような処理を用意しないことも、取られる範囲を狭める方法の一つです。
データベースに接続するための合言葉は、データベースに接続するバックエンドにだけ置きます。フロントには、データベースの合言葉も、外部のサービスの管理用の鍵も置きません。フロントの設定ファイルに合言葉が書かれていれば、図のようにネットワークを分けていても、それを読んだ相手は別の経路からデータベースに近づく手がかりを得てしまいます。
| 機械 | 受け付ける相手 | 置く秘密情報 |
|---|---|---|
| フロントの Web サーバー | インターネット(画面の通信の入口だけ) | 置かない(画面の暗号化に使う証明書の鍵を除く) |
| バックエンド | フロントの Web サーバーだけ | データベースの合言葉、外部サービスの鍵 |
| データベース | バックエンドだけ | データそのもの |
表の右の列を縦に読むと、外に近い機械ほど持っているものが少なくなっているのが分かります。外に近い機械ほど突かれやすいので、突かれやすい機械に大事なものを置かない、という並べ方です。今の構成で、フロントの設定ファイルにデータベースの合言葉が書かれていないかは、最初に確かめたい点です。
プログラムもコンテナも、root ではないユーザーで動かす
ネットワークを分けたら、次は機械の中の権限です。Linux の Web サーバーは、昔からこの考え方で作られています。代表的な Web サーバーの nginx は、通信を受けて実際に処理するを、設定で指定したユーザーで動かします。nginx の公式文書によると、指定がないときの既定は nobody という権限の低いユーザーです。運用では、Web サーバー専用のユーザーを作って指定し、そのユーザーが読めるファイルを公開に要るものだけに絞ります。
同じ機械で複数のプログラムを動かすなら、プログラムごとに別のユーザーを割り当てます。Web サーバーと、業務の処理と、定期的に動く集計の処理が同じユーザーで動いていると、一つを乗っ取られただけで残りの二つのファイルにも手が届きます。ユーザーを分けておけば、OS がプログラムごとに読み書きできる範囲を分けます。
コンテナで動かす場合も考え方は同じです。Docker の公式文書は、コンテナの中の root は、本物の root よりずっと少ない権限しか持たないように既定で絞られていると説明したうえで、コンテナの中のプロセスを特権のないユーザーで動かせば、さらに安全になると書いています。コンテナの作り方の推奨事項にも、特権が要らないサービスは USER という指定で root 以外のユーザーに切り替えるよう書かれています。
| 何を防ぐか | Docker の指定 | Kubernetes の指定 |
|---|---|---|
| root で動かさない | コンテナの作り方で USER を指定、起動時の --user | runAsNonRoot を true にする |
| あとから権限を増やさせない | --security-opt no-new-privileges | allowPrivilegeEscalation を false にする |
| root の権限の部品を外す | --cap-drop | capabilities の drop に ALL を書く |
| プログラムの置き場所を書き換えさせない | --read-only | readOnlyRootFilesystem を true にする |
| Docker 自体を root で動かさない | rootless モード | (該当なし) |
表の1行目から4行目は、コンテナを起動するときの指定です。2行目の「あとから権限を増やさせない」は、コンテナの中のプログラムが、実行すると持ち主の権限で動く特別なファイルを使って権限を引き上げるのを止めるものです。Kubernetes の公式文書は、allowPrivilegeEscalation を false にすると OS の が有効になると説明しており、指定しないと既定では権限の引き上げが許されたままだとも書いています。3行目のを全部外す指定と合わせると、コンテナの中で root に近い権限をさらに得にくくなります。
Kubernetes には、こうした指定をまとめた基準があります。Pod Security Standards という公式の基準のうち、最も厳しい Restricted という水準は、root 以外のユーザーで動かすこと、権限の引き上げを許さないこと、ケーパビリティを全部外すこと(1024番より小さい番号の通信の受付口を使う権限だけは戻してよい)などを条件にしています。書き込みを禁じる readOnlyRootFilesystem はこの基準には含まれていませんが、コンテナの設定として用意されています。最後の行の rootless モードは、Docker の公式文書によると、Docker の本体とコンテナを root ではないユーザーで動かし、Docker 自体の不具合の影響を抑える仕組みです。Docker の本体は、この仕組みを選ばない限り root の権限を必要とします。
専用のユーザーで動かしても、root に上がる道はふさぎ続ける
プログラムを専用のユーザーで動かせば、それで終わりというわけではありません。入り込んだ相手は、次に root の権限を得る方法を探します。その手段になりやすいのは、OS の中心部分の不具合と、設定の誤りです。
実際に起きた例を一つ挙げます。2021年1月、管理の操作を一時的に root の権限で行うための sudo というプログラムに、深刻な不具合が見つかったと開発元が発表しました。発表によると、この不具合はサーバーにログインできるどのユーザーからでも突けるもので、sudo を使う許可を与えられていないユーザーでも root に上がれるおそれがありました。対象は 1.8.2 から 1.8.31p2 まで、1.9.0 から 1.9.5p1 までなどで、不具合は 1.8.32 と 1.9.5p2 で直されています。Web サーバー専用のユーザーで動かしていても、この不具合が残ったサーバーでは、入り込んだ相手が root に上がる道が開いていたことになります。
設定の誤りも同じ結果を招きます。の印が要らないファイルに付いている場合や、sudo の設定で Web サーバーのユーザーに広い操作を許している場合が、その例です。運用の手間を省くために付けた設定が、そのまま root の権限を得る手段になります。
こうした道をふさぐ方法は二つです。一つは、OS と部品を新しく保ち、見つかった不具合の修正を早く入れることです。古いまま動き続けている部品が狙われやすい理由は、3本目にまとめています。もう一つは、機械に入れるものを減らすことです。入っていないプログラムの不具合は突かれません。Web サーバーに要らない機能を外す考え方は4本目で扱っています。権限を絞る設計には、修正を早く入れる運用と、入れるものを減らす組み方も合わせて使います。
今の構成を確かめる6つの質問
自社のサイトや社内ツールがこの記事の構成になっているかは、作った会社や担当者に次の6つを聞くと確かめられます。「分からない」と返ってきた項目から見直してください。
- インターネットから直接届く機械は何台ありますか。データベースはその中に入っていませんか。
- データベースは、どの機械からの接続を受け付ける設定になっていますか。
- フロントの Web サーバーの設定ファイルに、データベースの合言葉や外部サービスの鍵が書かれていませんか。
- Web サーバーや業務のプログラムは、どのユーザーで動いていますか。root で動いているものはありませんか。
- コンテナを使っている場合、root 以外のユーザーで動かし、権限の引き上げを止める指定をしていますか。
- OS と sudo などの部品の修正は、いつ、誰が入れていますか。
6つのうち1つ目から3つ目はネットワークと秘密情報の置き場所、4つ目と5つ目は機械の中の権限、6つ目は運用の質問です。前任者から引き継いだ WordPress のサイトなら、「引き継いだWordPressで最初に見る10項目」も合わせて確かめると、管理者のアカウントや更新の状況も確認できます。AI に作らせた社内ツールを公開する前なら、「公開前に確かめる7項目」が使えます。
Web サーバーへの侵入を完全に防ぐことはできません。それでも、外に出す機械を1台に絞り、データベースを内側に置き、どのプログラムにも root を持たせない構成にしておけば、入り込まれたときに取られる範囲を抑えられます。開発者の手元のパソコンにも同じ考え方を当てはめる話は、このシリーズの6本目で書いています。
作った人に聞けないサイトや社内ツールの、危ない箇所を直します
データベースがインターネットから見えている、設定ファイルに合言葉が書かれている、どのユーザーで動いているか誰も説明できない、といった箇所の修正や、引き継げないものの作り直しを承っています。