Kubernetes Ingress — NGINX

Prodan Labs 等级 199 0 0

Kubernetes Ingress — NGINX 在 Kubernetes 中,Service 是一种抽象的概念,它定义了每一组 Pod 的逻辑集合和访问方式,并提供一个统一的入口,将请求进行负载分发到后端的各个 Pod 上。Service 默认类型是 ClusterIP,集群内部的应用服务可以相互访问,但集群外部的应用服务无法访问。为此 Kubernetes 提供了 NodePorts,LoadBalancer 和 Ingress 三种外部访问 Kubernetes 集群的方式。

Ingress 是 Kubernetes 中的一个 API 对象(在1.19版本GA),它提供路由规则来管理外部用户对 Kubernetes 集群中服务的访问。Ingress Controller 是 Ingress API 的实际实现,通过和 Kubernetes API 交互,动态的去感知集群中 Ingress 规则变化,将外部流量路由到 Kubernetes 集群,同时提供负载平衡,并负责L4-L7网络服务。 Kubernetes Ingress — NGINX

目前社区上的 Ingress Controller 有十几种,如 Nginx Ingress、Kong、Traefik、Istio Ingress、APISIX 等,可根据自己的功能需求选型。

Nginx Ingress

Nginx Ingress 是由 Kubernetes SIGs 小组开发的。顾名思义,它基于 nginx,并补充了一组用于实现额外功能的 Lua 插件。由于 nginx 的普及以及在用作控制器时对其进行的最小改动,对于大部分人来说,它可能是最简单,最直接的选择。 Kubernetes Ingress — NGINX

Nginx Ingress 安装非常简单,在裸机上部署的 kubernetes 集群,使用 NodePort

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.42.0/deploy/static/provider/baremetal/deploy.yaml

其他环境查看社区文档。

[root@k8s-test01 ~]# kubectl get po,svc -n ingress-nginx 
NAME                                            READY   STATUS      RESTARTS   AGE
pod/ingress-nginx-admission-create-xtjfl        0/1     Completed   0          97m
pod/ingress-nginx-admission-patch-wx2jt         0/1     Completed   0          97m
pod/ingress-nginx-controller-848bfcb64d-6spj4   1/1     Running     0          97m

NAME                                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                 AGE
service/ingress-nginx-controller             NodePort    10.254.14.4     <none>        80:80/TCP,443:443/TCP   97m
service/ingress-nginx-controller-admission   ClusterIP   10.254.94.128   <none>        443/TCP                 97m
[root@k8s-test01 ~]# 

创建一个 ingress 示例

[root@k8s-test01 ~]# cat ingress-nginx-demo.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wildcard-host
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: "foo.bar.com"
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: nginx
            port:
              number: 80
[root@k8s-test01 ~]# kubectl describe ingress ingress-wildcard-host 
Name:             ingress-wildcard-host
Namespace:        default
Address:          172.31.9.226
Default backend:  default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
  Host         Path  Backends
  ----         ----  --------
  foo.bar.com  
               /   nginx:80 (192.168.47.155:80)
Annotations:   kubernetes.io/ingress.class: nginx
Events:
  Type    Reason  Age                  From                      Message
  ----    ------  ----                 ----                      -------
  Normal  Sync    103s (x2 over 111s)  nginx-ingress-controller  Scheduled for sync
[root@k8s-test01 ~]# kubectl get po -o wide | grep nginx
nginx-546585459c-zxfmh   1/1     Running     0          94m   192.168.47.155   k8s-test02   <none>           <none>
[root@k8s-test01 ~]# 

Kubernetes Ingress — NGINX

  1. NGINX Ingress Controller 提供了有三种方式配置 NGINX :
  2. ConfigMap:使用 ConfigMap 在 NGINX 中设置全局配置。
  3. Annotations:在特定 Ingress 规则的特定配置。
  4. Custom template:当需要更具体的设置(如打开文件缓存)时,可以使用自定义 nginx 模板。

配置 SSL

通过 Annotations 来配置某一个 ingress 使用 SSL。

创建secret

kubectl create secret tls ingress-cert --key=fullchain.com.key --cert=fullchain.cer 

