ブログ | NGINX

NGINX チュートリアル: Kubernetes アプリを SQL インジェクションから保護する

NGINX-F5 水平黒タイプ RGB の一部
ダニエレ・ポレンチッチ サムネイル
ダニエレ・ポレンチッチ
2022年3月22日公開

このチュートリアルは、2022 年 3 月の Microservices の概念を実践する 4 つのチュートリアルのうちの 1 つです。 Kubernetes ネットワーキング:

さらに多くの Kubernetes ネットワークユースケースで NGINX を使用するための詳細なガイダンスが必要ですか? 無料の電子書籍「NGINX を使用した Kubernetes トラフィックの管理」をダウンロードしてください。 実用ガイド

あなたは、枕から自転車までさまざまな商品を販売する地元の人気店の IT 部門で働いています。 彼らは最初のオンラインストアを立ち上げようとしていますが、公開前にセキュリティの専門家にサイトの侵入テストを依頼しました。 残念ながら、セキュリティ専門家が問題を発見しました。 オンラインストアはSQL インジェクションに対して脆弱です。 セキュリティ専門家は、サイトを悪用して、ユーザー名やパスワードなどのデータベースから機密情報を取得することができました。

あなたのチームは、Kubernetes エンジニアであるあなたに、この状況を救うためにやって来ました。 幸いなことに、SQL インジェクション (およびその他の脆弱性) は、Kubernetes トラフィック管理ツールを使用して軽減できることはご存じでしょう。 アプリを公開するために Ingress コントローラーをすでにデプロイしており、単一の構成で脆弱性が悪用されないことを保証できます。 これで、オンライン ストアを予定通りに立ち上げることができます。 よくやった!

ラボとチュートリアルの概要

このブログは、マイクロサービス 2022 年 3 月のユニット 3 – Kubernetes でのマイクロサービス セキュリティ パターンのラボに付随するもので、NGINX と NGINX Ingress Controller を使用して SQL インジェクションをブロックする方法を示します。

チュートリアルを実行するには、次の条件を満たすマシンが必要です。

  • 2CPU以上
  • 2 GBの空きメモリ
  • 20 GB の空きディスク容量
  • インターネット接続
  • Docker、Hyperkit、Hyper-V、KVM、Parallels、Podman、VirtualBox、VMware Fusion/Workstation などのコンテナまたは仮想マシン マネージャー
  • minikubeがインストールされている
  • Helmがインストールされました
  • ブラウザ ウィンドウを起動できるようにする構成。 それが不可能な場合は、ブラウザ経由で関連サービスにアクセスする方法を検討する必要があります。

ラボとチュートリアルを最大限に活用するには、開始する前に次のことを実行することをお勧めします。

このチュートリアルでは次のテクノロジを使用します。

各チャレンジの手順には、アプリを構成するために使用される YAML ファイルの完全なテキストが含まれています。 GitHub リポジトリからテキストをコピーすることもできます。 各 YAML ファイルのテキストとともに GitHub へのリンクが提供されます。

このチュートリアルには 4 つの課題が含まれています。

  1. クラスターと脆弱なアプリをデプロイする
  2. アプリをハックする
  3. NGINX サイドカー コンテナを使用して特定のリクエストをブロックする
  4. リクエストをフィルタリングするための NGINX Ingress コントローラの設定

課題1: クラスターと脆弱なアプリをデプロイする

このチャレンジでは、 minikube クラスターをデプロイし、セキュリティ上の脆弱性があるサンプル アプリとしてPodinfo をインストールします

Minikube クラスターを作成する

minikubeクラスターをデプロイします。 数秒後、デプロイメントが成功したことを確認するメッセージが表示されます。

$ minikube start 🏄 完了!kubectl はデフォルトで「minikube」クラスターと「default」名前空間を使用するように設定されました 

脆弱なアプリをインストールする

ここでは、2 つのマイクロサービスで構成されるシンプルな e コマース アプリをデプロイします。

  • MariaDBデータベース
  • データベースに接続してデータを取得するPHPマイクロサービス

