k8s生產實踐之獲取客戶端真實IP
目錄
1、概述
2、環(huán)境介紹
3、相關說明
4、環(huán)境準備
5、負載配置
6、Ingress Controller 配置
7、服務端驗證
8、小結

1、概述
通常web應用獲取用戶客戶端的真實ip一個很常見的需求,例如將用戶真實ip取到之后對用戶做白名單訪問限制、將用戶ip記錄到數(shù)據(jù)庫日志中對用戶的操作做審計等等
在vm時代是一個比較容易解決的問題,但當一切云原生化(容器化)之后變得稍微復雜了些
k8s中運行的應用通過Service抽象來互相查找、通信和與外部世界溝通,在k8s中是kube-proxy組件實現(xiàn)了Service的通信與負載均衡,流量在傳遞的過程中經過了源地址轉換SNAT,因此在默認的情況下,常常是拿不到用戶真實的ip的
這個問題在k8s官方文檔(https://kubernetes.io/zh/docs/tutorials/services/source-ip/)中基于Cluster IP、NodePort、LoadBalancer三種不同的Service類型進行了一定的說明,這里不再剖析
2、環(huán)境介紹
本篇僅介紹私有云+外部硬件負載+k8s集群的真實場景下如何進行配置
相關環(huán)境及設備說明如下
| 組件名 | 型號或版本 |
|---|---|
| 硬件負載設備 | SANGFOR(深信服) AD 6.5R1 |
| k8s Ingress 控制器 | NGINX Ingress Controller 0.25.0 |
| k8s 集群 | Kubernetes 1.17.0 |
3、相關說明
真實生產場景下,一般提供給用戶的都是七層https服務
首先域名解析在外部負載設備綁定的公網ip上,負載周邊可能還會有一些安全設備例如WAF等,這里不多介紹
流量經過負載后進入到k8s集群中,其中Ingress Controller以DaemonSet方式部署并使用hostNetwork模式接收并處理到達宿主機的80、443端口流量
關于https證書的配置,一般有以下兩種可選方式:
配置在負載設備(負載類型如果只考慮七層負載),由負載負責將數(shù)據(jù)包封包解包,并轉發(fā)到后端,如果用戶通過
https形式訪問,流量經過的流程是:用戶端——>負載80端口——>負載443端口——>服務端(k8s node)的80端口配置在后端,例如
Ingress資源上,如果用戶通過https形式訪問,流量經過的流程是:用戶端——>負載80端口——>服務端(k8s node)的80端口——>服務端(k8s node)的443端口
但是為了獲取用戶的真實ip,只能選擇方式一,因為如果證書配置在后端服務,流量經過負載時是加密的,負載一般在沒有證書的情況下,是無法對數(shù)據(jù)包進行解包操作透傳用戶ip的
以上在公有云環(huán)境下,例如騰訊云CLB、阿里云新的應用型負載ALB或傳統(tǒng)型負載CLB均有涉及,可能不盡詳細
4、環(huán)境準備
首先需要準備一個后端獲取用戶請求,顯示打印或輸出的應用,可以自己手擼一個簡單應用,當然為了操作簡單也可以選擇nginx容器在應用日志中查看,更好的方式是選擇whoami、echoserver這類鏡像
其中whoami可以在控制臺訪問服務時打印用戶請求等相關信息,echoserver可以在瀏覽器呈現(xiàn)用戶請求等相關信息
這里為了模擬和真實應用一樣的場景,選擇更為直觀的echoserver,其源鏡像地址為gcr.io/google-containers/echoserver
如果網絡不佳,可以從我的地址獲取ssgeek/echoserver
首先基于k8s部署該應用,創(chuàng)建deploy、svc、ing,定義如下
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證書外,還需要在轉發(fā)的請求頭中插入X-Forwarded-For頭部,確保用戶ip在經過負載時作為請求頭的一部分傳遞到后端服務器

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


未添加請求頭部改寫時,對請求抓包的現(xiàn)象對比如下(分別為無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-*頭,用看到的請求信息填充它們。如果直接暴露在互聯(lián)網上,或者它在基于L3/packet-based load balancer后面,并且不改變數(shù)據(jù)包中的源IP時使用此選項forwarded-for-header
設置標頭字段以標識客戶端的原始
IP地址。默認: X-Forwarded-Forcompute-full-forwarded-for
將遠程地址附加到
X-Forwarded-For標頭,而不是替換它。啟用此選項后,upstreams應用程序將根據(jù)其自己的受信任代理列表提取客戶端IP
7、服務端驗證
服務端請求暴露及應用獲取ip效果如下

正常情況可拿到以下幾類ip
pod ip
k8s pod自身的ip
node ip
k8s pod所在node的ip
負載 ip
位于請求頭X-Forwarded-For字段中
用戶真實 ip
位于請求頭X-Forwarded-For字段、x-original-forwarded-for字段、x-real-ip字段中
關于x-forwarded-for、x-original-forwarded-for、x-real-ip的說明:
X-Forwarded-For用于記錄從客戶端地址到最后一個代理服務器的所有地址
X-Real-IP用于記錄請求的客戶端地址
X-Original-Forwarded-For字面意思是原始轉發(fā) 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")) {
// 根據(jù)網卡取本機配置的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的相關實現(xiàn)邏輯及關鍵處理,希望能幫助到大家
See you ~