创建Ingress

[root@k8s-test01 cert]# cat ingress-nginx-demo.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wildcard-host
  annotations:
    kubernetes.io/ingress.class: nginx
    ingress.kubernetes.io/force-ssl-redirect: "true"
    kubernetes.io/tls-acme: "true"
spec:
  rules:
  - host: "foo.xxx.com"
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: nginx
            port:
              number: 80
  tls:
   - secretName: ingress-cert
[root@k8s-test01 cert]# kubectl create -f  ingress-nginx-demo.yaml
ingress.networking.k8s.io/ingress-wildcard-host created

Kubernetes Ingress — NGINX

配置ModSecurity防火墙与OWASP规则

ModSecurity是一个免费、开源的 Apache 模块,用于入侵探测与拦截,目前已经支持 Nginx,可以充当 Web 应用防火墙(WAF),旨在增强 Web 应用程序的安全性和避免遭受来自已知与未知的攻击。而 OWASP 是安全社区开发和维护的一套免费的应用程序保护规则,是 MoodSecurity 的核心规则集。Nginx-ingress 集成了 ModSecurity 模块和 OWASP 规则,默认没有开启。

测试简单 XSS 攻击,没开启 Modsecurity 之前: Kubernetes Ingress — NGINX

状态200,没有拦截。

开启 Modsecurity 模块

[root@k8s-test01 ~]# cat ingress-nginx-demo.yaml 
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wildcard-host
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/enable-modsecurity: "true"
    nginx.ingress.kubernetes.io/enable-owasp-modsecurity-crs: "true"
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On
      SecRequestBodyAccess On
      SecAuditEngine RelevantOnly
      SecAuditLogParts ABIJDEFHZ
      SecAuditLog /var/log/nginx/modsec_audit.log
      Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf
spec:
  rules:
  - host: "foo.bar.com"
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: nginx
            port:
              number: 80
[root@k8s-test01 ~]#

再进行 XSS 攻击测试 Kubernetes Ingress — NGINX

状态403,已拦截。

查看 Nginx 拦截日志

2020/12/27 09:37:56 [error] 3001#3001: *213189 [client 47.242.91.20] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `5' ) [file "/etc/nginx/owasp-modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "80"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [data ""] [severity "2"] [ver "OWASP_CRS/3.3.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "192.168.123.79"] [uri "/"] [unique_id "160906187674.566940"] [ref ""], client: 47.242.91.20, server: foo.bar.com, request: "HEAD /?search=<scritp>alert(xss);</script> HTTP/1.1", host: "foo.bar.com"

获取客户端真实IP

Nginx Ingress 部署使用 nodePort 模式, 在讲获取客户端真实 IP 之前,我们大概了解下 nodePort 模式的链路。从 Kubernetes 1.5 开始,NodePort 类型的 Services 的数据包默认进行源地址 NAT。

假设某一个 Pod 运行在 node1 节点,客户端访问 node2:nodeport,过程如下:

            client
             \ ^
              \ \
               v \
   node 1 <--- node 2
    | ^   SNAT
    | |   --->
    v |
 endpoint

1、客户端发送数据包到 node2:nodePort 2、node2 使用它自己的 IP 地址替换数据包的源 IP 地址(SNAT) 3、node2 使用 pod IP 地址替换数据包的目的 IP 地址 4、数据包被路由到 node1,然后交给 endpoint 5、Pod 的回复被路由回 node2 6、Pod 的回复被发送回给客户端

所以 nodePort 模式下源地址被转换了(SNAT),服务端获取的并不是正确的客户端 IP,它们是集群的内部 IP。为什么会这样呢?原因是为了支持从任一节点IP+NodePort 都可以访问应用,而不得不做的 SNAT。

当然,Kubernetes 也提供了一个特性来保留客户端的源 IP 地址,通过设置 externalTrafficPolicy 的值为 Local,请求就只会被代理到本地 endpoints 而不会被转发到其它节点。这样就保留了最初的源 IP 地址。

