VPNに頼らずAndroidアプリで平文通信を遮断する
ペイロード内の平文と経路上の平文は別の問題です。前者はアプリで、後者はトンネルか端末ポリシーで解消します。
Androidで平文を遮断すると呼ばれるものは二つあり、まったく別物です。一つはアプリ内部のポリシーで、自身のネットワークスタックに素のHTTPを拒否させます。もう一つは端末レベルの仕組みで、通常はVPN経由で通信を流すことで端末全体をまとめて守ろうとします。両者は別の問題を解き、後者で前者は直りません。トンネルが守るのは端末と出口の間だけで、HTTPを話すアプリは出口の先に対してもHTTPを話し続けるからです。
一つの名前に二つの問題
第一の問題はペイロードです。アプリが認証情報やトークンを、端から端まで暗号化されていないリクエストに書き込むため、見える経路はすべて読めてしまいます。第二は経路です。アプリが完全に正しくても、接続先をローカルのWi-Fi事業者に知られたくない場合があります。VPNが扱うのは第二だけです。
この区別が修正の持ち主を決めます。ペイロードはアプリを配布する側のもので、どのURLを呼ぶか、証明書を検証するかを決めるのはそのコードだけです。経路は端末かネットワーク側のもので、パケットがどこへ行くかの話であり、中身の話ではありません。
経路その一:自分で管理するアプリ
アプリを配布するなら、修正は通信方式の決定とそれを強制するポリシーです。全エンドポイントをHTTPSに向け、ホスト名と証明書の検証を有効にしたまま、素のHTTPへのフォールバックは設定フラグ一つ分も残さず削除します。リダイレクトは役に立ちません。301を受け取るHTTPリクエストはすでにヘッダーと本文を平文で送っており、リダイレクトが守るという前提で認証情報を流してはいけません。
Android 7.0以降ではNetwork Security Configurationがポリシーを表現します。下のファイルは最小の実用版で、ドメイン固有の例外がない限り平文を全面的に拒否します。
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>そのリソースをマニフェストから参照し、配布前に既存の例外を読みます。古いデバッグ用ドメインや決済事業者のSDKエントリが、守ろうとしたホストの平文を静かに再許可することがあります。
バージョンについて二点知っておく価値があります。API 28以降を対象とするアプリは既定で平文を拒否するため、まだ素のHTTPを話すアプリは古いAPIレベルを対象にしているか、どこかに例外があります。またネットワークセキュリティ設定がある場合、古いandroid:usesCleartextTrafficマニフェスト属性は判断に関与しなくなり、そのフラグを変えても何も起きないように見えることがあります。
経路その二:管理できないアプリ
インストール済みの第三者アプリは、ユーザー設定でセキュリティ設定を書き換えられません。現実的な選択肢は、製品自身の安全な接続の制御、すでにTLSを使う保守された代替、端末が実行できるものを制限する管理端末ポリシーです。これらは別の約束であり、端末群に広げられるのは三つ目だけです。
ネットワークルールは見た目より弱い代替です。ポート443だけを許可しても認証済みTLSは強制できません。多くの平文プロトコルがそのポートへ移せるからです。ポート80を遮断しても他のポートの平文は覆えません。ブラウザーのHTTPS限定モードもそのブラウザー内に限られ、同じ端末の別のメールやチャットアプリには及びません。
通信を暗号化されて見せるために傍受証明書を入れるのは、信頼の変更であって修正ではありません。アプリのサーバー認証を自らの認証に置き換えるだけで、もともと証明書を検証していないアプリは直りません。読める相手が変わっただけです。
VPNが役立つ場面とそうでない場面
トンネルは、ローカルネットワークに接続先を知られたくない、あるいは他に到達できないプライベートサービスへ届かせたい場合に適した道具です。HTTPクライアントにHTTPSを話させるための道具ではありません。リクエストは入ったときとまったく同じ形でトンネルを出ていくからです。
Androidには実務上の制約があります。プラットフォームは同時に一つのVPNサービスしか動かせません。その枠を取るローカルフィルタアプリは、本当に必要な遠隔トンネルと競合し、両者の切り替えはアプリ単位ではなく端末単位の操作になります。その一枠があるからこそ、ペイロードのポリシーはアプリに置き、トンネルは経路のために取っておくべきです。
両者が重なる唯一の例は、自分で運用するプライベートサーバーです。そこではプロトコルが平文のままでもトンネルで経路を私的にできますが、露出が減るだけでペイロードの中身は変わりません。これはリスク低減であり、データの暗号化ではありません。
遮断できたと呼ぶ前に確認すること
まず人を驚かせる既定値から始めます。API 28以降を対象とするアプリはすでに平文を拒否するはずなので、既定を重複させる設定を足す前に目標レベルを確認します。次に失敗する場合を意図的に試します。素のHTTPホストへのリクエストは、フォールバックで静かに成功するのではなく失敗しなければなりません。
adb shell dumpsys package com.example.app | grep -E 'targetSdk|versionName'
adb logcat | grep -i cleartext成功する場合は、HTTPS呼び出しが実際の証明書チェーンを検証すること、信頼できない証明書のプロキシが許容されず拒否されることを確認します。どの証明書でも成功するハンドシェイクは、通信の安全性を何も証明していません。
第三者のアプリについては、実際に強制できる境界を書き出します。管理端末ポリシー、ネットワークルール、あるいは何もないのかを、受け入れ基準に明記します。Androidのセキュリティ設定ガイドには、プラットフォームの既定値と、どのコンポーネントがポリシーに従うかが載っており、設定が意味を持つかどうかを決めるのはこの部分です。