このチュートリアルは、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 インジェクションをブロックする方法を示します。
チュートリアルを実行するには、次の条件を満たすマシンが必要です。
ラボとチュートリアルを最大限に活用するには、開始する前に次のことを実行することをお勧めします。
このチュートリアルでは次のテクノロジを使用します。
各チャレンジの手順には、アプリを構成するために使用される YAML ファイルの完全なテキストが含まれています。 GitHub リポジトリからテキストをコピーすることもできます。 各 YAML ファイルのテキストとともに GitHub へのリンクが提供されます。
このチュートリアルには 4 つの課題が含まれています。
このチャレンジでは、 minikube クラスターをデプロイし、セキュリティ上の脆弱性があるサンプル アプリとしてPodinfo をインストールします。
minikubeクラスターをデプロイします。 数秒後、デプロイメントが成功したことを確認するメッセージが表示されます。
$ minikube start 🏄 完了!kubectl はデフォルトで「minikube」クラスターと「default」名前空間を使用するように設定されました
ここでは、2 つのマイクロサービスで構成されるシンプルな e コマース アプリをデプロイします。
次の手順を実行します。
任意のテキスト エディターを使用して、次の内容を含む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
アプリと API をデプロイします。
$ kubectl apply -f 1-app.yamlデプロイメント.apps/app が作成されました サービス/app が作成されました デプロイメント.apps/db が作成されました サービス/db が作成されました
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 秒
ブラウザでアプリを開きます:
$ 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 を開いています...
サンプルアプリケーションはかなり基本的なものです。 これには、アイテムのリスト (枕など) を含むホームページと、説明や価格などの詳細を含む一連の製品ページが含まれます。 データは MariaDB データベースに保存されます。 ページが要求されるたびに、データベースに対して SQL クエリが発行されます。
枕の製品ページを開くと、URL が/product/1で終わっていることに気づくでしょう。 の1は製品の ID です。SQL クエリに悪意のあるコードが直接挿入されるのを防ぐには、リクエストをバックエンド サービスに転送する前にユーザー入力をサニタイズすることがベスト プラクティスです。 しかし、アプリが適切に構成されておらず、入力がデータベースに対する SQL クエリに挿入される前にエスケープされていない場合はどうなるでしょうか?
アプリが入力を適切にエスケープしているかどうかを確認するには、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 -- //
結果は、データベース内の最後のアイテムの製品ページになります。
データベースに強制的に結果を順序付けさせるのは興味深いですが、ハッキングが目的の場合は特に役に立ちません。 データベースからユーザー名とパスワードを抽出しようとする方が、はるかに価値があります。
データベース内にユーザー名とパスワードを含むユーザー テーブルが存在すると想定しても問題ありません。 しかし、製品テーブルからユーザー テーブルへのアクセスをどのように拡張するのでしょうか?
答えは次のようなコードを挿入することです:
-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 -- //
オンライン ストア アプリの開発者は、ユーザー入力のサニタイズ (パラメーター化されたクエリの使用など) にさらに注意を払う必要があるのは明らかですが、Kubernetes エンジニアとして、攻撃がアプリに到達するのをブロックすることで、SQL インジェクションを防ぐこともできます。そうすれば、アプリが脆弱であることはそれほど問題になりません。
アプリを保護する方法はたくさんあります。 このラボの残りの部分では、次の 2 つに焦点を当てます。
このチャレンジでは、ポッドにサイドカー コンテナを挿入してすべてのトラフィックをプロキシし、URL にUNION が
含まれるすべてのリクエストを拒否します。
まず、 NGINX Open Source をサイドカーとしてデプロイし、悪意のあるクエリがフィルタリングされるかどうかをテストします。
注記: この手法は説明目的のみに利用しています。 実際には、プロキシをサイドカーとして手動で展開することは最善の解決策ではありません (これについては後で詳しく説明します)。
次の内容を含む2-app-sidecar.yamlという YAML ファイルを作成します (またはGitHub からコピーします)。 構成の重要な側面は次のとおりです。
SELECT
またはUNION
(その他の文字列を含む) を含むリクエストはすべて拒否されます ( ConfigMap
セクションの最初のロケーション
ブロックを参照)。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
サイドカーをデプロイします。
$ 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 -- //
チャレンジ 3のようにアプリを保護することは、教育的な体験としては興味深いものですが、次の理由から本番環境ではお勧めしません。
はるかに優れたソリューションは、NGINX Ingress Controller を使用して、すべてのアプリに同じ保護を拡張することです。 Ingress コントローラを使用すると、Web アプリケーション ファイアウォール (WAF) のようなリクエストのブロックから認証や承認まで、あらゆる種類のセキュリティ機能を一元管理できます。
このチャレンジでは、 NGINX Ingress Controller をデプロイし、トラフィック ルーティングを構成し、フィルターが SQL インジェクションをブロックすることを確認します。
NGINX Ingress Controller をインストールする最も速い方法は、 Helmを使用することです。
NGINX リポジトリを Helm に追加します。
$ helm リポジトリに nginx-stable を追加します https://helm.nginx.com/stable
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 がインストールされました。
STATUS
列の値がRunning
であることで示されるように、NGINX Ingress Controller ポッドがデプロイされていることを確認します。
$ kubectl get pods NAME READY STATUS ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 実行中 ... ... 年齢をリスタート... 0 18秒
次の内容を含む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
パス: /
パスタイプ: プレフィックス
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar が作成されました ingress.networking.k8s.io/entry が作成されました
使い捨てのBusyBoxコンテナを起動して、正しいホスト名で NGINX Ingress Controller ポッドにリクエストを発行します。
$ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: example.com" -qO- main-nginx-ingress # ...
SQL インジェクションを試みます。 の403
Forbidden
ステータス コードは、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 コンテンツにリダイレクトされます。"