---
# Source: ingress-nginx/templates/controller-service.yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
  labels:
    helm.sh/chart: ingress-nginx-3.17.0
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/version: 0.42.0
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/component: controller
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  externalTrafficPolicy: Local
  type: NodePort
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: http
      nodePort: 80
    - name: https
      port: 443
      protocol: TCP
      targetPort: https
      nodePort: 443
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/component: controller

但是,如果没有本地 endpoints,发送到这个节点的数据包将会被丢弃。即请求 node2:nodePort , 但 node2 上没有运行 Pod , 故本地没有 endpoints ,所以请 node2:nodePort 是失败的,node1:nodePort 正常。

          client
       ^ /   \
      / /     \
     / v       X
   node 1     node 2
    ^ |
    | |
    | v
 endpoint

所以 Nginx Ingress如需获取客户端真实 IP 需要设置 externalTrafficPolicy 或设置容器网络使用主机模式 hostNetwork: true ,然后 daemonset 部署,或通过亲和性把 Pod 固定在某些节点,客户端访问 Pod 所在的节点,源 IP 地址便不会 SNAT。

Nginx Ingress 获取客户端真实 IP 的配置如下:

---
# Source: ingress-nginx/templates/controller-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    helm.sh/chart: ingress-nginx-3.17.0
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/version: 0.42.0
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/component: controller
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  server-tokens: "false"
  forwarded-for-header: "X-Forwarded-For"
  use-forwarded-headers: "true"
  compute-full-forwarded-for: "true"

还可以通过http-snippet 获取 客户端真实 IP

data:
  server-tokens: "false"
  http-snippet: |
    real_ip_header X-Forwarded-For;
    set_real_ip_from 0.0.0.0/0;

通过日志可以看到已经获取到真实的IP Kubernetes Ingress — NGINX

当 Nginx Ingress 在转发请求时会通过 X-Forwarded-For 和 X-Real-IP 字段来记录客户端源 IP,后端可以通过此字段获得客户端真实源 IP,可以写一个简单的程序来验证下

package main

import (
    "log"
    "net"
    "net/http"
    "strings"
)

func myHandle(w http.ResponseWriter, r *http.Request) {
    _, _ = w.Write([]byte(remoteIP(r)))
}

func main() {

    serveMux := http.NewServeMux()
    serveMux.HandleFunc("/", myHandle)

    err := http.ListenAndServe("0.0.0.0:80", serveMux)
    if err != nil {
        log.Printf("http.ListenAndServe():%v\n", err)
        return
    }
}
func remoteIP(r *http.Request) string {
    ip := r.Header.Get("X-Original-Forwarded-For")
    log.Printf("X-Original-Forwarded-For : %s", r.Header.Get("X-Original-Forwarded-For"))
    if ip != "" {
        return ip
    }

    ip = r.Header.Get("X-Forwarded-For")
    log.Printf("X-Forwarded-For: %s", r.Header.Get("X-Forwarded-For"))
    if ip != "" {
        return ip
    }

    ip = r.Header.Get("X-Real-Ip")
    log.Printf("X-Real-Ip : %s", r.Header.Get("X-Real-Ip"))
    if ip != "" {
        return ip
    }

    if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
        return ip
    }

    return ""
}

程序运行日志

[root@k8s-test01 ~]# kubectl cp app nginx-d8d5f47c9-n9bnm:/
[root@k8s-test01 ~]# kubectl exec -it nginx-d8d5f47c9-n9bnm -- bash
root@nginx-d8d5f47c9-n9bnm:/# ./app 
2020/12/27 10:40:57 X-Original-Forwarded-For : 
2020/12/27 10:40:57 X-Forwarded-For: 47.112.119.36

路由配置

通过 iris 框架写一个简单的测试程序

