はじめに

従来のVPNでは全ての通信はVPNサーバーを経由するよう設定することでトラフィックのアクセス制御が可能でした。ですが、昨今のオフィスを離れた働き方では通信の遅延が大きな問題となります。例えば東京にVPNサーバーがあり、バンガロールからリモートワークするようなケースでは、バンガロールの隣に座っている同僚のPCに接続するために一度東京を経由する必要がありました。そこでLinux kernelに含まれるWireGuardや、Cloudflareが提供するWARP、SlackがOSSで開発しているNebulaに代表されるP2P型VPNが注目され始めています。

ですが、P2P型のトラフィックは監視がしづらいというデメリットがあります。またVPNは外からの守りは堅牢ですが一度中に入ればどこでも行けるという問題(Lateral Movement)もあり、IoTによる施設の監視に利用される小型コンピューター間のアクセス制御や、リモートワークの普及による物理的に遠く離れたPC間のアクセス制御に対するセキュリティの課題解決は今後さらに重要視されます。これにはVPNに頼らず細かい単位で認証認可を実施するゼロトラストなアプローチが有効とされています。

この記事ではそういった悪意のある未知の侵入者を防ぐという目的ではなく、信頼されている複数人のメンバーに細かくアクセス権を設定したい(特にソフトウェア開発という文脈において)という点にフォーカスし、社内LAN・VPN・ホームネットワークといったプライベートネットワークにおいて粒度の細かいアクセス制御をする際に、プライベートネットワークの特性を活かしてスケーラブルな端末間アクセス制御を実施する方法を紹介します。

例題

以下の例題が解決したい内容です。

端末1は端末2へのアクセスが許可されている。これを「端末1の権利」とする。
また、ユーザーAは端末1と3へのアクセスが許可されている。これを「ユーザーAの権利」と呼ぶ。
このとき、「端末1の権利」からすると端末2へのアクセスは許可されて、「ユーザーAの権利」からすると拒否されている。
権利が重複した場合には最小権限を適用するというルールを設けることとし、ユーザーAによる端末2へのアクセスを拒否したい。

下図は、例題を図示したものです。
ユーザーAはどこを経由したとしても端末2にはアクセスしてはいけません。
仮にユーザーBは全端末へのアクセス権があるとした場合、端末3 -> 2は端末側のアクセス制御により拒否されているのでアクセス不可となります。

これを判断するには少なくとも「誰が操作しているか」を知る必要があります。端末1から2へのアクセス時、端末2を操作しているユーザーがAであることを知るにはどうしたら良いでしょうか。ユーザーAが端末1 -> 3 -> 2と迂回してアクセスしたときにも同様です。

前提としてTCP/IP通信におけるアクセス制御とします。
また、端末へのログイン前にユーザーがAであることを証明済みであることとします。
具体的にイメージしやすいよう以下を実現したいと仮定します。

具体例

ユーザーAは端末1にSSH接続し、端末1から2に対してcurlコマンドでデータ取得を試みる。このときcurlコマンドはエラーになること。

実現方法

端的に言えば、全端末上で誰が操作しているかがわかれば良いのですが、果たしてそのようなことが実現可能でしょうか?具体的に見ていきます。
下図はTCP/IPにおけるIPパケットの内容を表しています。IPパケットには送信元IPアドレスはあるので、送信元は判断可能ですが、この送信元が必ずしも操作者のアドレスとは限りません。例えば、端末間にプロキシーサーバーを挟んでいる場合にはプロキシーサーバーのIPアドレスが設定されることがあります。そのためIPパケットを操作者(ユーザー)の判断に利用することはできません。

データ部にはTCPやUDPといったプロトコルに応じてデータが設定されますが、ここはSSH等のアプリケーションが利用する部分のため、操作者情報をどこにも持つことができません。ですが、実現したいことは プライベートネットワーク内 で操作者を判断することです。その条件下においては、操作者情報を持つことが可能になります。それがプライベートネットワーク内でのみ有効なレイヤーをTCP/IPのレイヤーのどこかに1層挟み込むという方法です。このデータ部は暗号化できるため盗聴の回避も可能です。

具体的にはLinuxやWindowsといった広く使われるOSに含まれるTUN/TAPドライバーでこれを実現できます。TUN、TAPはそれぞれOSIレイヤー3層、2層を任意の処理で置き換えることができ、ここでは第3層であるTUNを例に利用します。

コンピューターはデータリンク層に持っているMACアドレスを利用し通信対象を特定しています。IPアドレスは仮想的なもので、TUNを利用するとプライベートネットワーク専用のIPアドレスを1つのMACアドレスに対して複数付与することが可能になります。また、SSHやcurlなどのアプリケーションがデータを受信する前にIPパケットの内容を操作することができるため、端末1から2に送信する際にIPパケットのデータ部に操作者を埋め込み、端末2でIPパケットを受取った際にデータ部から操作者を消すことが可能となります。

