在 Spring Boot 啟用 mutual TLS(mTLS)
本篇文章我們將會詳細介紹如何透過設定的方式在 Spring Boot 啟用 mutual TLS(mTLS),
$ openssl genrsa -des3 -out rootCA.key 2048
$ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1825 -out rootCA.pem
days 參數代表的是,從現在開始指定證書的到期日期。而我們將證書命名為 rootCA.pem。在此,它會要求你提供一些識別訊息以放入證書中。
恭喜!你現在是一個憑證授權中心,可以為其他實體簽署憑證。申請憑證的實體需要向CA提供包含請求訊息的CSR。在公鑰為基礎的系統中,憑證簽署請求是由申請人發送給公鑰管理機構的一條訊息,以便申請數位身份證書。
$ openssl genrsa -des3 -out server.key 2048
建立私鑰後,建立 CSR。
$ openssl req -new -sha256 -key server.key -out server.csr
$ openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.pem -days 365 -sha256
如前面所提,mTLS 是用在雙方彼此的驗證。如果只有單向傳輸層安全協定,我們就不需要客戶端憑證。在這個案例中,我們希望客戶端提供其憑證,並希望伺服器對其進行驗證。所以現在要創建客戶端憑證,以便我們可以使用它來存取API。
$ openssl genrsa -des3 -out client.key 2048
接著以相同方式為客戶建立CSR。
$ openssl req -new -sha256 -key client.key -out client.csr
然後,以同樣的方式簽署客戶證書。
$ openssl x509 -req -in client.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out client.pem -days 365 -sha256
我們使用 Gradle 建置 Spring Boot 專案
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.httpcomponents.client5:httpclient5'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
這裡我以一個簡單的 Spring Boot API 作為例子。這個 API 會在我們發出請求後得到 Connect Succeed! 的訊息。
package com.example.mtlsdemo.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
"/api/v1/mtls") (
public class ServerController {
"/connect") (
public ResponseEntity<Map<String, String>> connect(){
try {
Map<String, String> body = new HashMap<>();
body.put("message", "Connect Succeed!");
return new ResponseEntity<>(body, HttpStatus.OK);
}catch (Exception e){
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
$ ./gradlew bootRun
如果啟動過程沒有任何例外,可以嘗試呼叫這個 API,確認尚未啟動 SSL 的 API 是否可以正常運行。
POST http://localhost:8080/api/v1/mtls/connect
Spring Boot 提供一系列的配置,可以引入憑證。憑證及其相應的私鑰以 JKS 或 PKCS#12 格式綁定在一個密鑰庫中。儘管生成密鑰庫的工具 keytool 提供 JKS 或 PKCS#12 格式兩種選項,但從 JDK 9+ 開始已將預設的存儲類型統一為 PKCS#12,並將不再支援 JKS。因此我們將在PKCS#12 密鑰庫中綁定我們的伺服器證書及其密鑰。前往存放證書的目錄並執行以下命令,以伺服器證書和私人金鑰建立金鑰庫。
$ openssl pkcs12 -export -in server.pem -out keystore.p12 -name server -nodes -inkey server.key
這將匯出證書和私鑰到一個 PKCS 格式的密鑰庫,我們可以用它來配置 Spring Boot應用程式。預設情況下,私鑰被匯入到加密的密鑰庫中,這樣的話我們需要 Spring Boot使用密碼來進行解密。
keystore.p12 文件放入應用程式的 src/main/resources 目錄中(雖然在實際的應用情境這可能透過某種外部配置提供)。將以下程式碼片段放入 application.properties,然後重新啟動應用程式。
server.ssl.enabled=true
#key-store 是 PKCS12 檔案的路徑
server.ssl.key-store=classpath:keystore.p12
#key-store-password 則是創建密鑰庫時輸入的密碼。
server.ssl.key-store-password=password
server.ssl.key-store-type=PKCS12
重新啟動應用程式後,注意以下 log:
Tomcat initialized with port 8080 (https)
這表示我們內嵌的 Tomcat 伺服器已啟用SSL。現在我們再次嘗試呼叫範例 API
POST http://localhost:8080/api/v1/mtls/connect
如果你使用了類似 Postman 這類的 API Client,你會得到以下的錯誤訊息。
POST https://localhost:8080/api/v1/mtls/connect
前往 Postman Settings,然後點選 Certificates 標籤,在頂部有一個名為 CA certificates 的選項,它提供了手動信任CA的功能,加入證書到您的請求中。這是建立 TLS 的步驟之一,無論是單向還是雙向。
目前為止我們的Spring Boot 應用程式已配置了單向TLS,並完成信任根憑證 (root CA) 的配置,客戶端可以發送請求。但為實現客戶端和伺服器之間的互相驗證,我們尚需要微調配置,以便讓我們的伺服器也能要求客戶端憑證。請到application.properties 文件,加入以下屬性:
client-auth: need
儘管錯誤訊息非常簡短且不是很有幫助,但如果打開紀錄觀察,會看到與伺服器的交握失敗了。在這種情況下,由於客戶端未提供任何類型的憑證,並且在握手期間連接已中斷,因此伺服器無法驗證客戶端。為解決此問題,我們需要在呼叫API時一起發送客戶端證書。
-
host: localhost
-
port: 8080
-
CRT File: /path/to/client.pem
-
KEY File: /path/to/client.key
-
Password: 簽署客戶端憑證階段時設定的密碼
重新發送請求,但是 Postman 仍然出現相同的錯誤。雖然客戶端和伺服器彼此展示了它們的證書,但仍然無法建立連線。如果查看 server log,應該能夠追蹤到當客戶端呼叫 API 時引發了 SslHandShakeException 並有一條訊息"unknown certificate"。我們的伺服器無法驗證客戶端證書,它不信任客戶端證書的根憑證 (root CA),所以會引發此異常。它知道這是一個有效的證書,但不知道它是在哪個地方簽署的,因此拒絕了請求。
讓我們建立一個信任庫,並將簽署用戶端證書的根憑證 (root CA) 放入其中。前往用戶端證書所在的位置,執行以下命令:
# 要使用 keytool,需要先安裝 Java
$ keytool -import -file rootCA.pem -alias rootCA -keystore truststore.p12
src/main/resources 文件夾中,並到 application.properties 文件,加入以下屬性:
server.ssl.trust-store=classpath:truststore.p12
server.ssl.trust-store-password=password
server.ssl.trust-store-type=PKCS12
下一篇我們將會介紹如何使用 Java 實作 mTLS Client 的方法。
參考來源:
1. Security: mTLS in Spring Boot. Welcome to this technical walkthrough… | by Serhii Bohutskyi | Medium
2. A simple mTLS guide for Spring Boot microservices | by Mihaita Tinta | ING Hubs Romania | Medium
3. Consuming a Secure API with Mutual TLS Authentication in Spring Boot | by Nazeer Arus | Medium
4. Creating a Self-Signed Certificate With OpenSSL | Baeldung