次の手順を実行します。

  1. 任意のテキスト エディターを使用して、次の内容を含む1-app.yamlという YAML ファイルを作成します (またはGitHub からコピーします)。

    apiバージョン: apps/v1 種類: デプロイメント 
    メタデータ: 
    名前: app 
    仕様: 
    セレクター: 
    matchLabels: 
    app: app 
    テンプレート: 
    メタデータ: 
    ラベル: 
    app: app 
    仕様: 
    コンテナ: 
    - 名前: app 
    イメージ: f5devcentral/microservicesmarch:1.0.3 
    ポート: 
    - コンテナポート: 80 
    環境: 
    - 名前: MYSQL_USER 
    値: dan 
    - 名前: MYSQL_PASSWORD 
    値: dan 
    - 名前: MYSQL_DATABASE 
    値: sqlitraining 
    - 名前: DATABASE_HOSTNAME 
    値: db.default.svc.cluster.local 
    --- 
    apiVersion: v1 
    種類: サービス 
    メタデータ: 
    名前: アプリ 
    仕様: 
    ポート: 
    - ポート: 80 
    ターゲットポート: 80 
    ノードポート: 30001 
    セレクタ: 
    アプリ: アプリ 
    タイプ: NodePort 
    --- 
    apiVersion: apps/v1 
    種類: デプロイメント 
    メタデータ: 
    名前: db 
    仕様: 
    セレクター: 
    matchLabels: 
    アプリ: db 
    テンプレート: 
    メタデータ: 
    ラベル: 
    アプリ: db 
    仕様: 
    コンテナ: 
    - 名前: db 
    イメージ: mariadb:10.3.32-focal 
    ポート: 
    - コンテナポート: 3306 
    環境: 
    - 名前: MYSQL_ROOT_PASSWORD 
    値: root 
    - 名前: MYSQL_USER 
    値: dan 
    - 名前: MYSQL_PASSWORD 
    値: dan 
    - 名前: MYSQL_DATABASE 
    値: sqlitraining 
    
    --- 
    apiVersion: v1 
    種類: サービス 
    メタデータ: 
    名前: db 
    仕様: 
    ポート: 
    - ポート: 3306 
    ターゲットポート: 3306 
    セレクタ: 
    アプリ: db 
    
  2. アプリと API をデプロイします。

    $ kubectl apply -f 1-app.yamlデプロイメント.apps/app が作成されました サービス/app が作成されました デプロイメント.apps/db が作成されました サービス/db が作成されました 
    
  3. STATUS列の値が Running であることで示されるように、Podinfo ポッドがデプロイされていることを確認します。 完全にデプロイされるまでに 30 ~ 40 秒かかることがあるため、両方のポッドのステータスが「実行中」になるまで待ってから、次の手順に進みます (必要に応じてコマンドを再発行します)。

    $ kubectl get pods NAME READY STATUS RESTARTS AGE app-d65d9b879-b65f2 1/1 実行中 0 37 秒 db-7bbcdc75c-q2kt5 1/1 実行中 0 37 秒 
    
  4. ブラウザでアプリを開きます:

    $ minikube service app |-----------|------|-------------|--------------| | NAMESPACE | NAME | TARGET PORT | URL | |-----------|------|-------------|--------------| | default | app | | ノード ポートがありません | |-----------|------|-------------|--------------| 😿 service default/app にはノード ポートがありません 🏃 サービス app のトンネルを開始しています。 |-----------|------|-------------|------------------------| | NAMESPACE | NAME | TARGET PORT | URL | |-----------|------|-------------|-----------|-------------------------| | default | app | | http://127.0.0.1:55446 | |-----------|------|------------|-------------------------| 🎉 デフォルトのブラウザでサービス default/app を開いています... 
    

チャレンジ2: アプリをハックする

サンプルアプリケーションはかなり基本的なものです。 これには、アイテムのリスト (枕など) を含むホームページと、説明や価格などの詳細を含む一連の製品ページが含まれます。 データは MariaDB データベースに保存されます。 ページが要求されるたびに、データベースに対して SQL クエリが発行されます。

  • ホームページの場合、データベース内のすべての項目が取得されます。
  • 商品ページの場合、アイテムは ID によって取得されます。

