SSO服務,說白了一開始就是作為AD驗證的中介、LDAP協議到OpenID的轉換,讓有身份驗證需求的應用,可以透過OAuth 2.0的流程進行。
在最初設定上,使用ldap://<REALM>.tw
的形式與AD服務進行連線。比如AD的基礎位置是dc=company,dc=tw
,就會成爲ldap://company.tw
的格式。(AD域帳戶會是company.tw\user
)
而ldap://company.tw
這個位置,實際上有可能是由多臺DC主機提供服務。可以透過nltest.exe /DCLIST:company.tw
查詢:
或是透過dig company.tw
查詢DNS伺服器記錄內容:
公司內的DNS記錄,就包含了12個IPv4的A類型記錄。如果換用nslookup company.tw
查詢,每次取得的第一個IP也都不同:
這是因爲DNS伺服器會輪詢提供查詢結果,這也能夠達到基於DNS的負載平衡。
有些DNS伺服器除了輪詢,還可以依據請求來源IP的地理位置、CIDR提供不同查詢結果。
這樣出現一個問題就是:當某一台DC掛掉的時候,會導致部分使用者或系統的認證失敗,
-- HAproxy + Keepalive實現LDAP代理服務
因爲是輪詢使用DC伺服器,SSO服務遇過一種情況是:時而可以;時而不行。當時為了找到問題,利用了ping
和ldapsearch
一個個針對DC伺服器檢查,竟有一段時間,接近4臺DC連線是存在問題的(34%),但DNS伺服器記錄依然存在。也是在這過後,將伺服器連線設定調整成了ldap://dc01.company.tw, ldap://dc02.company.tw
的形式。
故障轉移 (Failover)
這是最常見的用法。當主要 LDAP 伺服器無法連線時,系統會自動切換到備援的 LDAP 伺服器,確保服務的可用性。
設定方式:
大多數支援此功能的軟體,通常允許你在設定檔中用空格將多個 URL 隔開。
範例格式:
ldap://server1.example.com:389/dc=example,dc=com ldap://server2.example.com:389/dc=example,dc=com
運作原理:系統會依序嘗試連線每一個 URL。如果第一個 URL 連線失敗,它就會嘗試第二個,直到成功為止。
(由Gemini生成)
運作上:如果第一個 URL 連線失敗,它就會嘗試第二個,直到成功為止。但奇怪的是,最近又因爲AD伺服器滾動式更新,卻依然造成了SSO服務登入提供能力異常Orz。
雖然在Keycloak 16.1.1版本驗證過這種寫法,但疑似在21版後不再支援此種寫法。不巧的是,在架構調整時,順便做了跳板升級到22.0.5版本。
(以下是Gemini回答)
根據 Keycloak 的官方文件和社群討論,在 Keycloak 21.0.1 版本之後,已經不再支援在單一「連線 URL (Connection URL)」欄位中使用空格來分隔多個 LDAP URL 的寫法。變動詳情
在 Keycloak 20.0.1 及更早的版本中,許多使用者會透過以下格式來設定多個 LDAP 伺服器進行故障轉移:
ldap://server1.example.com:389 ldap://server2.example.com:389
然而,從 Keycloak 21.0.1 開始,此寫法會導致連線失敗,並拋出
java.lang.IllegalArgumentException: URI '...' is not valid
的錯誤。解決方案
如果你的 Keycloak 版本是 21.0.1 或更高,並需要設定多個 LDAP 伺服器,官方建議的作法是:
- 為每一個 LDAP 伺服器建立獨立的 User Federation 設定。
- 透過設定這些 User Federation 的優先順序(Priority)來實現故障轉移。 系統會依序檢查每個供應者,直到成功連線並找到使用者為止。
這種方式提供了更清晰且可控的管理機制,取代了舊版在單一欄位中放入多個 URL 的作法。
為了讓SSO服務優先使用接近的DC伺服器,最後決定由APISIX提供LDAP的代理。不但APISIX會針對同資料區域的DC伺服器進行負載平衡,當該區域DC伺服器都發生問題時,也會嘗試其他資料中心的伺服器。
為了達到高可用性,我們更改了環境拓撲,用四台linux伺服器充當代理伺服器,代理所有ldap請求。
-- HAproxy + Keepalive實現LDAP代理服務
在「「Quick Start」中,docker-compose.yaml
已經轉發389
端口,這是LDAP在沒有使用TLS情況下的預設端口。接著看看apisix_config/config.yaml
的部分內容:
apisix:
stream_proxy:
tcp:
- 389
nginx_config:
stream_configuration_snippet: |
log_format basic '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time $upstream_addr $upstream_bytes_sent $upstream_bytes_received $upstream_connect_time';
access_log /dev/stdout basic;
apisix.stream_proxy
告訴APISIX,同樣監聽389
端口作爲TCP的「流」代理。nginx_config.stream_configuration_snippet
則設定「流」代理時的記錄保存格式與位置。
類似設定SSO服務的上游,也同樣需要設定AD伺服器上游:
curl --request PUT \
--url http://127.0.0.1:9180/apisix/admin/upstreams/ad-server \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: SaiTJp7TEa9K39oy7D5A4ouXmdqHvL9a' \
--data '{
"nodes": [
{
"host": "ad-11.company.tw",
"port": 389,
"weight": 1,
"priority": 1
},
{
"host": "ad-12.company.tw",
"port": 389,
"weight": 1,
"priority": 1
},
{
"host": "ad-21.company.tw",
"port": 389,
"weight": 1,
"priority": -1
},
{
"host": "ad-22.company.tw",
"port": 389,
"weight": 1,
"priority": -1
},
{
"host": "ad-31.company.tw",
"port": 389,
"weight": 1,
"priority": -2
},
{
"host": "ad-32.company.tw",
"port": 389,
"weight": 1,
"priority": -2
},
{
"host": "ad-41.company.tw",
"port": 389,
"weight": 1,
"priority": -4
},
{
"host": "ad-42.company.tw",
"port": 389,
"weight": 1,
"priority": -4
}
],
"timeout": {
"connect": 6,
"send": 6,
"read": 6
},
"type": "roundrobin",
"checks": {
"active": {
"concurrency": 10,
"healthy": {
"interval": 1,
"successes": 2
},
"http_path": "/",
"timeout": 1,
"type": "tcp",
"unhealthy": {
"interval": 1,
"tcp_failures": 2,
"timeouts": 3
}
},
"passive": {
"healthy": {
"successes": 5
},
"type": "tcp",
"unhealthy": {
"tcp_failures": 2,
"timeouts": 7
}
}
},
"scheme": "tcp",
"pass_host": "pass",
"name": "AD Server",
"keepalive_pool": {
"idle_timeout": 60,
"requests": 1000,
"size": 320
}
}
'
不同的是scheme
是tcp
,健康檢查時也是。
不同於第7層HTTP的代理,可以設定host
和path
的匹配,「流」代理路由主要設定的就只是監聽端口和代理的上游,且使用的Admin API端點也不一樣,用的是/apisix/admin/stream_routes
:
流的代理是第四層的負載平衡,並不明白上層協議內容,沒有
host
、path
可以使用
curl --request POST \
--url http://127.0.0.1:9180/apisix/admin/stream_routes \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: SaiTJp7TEa9K39oy7D5A4ouXmdqHvL9a' \
--data '{
"name": "AD Server",
"desc": "代理AD Server的LDAP服務",
"upstream_id": "ad-server",
"server_port": 389
}'
同一個Admin API端點,可以將請求方法改成
GET
看看有沒有建立成功。
由於並沒有建立LDAP服務,這個Lab沒辦法驗收。
因此只能簡單說說驗收方式:透過ldapsearch
、telnet
等工具向389
端口發起請求,同時查看透過tcpdump
或Wireshark等網路封包監聽工具查看記錄(sudo tcpdump -i <interface> port 389
),或是查看APISIX的後臺記錄。以我Lab環境例子,因為實際LDAP服務不存在,APISIX後臺記錄存在相關錯誤記錄:
有興趣者,可以透過OpenLDAP或其他工具建立LDAP服務。