mTLS Java SpringBoot API

在 Spring Boot 啟用 mutual TLS(mTLS)

邱雍翔 Alvin Chiu 2024/04/29 18:48:55
147

本篇文章我們將會詳細介紹如何透過設定的方式在 Spring Boot 啟用 mutual TLS(mTLS)首先我假設您已經安裝 openssl CLI,如果尚未安裝,請自行到官方網站下載。請按照以下步驟生成自簽根憑證。稍後我們會用它來簽署客戶端和服務器憑證。

產生根憑證 (root certificate)

$ openssl genrsa -des3 -out rootCA.key 2048

這個指令會產生根憑證的私鑰,並會要求輸入一個密碼作為該私鑰的密碼。這十分重要!請必須妥善保管,以防洩露後被他人拿來冒充簽署憑證。指令執行後會產生如下的輸出結果:

generate root certificate

使用前一步驟中生成的金鑰,向 openssl 請求建立證書。

$ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1825 -out rootCA.pem

days 參數代表的是,從現在開始指定證書的到期日期。而我們將證書命名為 rootCA.pem。在此,它會要求你提供一些識別訊息以放入證書中。

reqCACert

恭喜!你現在是一個憑證授權中心,可以為其他實體簽署憑證。申請憑證的實體需要向CA提供包含請求訊息的CSR。在公鑰為基礎的系統中,憑證簽署請求是由申請人發送給公鑰管理機構的一條訊息,以便申請數位身份證書。

簽署伺服器端憑證 (Server Certificate)

我們先建立私鑰,然後為伺服器證書建立 CSR。

$ openssl genrsa -des3 -out server.key 2048

建立私鑰後,建立 CSR。

$ openssl req -new -sha256 -key server.key -out server.csr

接下來會提示輸入與建立根憑證(root CA)所需相同的資訊。CN欄位必需是完全合格的主機名稱,也就是伺服器可被訪問的位置,在這個案例中我們使用的是本機(localhost)。

generate csr

CSR 由申請憑證的實體提供,再傳遞給根憑證授權機構。在此案例中,由於我們也是根憑證授權機構,所以我們使用這個 CSR 簽署伺服器憑證。

$ openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.pem -days 365 -sha256

這個過程中你需要輸入根憑證的密碼,以簽署新的憑證。接著會建立一個名為 server.pem 的伺服器憑證,該憑證將與 server.key 一起用在配置 Tomcat 以啟用 SSL。

簽署客戶端憑證

如前面所提,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

現在我們擁有客戶端和伺服器端的證書。讓我們使用這些證書來保護我們的Spring Boot應用程式。

在 Spring Boot 範例程式啟動 SSL

我們使用 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;

@RestController
@RequestMapping("/api/v1/mtls")
public class ServerController {
   @PostMapping("/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);
      }
  }
}

現在透過 Gradle 啟動應用程式

$ ./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使用密碼來進行解密。CLI 也會要求輸入密鑰庫的密碼,我們需要將其記錄下來以供稍後給 Spring Boot 使用。所以請務必記下這個密碼!

create keystore

我們將生成的 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,你會得到以下的錯誤訊息。

Bad Request. This combination of host and port requires TLS.

400 error

這告訴我們應使用 https

POST https://localhost:8080/api/v1/mtls/connect

cantverifyCert

這時你會發現,即便 API 回應正確,但 Postman 在交握期間無法驗證我們伺服器的憑證,這是很正常的現象!因為 Postman 不知道我們是憑證授權機構。我們要讓 Postman 信任根憑證 (root CA) 是我們的。

前往 Postman Settings,然後點選 Certificates 標籤,在頂部有一個名為 CA certificates 的選項,它提供了手動信任CA的功能,加入證書到您的請求中。這是建立 TLS 的步驟之一,無論是單向還是雙向。將開關切換至開啟狀態,然後選擇 rootCA.pem 檔案,以便 Postman 可以信任這個 CA 。再次執行請求,現在應該已經確認了連線。

add cert

在 Spring Boot 範例程式啟用 mTLS

目前為止我們的Spring Boot 應用程式已配置了單向TLS,並完成信任根憑證 (root CA) 的配置,客戶端可以發送請求。但為實現客戶端和伺服器之間的互相驗證,我們尚需要微調配置,以便讓我們的伺服器也能要求客戶端憑證。請到application.properties 文件,加入以下屬性:

client-auth: need

這個 Key 讓我們可以設定是否需要客戶端驗證(也稱為雙向認證)。它可以是NEED、WANT和NONE三種選項。NEED表示伺服器必須驗證客戶端證書,而 WANT 也要求客戶端證書,但如果沒有提供驗證,它會保持連接。使用NONE 時,不會要求客戶端證書。重新啟動應用程式,再次嘗試呼叫 API。

couldNotGetResponse

儘管錯誤訊息非常簡短且不是很有幫助,但如果打開紀錄觀察,會看到與伺服器的交握失敗了。在這種情況下,由於客戶端未提供任何類型的憑證,並且在握手期間連接已中斷,因此伺服器無法驗證客戶端。為解決此問題,我們需要在呼叫API時一起發送客戶端證書。

handle shake error

再次進入Postman偏好設定的憑證標籤,點擊新增憑證,然後添加以下項目:

  • 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

這個指令使用上述提到的 keytool CLI 建立一個 PKCS#12 格式的信任存庫。它會提示輸入密碼,我們在配置應用程式時需要使用這個密碼。CLI 會顯示證書訊息,並詢問是否應該信任該證書。選擇“是”並創建信任庫。

create truestore

現在,將 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

重新啟動應用程式,再次嘗試呼叫 API:

connect success

現在我們可以看到,伺服器也接受了用戶端憑證,因為它信任簽署它的根憑證 (root CA)。這就是我們在 Spring Boot 中實現 mTLS 的方法!

下一篇我們將會介紹如何使用 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

邱雍翔 Alvin Chiu