枕の製品ページを開くと、URL が/product/1で終わっていることに気づくでしょう。 の1は製品の ID です。SQL クエリに悪意のあるコードが直接挿入されるのを防ぐには、リクエストをバックエンド サービスに転送する前にユーザー入力をサニタイズすることがベスト プラクティスです。 しかし、アプリが適切に構成されておらず、入力がデータベースに対する SQL クエリに挿入される前にエスケープされていない場合はどうなるでしょうか?

エクスプロイト 1

アプリが入力を適切にエスケープしているかどうかを確認するには、ID をデータベースに存在しない ID に変更して簡単な実験を実行します。

URLの最後の要素を手動で変更します1-1。 エラー メッセージ「無効な製品ID "-1"」は、製品 ID がエスケープされていないことを示します。代わりに、文字列がクエリに直接挿入されます。 ハッカーでない限り、それは良くありません!

データベースクエリが次のようなものであると仮定します。

some_table から SELECT * WHERE id = "1"

入力をエスケープしないことによって生じる脆弱性を利用するには、 1-1" <悪意のあるクエリ> -- // つまり:

  • 引用符( " )の後ろ-1最初のクエリを完了します。
  • 引用符の後に独自の悪意のあるクエリを追加できます。
  • -- //シーケンスはクエリの残りの部分を破棄します。

たとえば、URLの最後の要素を‑1に変更したり、 1-- // 、クエリは次のようにコンパイルされます。

SELECT * FROM some_table WHERE id = "-1" OR 1 -- //" -------------- ^ 挿入されました ^ 

これはデータベースからすべての行を選択するもので、ハックに役立ちます。 これが当てはまるかどうかを確認するには、URL の末尾を「‑1」に変更します。 結果のエラー メッセージには、データベースに関するより有用な情報が表示されます。

致命的なエラー: キャッチされない mysqli_sql_exception: SQL 構文にエラーがあります。/var/www/html/product.php:23 の 1 行目の '"-1""' 付近で使用する正しい構文については、MariaDB サーバーのバージョンに対応するマニュアルを確認してください。スタック トレース: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} が /var/www/html/product.php の 23 行目でスローされました。

これで、挿入されたコードを操作して、データベースの結果を ID 順に並べ替えることができます。

-1" OR 1 ORDER BY id DESC -- //

結果は、データベース内の最後のアイテムの製品ページになります。

エクスプロイト2

データベースに強制的に結果を順序付けさせるのは興味深いですが、ハッキングが目的の場合は特に役に立ちません。 データベースからユーザー名とパスワードを抽出しようとする方が、はるかに価値があります。

データベース内にユーザー名とパスワードを含むユーザー テーブルが存在すると想定しても問題ありません。 しかし、製品テーブルからユーザー テーブルへのアクセスをどのように拡張するのでしょうか?

答えは次のようなコードを挿入することです:

-1" UNION SELECT * FROM ユーザー -- //

どこ

  • 「-1」は最初のクエリから空のセットを返すことを強制します。
  • UNION は、2 つのデータベース テーブル (この場合は、製品ユーザー) を強制的に結合し、元の (製品) テーブルにはない情報 (パスワード) を取得できるようにします。
  • SELECT * FROM users は、 usersテーブル内のすべての行を選択します。
  • -- //シーケンスは、悪意のあるクエリの後のすべてを破棄します。

挿入されたコードで終わるように URL を変更すると、新しいエラー メッセージが表示されます。

致命的なエラー: キャッチされない mysqli_sql_exception: 使用された SELECT ステートメントは、/var/www/html/product.php:23 で列数が異なります。スタック トレース: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} が /var/www/html/product.php の 23 行目でスローされました。

このメッセージは、 productsテーブルとusersテーブルの列数が同じではないため、 UNION命令を実行できないことを示しています。 ただし、 SELECT命令にパラメータとして列 (フィールド名) を 1 つずつ追加することで、試行錯誤を通じて列の数を調べることができます。 ユーザーテーブルのフィールド名はpasswordであると推測されるので、次のように試してください。