func NewAPP() *iris.Application {
    // 创建app结构体对象
    app := iris.New()
    // 配置字符编码
    app.Configure(iris.WithConfiguration(iris.Configuration{
        Charset: "UTF-8",
    }))

    // 配置日志
    customLogger := logger.New(logger.Config{
        //状态显示状态代码
        Status: true,
        // IP显示请求的远程地址
        IP: true,
        //显示http方法
        Method: true,
        // Path显示请求路径
        Path: true,
        // Query将url查询附加到Path。
        Query: true,
        //Columns:true,
        // 如果不为空然后它的内容来自`ctx.Values(),Get("logger_message")
        //将添加到日志中。
        MessageContextKeys: []string{"logger_message"},
        //如果不为空然后它的内容来自`ctx.GetHeader(“User-Agent”)
        MessageHeaderKeys: []string{"User-Agent"},
    })
    // 捕获所有http错误:
    app.OnAnyErrorCode(customLogger, func(ctx iris.Context) {
        switch ctx.GetStatusCode() {
        case 404:
            ctx.Values().Set("logger_message", "a dynamic message passed to the logs")
            ctx.Writef("My Custom 404 error page")
        default:
            ctx.Values().Set("logger_message", "a dynamic message passed to the logs")
            ctx.Writef("%v unknown error page", ctx.GetStatusCode())
        }
    })
    app.Use(customLogger)

    // favicons
    app.Favicon("./static/favicons/favicon.ico")

    // static
    app.HandleDir("/", "static")

    return app
}

入口程序

func main() {
    app := config.NewAPP()
    resAPI := app.Party("/api/v1")
    resAPI.Get("/namespaces",handle.GetNameSpace)
    resAPI.Get("/ip",handle.GetIP)
    resAPI.Get("/hostname",handle.GetHostname)
    app.Run(iris.Addr("0.0.0.0:8080"), iris.WithoutServerError(iris.ErrServerClosed), iris.WithOptimizations)
}

创建Ingress

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/enable-modsecurity: "true"
    nginx.ingress.kubernetes.io/enable-owasp-modsecurity-crs: "true"
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On
      SecRequestBodyAccess On
      SecAuditEngine RelevantOnly
      SecAuditLogParts ABIJDEFHZ
      SecAuditLog /var/log/nginx/modsec_audit.log
      Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf
spec:
  rules:
  - host: "demo.bar.com"
    http:
      paths:
      - pathType: Prefix
        path: "/api/v1"
        backend:
          service:
            name: demo
            port:
              number: 8080
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: demo
            port:
              number: 8080

Kubernetes Ingress — NGINX

静态资源加一级路由测试,重写路径

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/enable-modsecurity: "true"
    nginx.ingress.kubernetes.io/enable-owasp-modsecurity-crs: "true"
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On
      SecRequestBodyAccess On
      SecAuditEngine RelevantOnly
      SecAuditLogParts ABIJDEFHZ
      SecAuditLog /var/log/nginx/modsec_audit.log
      Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf
    nginx.ingress.kubernetes.io/app-root: /test/
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/configuration-snippet: |
      rewrite ^/css/(.*)$ /test/css/$1 redirect;
      rewrite ^/js/(.*)$ /test/js/$1 redirect;
      rewrite ^/img/(.*)$ /test/img/$1 redirect;
spec:
  rules:
  - host: "demo.bar.com"
    http:
      paths:
      - pathType: Prefix
        path: "/api/v1"
        backend:
          service:
            name: demo
            port:
              number: 8080
      - pathType: Prefix
        path: "/test(/|$)(.*)"
        backend:
          service:
            name: demo
            port:
              number: 8080

显示正常。(这里的前端样式文件用的是相对路径) Kubernetes Ingress — NGINX

开启压缩

开启压缩前 Kubernetes Ingress — NGINX

开启压缩

    nginx.ingress.kubernetes.io/server-snippet: |
        gzip on;
        gzip_disable "MSIE [1-6]\.";
        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 5;
        gzip_min_length 512;
        gzip_buffers 16 128k;
        gzip_http_version 1.1;
        gzip_types
            application/json
            application/javascript
            application/xml
            application/x-javascript
            application/vnd.api+json
            application/json
            application/x-font-ttf
            text/javascrip
            text/css
            text/plain
            image/jpeg
            image/png
            image/jpg
            image/svg+xml
            image/x-icon;

Kubernetes Ingress — NGINX

其他

Nginx Ingress 的配置参数与 Nginx 相差无几,更多配置请参考官方文档:

https://www.helloworld.net/redirect?target=https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/

感兴趣的读者可以关注下微信号 Kubernetes Ingress — NGINX

收藏
评论区