/

一次 Traefik 與 TLS 的踩雷經驗

問題點

部署 ArgoCD 進 Kubernetes 完畢時,發現 ArgoCD 開了 80, 443 兩個 Service Port,對應到的 Container Port 都同樣是 8080。如果你用 http 請求 ArgoCD 的 Web 時,他會請你跳轉到 https。

為了讓外部的 User 可以使用 ArgoCD,我設定了 Traefik 的 IngressRoute 來存取該資源,但是卻一直沒有成功。

我的架構環境為

1
2
3
                    |
Nginx-proxy(外部) | -> K8s-worker(Traefik/Ingress) -> ArgoCD-server(service)
|

Debug

IngressRoute

為了簡化環境(debug),於是我從 Cluster 內部去打 Ingress,發現回應一直都是 404,但是我把 Service 做 port forward 到 local 端,卻可以正常運作,所以推測是 ArgoCD 的 IngressRoute 規則有誤,而我使用的規則是ArgoCD 官方文件提到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: argocd-server
namespace: argocd
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`argocd.example.com`)
priority: 10
services:
- name: argocd-server
port: 80
- kind: Rule
match: Host(`argocd.example.com`) && Headers(`Content-Type`, `application/grpc`)
priority: 11
services:
- name: argocd-server
port: 80
scheme: h2c
tls:
certResolver: default
options: {}

於是我使用(從 Cluster 節點上直接打 Ingress)

1
2
3
4
5
curl https://argocd.example.com -k
> href="https://argocd.example.com/">Temporary Redirect.

curl https://argocd.example.com -k -L
> curl: (47) Maximum (50) redirects followed

我查了 Traefik 的官方文件發現

1
2
3
4
5
6
7
There are 3 ways to configure the backend protocol for communication between Traefik and your pods:

Setting the scheme explicitly (http/https/h2c)
Configuring the name of the kubernetes service port to start with https (https)
Setting the kubernetes service port to use port 443 (https)

If you do not configure the above, Traefik will assume an http connection.

簡單來說,Treafik 並不會因為你使用 https 去打 Ingress,就使用該協定幫你打後端的 Pod (or Service),如果你指定 Service 為 443 Port 那麼他就幫你用 https 去跟後端的 Pod 建立連線,而預設都是走 http 連線。

所以應該將 yaml 檔案改為

1
2
3
4
5
6
7
routes:
- kind: Rule
match: Host(`argocd.example.com`)
priority: 10
services:
- name: argocd-server
port: 443

或是指定 protocol

1
2
3
4
5
6
7
8
routes:
- kind: Rule
match: Host(`argocd.tcs-proxy.cscc`)
priority: 10
services:
- name: argocd-server
port: 80
scheme: https

來強制 IngressRoute 與 ArgoCD 使用 https 建立連線。

另外,因為 ArgoCD 的憑證是自行簽發的,Traefik 並認不得該單位,所以 Traefik 必須設定 --serversTransport.insecureSkipVerify=true 來忽略憑證的驗證

Reverse Proxy to Cluster

1
2
3
                    |
Nginx-proxy(外部) | -> K8s-worker(Traefik/Ingress) -> ArgoCD-server(service)
|

我的情境中,Cluster 外部還有一台 Load Balancer 會將流量導致 Cluster 的 Worker Nodes,但是 Traefik 已經有憑證了,後端的 ArgoCD 也有,我並不想要有這麼多的憑證會造成連線的效能耗損,於是查到可以使用 tls passthrough 的方式來讓 Nginx 不處理封包的加密解密,直接將該封包 reverse proxy 到後端。

但是該方式會產生一個問題,Nginx 如果不做解密,那麼也看不到封包 L7 的 Host 欄位,於是原本的 virtual host 方式就無法正常路由,解決方式為使用 TLS 的 SNI 來判斷路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
stream {
map $ssl_preread_server_name $name {
# 在此設定不同 SNI 對應到不同後端;
argocd.tcs-proxy.cscc K8S_WORKER_443;
default https_default_backend;
}

upstream K8S_WORKER_443 {
server worker-node-1:443;
server worker-node-2:443;
server worker-node-3:443;
}

server {
listen 443;
proxy_pass $name; # 如果沒有路由需求,也可以直接設定 proxy_pass K8S_WORKER_443;
ssl_preread on; # 該選項需要打開,才有辦法讀取 SNI
}
}

要注意的是,通常 Nginx 的設定都會在 http directive,但是 tls passthrough 必須要設定在 stream directive 下,意味著原本的 log formatter 也無法使用了(可以去 nginx.conf 看一下)。

所以我們必須增加新的 log formatter 在 steam directive 下方

1
2
3
4
5
6
7
8
9
10
11
12
stream {
log_format proxy '$remote_addr - [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$upstream_addr" '
'"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time" '
'$remote_addr $remote_port $server_addr $server_port';
server {
access_log /var/log/tcp-stream-access.log proxy;
#...
#...
}
}

如此一來會發現外層的 Nginx 沒有上憑證也可以正常使用叢集內部憑證來對連線進行加密!