k8s生產實踐之獲取客戶端真實IP


1、概述

通常web應用獲取用戶客戶端的真實ip一個很常見的需求,例如將用戶真實ip取到之后對用戶做白名單訪問限制、將用戶ip記錄到數據庫日志中對用戶的操作做審計等等

vm時代是一個比較容易解決的問題,但當一切雲原生化(容器化)之后變得稍微復雜了些

k8s中運行的應用通過Service抽象來互相查找、通信和與外部世界溝通,在k8s中是kube-proxy組件實現了Service的通信與負載均衡,流量在傳遞的過程中經過了源地址轉換SNAT,因此在默認的情況下,常常是拿不到用戶真實的ip

這個問題在k8s官方文檔(https://kubernetes.io/zh/docs/tutorials/services/source-ip/)中基於Cluster IPNodePortLoadBalancer三種不同的Service類型進行了一定的說明,這里不再剖析

2、環境介紹

本篇僅介紹私有雲+外部硬件負載+k8s集群的真實場景下如何進行配置

相關環境及設備說明如下

組件名 型號或版本
硬件負載設備 SANGFOR(深信服) AD 6.5R1
k8s Ingress 控制器 NGINX Ingress Controller 0.25.0
k8s 集群 Kubernetes 1.17.0

3、相關說明

真實生產場景下,一般提供給用戶的都是七層https服務

首先域名解析在外部負載設備綁定的公網ip上,負載周邊可能還會有一些安全設備例如WAF等,這里不多介紹

流量經過負載后進入到k8s集群中,其中Ingress ControllerDaemonSet方式部署並使用hostNetwork模式接收並處理到達宿主機的80443端口流量

關於https證書的配置,一般有以下兩種可選方式:

  • 配置在負載設備(負載類型如果只考慮七層負載),由負載負責將數據包封包解包,並轉發到后端,如果用戶通過https形式訪問,流量經過的流程是:用戶端——>負載80端口——>負載443端口——>服務端(k8s node)的80端口

  • 配置在后端,例如Ingress資源上,如果用戶通過https形式訪問,流量經過的流程是:用戶端——>負載80端口——>服務端(k8s node)的80端口——>服務端(k8s node)的443端口

但是為了獲取用戶的真實ip,只能選擇方式一,因為如果證書配置在后端服務,流量經過負載時是加密的,負載一般在沒有證書的情況下,是無法對數據包進行解包操作透傳用戶ip

以上在公有雲環境下,例如騰訊雲CLB、阿里雲新的應用型負載ALB或傳統型負載CLB均有涉及,可能不盡詳細

4、環境准備

首先需要准備一個后端獲取用戶請求,顯示打印或輸出的應用,可以自己手擼一個簡單應用,當然為了操作簡單也可以選擇nginx容器在應用日志中查看,更好的方式是選擇whoamiechoserver這類鏡像

其中whoami可以在控制台訪問服務時打印用戶請求等相關信息,echoserver可以在瀏覽器呈現用戶請求等相關信息

這里為了模擬和真實應用一樣的場景,選擇更為直觀的echoserver,其源鏡像地址為gcr.io/google-containers/echoserver

如果網絡不佳,可以從我的地址獲取ssgeek/echoserver

首先基於k8s部署該應用,創建deploysvcing,定義如下

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echoserver
  labels:
    app: echoserver
spec:
  selector:
    matchLabels:
      app: echoserver
  template:
    metadata:
      labels:
        app: echoserver
    spec:
      containers:
      - name: echoserver
        image: ssgeek/echoserver:latest
        ports:
        - containerPort: 8080
        env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
          - name: POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP

---
apiVersion: v1
kind: Service
metadata:
  name: echoserver
  labels:
    app: echoserver
spec:
  ports:
  - port: 80
    targetPort: 8080
    name: http
  selector:
    app: echoserver

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: echoserver
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: echo.ssgeek.com
    http:
      paths:
      - backend:
          serviceName: echoserver
          servicePort: 80
        path: /

5、負載配置

這里簡單分析及列出關鍵配置

  • 插入請求頭以透傳ip

部署好后端服務后,開始配置外部(深信服)負載,除了導入https證書外,還需要在轉發的請求頭中插入X-Forwarded-For頭部,確保用戶ip在經過負載時作為請求頭的一部分傳遞到后端服務器

  • 負載設備到后端請求頭部改寫

由於負載設備到后端的80端口,因此后端只接收http請求,也就是請求經過負載處理https及證書相關動作

未添加請求頭部改寫時,對請求抓包的現象對比如下(分別為無https配置時和有https配置但未改寫請求頭部時)

6、Ingress Controller 配置

修改Nginx Ingress Controller配置,添加如下內容

參考:https://kubernetes.github.io/ingress-nginx/user-guide/

data:
  use-forwarded-headers: "true"
  compute-full-forwarded-for: "true"
  forwarded-for-header: "X-Forwarded-For"
  • use-forwarded-headers

    如果為true,會將傳入的X-Forwarded-*頭傳遞給upstreams

    如果為false,會忽略傳入的X-Forwarded-*頭,用看到的請求信息填充它們。如果直接暴露在互聯網上,或者它在基於L3/packet-based load balancer后面,並且不改變數據包中的源IP時使用此選項

  • forwarded-for-header

    設置標頭字段以標識客戶端的原始IP地址。 默認: X-Forwarded-For

  • compute-full-forwarded-for

    將遠程地址附加到 X-Forwarded-For標頭,而不是替換它。 啟用此選項后,upstreams應用程序將根據其自己的受信任代理列表提取客戶端IP

7、服務端驗證

服務端請求暴露及應用獲取ip效果如下

正常情況可拿到以下幾類ip

  • pod ip

k8s pod自身的ip

  • node ip

k8s pod所在nodeip

  • 負載 ip

位於請求頭X-Forwarded-For字段中

  • 用戶真實 ip

位於請求頭X-Forwarded-For字段、x-original-forwarded-for字段、x-real-ip字段中

關於x-forwarded-forx-original-forwarded-forx-real-ip的說明:

X-Forwarded-For用於記錄從客戶端地址到最后一個代理服務器的所有地址

X-Real-IP用於記錄請求的客戶端地址

X-Original-Forwarded-For字面意思是原始轉發 IP,這是Ingress的功能,Ingress將用戶的真實IP記錄到了這個字段

對應用來說,以java應用為例,獲取用戶ip的代碼如下

/**
 * 獲取操作用戶ip
 * @return
 */
private String getIp() {

    HttpServletRequest request;
    try {
        request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    } catch (NullPointerException e) {
        return "127.0.0.1";
    }

    //取客戶端ip
    String ipAddress = request.getHeader("x-forwarded-for");
    if (ipAddress == null || ipAddress.length() == 0
            || "unknown".equalsIgnoreCase(ipAddress)) {
        ipAddress = request.getHeader("Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.length() == 0
            || "unknown".equalsIgnoreCase(ipAddress)) {
        ipAddress = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.length() == 0
            || "unknown".equalsIgnoreCase(ipAddress)) {
        ipAddress = request.getRemoteAddr();
        if (ipAddress.equals("127.0.0.1")) {
            // 根據網卡取本機配置的IP
            InetAddress inet = null;
            try {
                inet = InetAddress.getLocalHost();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
            ipAddress = inet.getHostAddress();
        }
    }
    // 對於通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
    if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
        // = 15
        if (ipAddress.indexOf(",") > 0) {
            ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
        }
    }
    return ipAddress;
}

8、小結

本文記錄了私有雲和有外部負載的真實場景下,k8s集群中的應用獲取用戶ip的相關實現邏輯及關鍵處理,希望能幫助到大家

See you ~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM