在 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网络服务。
目前社区上的 Ingress Controller 有十几种,如 Nginx Ingress、Kong、Traefik、Istio Ingress、APISIX 等,可根据自己的功能需求选型。
Nginx Ingress
Nginx Ingress 是由 Kubernetes SIGs 小组开发的。顾名思义,它基于 nginx,并补充了一组用于实现额外功能的 Lua 插件。由于 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 ~]#
- NGINX Ingress Controller 提供了有三种方式配置 NGINX :
- ConfigMap:使用 ConfigMap 在 NGINX 中设置全局配置。
- Annotations:在特定 Ingress 规则的特定配置。
- 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
配置ModSecurity防火墙与OWASP规则
ModSecurity是一个免费、开源的 Apache 模块,用于入侵探测与拦截,目前已经支持 Nginx,可以充当 Web 应用防火墙(WAF),旨在增强 Web 应用程序的安全性和避免遭受来自已知与未知的攻击。而 OWASP 是安全社区开发和维护的一套免费的应用程序保护规则,是 MoodSecurity 的核心规则集。Nginx-ingress 集成了 ModSecurity 模块和 OWASP 规则,默认没有开启。
测试简单 XSS 攻击,没开启 Modsecurity 之前:
状态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 攻击测试
状态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
当 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
静态资源加一级路由测试,重写路径
---
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
显示正常。(这里的前端样式文件用的是相对路径)
开启压缩
开启压缩前
开启压缩
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;
其他
Nginx Ingress 的配置参数与 Nginx 相差无几,更多配置请参考官方文档:
https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/
感兴趣的读者可以关注下微信号