Contents

mTLS in Go

mTLS in Go

https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go

mTLS是指在服務器端和客戶端之間使用雙向加密通道。如今,mTLS是確保雲原生應用程序中微服務之間的通信安全的首選協議。

What is mutual TLS (mTLS)?

Mutual TLS,簡稱mTLS,是一種相互認證的方法(mutual authentication)。

mTLS 通過驗證他們都擁有正確的私鑰來確保網絡連接的每一端的各方都是他們聲稱的身份。它們各自的 TLS 證書(TLS certificates)中的信息提供了額外的驗證。

mTLS 通常用於零信任(Zero Trust)安全框架,以驗證組織內的用戶、設備和服務器。它還可以幫助保持 API 的安全。

零信任意味著默認情況下不信任任何用戶、設備或網絡流量,這種方法有助於消除許多安全​​漏洞。

原文 : https://www.cloudflare.com/zh-tw/learning/access-management/what-is-mutual-tls/

Mutual TLS, or mTLS for short, is a method for mutual authentication. mTLS ensures that the parties at each end of a network connection are who they claim to be by verifying that they both have the correct private key. The information within their respective TLS certificates provides additional verification.

mTLS is often used in a Zero Trust security framework* to verify users, devices, and servers within an organization. It can also help keep APIs secure.

Zero Trust means that no user, device, or network traffic is trusted by default, an approach that helps eliminate many security vulnerabilities.

mTLS是指在服務器端和客戶端之間使用雙向加密通道。如今,mTLS是確保雲原生應用程序中微服務之間的通信安全的首選協議。

雙向認證,顧名思義,客戶端和服務器端都需要驗證對方的身份,在建立Https連接的過程中,握手的流程比單向認證多了幾步。單向認證的過程,客戶端從服務器端下載服務器端公鑰證書進行驗證,然後建立安全通信通道。雙向通信流程,客戶端除了需要從服務器端下載服務器的公鑰證書進行驗證外,還需要把客戶端的公鑰證書上傳到服務器端給服務器端進行驗證,等雙方都認證通過了,才開始建立安全通信通道進行數據傳輸。

/mtls_in_go/mTSL.png https://thenewstack.io/mutual-tls-microservices-encryption-for-service-mesh/

mTLS在服務網格中的工作方式

Citrix那裡我們學到的是,在更高層次上,在服務網格中基於mTLS來身份驗證和建立加密通道的過程涉及以下步驟:

  1. 微服務A發送對微服務B證書的請求。
  2. 微服務B回復其證書,並請求微服務A的證書。
  3. 微服務A向證書頒發機構檢查證書是否屬於微服務B。
  4. 微服務A將其證書發送到微服務B,並且還共享會話加密密鑰(使用微服務B的公共密鑰加密)。
  5. 微服務B向證書頒發機構檢查其收到的證書是否屬於微服務A。
  6. 通過對兩個微服務進行相互認證並創建會話密鑰,可以對它們之間的通信進行加密並通過安全鏈接發送。

OpenSSL, TLS, SSL, CSR, CRT, X.509, Key

TLS (Transport Layer Security)

傳輸層安全性協定

SSL (Secure Socket Layer)

安全通訊協定

傳輸層安全性協定(英語:Transport Layer Security,縮寫:TLS)及其前身安全通訊協定(英語:Secure Sockets Layer,縮寫:SSL)是一種安全協定,目的是為網際網路通訊提供安全及資料完整性保障

TLS與SSL對於不是專業搞安全的開發人員來講,可以認為是差不多的,這二者是並列關係,詳細差異見 http://kb.cnblogs.com/page/197396/

CSR (Certificate Signing Request)

即證書籤名請求,這不是證書,可以簡單理解成公鑰,生成證書時要把這個提交給權威的證書頒發機構. 一種包含憑證簽發時所需的公鑰及相關資訊的檔案,不含私鑰;詳見X.509

CRT (certificate)

即證書

X.509

X.509 是一種證書格式.對X.509證書來說,認證者總是CA或由CA指定的人,一份X.509證書是一些標準欄位的集合,這些欄位包含有關使用者或裝置及其相應公鑰的資訊。

X.509的證書檔案,一般以.crt結尾,根據該檔案的內容編碼格式,可以分為以下二種格式:

PEM – Privacy Enhanced Mail,開啟看文字格式,以"—–BEGIN…“開頭, “—–END…“結尾,內容是BASE64編碼. Apache和*NIX伺服器偏向於使用這種編碼格式.

DER – Distinguished Encoding Rules,開啟看是二進位制格式,不可讀. Java和Windows伺服器偏向於使用這種編碼格式

wiki

X.509是密碼學裡公鑰憑證的格式標準。X.509憑證已應用在包括TLS/SSL在內的眾多網路協定裡,同時它也用在很多非線上應用場景里,比如電子簽章服務。X.509憑證里含有公鑰、身分資訊(比如網路主機名,組織的名稱或個體名稱等)和簽章資訊(可以是憑證簽發機構CA的簽章,也可以是自簽章)。1

X.509還附帶了憑證吊銷列表和用於從最終對憑證進行簽章的憑證簽發機構直到最終可信點為止的憑證合法性驗證演算法。X.509是ITU-T標準化部門基於他們之前的ASN.1定義的一套憑證標準。

Implementation

建立一個 8080 port 的 HTTP Server => server.go, 然後回傳 Hello World

Step 1 - Build a simple HTTP Server and Client

server.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"io"
	"log"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	// Write "Hello, world!" to the response body
	io.WriteString(w, "Hello, world!\n")
}

func main() {
	// Set up a /hello resource handler
	http.HandleFunc("/hello", helloHandler)

	// Listen to port 8080 and wait
	log.Fatal(http.ListenAndServe(":8080", nil))
}

client.go

Get Method 對著 http://localhost:8080 發出 Request /hello

 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
26
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	// Request /hello over port 8080 via the GET method
	r, err := http.Get("http://localhost:8080/hello")
	if err != nil {
		log.Fatal(err)
	}

	// Read the response body
	defer r.Body.Close()
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}

	// Print the response body to stdout
	fmt.Printf("%s\n", body)
}

啟動server : go run server.go 執行client 發起 request : go run client.go 最後可以在client看到 Hello, world!

Step 2 - Generate and use the Certificates with the Server

使用以下命令生成證書。該命令創建一個有效期為 10 年的 2048 bit key certificate。 此外,CN=localhost 斷言證書對localhost域有效。

1
2
3
4
5
6
7
openssl req -newkey rsa:2048 \
  -new -nodes -x509 \
  -days 3650 \
  -out cert.pem \
  -keyout key.pem \
  -addext "subjectAltName = DNS:localhost"\
  -subj "/C=US/ST=California/L=Mountain View/O=Your Organization/OU=Your Unit/CN=localhost"
FIX
  1. How do I use SANs with openSSL instead of common name? https://stackoverflow.com/questions/64814173/how-do-i-use-sans-with-openssl-instead-of-common-name

  2. -addext 不能用, openssl 版本問題 openssl upgrade: https://cloud.tencent.com/developer/article/1883983

    1
    
    brew install openssl
    
    1
    
    vi ~/.zshrc
    
    1
    2
    3
    4
    
    # brew openssl
    export PATH="/usr/local/opt/openssl@3/bin:$PATH"
    export LDFLAGS="-L/usr/local/opt/openssl@3/lib"
    export CPPFLAGS="-I/usr/local/opt/openssl@3/include"
    
    1
    
    source ~/.zshrc
    

最後會產出 cert.pemkey.pem 兩隻檔案.

server.go

現在就可以導入 TLS 到 HTTP server上, 只要將剛剛的 server.golog.Fatal(http.ListenAndServe(":8080", nil)) 換成 http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil) 此時就會有一個 HTTPS的 connections 監聽著 8443 port

此時可以打開瀏覽器輸入 https://localhost:8443/hello 來驗證

client.go

現在來修改client.go 讓他打打看 https://localhost:8443/hellor, err := http.Get("http://localhost:8080/hello") 改成 r, err := http.Get("https://localhost:8443/hello")

此時 client 端還不知道此證書, 所以可以在 server端 看到出現了以下的錯誤

1
2022/09/16 11:01:08 http: TLS handshake error from [::1]:62419: remote error: tls: bad certificate

client端出現

1
2
2022/09/16 11:01:08 Get "https://localhost:8443/hello": x509: “localhost” certificate is not standards compliant
exit status 1

Step 3 - Supply the Certificates to the Client

修改 client.go 讓他讀取先前建立的 certificates . 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
-	// Request /hello over HTTPS port 8443 via the GET method
-	r, err := http.Get("https://localhost:8443/hello")

+	// Create a CA certificate pool and add cert.pem to it
+	caCert, err := ioutil.ReadFile("cert.pem")
+	if err != nil {
+		log.Fatal(err)
+	}
+	caCertPool := x509.NewCertPool()
+	caCertPool.AppendCertsFromPEM(caCert)
+
+	// Create a HTTPS client and supply the created CA pool
+	client := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: caCertPool,
+			},
+		},
+	}
+
+	// Request /hello via the created HTTPS client over port 8443 via GET
+	r, err := client.Get("https://localhost:8443/hello")

我們在創建 client時 讀取cert.pem 並將此當作為 root CA 此時執行 go run client.go 可看到

1
Hello, world!

Final Step - Enable mTLS

client.go

On the Client, read and supply the key pair as the client certificate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
+	// Read the key pair to create certificate
+	cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
+	if err != nil {
+		log.Fatal(err)
+	}

...

-	// Create a HTTPS client and supply the created CA pool
+	// Create a HTTPS client and supply the created CA pool and certificate
	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				RootCAs: caCertPool,
+				Certificates: []tls.Certificate{cert},
			},
		},
	}

server.go

On the Server, we create a similar CA pool and supply it to the TLS config to serve as the authority to validate Client certificates. We also use the same key pair for the Server certificate.

 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
26
-	// Listen to HTTPS connections on port 8443 and wait
-	log.Fatal(http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil))

+	// Create a CA certificate pool and add cert.pem to it
+	caCert, err := ioutil.ReadFile("cert.pem")
+	if err != nil {
+		log.Fatal(err)
+	}
+	caCertPool := x509.NewCertPool()
+	caCertPool.AppendCertsFromPEM(caCert)
+
+	// Create the TLS Config with the CA pool and enable Client certificate validation
+	tlsConfig := &tls.Config{
+		ClientCAs: caCertPool,
+		ClientAuth: tls.RequireAndVerifyClientCert,
+	}
+	tlsConfig.BuildNameToCertificate()
+
+	// Create a Server instance to listen on port 8443 with the TLS config
+	server := &http.Server{
+		Addr:      ":8443",
+		TLSConfig: tlsConfig,
+	}
+
+	// Listen to HTTPS connections with the server certificate and wait
+	log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))

Final Code

server.go

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
	"crypto/tls"
	"crypto/x509"
	"io"
	"io/ioutil"
	"log"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	// Write "Hello, world!" to the response body
	io.WriteString(w, "Hello, world!\n")
}

func main() {
	// Set up a /hello resource handler
	http.HandleFunc("/hello", helloHandler)

	// Create a CA certificate pool and add cert.pem to it
	caCert, err := ioutil.ReadFile("cert.pem")
	if err != nil {
		log.Fatal(err)
	}
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	// Create the TLS Config with the CA pool and enable Client certificate validation
	tlsConfig := &tls.Config{
		ClientCAs:  caCertPool,
		ClientAuth: tls.RequireAndVerifyClientCert,
	}
	tlsConfig.BuildNameToCertificate()

	// Create a Server instance to listen on port 8443 with the TLS config
	server := &http.Server{
		Addr:      ":8443",
		TLSConfig: tlsConfig,
	}

	// Listen to HTTPS connections with the server certificate and wait
	log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}

client.go

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	// Read the key pair to create certificate
	cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
	if err != nil {
		log.Fatal(err)
	}

	// Create a CA certificate pool and add cert.pem to it
	caCert, err := ioutil.ReadFile("cert.pem")
	if err != nil {
		log.Fatal(err)
	}
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	// Create a HTTPS client and supply the created CA pool and certificate
	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				RootCAs:      caCertPool,
				Certificates: []tls.Certificate{cert},
			},
		},
	}

	// Request /hello via the created HTTPS client over port 8443 via GET
	r, err := client.Get("https://localhost:8443/hello")
	if err != nil {
		log.Fatal(err)
	}

	// Read the response body
	defer r.Body.Close()
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}

	// Print the response body to stdout
	fmt.Printf("%s\n", body)
}

Reference