# 1 列を選択-1" UNION SELECT password FROM users; -- // # 2 列を選択-1" UNION SELECT password,password FROM users; -- // # 3 列を選択-1" UNION SELECT password,password,password FROM users; -- / # 4 列を選択-1" UNION SELECT password,password,password,password FROM users; -- // # 5 列を選択-1" UNION SELECT password,password,password,password,password FROM users; -- //

最後のクエリは成功し (ユーザーテーブルに 5 つの列があることが通知されます)、ユーザー パスワードが表示されます。

この時点では、このパスワードに対応するユーザー名はわかりません。 ただし、ユーザーテーブル内の列数がわかっている場合は、以前と同じ種類のクエリを使用してその情報を公開できます。 関連するフィールド名がusernameであると仮定します。 そしてそれは正しいことが判明しました。次のクエリは、ユーザーテーブルからユーザー名とパスワードの両方を公開します。 これは素晴らしいことですが、このアプリがあなたのインフラストラクチャでホストされている場合を除きます。

-1" UNION SELECT ユーザー名、ユーザー名、パスワード、パスワード、ユーザー名 FROM users where id=1 -- //

課題3: NGINX サイドカー コンテナを使用して特定のリクエストをブロックする

オンライン ストア アプリの開発者は、ユーザー入力のサニタイズ (パラメーター化されたクエリの使用など) にさらに注意を払う必要があるのは明らかですが、Kubernetes エンジニアとして、攻撃がアプリに到達するのをブロックすることで、SQL インジェクションを防ぐこともできます。そうすれば、アプリが脆弱であることはそれほど問題になりません。

アプリを保護する方法はたくさんあります。 このラボの残りの部分では、次の 2 つに焦点を当てます。

NGINXオープンソースをサイドカーとして導入する

  1. 次の内容を含む2-app-sidecar.yamlという YAML ファイルを作成します (またはGitHub からコピーします)。 構成の重要な側面は次のとおりです。

    • NGINX Open Source を実行するサイドカー コンテナーがポート 8080 で起動されます。
    • NGINX はすべてのトラフィックをアプリに転送します。
    • SELECTまたはUNION (その他の文字列を含む) を含むリクエストはすべて拒否されます ( ConfigMapセクションの最初のロケーションブロックを参照)。
    • アプリのサービスは、まずすべてのトラフィックを NGINX コンテナにルーティングします。
    apiバージョン: apps/v1 
    種類: デプロイメント 
    メタデータ: 
    名前: app 
    仕様: 
    セレクター: 
    matchLabels: 
    app: app 
    テンプレート: 
    メタデータ: 
    ラベル: 
    app: app 
    仕様: 
    コンテナ: 
    - 名前: app 
    イメージ: f5devcentral/microservicesmarch:1.0.3 
    ポート: 
    - コンテナポート: 80 
    環境: 
    - 名前: MYSQL_USER 
    値: dan 
    - 名前: MYSQL_PASSWORD 
    値: dan 
    - 名前: MYSQL_DATABASE 
    値: sqlitraining 
    - 名前: DATABASE_HOSTNAME 
    値: db.default.svc.cluster.local 
    - 名前: proxy # <-- sidecar 
    イメージ: "nginx" 
    ポート: 
    - containerPort: 8080 
    volumeMounts: 
    - mountPath: /etc/nginx 
    name: nginx-config 
    volumes: 
    - name: nginx-config 
    configMap: 
    name: sidecar 
    --- 
    apiVersion: v1 
    kind: サービス 
    メタデータ: 
    名前: アプリ 
    仕様: 
    ポート: 
    - ポート: 80 
    ターゲットポート: 8080 # <-- トラフィックはプロキシにルーティングされます 
    nodePort: 30001 
    セレクタ: 
    アプリ: アプリ 
    タイプ: NodePort 
    --- 
    apiVersion: v1 
    種類: ConfigMap 
    メタデータ: 
    名前: サイドカー 
    データ: 
    nginx.conf: |- 
    イベント {} 
    http { 
    サーバー { 
    listen 8080 default_server; 
    listen [::]:8080 default_server; 
    
    場所 ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 
    すべてを拒否; 
    } 
    
    場所 / { 
    proxy_pass http://localhost:80/; 
    } 
    } 
    } 
    --- 
    apiVersion: apps/v1 
    種類: デプロイメント 
    メタデータ: 
    名前: db 
    仕様: 
    セレクター: 
    matchLabels: 
    アプリ: db 
    テンプレート: 
    メタデータ: 
    ラベル: 
    アプリ: db 
    仕様: 
    コンテナ: 
    - 名前: db 
    イメージ: mariadb:10.3.32-focal 
    ポート: 
    - コンテナポート: 3306 
    環境: 
    - 名前: MYSQL_ROOT_PASSWORD 
    値: root 
    - 名前: MYSQL_USER 
    値: dan 
    - 名前: MYSQL_PASSWORD 
    値: dan 
    - 名前: MYSQL_DATABASE 
    値: sqlitraining 
    
    --- 
    apiVersion: v1 
    種類: サービス 
    メタデータ: 
    名前: db 
    仕様: 
    ポート: 
    - ポート: 3306 
    ターゲットポート: 3306 
    セレクタ: 
    アプリ: db
    
  2. サイドカーをデプロイします。

    $ kubectl apply -f 2-app-sidecar.yaml deploy.apps/app 設定済み service/app 設定済み configmap/sidecar 作成済み deploy.apps/db 未変更 service/db 未変更 
    

