このチュートリアルは、以下の「Microservices March 2022: Kubernetes Networking」で紹介した概念を実践する4つのチュートリアルのうちの1つです:
- 自動スケーリングでKubernetesのレイテンシを短縮
- レート制限でKubernetes APIを保護
- KubernetesアプリをSQLインジェクションから保護(本記事)
- カナリアデプロイで稼働時間とレジリエンスを向上
NGINXをさらに多くのKubernetesネットワーキングのユースケースで活用するための詳しいガイダンスをお探しですか?無料のeBook「Managing Kubernetes Traffic with NGINX: A Practical Guide」をダウンロードしてください。
あなたは、枕から自転車までさまざまな商品を扱う、地元で人気の店舗のIT担当者です。店舗では初のオンラインストアをまもなく開設する予定ですが、公開前にセキュリティ専門家へサイトのペネトレーションテストを依頼しました。残念ながら、セキュリティ専門家が問題を発見しました。オンラインストアにはSQLインジェクションの脆弱性があります。セキュリティ専門家はこの脆弱性を悪用し、ユーザー名やパスワードなど、データベース内の機密情報を取得できました。
チームは、この窮地を救うため、Kubernetesエンジニアであるあなたに助けを求めました。幸い、Kubernetesのトラフィック管理ツールを使用すれば、SQLインジェクションをはじめとする脆弱性を緩和できることをご存じです。アプリを公開するためのIngressコントローラーはすでにデプロイ済みで、1つの設定を追加するだけで、この脆弱性が悪用されないようにできます。これで、オンラインストアを予定どおりに開設できます。お見事です!
ラボとチュートリアルの概要
このブログは、Microservices March 2022のユニット3のラボ「Kubernetesにおけるマイクロサービスのセキュリティパターン」に対応しており、NGINXとNGINX Ingress Controllerを使用してSQLインジェクションをブロックする方法を紹介します。
このチュートリアルを実行するには、以下を備えたマシンが必要です。
- 2 CPU以上
- 2 GBの空きメモリ
- 20 GBの空きディスク容量
- インターネット接続
- Docker、Hyperkit、Hyper-V、KVM、Parallels、Podman、VirtualBox、VMware Fusion/Workstationなどのコンテナまたは仮想マシンマネージャー
- minikubeがインストールされていること
- Helmがインストール済み
- ブラウザーウィンドウを起動できる構成。起動できない場合は、ブラウザーから該当するサービスにアクセスする方法を確認する必要があります。
ラボとチュートリアルを最大限に活用するため、開始前に以下を行うことを推奨します。
- 視聴する:ライブ配信されたコンセプト概要の録画
- ご確認ください:背景情報を紹介するブログ、ウェビナー、動画
- ラボの内容をまとめた16分間の動画をご覧ください:
 
