背景介绍
kubeadm是Kubernetes提供的自动化部署方案,可以极大地简化广为诟病的Kubernetes集群搭建的复杂性。kubeadm init
命令对Kubernetes集群主节点的初始化流程中包含了以下若干步骤:
- PreflightPhase
- KubeletStartPhase
- CertsPhase
- KubeConfigPhase
- ControlPlanePhase
- EtcdPhase
- UploadConfigPhase
- UploadCertsPhase
- MarkControlPlanePhase
- BootstrapTokenPhase
- AddonPhase
CertsPhase
用于生成https服务端/客户端需要使用的证书、公钥/秘钥等。
这一部分大量使用到了go函数库中crypto模块的功能。一般而言,crypto模块的功能在日常开发中涉及较少,对kubeadm代码会造成不小的障碍。本文梳理了go语言标准库中crypto模块常用的场景,以及针对各场景给出了简短的代码示例。本文主要关注加密、解密等安全功能在go语言中的实践,相关的理论知识(比如各算法的原理、产生背景以及应用)此处不再赘述。
小文件的hash计算
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"fmt"
"io/ioutil"
)
data, err := ioutil.ReadFile(filename)
// 计算文件内容的hash值,并输出
fmt.Printf("Md5: %x\n\n", md5.Sum(data))
fmt.Printf("Sha1: %x\n\n", sha1.Sum(data))
fmt.Printf("Sha256: %x\n\n", sha256.Sum256(data))
fmt.Printf("Sha512: %x\n\n", sha512.Sum512(data))
大文件的hash计算
// 打开文件
file, err := os.Open(filename)
// 构建hasher对象(实现了writer接口)
hasher := md5.New()
_, err = io.Copy(hasher, file)
checksum := hasher.Sum(nil)
对称加密
生成随机数的三种方法
我们可以将生成随机数视为生成秘钥。
方法1
import (
"crypto/rand"
"math"
"math/big"
)
limit := int64(math.MaxInt64) // 允许的最大值
randInt, err := rand.Int(rand.Reader, big.NewInt(limit))
方法2
binary.Read()
方法将读取足够的字节以填充相应的数据类型
import (
"crypto/rand"
"encoding/binary"
)
var number uint32
err = binary.Read(rand.Reader, binary.BigEndian, &number)
方法3
直接生成相应长度的字节码slice
import (
"crypto/rand"
)
numBytes := 4
randomBytes := make([]byte, numBytes)
rand.Read(randomBytes)
fmt.Println("Random byte values: ", randomBytes)
加密算法
常用的对称加密算法是AES,而我们偶尔能够听到的DES是一个相对于AES更老的版本。下面我们演示的例子是基于go标准库中提供的aes算法实现。初始向量(initialization vector)的作用是使得对相同消息的加密结果也不一样,进一步提升数据的安全性。
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
)
func encrypt(key, message []byte) ([]byte, error) {
// 初始化block cipher
block, err := aes.NewCipher(key)
// 创建字节slice以持有后续产生的加密消息
cipherText := make([]byte, aes.BlockSize+len(message))
// 生成Initialization Vector (IV) nonce
// 这部分数据存储在slice的头部
// 其长度与AES的block大小一样
iv := cipherText[:aes.BlockSize]
_, err = io.ReadFull(rand.Reader, iv)
// 选择block cipher的工作模式
// 此处使用cipher feedback (CFB)模式
// 当然,CBCEncrypter也是一个备选项
cfb := cipher.NewCFBEncrypter(block, iv)
// 生成加密的消息,并将其存储在剩下的字节slice中
cfb.XORKeyStream(cipherText[aes.BlockSize:], message)
return cipherText, nil
}
解密
// AES解密
func decrypt(key, cipherText []byte) ([]byte, error) {
// 初始化block cipher
block, err := aes.NewCipher(key)
// 将IV nonce与加密消息分开
iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
// 使用CFB block模式将加密消息解密
cfb := cipher.NewCFBDecrypter(block, iv)
cfb.XORKeyStream(cipherText, cipherText)
return cipherText, nil
}
非对称加密
由于对称加密需要通讯双方共享秘钥,这一要求在互联网环境中会成为安全的隐患。由此安全专家们提出了RSA等非对称加密算法。
生成公钥/秘钥对
等价的openssl命令如下
# 生成秘钥
openssl genrsa -out priv.pem 2048
# 从私钥中提取公钥
openssl rsa -in priv.pem -pubout -out public.pem
对应的go代码如下
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"os"
)
// 生成公钥/秘钥对,并将其存储在PEM格式的文件中
privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
// 分别将秘钥公钥进行PEM格式的编码
privatePem := getPrivatePemFromKey(privateKey)
publicPem := generatePublicPemFromKey(privateKey.PublicKey)
// Save the PEM output to files
savePemToFile(privatePem, privatePemFilename)
savePemToFile(publicPem, publicPemFilename)
其中getPrivatePemFromKey
的实现为
// 将私钥编码到PEM文件中,本质上PEM是基于base64的
func getPrivatePemFromKey(privateKey *rsa.PrivateKey) *pem.Block {
encodedPrivateKey := x509.MarshalPKCS1PrivateKey(privateKey)
var privatePem = &pem.Block {
Type: "RSA PRIVATE KEY",
Bytes: encodedPrivateKey,
}
return privatePem
}
generatePublicPemFromKey
的实现如下
// 将公钥编码到PEM文件中
func generatePublicPemFromKey(publicKey rsa.PublicKey) *pem.Block {
encodedPubKey, err := x509.MarshalPKIXPublicKey(&publicKey)
// 构建公钥的PEM数据结构
var publicPem = &pem.Block{
Type: "PUBLIC KEY",
Bytes: encodedPubKey,
}
return publicPem
}
保存到pem文件的实现如下
func savePemToFile(pemBlock *pem.Block, filename string) {
// 将公钥PEM保存到文件中
publicPemOutputFile, err := os.Create(filename)
defer publicPemOutputFile.Close()
// pem.Encode()方法是将pem.Block序列化到字节流的方法
err = pem.Encode(publicPemOutputFile, pemBlock)
}
为消息签名
为了对消息进行签名,我们首先需要生成消息的hash值,随后根据私钥将该hash值进行加密;我们得到的加密hash值即所谓的“签名”;go的核心函数是rsa.SignPKCS1v15
下面的程序示例会根据消息和私钥,生成其签名
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
)
// 将文件中加载消息和私钥
message,err := ioutil.ReadFile(messageFilename)
// 从文件中加载私钥
privateKey,err := loadPrivateKeyFromPemFile(privateKeyFilename)
// 对消息进行签名
hashed := sha256.Sum256(message)
signature, err := rsa.SignPKCS1v15(
rand.Reader,
privateKey,
crypto.SHA256,
hashed[:],
)
加载秘钥的步骤为
// 从PEM格式的文件中加载RSA私钥
func loadPrivateKeyFromPemFile(privateKeyFilename string) *rsa.PrivateKey {
// 将文件快速加载到内存
fileData, err := ioutil.ReadFile(privateKeyFilename)
// 从PEM格式中解码
block, _ := pem.Decode(fileData)
// 将字节码解析到合适的数据结构中
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
return privateKey
}
对签名的验证
我们收到消息和签名之后,首先使用发送者的公钥将消息解密,然后将该值与原消息的hash值进行比对;核心函数为rsa.VerifyPKCS1v15()
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"os"
)
// 从磁盘中加载所有数据
publicKey := loadPublicKeyFromPemFile(publicKeyFilename)
// 生成消息的hash值
hashedMessage := sha256.Sum256(message)
// 验证签名是否合法的核心函数
err := rsa.VerifyPKCS1v15(
publicKey,
crypto.SHA256,
hashedMessage[:],
signature,
)
if err != nil {
// 那么说明签名不合法
}
从文件中加载公钥的实现如下
// 从PEM格式的文件中解码出公钥
func loadPublicKeyFromPemFile(publicKeyFilename string) *rsa.PublicKey {
// 将文件中的数据快速加载到内存
fileData, err := ioutil.ReadFile(publicKeyFilename)
// 从PEM格式解码到普通字节数据
block, _ := pem.Decode(fileData)
// 从字节数据中解析出公钥
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
return publicKey.(*rsa.PublicKey) // 转换到PublicKey格式
}
TLS
由于算法的限制,非对称加密只能对不大于key长度的信息进行加密,而key常见的长度为2048字节。由于大小的限制,非对称RSA算法用于加密整个文档是不实际的;对称加密比如AES可以用于加密整个大文件,但是需要在两边共享秘钥。TLS/SSL使用两者的结合。在握手阶段使用非对称加密生成并共享秘钥,连接建立之后使用对称加密传输数据。TLS本质上是一种结合了对称加密与非对称加密的技术。
生成自签名的证书
我们首先需要下面两个信息
- 公钥/秘钥对
- 证书模板(certificate template)
如果我们生成自签名证书,那么证书模板也用作颁发认证的parent certificate;核心的方法是x509.CreateCertificate()
函数签名为
func CreateCertificate (rand io.Reader, template, parent *Certificate, pub,
priv interface{}) (cert []byte, err error)
如果使用cfssl创建自签名证书(可作为CA)的命令如下
// 创建 ca-csr.json 文件
{
"CN": "kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "k8s",
"OU": "System"
}
],
"ca": {
"expiry": "87600h"
}
}
// 生成证书和私钥
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
cfssl生成默认的配置文件
cfssl print-defaults config > config.json cfssl print-defaults csr > csr.json
当我们运行TLS Server的时候,只需要提供证书与秘钥即可。为了演示的简单,此处证书的owner信息和hostname被设置为localhost
go代码如下
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509/pkix"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"math/big"
"net"
"os"
"time"
)
// 证书签发者的秘钥
privKey := loadPrivateKeyFromPemFile(privPemFilename)
// 证书申请者的公钥,而此处由于是自签名证书,所以签发者和申请者是同一人
pubKey := privKey.PublicKey
// 设置证书模板的信息
certTemplate := setupCertificateTemplate(isCA)
// 创建证书(核心为使用私钥进行签名)
certificate, err := x509.CreateCertificate(
rand.Reader,
&certTemplate,
&certTemplate,
&pubKey,
privKey,
)
// 将证书保存为PEM格式的文件
writeCertToPemFile(certOutputFilename, certificate)
而其中setupCertificateTemplate
的实现如下所示
func setupCertificateTemplate(isCA bool) x509.Certificate {
// 设置证书的有效日期
notBefore := time.Now()
notAfter := notBefore.Add(time.Hour * 24 * 365) // 1年即365天
// 生成随机的序列号
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
randomNumber, err := rand.Int(rand.Reader, serialNumberLimit)
// 证书所有者的信息:姓名、单位、住址等
nameInfo := pkix.Name{
Organization: []string{"My Organization"},
CommonName: "localhost",
OrganizationalUnit: []string{"My Business Unit"},
Country: []string{"US"}, // 2-character ISO code
Province: []string{"Texas"}, // State
Locality: []string{"Houston"}, // City
}
// 生成证书模板:随机数、个人信息、过期时间等
certTemplate := x509.Certificate{
SerialNumber: randomNumber,
Subject: nameInfo,
EmailAddresses: []string{"test@localhost"},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature,
// ExtKeyUsage字段默认是any
// 但是可以被设置为仅作为服务端认证、客户端认证、签发等
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
BasicConstraintsValid: true,
IsCA: false,
}
// 创建可以对CSR进行签名的CA证书
if isCA {
certTemplate.IsCA = true
certTemplate.KeyUsage = certTemplate.KeyUsage |
x509.KeyUsageCertSign
}
// 将此证书可以覆盖的IP地址或者hostname添加到此处
// 示例中仅覆盖到localhost
certTemplate.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
certTemplate.DNSNames = []string{"localhost", "localhost.local"}
return certTemplate
}
将证书保存到pem文件中
// 将证书保存到PEM编码的文件中Save the certificate as a PEM encoded file
func writeCertToPemFile(outputFilename string, derBytes []byte ) {
// 从证书创建PEM块
certPem := &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}
// 打开文件以便写入
certOutfile, err := os.Create(outputFilename)
pem.Encode(certOutfile, certPem)
certOutfile.Close()
}
创建CSR
CSR是certificate signing request的缩写。
核心的函数调用为
x509.CreateCertificateRequest()
等价的openssl命令为
# Create CSR
openssl req -new -key priv.pem -out csr.pem
# View details to verify request was created properly
openssl req -verify -in csr.pem -text -noout
等价的的cfssl命令为
// 创建 kubernetes 证书签名请求文件 kubernetes-csr.json
{
"CN": "kubernetes",
"hosts": [
"127.0.0.1",
"172.20.0.112",
"172.20.0.113",
"172.20.0.114",
"172.20.0.115",
"10.254.0.1",
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "k8s",
"OU": "System"
}
]
}
// 生成证书和私钥
$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubernetes-csr.json | cfssljson -bare kubernetes
其中ca-config.json定义了若干profile,这些profile规定了生成的证书的某些特性,比如过期时间等
cat > ca-config.json <<EOF
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"kubernetes": {
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
],
"expiry": "87600h"
}
}
}
}
EOF
go代码实现为
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net"
)
privKey := loadPrivateKeyFromPemFile(privKeyFilename)
// 准备证书的相关信息:姓名、单位、住址等
nameInfo := pkix.Name{
Organization: []string{"My Organization Name"},
CommonName: "localhost",
OrganizationalUnit: []string{"Business Unit Name"},
Country: []string{"US"}, // 2-character ISO code
Province: []string{"Texas"},
Locality: []string{"Houston"}, // City
}
// 准备CSR模板,其实与上节创建Certificate的信息很像,我们列在下面以便对比。有不同者在于:
// 1. 随机序列号
// 2. 过期时间
// certTemplate := x509.Certificate{
// SerialNumber: randomNumber,
// Subject: nameInfo,
// EmailAddresses: []string{"test@localhost"},
// NotBefore: notBefore,
// NotAfter: notAfter,
// KeyUsage: x509.KeyUsageKeyEncipherment |
// x509.KeyUsageDigitalSignature,
// // ExtKeyUsage字段默认是any
// // 但是可以被设置为仅作为服务端认证、客户端认证、签发等
// ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
// BasicConstraintsValid: true,
// IsCA: false,
// }
// certTemplate.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
// certTemplate.DNSNames = []string{"localhost", "localhost.local"}
csrTemplate := x509.CertificateRequest{
Version: 2, // Version 3, zero-indexed values
SignatureAlgorithm: x509.SHA256WithRSA,
PublicKeyAlgorithm: x509.RSA,
PublicKey: privKey.PublicKey,
Subject: nameInfo,
// Subject Alternate Name values.
DNSNames: []string{"Business Unit Name"},
EmailAddresses: []string{"test@localhost"},
IPAddresses: []net.IP{},
}
// Create the CSR based off the template
csr, err := x509.CreateCertificateRequest(rand.Reader,
&csrTemplate, privKey)
saveCSRToPemFile(csr, csrOutFilename)
其中持久化CSR到文件的实现为
func saveCSRToPemFile(csr []byte, filename string) {
csrPem := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr,
}
csrOutfile, err := os.Create(filename)
pem.Encode(csrOutfile, csrPem)
}
为CSR签名
使用签发者的证书作为parent certificate
等价的openssl命令如下
# Create signed certificate using
# the CSR, CA certificate, and private key
openssl x509 -req -in csr.pem -CA cacert.pem \
-CAkey capriv.pem -CAcreateserial \
-out cert.pem -sha256
# Print info about cert
openssl x509 -in cert.pem -text -noout
而核心的go函数为
func CreateCertificate(rand io.Reader, template, parent *Certificate, pub,
priv interface{}) (cert []byte, err error)
其中各个参数的含义如下
rand
: 这是安全的随机数生成器template
: 从CSR中提取的证书模板parent
: 签发者的证书pub
: 请求者的公钥priv
: 签发者的秘钥
TLS服务器
下面演示的例子是
import (
"bufio"
"crypto/tls"
"fmt"
"log"
"net"
"os"
)
// 从文件中加载证书与私钥
serverCert, err := tls.LoadX509KeyPair(certFilename, privKeyFilename)
// 设置证书、host/ip和端口
config := &tls.Config{
// 指定服务器端的证书
Certificates: []tls.Certificate{serverCert},
// 默认情况下不需要客户端证书By
// ClientAuth指定的类型可以是以下几种:
// NoClientCert, RequestClientCert, RequireAnyClientCert,
// VerifyClientCertIfGiven, RequireAndVerifyClientCert
// ClientAuth: tls.RequireAndVerifyClientCert
// 指定可以信任的客户端的CA
// ClientCAs: *x509.CertPool
}
// 构建TLS socket
listener, err := tls.Listen("tcp", hostString, config)
defer listener.Close()
// 在循环中监听连接
for {
clientConnection, err := listener.Accept()
// 对每个connection启动goroutine进行处理
go handleConnection(clientConnection)
}
// 处理connection的函数
func handleConnection(clientConnection net.Conn) {
defer clientConnection.Close()
socketReader := bufio.NewReader(clientConnection)
for {
// 从客户端读取消息
message, err := socketReader.ReadString('\n')
// 将消息返回客户端
numBytesWritten, err := clientConnection.Write([]byte(message))
fmt.Printf("Wrote %d bytes back to client.\n", numBytesWritten)
}
}
TLS客户端
// TLS配置
tlsConfig := &tls.Config{
// 是否接收自签名的证书
InsecureSkipVerify: true,
// 可以在此处给出自身的证书
// Certificates: []Certificate
// 用于验证hostname
// ServerName: string,
// 指定新人的CA
// RootCAs: *x509.CertPool
}
// 设置dialer以连接到服务器端
connection, err := tls.Dial("tcp", hostString, tlsConfig)、
defer connection.Close()
// 向socket写数据
numBytesWritten, err := connection.Write([]byte(messageToSend))
fmt.Printf("Wrote %d bytes to the socket.\n", numBytesWritten)
// 从socket中读取数据并输出到stdout
buffer := make([]byte, 100)
numBytesRead, err := connection.Read(buffer)
fmt.Printf("Read %d bytes to the socket.\n", numBytesRead)
fmt.Printf("Message received:\n%s\n", buffer)
https服务器
import (
"fmt"
"net/http"
"log"
)
func indexHandler(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "You requested: "+request.URL.Path)
}
func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServeTLS(
"localhost:8181",
"cert.pem",
"privateKey.pem",
nil,
)
}
在https服务器使用ca
func main() {
s := &http.Server{
Addr: ":443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
commonName := r.TLS.PeerCertificates[0].Subject.CommonName
fmt.Fprintf(w, "Hello, %s\n", commonName)
}),
TLSConfig: &tls.Config{
ClientCAs: loadCA("ca.crt"),
ClientAuth: tls.RequireAndVerifyClientCert,
},
}
e := s.ListenAndServeTLS("server.crt", "server.key")
if e != nil {
log.Fatal("ListenAndServeTLS: ", e)
}
}
// loadCA的实现为
func loadCA(caFile string) *x509.CertPool {
pool := x509.NewCertPool()
if ca, e := ioutil.ReadFile(caFile); e != nil {
log.Fatal("ReadFile: ", e)
} else {
pool.AppendCertsFromPEM(ca)
}
return pool
}
https客户端使用CA
func main() {
pair, e := tls.LoadX509KeyPair("client.crt", "client.key")
if e != nil {
log.Fatal("LoadX509KeyPair:", e)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: loadCA("ca.crt"),
Certificates: []tls.Certificate{pair},
},
}}
resp, e := client.Get("https://localhost")
if e != nil {
log.Fatal("http.Client.Get: ", e)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
总结
本文梳理了golang标准库中crypto模块常用的功能以及给出了代码示例,我们涉及到的功能主要是以下几种:
- 针对不同文件大小生成hash值的方法
- 对称加密/解密
- 非对称加密/解密
- TLS涉及的证书生成方法
- TLS服务端/客户端的使用(基于tcp层面)
- HTTPS服务器的使用
Kubernetes中将上述的功能封装到pkiutil
包中,在下篇我们会为大家梳理kubeadm生成https相关的安全凭证的步骤。
(360技术原创内容,转载请务必保留文末二维码,谢谢~)
关于360技术
360技术是360技术团队打造的技术分享公众号,每天推送技术干货内容 更多技术信息欢迎关注“360技术”微信公众号