ブログ | NGINX

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

ダニエレ・ポレンチッチ サムネイル
ダニエレ・ポレンチッチ
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 
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

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

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

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

次の手順を実行します。

  1. 任意のテキスト エディターを使用して、次の内容を含む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 
    
  2. アプリと API をデプロイします。

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

    $ kubectl get podsNAME                  READY   STATUS    RESTARTS   AGE 
    app-d65d9b879-b65f2   1/1     Running   0          37s 
    db-7bbcdc75c-q2kt5    1/1     Running   0          37s 
    
  4. ブラウザでアプリを開きます:

    $ 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 によって取得されます。

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

エクスプロイト 1

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

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

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

SELECT * FROM some_table WHERE id = "1"

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

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

たとえば、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 つのデータベース テーブル (この場合は、製品ユーザー) を強制的に結合し、元の (製品) テーブルにはない情報 (パスワード) を取得できるようにします。
  • SELECT * FROM users は、 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 つずつ追加することで、試行錯誤を通じて列の数を調べることができます。 ユーザーテーブルのフィールド名は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; -- //

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

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

-1" UNION SELECT username,username,password,password,username 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 コンテナにルーティングします。
    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
    
  2. サイドカーをデプロイします。

    $ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured 
    service/app configured 
    configmap/sidecar created 
    deployment.apps/db unchanged 
    service/db unchanged 
    

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

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

-1" UNION SELECT username,username,password,password,username 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 repo add 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
    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.  
    
  3. STATUS列の値がRunningであることで示されるように、NGINX Ingress Controller ポッドがデプロイされていることを確認します。 

    $ kubectl get pods   NAME                                READY   STATUS  ...
    main-nginx-ingress-779b74bb8b-mtdkr 1/1     Running ...
    
          ... RESTARTS   AGE 
          ... 0          18s
    

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

  1. 次の内容を含む3-ingress.yamlという YAML ファイルを作成します (またはGitHub からコピーします)。 これは、トラフィックをアプリにルーティングするために必要な Ingress マニフェストを定義します (今回はサイドカー プロキシを経由しません)。 チャレンジ 3 の ConfigMap 定義と同じlocationブロックを使用して NGINX Ingress Controller 構成をカスタマイズするためにスニペットが使用されている、 annotations:ブロックに注目してください。このブロックは、(他の文字列とともに) 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 
    
  2. Ingress リソースをデプロイします。 
  3. $ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created 
    ingress.networking.k8s.io/entry created 
    

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

  1. 使い捨ての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> 
    # ...
    
  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: server returned error: HTTP/1.1 403 Forbidden 
    

次のステップ

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 の一部です。 q。"