このチュートリアルでは、以下のテクノロジーを使用します。
- NGINX Open Source
- NGINX Ingress Controller(NGINX Open Sourceをベースとしています)
- BusyBox
- Helm
- minikube
- セキュリティ脆弱性のあるシンプルなアプリ(このラボ用に開発)
各チャレンジの手順には、アプリの設定に使用するYAMLファイルの全文が記載されています。また、F5のGitHubリポジトリからテキストをコピーすることもできます。各YAMLファイルのテキストには、GitHubへのリンクも併記されています。
このチュートリアルには、4つの課題が含まれています。
- クラスターと脆弱なアプリをデプロイ
- アプリをハッキング
- NGINXサイドカーコンテナを使用して特定のリクエストをブロックする
- NGINX Ingress Controllerを設定してリクエストをフィルタリングする
課題1:クラスターと脆弱なアプリをデプロイする
このチャレンジでは、minikubeクラスターをデプロイし、セキュリティ脆弱性のあるサンプルアプリとしてPodinfoをインストールします。
Minikubeクラスターの作成
Deploy a minikubeクラスターをデプロイします。数秒後、デプロイが成功したことを示すメッセージが表示されます。
$ minikube start
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default脆弱性のあるアプリをインストール
ここでは、2つのマイクロサービスで構成されるシンプルなeコマースアプリをデプロイします。
- MariaDBデータベース
- データベースに接続してデータを取得するPHPマイクロサービス
次の手順を実行します。
- 任意のテキストエディターを使用して、次の内容で1-app.yamlというYAMLファイルを作成します(または、GitHubからコピーします)。
apiVersion: apps/v1 kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 80
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db - アプリとAPIをデプロイします:
$ kubectl apply -f 1-app.yaml deployment.apps/app created
service/app created
deployment.apps/db created
service/db created - Podinfoポッドがデプロイされたことを、
Running列のSTATUSという値で確認します。完全にデプロイされるまで30~40秒かかる場合があるため、両方のポッドのステータスがRunningになるまで待ってから、次の手順に進んでください(必要に応じてコマンドを再実行します)。
$ kubectl get podsNAME READY STATUS RESTARTS AGE
app-d65d9b879-b65f2 1/1 Running 0 37s
db-7bbcdc75c-q2kt5 1/1 Running 0 37s - ブラウザでアプリを開きます:
$ minikube service app|-----------|------|-------------|--------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|--------------|
| default | app | | No node port |
|-----------|------|-------------|--------------|
😿 service default/app has no node port
🏃 Starting tunnel for service app.
|-----------|------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|------------------------|
| default | app | | http://127.0.0.1:55446 |
|-----------|------|-------------|------------------------|
🎉 Opening service default/app in default browser... チャレンジ2:アプリをハッキング
このサンプルアプリケーションは非常に基本的なものです。ホームページには商品(枕など)の一覧が表示され、各商品ページには説明や価格などの詳細情報が掲載されています。データはMariaDBデータベースに保存されています。ページがリクエストされるたびに、データベースに対してSQLクエリが実行されます。
- ホームページでは、データベース内のすべてのアイテムが取得されます。
- 商品ページでは、IDを使用して項目を取得します。
When you open the pillowsの商品ページを開くと、URLの末尾が「/product/1」になっていることがわかります。「1」は商品のIDです。SQLクエリに悪意のあるコードが直接挿入されるのを防ぐため、バックエンドサービスにリクエストを転送する前にユーザー入力をサニタイズすることがベストプラクティスです。しかし、アプリが適切に設定されておらず、データベースに対するSQLクエリに挿入される前に入力がエスケープされていない場合はどうなるでしょうか?
エクスプロイト1
アプリが入力を適切にエスケープしているかどうかを確認するには、IDをデータベースに存在しないものに変更して、簡単なテストを実行します。
URLの最後の要素を手動で1から-1に変更します。エラーメッセージ「Invalid product id "-1"」は、製品IDがエスケープされず、文字列がクエリに直接挿入されていることを示します。ハッカーにとっては好都合ですが、これは望ましくありません。