サイドカーをフィルターとしてテストする

アプリに戻って SQL インジェクションを再度試し、サイドカーがトラフィックをフィルタリングしているかどうかをテストします。 NGINX はリクエストがアプリに到達する前にブロックします。

-1" UNION SELECT ユーザー名、ユーザー名、パスワード、パスワード、ユーザー名 FROM users where id=1 -- //

課題4: リクエストをフィルタリングするための NGINX Ingress コントローラの設定

チャレンジ 3のようにアプリを保護することは、教育的な体験としては興味深いものですが、次の理由から本番環境ではお勧めしません。

  • 完全なセキュリティソリューションではありません。
  • 拡張性がありません (この保護を複数のアプリに簡単に適用することはできません)。
  • 更新は複雑で非効率的です。

はるかに優れたソリューションは、NGINX Ingress Controller を使用して、すべてのアプリに同じ保護を拡張することです。 Ingress コントローラを使用すると、Web アプリケーション ファイアウォール (WAF) のようなリクエストのブロックから認証や承認まで、あらゆる種類のセキュリティ機能を一元管理できます。

このチャレンジでは、 NGINX Ingress Controller をデプロイしトラフィック ルーティングを構成しフィルターが SQL インジェクションをブロックすることを確認します

NGINX Ingress Controller をデプロイする 

NGINX Ingress Controller をインストールする最も速い方法は、 Helmを使用することです。  

  1. NGINX リポジトリを Helm に追加します。 

    $ helm リポジトリに nginx-stable を追加します https://helm.nginx.com/stable  
    
  2. F5 NGINX によって管理されている NGINX オープンソースベースのNGINX Ingress Controllerをダウンロードしてインストールします。enableSnippets =trueパラメータに注意してください。スニペットは、SQL インジェクションをブロックするように NGINX を構成するために使用されます。 出力の最後の行はインストールが成功したことを確認します。

    $ helm install main nginx-stable/nginx-ingress \ --set controller.watchIngressWithoutClass=true --set controller.service.type=NodePort \ --set controller.service.httpPort.nodePort=30005 \ --set controller.enableSnippets=true名前: main 最終デプロイ日: 日 月 DD hh:mm:ss YYYY名前空間: デフォルト ステータス: デプロイ済み リビジョン: 1 テストスイート: なし 注記: NGINX Ingress Controller がインストールされました。  
    
  3. STATUS列の値がRunningであることで示されるように、NGINX Ingress Controller ポッドがデプロイされていることを確認します。 

    $ kubectl get pods NAME READY STATUS ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 実行中 ... ... 年齢をリスタート... 0 18秒
    