これにより操作者情報を得ることができました。ですがまだユーザーAが端末2にアクセスできないという判断に足る情報が揃ったわけではありません。
必要な情報は「ユーザーAの権利」です。「端末1の権利」(端末1が2にアクセスできるという情報)は端末側に静的に持てるためOS付属のFirewallでも十分です。問題となるのは、「ユーザーAの権利」のようなコンテキストに依存する権利はFirewallのような静的な仕組みでは制御できないということです。

話は少し逸れますが、ユーザーAが端末1上のバックグラウンドプロセス(デーモンプロセス)として動いているVSCode serverを経由し端末2にアクセスしたなら端末2にアクセスできてしまうのではと考えた方もいるのではないでしょうか(参照 – 混乱した使節の問題(ウィキペディア日本語版))。これはCapabilityベースのシステムであれば回避可能です。具体的にはあるプログラムを操作する際にCapability(後述)を渡して操作の都度権限チェックをするというものなのですが、これを実現するにはOSとその上で動くツールから根本的に変える必要があり簡単には実現できません。

Capabilityとは、トークン(もしくはチケットやキー)に、コンピューター内の対象に対する操作権を持たせたものです。

A capability is a token, ticket, or key that gives the possessor permission to access an entity or object in a computer systemDennis and Van Horn in 1966

ウェブアプリケーションで例えると、CIツールからGitHubリポジトリをクローンするためにGitHubでデプロイキーを発行して、そのキーをCIツールに渡すと、CIツールはキーを使ってGitHubにアクセスできるというものです。
汎用的に言い換えると、権利情報(リポジトリへのアクセス権)を対象となるもの(GitHub)自体に持たせるのではなく、操作するユーザー(CIツール)に持たせる方式です。もしGitHub側でどのユーザーならリポジトリをクローンできるという権限を1つ1つ持っているとした場合、ユーザーであるCIツールが利用する前にGitHubへの権限変更が必要になってしまいCIツールのスケールをしにくくなります。CIツールであればみんな同じユーザーを使い回すもの有りかもしれませんが、ある開発者が別の開発者にログイン情報を渡すといったユーザーを偽証することは後に混乱を招くので避けた方が良いでしょう。

OSレベルでのCapabilityベースのアクセス制御は難しいですが、GitHubの例のようにCapabitlity based Access Control(CapBAC)自体は広く利用されているもので、「ユーザーAの権利」のようなコンテキストに依存する権利を扱う問題には有効です。

話を戻すと、IPパケットにCapability(操作者の権利を持つトークン)を持たせれば良さそうです。こうすることでTUNデバイスでは操作者が端末2に対してアクセス権があるかをどこかに問い合わせる必要がなくなり、プライベートネットワークのスケールも容易になります。デメリットは制御対象が多いとCapabilityのデータ量が大きくなってしまうことです。

仮に、端末をCapabilityの各bitとして表すこととします。この場合、端末数が1000台程度であればCapabilityは1000 bits(125 bytes)程度で良いので許容範囲とします。

あとは、端末1から2へ出る際にトークンを伝搬する方法があれば問題解決なのですが、ユーザーAが端末1にアクセスするときに利用したSSHと、端末1から端末2にアクセスするときに利用したcurlはプロセスが分離しているため、この2つを紐付ける方法を見つけなければいけません。

これには全トークンを1つに統合する方法で対応可能です。

これを説明する前に、ログインシェルで判断する方法も考えられますが、端末に入るユーザーは特権を持つことができなくなるというデメリットがあります。特権を持てると別のログインシェルになりすますことができるためログインシェルで判断することができなくなるためです。またデーモンプロセスを踏み台にして別端末へアクセスするようなログインシェルを利用しない通信を制御することはできません。

特権を持つことができ他のユーザーになりすますことができる以上、誰が端末にアクセスしているかを判断したところで意味がありません。そのため、現在ログイン中の全トークンの内、最小権限が適用されるようトークンを1つに統合する方法を採用することにします。全トークンを統合するには端末を表すbitを図のようにandすれば良いでしょう。

デメリットとしては、他にログイン中のユーザー次第で突然権利が剥奪されてしまうため、なぜそうなったかを正しく表現する方法がないとユーザーが混乱してしまうことが考えられます。ですがこの方法であれば、全プロセスのTCP/IP通信に対して一律暗黙的に有効になるため、先に触れた「混乱した使節の問題」も(本来アクセスしていいユーザーまで拒否されるという点で)悲観的にではありますが回避することが可能です。


Contributed by Yuki Sanada

お問い合わせはこちら

当コンセプトは内製化に伴う流動的な外部開発メンバーとのコラボレーションを支えるGigOpsの1コア技術として開発途中のもので、実用にあたってはいくつかの課題が残っています。この開発をしたい優秀なプログラマや、プロトタイプでも良いので次世代の内製化技術を体験してみたいユーザーを随時募集していますのでお気軽にお問い合わせください。