データベースクエリが次のようなものだと仮定します。
SELECT * FROM some_table WHERE id = "1"入力がエスケープされていないことに起因する脆弱性を悪用するには、1を-1" <malicious_query> -- //に置き換え、次のようにします。
- The quotation mark (
「」は、-1の後に置くことで、最初のクエリを完結させます。 - 引用符の後に、独自の悪意のあるクエリを追加できます。
- The
--//シーケンスにより、クエリの残りの部分が破棄されます。
たとえば、URLの最後の要素を「‑1"」または「1 -- //」に変更すると、クエリは次のようにコンパイルされます。
SELECT * FROM some_table WHERE id = "-1" OR 1 -- //"
--------------
^ injected ^ これによりデータベース内のすべての行が選択されるため、ハッキングに利用できます。該当するかどうかを確認するには、URLの末尾を「‑1"」に変更します。表示されるエラーメッセージから、データベースに関するさらに有用な情報を得られます。
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"-1""' at line 1 in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23次に、インジェクションしたコードを操作して、データベースの結果をID順に並べてみます。
-1" OR 1 ORDER BY id DESC -- //結果として、データベース内の最後の項目の商品ページが表示されます。
エクスプロイト2
データベースに結果を並べ替えさせるのは興味深いものの、ハッキングが目的なら、特に有用とはいえません。データベースからユーザー名とパスワードを抽出する方が、はるかに試す価値があります。
データベースには、ユーザー名とパスワードを格納したユーザーテーブルがあると考えてよいでしょう。しかし、製品テーブルへのアクセスをユーザーテーブルにまで拡大するには、どうすればよいのでしょうか。
その答えは、次のようなコードをインジェクションすることです。
-1" UNION SELECT * FROM users -- //ここで
‑1"を指定すると、最初のクエリは強制的に空のセットを返します。UNIONは2つのデータベーステーブル(この場合はproductsとusers)を強制的に結合します。これにより、元のproductsテーブルにはない情報(パスワード)を取得できます。SELECT*FROMusersは、users テーブルのすべての行を選択します。- この
--//シーケンスにより、悪意のあるクエリより後のすべてが破棄されます。
URLの末尾がインジェクションしたコードになるように変更すると、新しいエラーメッセージが表示されます。
Fatal error: Uncaught mysqli_sql_exception: The used SELECT statements have a different number of columns in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23このメッセージから、productsテーブルとusersテーブルでは列数が異なるため、UNION命令を実行できないことがわかります。ただし、SELECT命令のパラメーターとして列(フィールド名)を1つずつ追加して試行錯誤することで、列数を特定できます。usersテーブルのフィールド名としては、passwordが妥当だと考えられるため、これを試してみます。
# select 1 column-1" UNION SELECT password FROM users; -- //
# select 2 columns
-1" UNION SELECT password,password FROM users; -- //
# select 3 columns
-1" UNION SELECT password,password,password FROM users; -- /
# select 4 columns
-1" UNION SELECT password,password,password,password FROM users; -- //
# select 5 columns
-1" UNION SELECT password,password,password,password,password FROM users; -- //最後のクエリは成功し(usersテーブルに5つの列があることが分かります)、ユーザーのパスワードが表示されます:

この時点では、このパスワードに対応するユーザー名は不明です。しかし、usersテーブルの列数が分かっているため、先ほどと同じ種類のクエリを使用して、その情報を明らかにできます。該当するフィールド名をusernameと仮定します。この仮定は正しく、次のクエリによってusersテーブルからユーザー名とパスワードの両方が明らかになります。これは素晴らしい結果です。ただし、このアプリが自社のインフラストラクチャでホストされている場合は別ですが。
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //
課題3:NGINXサイドカーコンテナを使用して特定のリクエストをブロックする
オンラインストアアプリの開発者は、パラメーター化クエリの使用など、ユーザー入力のサニタイズにさらに注意を払う必要があります。一方、Kubernetesエンジニアも、攻撃がアプリに到達しないようブロックすることで、SQLインジェクションの防止に貢献できます。これにより、アプリに脆弱性があることによる影響を軽減できます。
アプリを保護する方法は数多くあります。このラボの残りの部分では、次の2つに焦点を当てます。
- この課題では、サイドカーコンテナをPodに挿入してすべてのトラフィックをプロキシし、URLに
UNIONが含まれるリクエストをすべて拒否します。まず、NGINX Open Sourceをサイドカーとしてデプロイし、次に、悪意のあるクエリが除外されるかどうかをテストします。注:この手法は説明のみを目的として使用しています。実際には、プロキシをサイドカーとして手動でデプロイする方法は最適なソリューションではありません(詳しくは後述します)。 - In チャレンジ4では、NGINX Ingress Controllerを使用して、クラスターに入るすべてのトラフィックをフィルタリングします。
NGINX Open Sourceをサイドカーとしてデプロイ
- 次の内容でYAMLファイル「2-app-sidecar.yaml」を作成します(または、GitHubからコピーします)。この構成の重要なポイントは次のとおりです。
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
- name: proxy # <-- sidecar
image: "nginx"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
volumes:
- name: nginx-config
configMap:
name: sidecar
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 8080 # <-- the traffic is routed to the proxy
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sidecar
data:
nginx.conf: |-
events {}
http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
location / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db- NGINX Open Sourceを実行するサイドカーコンテナがポート8080で起動します。
- NGINXはすべてのトラフィックをアプリに転送します。
- (その他の文字列に加えて)
SELECTまたはUNIONを含むリクエストはすべて拒否されます(ConfigMapセクションの最初のlocationブロックを参照してください)。 - アプリのサービスは、すべてのトラフィックを最初にNGINXコンテナへルーティングします。
- サイドカーをデプロイします:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured
service/app configured
configmap/sidecar created
deployment.apps/db unchanged
service/db unchanged Sidecarをフィルターとしてテスト
アプリに戻ってSQLインジェクションを再度試し、サイドカーがトラフィックをフィルタリングしているかどうかをテストします。NGINXは、リクエストがアプリに到達する前にブロックします。
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 
課題4:リクエストをフィルタリングするようにNGINX Ingress Controllerを構成
「チャレンジ3」のようにアプリを保護する方法は、学習体験としては興味深いものですが、次の理由から本番環境での使用は推奨しません。
- 包括的なセキュリティソリューションではありません。
- スケーラビリティがありません(この保護を複数のアプリに簡単に適用することはできません)。
- 更新が複雑で非効率です。
はるかに優れた解決策は、NGINX Ingress Controllerを使用して、同じ保護をすべてのアプリに拡張することです。Ingress Controllerを使用すれば、Webアプリケーションファイアウォール(WAF)のようなリクエストのブロックから認証や認可まで、さまざまなセキュリティ機能を一元化できます。

このチャレンジでは、NGINX Ingress Controllerをデプロイし、トラフィックルーティングを設定し、フィルターがSQLインジェクションをブロックすることを確認します。
NGINX Ingress Controllerのデプロイ
NGINX Ingress Controllerを最も迅速にインストールするには、Helmを使用します。
- NGINXリポジトリをHelmに追加します:
$ helm repo add nginx-stable https://helm.nginx.com/stable - NGINX Open SourceベースのNGINX Ingress Controller(F5 NGINXが保守)をダウンロードしてインストールします。
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
NAME: main
LAST DEPLOYED: Day Mon DD hh:mm:ss YYYY
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES: The NGINX Ingress Controller has been installed. - Confirm that the NGINX Ingress Controller pod deployed, as indicated by the value
RunningがSTATUS列に表示され、NGINX Ingress Controllerポッドがデプロイされたことを確認します。
$ kubectl get pods NAME READY STATUS ...
main-nginx-ingress-779b74bb8b-mtdkr 1/1 Running ...
... RESTARTS AGE
... 0 18sアプリへのトラフィックのルーティング
- 次の内容で3-ingress.yamlというYAMLファイルを作成します(またはGitHubからコピーします)。このファイルでは、アプリへのトラフィックのルーティングに必要なIngressマニフェストを定義します(今回はサイドカープロキシを経由しません)。
annotations:ブロックでは、スニペットを使用してNGINX Ingress Controllerの設定をカスタマイズしています。ここには、チャレンジ3のConfigMap定義と同じlocationブロックがあり、特定の文字列(SELECTやUNIONなど)を含むリクエストを拒否します。
apiVersion: v1 kind: Service
metadata:
name: app-without-sidecar
spec:
ports:
- port: 80
targetPort: 80
selector:
app: app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: entry
annotations:
nginx.org/server-snippets: |
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
spec:
ingressClassName: nginx
rules:
- host: "example.com"
http:
paths:
- backend:
service:
name: app-without-sidecar
port:
number: 80
path: /
pathType: Prefix - Ingressリソースをデプロイします:
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created
ingress.networking.k8s.io/entry created フィルターの動作確認
- 使い捨てのBusyBoxコンテナを起動し、正しいホスト名を指定してNGINX Ingress Controllerポッドにリクエストを送信します。
$ kubectl run -ti --rm=true busybox --image=busybox$ wget --header="Host: example.com" -qO- main-nginx-ingress
<!DOCTYPE html>
<html lang="en">
<head>
# ...- SQLインジェクションを試行します。NGINXが攻撃をブロックしたことは、ステータスコード「
403Forbidden」で確認できます。
$ 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: server returned error: HTTP/1.1 403 Forbidden 次のステップ
Kubernetesはデフォルトではセキュアではありません。Ingressコントローラーを使用すると、SQLインジェクションをはじめとする多くの脆弱性を緩和できます。ただし、ここでNGINX Ingress Controllerを使用して実装したWAFに類似する機能は、実際のWAFの代わりにはならず、アプリをセキュアに設計することの代替にもなりません。熟練したハッカーであれば、コードにわずかな変更を加えるだけで、引き続き「UNION」を使った攻撃を成功させることができます。このトピックの詳細については、「ペネトレーションテスター向けSQLインジェクション(SQLi)ガイド」を参照してください。
とはいえ、Ingressコントローラーは、セキュリティの大部分を一元化するための強力なツールです。認証と認可(mTLS、シングルサインオン)の一元化や、F5 NGINX App Protect WAFのような堅牢なWAFの導入など、効率とセキュリティの向上につながります。
アプリケーションやアーキテクチャが複雑な場合は、よりきめ細かな制御が必要になることがあります。組織でゼロトラストとエンドツーエンド暗号化が必要な場合は、常時無料で利用できるF5 NGINX Service Meshのようなサービスメッシュを使用して、Kubernetesクラスター内のサービス間通信(East-Westトラフィック)を制御することをご検討ください。サービスメッシュについては、ユニット4「高度なKubernetesデプロイ戦略」で説明します。
NGINX Open Sourceの入手方法とデプロイ方法の詳細については、nginx.orgをご覧ください。
NGINX App Protectを搭載したNGINX PlusベースのNGINX Ingress Controllerをお試しいただくには、今すぐ30日間の無料トライアルを開始するか、ユースケースについてご相談ください。
NGINX Open SourceベースのNGINX Ingress Controllerをお試しいただくには、F5のGitHubリポジトリで「NGINX Ingress Controller Releases」を参照するか、DockerHubからビルド済みコンテナをダウンロードしてください。
関連するブログ記事
チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。
AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。
JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。
F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。
NGINX PlusによるAWS Auto Scalingグループの負荷分散
新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります
