NGINX.COM
Web Server Load Balancing with NGINX Plus

注:本教程是Microservices June 2022 微服务之月项目第四单元“微服务的安全防护模式”的配套实验手册,您可点此报名参与这一免费线上教学项目以获取更多学习资源。

您在当地一家商店工作,商店销售从枕头到自行车在内的各类商品,您所在部门是 IT 部门。这家商店十分受欢迎,他们即将推出首个在线商店,但在向大众推出之前,他们请安全专家对网站进行了渗透测试。很不幸,安全专家发现了一个问题!在线商店容易遭到 SQL 注入攻击。安全专家能够利用该网站获取数据库中的敏感信息,包括用户名和密码。

您的团队找到了您 —— Kubernetes工程师来解决这个问题。幸运的是,您知道可以通过 Kubernetes 流量管理工具来规避 SQL 注入及其他漏洞。您部署了一个 Ingress controller 来暴露应用,能够确保此漏洞将不会被攻击者利用了。现在,在线商店可以准时上线了。干得漂亮!

 

实验和教程概述

本文是“Microservices June 2022 微服务之月”第四单元“微服务的安全防护模式”的配套文档,但您也可以在自己的环境中将其当作教程使用(从我们的 GitHub 仓库中获取示例)。它演示了如何使用 NGINX 和 NGINX Ingress Controller 拦截 SQL 注入攻击。

为了完成实验,您需要一台具有以下配置的电脑:

  • 至少 2 个 CPU
  • 2GB 可用内存
  • 20GB 可用磁盘空间
  • 互联网连接
  • 容器或虚拟机管理器,例如 Docker、HyperKit、Hyper-V、KVM、Parallels、Podman、VirtualBox 或 VMware Fusion/Workstation
  • 装有minikube

    根据您的环境,您可能需要设置代理以便安装能够正常进行。具体可参考https://minikube.sigs.k8s.io/docs/handbook/vpn_and_proxy/
  • 装有 Helm

注:本文提到的 minikube 需在可以启动浏览器窗口的台式/笔记本电脑上运行。如果您所处的环境做不到这一点,那么您需要解决如何通过浏览器访问服务的问题。

为了充分利用实验室和教程,我们建议在您开始实验之前:

本教程使用了以下技术:

本教程涉及到四个挑战:

  1. 部署集群和易受攻击的应用
  2. 入侵应用
  3. 使用 NGINX Sidecar 容器阻止某些请求
  4. 配置 NGINX Ingress Controller 以过滤请求

 

挑战 1:部署集群和易受攻击的应用

在这项挑战中,您需要部署 minikube 集群并安装 Podinfo 作为示例应用和 API。

创建 Minikube 集群

部署 minikube 集群。几秒钟后将出现一条确认部署成功的消息。

$ minikube start 
  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

创建易受攻击的应用

步骤 1:创建 Deployment
您将部署一个简单的在线商店应用,其中包括两个微服务:

  • MariaDB 数据库
  • 连接到数据库并检索数据的 PHP 应用
  1. 使用您选择的文本编辑器,创建一个名为 1-app.yaml 的 YAML 文件,该文件应包含以下内容:
  2. 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 
    
  3. 部署应用和 API:
  4. $ kubectl apply -f 1-app.yaml 
    deployment.apps/app created 
    service/app created 
    deployment.apps/db created 
    service/db created 
    
  5. 确认 pods 已经部署成功,如 STATUS 列中的值 Running 所示。它们可能需要 30-40 秒才能完全部署完成,所以在继续下一步之前,有必要再次运行该命令,以确认所有 pod 都在运行。
  6. $ kubectl get pods  
    NAME                  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...
    

    注:如果指定app参数无法显示,可使用 minikube service --all显示

     

    挑战 2:入侵应用

    示例应用非常简单。它包含了一个包含商品列表(例如枕头)的主页和一组提供详细信息(例如描述和价格)的产品页面。数据存储在 MariaDB 数据库中。每次请求页面时,都会向数据库发出 SQL 查询。

    • 对于主页,其页面将检索数据库中的所有商品。
    • 对于产品页面,商品将由 ID 获取。

    如果打开“枕头”产品页面,您可能会注意到 URL 以 /product/1 结尾。URL 末尾的“1”用于识别产品的 ID。为了防止将恶意代码直接插入到 SQL 查询中,最好的做法是在处理请求之前对用户输入进行筛查。但是,如果该应用未正确配置,并且在将用户输入插入 SQL 查询和数据库之前未转义,会发生什么?

    我们将通过一个简单的实验确定输入是否被正确转义:将 ID 更改为数据库中不存在的 ID。

    1. 修改 URL:
    2. 将 URL 结尾从 1 手动更改为 -1。这将返回错误消息 Invalid product id “-1”,说明产品的 ID 没有转义,原因是字符串直接插入到了查询中。大事不妙! (除非你是黑客。)

      我们可以假设数据库查询类似于 SELECT * FROM some_table WHERE id = "1"。如果想要利用它,我们可以将 1 替换为 -1″ -- //,那么:

      • 第一个引用 ” 完成了第一个查询。
      • 我们在该引用之后添加自己的查询。
      • -- //序列丢弃查询的其余部分。

      如果您要将 URL 末尾更改为 -1" or 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

      现在,我们可以操作结果了,我们可以使用 -1" OR 1 ORDER BY id DESC -- // 按 ID 对数据库结果进行排序。这将打开产品页面,其中包含了数据库中的最后一件商品。

      强制数据库对结果进行排序十分有趣,但如果我们想干坏事的话就没什么用了。也许我们还可以从数据库中提取更多信息,例如用户数据。

    3. 提取用户数据:
    4. 我们可以假设数据库中有一个包含用户名和密码的用户表。但是,我们如何从产品表跨越到用户表呢?

      我们可以通过 -1″ UNION SELECT \* FROM users -- //来实现。

      • -1" 强制从第一个查询返回一个空集。
      • UNION 将两个数据表(如产品表和用户表)强制放在一起,这样黑客便能够获取原本不应与原表(产品)关联的信息(密码)。
      • UNION SELECT * FROM users 从用户表中选择所有行。
      • -- //序列丢弃后面的所有内容。

      当我们将 URL 结尾修改为 -1" UNION SELECT * FROM users -- //,我们收到新的错误消息:

      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

      该消息告诉我们,产品表和用户表的列数不同,因此不能执行 UNION 声明。我们可以通过向 SELECT 添加列,反复进行试验,从而确定列数。用户表中可能有一个密码字段,因此我们可以尝试以下排列(注意,每个版本都会添加一个列):

      -1" UNION SELECT password FROM users; -- // <-- 1 column 
      -1" UNION SELECT password,password FROM users; -- // <-- 2 columns 
      -1" UNION SELECT password,password,password FROM users; -- // <-- 3 columns 
      -1" UNION SELECT password,password,password,password FROM users; -- // <-- 4 columns 
      -1" UNION SELECT password,password,password,password,password FROM users; -- // <-- 5 columns 
      

      成功!当我们使用五列的语句时就成功了。此响应显示了用户的密码。

      现在,我们知道用户表中共包含五列,所以我们可以使用相同的策略继续尝试查找其他列名。获取与您公开的密码相对应的用户名是不是很有用?以下查询暴露了用户表中的用户名和密码。这太好了 —— 除非该应用托管在您的基础架构上!

      -1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //  
      

     

    挑战 3:使用 NGINX Sidecar 容器阻止某些请求

    当然,该应用的开发人员更应注意转义用户输入(例如使用参数化查询),但是您 —— Kubernetes工程师也可以通过防止此攻击到达应用来帮助避免 SQL 注入。这样,即使应用容易遭受攻击,也可以及时将攻击拦截下来。

    保护应用的选项非常多。在接下来的实验中,我们将重点关注两个选项:

    1. 代理所有前往 pod 中应用的流量。
    2. 使用 Ingress controller 过滤所有进入集群的流量。

    这项挑战探讨了第一个选项的实现方式,即通过注入 sidecar 容器来过滤流量。我们将 NGINX 开源版用作 pod 内的 sidecar 容器 来代理所有流量,并拒绝 URL 中包含 UNION 语句的任何请求。

    注:本教程使用该技术仅仅是为了方便说明。实际上,将代理手动部署为 sidecar 不是最好的解决方案(稍后将详细介绍)。

    将 NGINX 开源版部署为 sidecar

    1. 创建一个名为 2-app-sidecar.yaml 的 YAML 文件(包含以下内容),并检查这些重要组件:
      • 运行 NGINX 的 sidecar 容器在端口 8080 上启动。
      • NGINX 进程将所有流量转发到应用。
      • 任何包含 SELECTUNION 关键字的请求都会被拒绝。
      • 应用的服务首先将所有流量路由到 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. 部署 sidecar:
    3. $ 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 注入来测试 sidecar 是否过滤流量。NGINX 会在其到达应用之前阻止该请求!

    -1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 
    

     

    挑战 4:配置 NGINX Ingress Controller 以过滤请求

    最后一项挑战中介绍的应用保护方式不仅有趣,而且具有教育意义,但不建议在生产环境中使用,原因如下:

    • 它不是一个完整的安全防护解决方案。
    • 它不支持扩展(您无法轻松将此防护措施应用到多个应用)。
    • 更新过程复杂低效。

    使用 Ingress controller 将相同的功能扩展到所有应用是一种更好的解决方案!Ingress controller 可用于集中管理从 Web 应用防火墙 (WAF) 到身份验证和授权在内的各种安全功能。

    部署 NGINX Ingress Controller

    安装 NGINX Ingress Controller 最快的方法是使用 Helm

    1. 将 NGINX 仓库添加到 Helm:
    2. $ helm repo add nginx-stable https://helm.nginx.com/stable  
      
    3. 下载并安装基于 NGINX 开源版的 NGINX Ingress Controller(由 F5 NGINX 维护)。请注意,此命令包括 enableSnippets=true。代码段将用于配置 NGINX 以阻止 SQL 注入。最后一行输出结果确认安装成功。
    4. 
      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: Tue Feb 22 19:49:17 2022  
      NAMESPACE: default  
      STATUS: deployed  
      REVISION: 1  
      TEST SUITE: None  
      NOTES: The NGINX Ingress Controller has been installed.  
      
    5. 确认 NGINX Ingress Controller pod 已经部署成功,如 STATUS 列中的值 Running 所示。
    6. $ kubectl get pods   
      NAME                                  READY   STATUS    RESTARTS   AGE  
      main-nginx-ingress-779b74bb8b-mtdkr   1/1     Running   0          18s  
      

    将流量路由到您的应用

    1. 创建一个名为 3-ingress.yaml 的 YAML 文件,该文件应包含以下内容。它定义了将流量路由到应用所需的 Ingress manifest(这一次,流量将不会通过 sidecar 代理)。请注意,NGINX Ingress Controller 使用定义为注释的代码段进行了自定义,该代码段包含与最后一项挑战相同的注入到 sidecar 容器中的代码行。
    2. 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 
      
    3. 部署 Ingress 资源:
    4. $ kubectl apply -f 3-ingress.yaml  
      service/app-without-sidecar created 
      ingress.networking.k8s.io/entry created 
      

    测试过滤器

    您需要一个新的 URL 将流量路由到 Ingress Controller 侦听的端口。要获取 URL,请启动临时的 busybox 容器,该容器使用正确的主机名向 NGINX Ingress pod 发出请求。

    $ kubectl run -ti --rm=true busybox --image=busybox 
    $ wget --header="Host: example.com" -qO- main-nginx-ingress 
    <!DOCTYPE html> 
    <html lang="en"> 
    
    <head> 
    # truncated output 
    

    现在,尝试实施 SQL 注入。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 controller 可以规避 SQL(和许多其他)漏洞。但是请记住:即使您刚刚实施了类似 WAF 的功能,Ingress controller 既不能取代 Web 应用防火墙 (WAF),也不能替代安全架构应用。狡诈的黑客仍然可以通过对 UNION 代码做一些手脚来实施攻击。

    也就是说,Ingress controller 是一款强大的工具,能够集中管理您的大部分安全防护功能,为您带来更出色的效率和安全性,包括身份验证和授权用例(mTLS,单点登录),甚至稳健的 WAF(如 NGINX App Protect WAF)。

    如欲继续深入了解和本实验相关的知识技能,您可以加入到 Microservices June 2022 微服务之月项目中来,并继续浏览本项目第四单元“微服务的安全防护模式” 的其他学习资源。

    如欲试用适用于 Kubernetes 且基于 NGINX Plus 的 NGINX Ingress Controller 和 NGINX App Protect,请立即下载 30 天免费试用版或者联系我们讨论您的用例

    如欲试用 NGINX 开源版 NGINX Ingress Controller,您可以获取源代码进行编译,或者从 DockerHub 下载预构建的容器。

Hero image
Kubernetes:
从测试到生产

通过多种流量管理工具提升弹性、可视性和安全性

关于作者

Daniele Polencic

Managing Director

关于作者

NGINX 中文社区官方团队

NGINX

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。