アプリへのトラフィックをルーティングする

  1. 次の内容を含む3-ingress.yamlという YAML ファイルを作成します (またはGitHub からコピーします)。 これは、トラフィックをアプリにルーティングするために必要な Ingress マニフェストを定義します (今回はサイドカー プロキシを経由しません)。 チャレンジ 3 の ConfigMap 定義と同じlocationブロックを使用して NGINX Ingress Controller 構成をカスタマイズするためにスニペットが使用されている、 annotations:ブロックに注目してください。このブロックは、(他の文字列とともに) SELECTまたはUNION を含むすべてのリクエストを拒否します。

    apiバージョン: v1 種類: サービス 
    メタデータ: 
    名前: app-without-sidecar 
    仕様: 
    ポート: 
    - ポート: 80 
    ターゲットポート: 80 
    セレクター: 
    アプリ: app 
    --- 
    apiVersion: networking.k8s.io/v1 
    種類: Ingress 
    メタデータ: 
    名前: エントリ 
    アノテーション: 
    nginx.org/server-snippets: | 
    location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 
    すべて拒否; 
    } 
    仕様: 
    ingressClassName: nginx 
    ルール: 
    - ホスト: "example.com" 
    http: 
    パス: 
    - バックエンド: 
    サービス: 
    名前: app-without-sidecar 
    ポート: 
    番号: 80 
    パス: / 
    パスタイプ: プレフィックス 
    
  2. Ingress リソースをデプロイします。 
  3. $ kubectl apply -f 3-ingress.yaml service/app-without-sidecar が作成されました ingress.networking.k8s.io/entry が作成されました 
    

フィルターの動作を確認する

  1. 使い捨てのBusyBoxコンテナを起動して、正しいホスト名で NGINX Ingress Controller ポッドにリクエストを発行します。

    $ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: example.com" -qO- main-nginx-ingress    # ...
    
  2. SQL インジェクションを試みます。 の403Forbiddenステータス コードは、NGINX が攻撃をブロックしたことを確認します。

     

    $ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20username,username,password,password,username%20FROM%20users%20where%2 0id=1%20--%20//' wget: サーバーがエラーを返しました: HTTP/1.1 403 禁止 
    

次のステップ

Kubernetes はデフォルトでは安全ではありません。 Ingress コントローラーは、SQL インジェクション (およびその他多くの) の脆弱性を軽減できます。 ただし、NGINX Ingress Controller で実装した WAF のような機能は、実際の WAF に代わるものではなく、アプリを安全に設計するための代替品でもないことに注意してください。 経験豊富なハッカーであれば、コードに少し変更を加えるだけで、 UNIONハックを機能させることができます。 このトピックの詳細については、 「A Pentester's Guide to SQL Injection (SQLi)」を参照してください。

とはいえ、Ingress コントローラーは依然としてセキュリティの大部分を集中管理するための強力なツールであり、集中認証および承認のユースケース (mTLS、シングル サインオン) や、 F5 NGINX App Protect WAFのような堅牢な WAF など、効率性とセキュリティの向上につながります。

アプリやアーキテクチャの複雑さによっては、よりきめ細かい制御が必要になる場合があります。 組織でゼロ トラストとエンドツーエンドの暗号化が必要な場合は、Kubernetes クラスター内のサービス間の通信 (東西トラフィック) を制御するために、常時無料のF5 NGINX Service Meshなどのサービス メッシュを検討してください。 サービス メッシュについては、ユニット 4 「高度な Kubernetes デプロイメント戦略」で説明します。

NGINX オープンソースの入手と展開の詳細については、 nginx.org をご覧ください。

NGINX App Protect を搭載した NGINX Plus に基づく NGINX Ingress Controller を試すには、今すぐ30 日間の無料トライアルを開始するか、弊社にお問い合わせの上、使用事例についてご相談ください。 

NGINX オープンソースに基づく NGINX Ingress Controller を試すには、GitHub リポジトリのNGINX Ingress Controller リリースを参照するか、 DockerHubからビルド済みコンテナをダウンロードしてください。 


「このブログ投稿には、入手できなくなった製品やサポートされなくなった製品が参照されている場合があります。 利用可能な F5 NGINX 製品およびソリューションに関する最新情報については、 NGINX 製品ファミリーをご覧ください。 NGINX は現在 F5 の一部です。 以前の NGINX.com リンクはすべて、F5.com の同様の NGINX コンテンツにリダイレクトされます。"