สร้าง Certificate Authority ไว้ใช้งานใน lab test environment ด้วย CloudFlare’s PKI/TLS toolkit (CFSSL)

Nont Banditwong
6 min readNov 21, 2021

ในการติดตั้ง application server เกือบทุกตัวมักมีความจำเป็นต้องเพิ่มความปลอดภัยของการส่งผ่านข้อมูลผ่านเครือข่ายด้วย secure channel ที่เรามักได้ยินบ่อยๆเช่น SSL และ TLS เบื้องหลังการทำงานของสิ่งเหล่านี้อยู่ภายใต้หลักการของ Public Key Infrastructure หรือ PKI

มาตรฐาน PKI เข้าใจได้ไม่ง่าย ต้องค่อยๆศึกษา และทดสอบจากการใช้งานจริงถึงจะทำให้เข้าใจได้มากขึ้น หลักการพื้นฐานสามารถศึกษาได้จาก บทความของ ETDA หรือบทความจากเว็บต่างประเทศหลายๆที่เขียนอธิบายไว้ได้ดีเช่น What is Public Key Infrastructure (PKI) by Securemetric หรือจะอ่านจากตัวมาตรฐาน rfc5280 — Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile ก็ได้เช่นกัน แต่ส่วนตัวผมก็รู้แค่ขั้นพื้นฐานก็เพียงพอสำหรับงานที่ใช้อยู่ในปัจจุบัน

ปกติถ้าเราติดตั้ง web server หรือ application server ถ้าเราต้องการเข้ารหัสข้อมูลที่ส่งผ่านระหว่าง client และ server ที่ server เราต้องสร้างสิ่งที่เรียกว่า private key ซึ่งถ้าเราใช้โปรแกรม openssl สร้างจะได้มาพร้อมกับ certificate signing request (CSR)

openssl req –new –newkey rsa:2048 –nodes –keyout server.key –out server.csr

เมื่อได้ CSR มาแล้วเราก็ส่งให้กับ Certificate Authority (CA) ในการออกใบรับรอง (signed และ issued certificate) ให้ โดยทาง CA จะส่ง certificate ของ web server หรือ application กลับมาให้เรา เพื่อเอาไปติดตั้งใน web server หรือ application server ร่วมกับ private key ที่สร้างไว้ก่อนหน้า พอ web brower หรือ client ซึ่งปกติจะมี Root Certificate ใส่ไว้เชื่อมต่อมาที่ server ของเรา protocol อย่าง TLS จะมีการตรวจสอบว่า certificate ที่อยู่ที่ server ถูกออก certificate จาก CA ที่ติดตั้งอยู่ใน browser หรือไม่ ถ้าถูกต้องก็จะแลกเปลี่ยน key และตกลง algorithm ที่จะใช้ในการเข้ารหัสรวมถึงสร้าง channel ที่ถูกเข้ารหัสเพื่อใช้ในการสื่อสารต่อไป

ถ้าเราต้องการ PKI ใช้ในระบบปิด เช่นในองค์กร ที่เราสามารถควบคุม client และ server ได้ เช่น web browser สามารถเข้าไปติดตั้ง Root Certificate ขององค์กรได้ เรามักจะไม่ใช้บริการของ external CA เช่นที่เป็นที่รู้จักได้แก่ VeriSign Entrust หรือ DigiCert เป็นต้น ซึ่งการขอ certificate จากองค์กรเหล่านี้จะมีค่าใช้จ่าย

CloudFlare’s PKI/TLS toolkit หรือที่นิยมเรียกกันสั้นๆว่า CFSSL ได้รับความนิยมในการนำมาสร้างเป็น Certificate Authority ภายในองค์กร เพราะไม่ต้องเสียค่าใช้จ่าย ใช้ง่าย ซึ่งแม้กระทั่งทาง Let’s Encrypt ก็ใช้ CFSSL ในการให้บริการ ซึ่งทาง Let’s Encrypt ได้ open source ซอฟต์แวร์ที่ชื่อว่า Boulder ให้เอาไป implement ได้ฟรี แต่ blog นี้ต้องการนำ CFSSL มา setup lab test environment ง่ายๆ ดังนั้นการใช้ CFSSL ก็น่าจะเพียงพอแล้ว

มีบทความจำนวนมากที่เขียนถึงวิธีใช้งาน CFSSL แต่หลักๆผมลอง implement ตามบทความ Private CA with CFSSL และติดตั้ง CFSSL ลงบน CentOS 8 ซึ่งทาง CFSSL แนะนำให้ ติดตั้ง Go แบบ manual (CFSSL พัฒนาด้วยภาษา Go) เนื่องจาก OS ตระกูล Redhat ได้นำบาง algorithm ออกไปจาก Go package สำหรับ CFSSL สามารถ download ได้จาก https://github.com/cloudflare/cfssl/releases ที่ใช้ใน blog นี้จะเป็น cfssl และ cfssljson เท่านั้น สำหรับข้อมูลอื่นสามารถศึกษาได้จาก Github ของ CFSSL

Download CFSSL

version ล่าสุด โดยตอนเขียน blog เป็น version 1.6.3

curl -SLO https://github.com/cloudflare/cfssl/releases/download/v1.6.3/cfssl_1.6.3_linux_amd64 \
&& curl -SLO https://github.com/cloudflare/cfssl/releases/download/v1.6.3/cfssljson_1.6.3_linux_amd64

ทำให้ CFSSL สามารถ execute ได้

chmod +x cfssl_1.6.3_linux_amd64 cfssljson_1.6.3_linux_amd64

ย้าย CFSSL ไปอยู่ใน /usr/local/bin พร้อมทั้งเปลี่ยนชื่อให้สั้นลงเพื่อให้เรียกใช้งานได้สะดวก

sudo mv cfssl_1.6.3_linux_amd64 /usr/local/bin/cfssl \
&& sudo mv cfssljson_1.6.3_linux_amd64 /usr/local/bin/cfssljson

Setup directory structure

ที่จะใช้ทำงานกับ PKI Certificate ทั้งหมด

mkdir -p my-pki/root my-pki/intermediate my-pki/certificates && cd my-pki

การสร้าง Root CA

เราเริ่มสร้าง Root CA โดยสร้าง root-csr.json ตามคำสั่งด้านล่าง

cat << "EOF" > root/root-csr.json
{
"CN": "Example Inc. Root Certificate Authority",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "TH",
"ST": "Bangkok",
"L": "Yannawa",
"O": "Example Inc."
}
],
"ca": {
"expiry": "87600h"
}
}
EOF

CN หรือ Common Name เป็นชื่อของ Root CA ถ้าหากเราต้องการทำ Root CA ของบริษัทสามารถใช้ชื่อบริษัทในส่วนของ CN เลยก็ได้

ในตัวอย่าง Key algorithm ผมใช้ RSA ส่วน key size = 2048
อาจจะใช้ Key algorithm ที่ใหม่กว่า เช่น ECSDSA และ key size = 256 ก็ได้เช่นกัน

names หรือเรียกว่า Certificate Attributes หรือ Distinguished Name (DN) ซึ่งจะให้ใส่ข้อมูลคล้ายกับเวลาที่เราสร้าง CSR และ Private key จากโปรแกรม openssl
C = Country Code ประเทศไทยใช้ TH
ST = State หรือ ชื่อจังหวัด
L = Locality ใช้เป็นชื่อ อำเภอหรือเขต
O = ชื่อองค์กร

Expiry หรือวันหมดอายุ ปกติ Root CA เราจะกำหนดเวลาหมดอายุค่อนข้างนาน เพื่อหลีกเลี่ยงการ สร้าง key ใหม่บ่อยๆ ในตัวอย่างใช้ 87,600 ชั่วโมง หรือ 10 ปี

จากนั้นใช้ cfssl สร้าง Root CA ซึ่งจะได้ private key public key รวมถึง CSR ของ Root CA ออกมา

cfssl gencert -initca root/root-csr.json \
| cfssljson -bare root/root-ca

ถ้าเราใช้คำสั่ง tree ดูจะมีไฟล์ตามรูปด้านล่าง

my-pki
├── certificates
├── intermediate
└── root
├── root-ca.csr
├── root-ca-key.pem
├── root-ca.pem
└── root-csr.json

ถ้าหากจะสร้าง PKI ไปใช้งานจริงจัง ปกติการสร้าง Root CA ซึ่งจะได้ private key ออกมา 1 ไฟล์ ซึ่งไฟล์นี้ควรถูกเก็บในสถานที่ปลอดภัย ถ้าเป็นคอมพิวเตอร์ก็ไม่ควรเชื่อมต่อระบบเครือข่าย ซึ่งไม่สะดวกในการออกcertificate ดังนั้นในทางปฏิบัติเราควรสร้างสิ่งที่เรียกว่า Intermediate CA สำหรับใช้ในการ ออกcertificate แทน Root CA ซึ่งถ้าหาก Intermidiate CA ถูก compromise จะไม่กระทบถึง Root CA ซึ่งเราสามารถใช้ Root CA ในการ ออก Intermediate CA ใหม่ได้ เพราะถ้า Root CA ถูก compromise การออก Root CA ใหม่ ต้องทำการนำ public key ใบใหม่ของ Root CA ไปติดตั้งในทุกๆ client ยิ่ง client มีจำนวนมากก็เป็นเรื่องยาก นอกจากนั้นเรายังสามารถมีหลาย Intermediate CA เช่นแบ่งแยกตามองค์กรย่อยๆเพื่อใช้ในการออก certificate สำหรับ server ที่อยู่ภายใต้องค์กรย่อยเหล่านั้นได้

การสร้าง Intermediate CA

cat << "EOF" > intermediate/intermediate-csr.json 
{
"CN": "Example Inc. Intermediate Certificate Authority",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "TH",
"ST": "Bangkok",
"L": "Yannawa",
"O": "Example Inc.",
"OU": "Example Inc. Internal Intermediate CA"
}
]
}
EOF

สร้าง private key และ CSR สำหรับ intermediate CA ด้วย cfssl

cfssl genkey intermediate/intermediate-csr.json \
| cfssljson -bare intermediate/intermediate-ca

ก่อนที่เราจะใช้ CFSSL ออก certificate สำหรับ Intermediates CA ได้นั้นต้องมีการสร้าง configure profile ของ CFSSL โดยตั้งชื่อว่า “intermediate” แต่ในตัวอย่างจะมี profile “host” ซึ่งเอาไว้สำหรับออกใบรับรองสำหรับ server อยู่ด้วย ซึ่งจะถูกใช้สำหรับการออกใบรับรองให้กับ server ต่อไป

Attribute ใน profile intermediate มีดังนี้
- Usage: cert sign และ crl sign เป็นตัวกำหนดว่า intermediate CA นี้สามารถใช้สำหรับออกและถอนใบอนุญาต
- Expiry: วันหมดอายุกำหนดเป็น 70,080 ชั่วโมง หรือ 8 ปี
- CA Constraint:
is_ca=true กำหนดให้ certificate นี้ใช้สำหรับเป็น CA
max_path_len=1 จำกัดให้ certificate นี้สามารถออก sub-intermediate CA ได้อีก 1 ระดับ แต่ถ้าเราไม่ต้องการให้ intermediate CA นี้สามารถออกใบอนุญาตเป็น sub-intermediate CA ได้ สามารถกำหนด max_path_len=0 และ max_path_len_zero=true

cat << "EOF" > ca-config.json
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"intermediate": {
"usages": ["cert sign", "crl sign"],
"expiry": "70080h",
"ca_constraint": {
"is_ca": true,
"max_path_len": 1
}
},
"host": {
"expiry": "8760h",
"usages": ["signing", "digital signing", "key encipherment", "server auth"]
}
}
}
}
EOF

ทำการออก certificate ให้ intermediate ด้วย Root CA ด้วย cfssl

cfssl sign -ca root/root-ca.pem \
-ca-key root/root-ca-key.pem \
-config ca-config.json \
-profile intermediate \
intermediate/intermediate-ca.csr \
| cfssljson -bare intermediate/intermediate-ca

ถึงตรงนี้เราควร backup private key (root/root-ca-key.pem) ของ Root CA ไปไว้ที่ปลอดภัย และลบออกจาก server นี้ ซึ่งในทางปฏิบัติแล้วสถานที่ที่ใช้ในการสร้าง Root CA ก็ไม่ควรจะเชื่อมต่อระบบเครือข่ายตั้งแต่แรก แต่สำหรับ lab test environment เราจะลบหรือไม่ลบ private key นี้ก็ได้

การออก Certificate ให้กับ server

การออก certificate ให้กับ application server เช่นจากตัวอย่างนี้จะออกใบรับรองให้ Apache Kafka ซึ่งใช้ hostname kaf01 ถ้าเป็น server ที่ใช้ในองค์กร หรือ อินเทอร์เน็ตปกติจะใช้ FQDN (Fully Qualified Domain Name) ซึ่งประกอบด้วย ชื่อโฮสต์ และชื่อโดเมนรวมถึง โดเมนระดับบนสุด ยกตัวอย่างเช่น kaf01.example.com แต่เพื่อความง่ายเราจะใช้แค่ hostname คือ kaf01

cat << "EOF" > certificates/kaf01-csr.json
{
"CN": "kaf01",
"hosts": ["kaf01"],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "TH",
"ST": "Bangkok",
"L": "Yannawa",
"O": "Example, Inc.",
"OU": "DevOps"
}
]
}
EOF

hosts กำหนด list ของ SANs (subject alternative name) คือชื่อของ identity สำหรับ certificate โดยเป็น list ของ server name ที่เมื่อ server รับมาจากฝั่ง client แล้วจะใช้ใบรับรองฉบับนี้ในการสื่อสาร โดยอาจจะประกอบด้วย kaf01,kaf01.example.com,192.168.100.10 เมื่อ client connect ไปที่ server ไม่ว่าจะเรียกด้วย kaf01 หรือ ip 192.168.100.10 ก็จะสามารถสร้าง secure channel ได้ แต่ในตัวอย่างจะเห็นว่าผมใช้ kaf01 อย่างเดียว ถ้าหาก client เชื่อมต่อมาด้วย IP จะไม่สามารถใช้งานได้

เราอาจตรวจสอบ SANs ของ server ด้วยคำสั่ง

openssl s_client -connect <HOST>:<PORT> </dev/null 2>/dev/null | openssl x509 -noout -text | grep DNS:

นอกจากนั้นเราสามารถส่งค่าของ SAN เพื่อทดสอบการเชื่อมต่อโดยใช้ openssl ได้ดังนี้

openssl s_client -connect <HOST>:<PORT> -servername <SAN>

ออก certificate ให้กับ server kaf01 โดยใช้ cfssl

cfssl gencert \
-ca intermediate/intermediate-ca.pem \
-ca-key intermediate/intermediate-ca-key.pem \
-config ca-config.json \
-profile host \
certificates/kaf01-csr.json \
| cfssljson -bare certificates/kaf01

ถึงจุดนี้จะได้ไฟล์ดังนี้ โดย certificate คือไฟล์ kaf01.pem ส่วน private key คือ kaf01-key.pem

my-pki
├── certificates
│ ├── kaf01.csr
│ ├── kaf01-csr.json
│ ├── kaf01-key.pem
│ └── kaf01.pem
├── ca-config.json
├── intermediate
│ ├── intermediate-ca.csr
│ ├── intermediate-ca-key.pem
│ ├── intermediate-ca.pem
│ └── intermediate-csr.json
└── root
├── root-ca.csr
├── root-ca.pem
└── root-csr.json

เราสามารถใช้คำสั่ง openssl ในการตรวจสอบ certificate ได้ดังนี้

openssl x509 -in certificates/kaf01.pem -text -noout

ลองใช้คำสั่ง openssl verify certificate กับ Root CA

openssl verify -verbose -CAfile root/root-ca.pem certificates/kaf01.pem

จะพบว่า verify ไม่ผ่าน

C = TH, ST = Bangkok, L = Yannawa, O = "Example, Inc.", OU = DevOps, CN = kaf01
error 20 at 0 depth lookup: unable to get local issuer certificate
error certificates/kaf01.pem: verification failed

ลองใช้คำสั่ง openssl verify certificate กับ Intermediate CA

openssl verify -verbose -CAfile intermediate/intermediate-ca.pem certificates/kaf01.pem

จะพบว่า verify ไม่ผ่าน เช่นกัน

C = TH, ST = Bangkok, L = Yannawa, O = Example Inc., OU = Example Inc. Internal Intermediate CA, CN = Example Inc. Intermediate Certificate Authority
error 2 at 1 depth lookup: unable to get issuer certificate
error certificates/kaf01.pem: verification failed

ที่เป็นแบบนี้เพราะ certificate chain ไม่ครบ สำหรับเรื่อง Certificate Chain ให้ศึกษาจากบทความ Get your certificate chain right

สร้าง CA bundle โดยนำเอา public key ของ Intermediate CA และ Root CA มาต่อกัน

cat intermediate/intermediate-ca.pem root/root-ca.pem > ca-bundle.pem

ลอง verify certificate โดยใช้ CA bundle

openssl verify -verbose -CAfile ca-bundle.pem certificates/kaf01.pem

จะพบว่า certificate valid

certificates/kaf01.pem: OK

เวลานำ certificate ไปใช้ผมมักจะเอา server certificate bundle ไปกับ Intermediate CA เพราะถ้า client มีเฉพาะ Root CA จะได้ครบ chain ถ้าหากขาดท่อนของ Intermediate CA ไปจะทำให้ verify ไม่ผ่าน

สร้าง certificate bundle ของ server และ Intermediate CA

cat intermediate/intermediate-ca.pem certificates/kaf01.pem > certificates/kaf01-bundle.pem

ลองใช้ certificate bundle verify กับ Root CA

openssl verify -verbose -CAfile root/root-ca.pem certificates/kaf01-bundle.pem

จะ verify ผ่าน

certificates/kaf01-bundle.pem: OK

เราสามารถนำ certificate bundle และ private key ไปติดตั้งใน server เช่น Apache หรือ Nginx เพื่อให้รองรับ TLS หรือ SSL ได้

สำหรับใน lab test ผมใช้ certificate นี้ใน Apache Kafka ซึ่งต้องมีการสร้าง keystore และ trust store ก่อน

สร้าง certificate ใน format PKS12

โดยกำหนดให้ passphase คือ changeme

openssl pkcs12 \
-export \
-out certificates/kaf01.p12 \
-inkey certificates/kaf01-key.pem \
-in certificates/kaf01-bundle.pem \
-password pass:changeme

สร้าง Java keystore certificates/kafka.kaf01.keystore.jks

keytool -importkeystore -srckeystore certificates/kaf01.p12 -srcstoretype pkcs12 -destkeystore certificates/kafka.kaf01.keystore.jks -srcstorepass changeme -deststorepass changeme -noprompt

ในทางปฏิบัติถ้าไม่ได้ใช้ automation tool ในการสร้าง ให้พิมพ์ password ด้วยมือปลอดภัยกว่า เนื่องจาก password พวกนี้สามารถเรียกดูได้จาก history ของ bash shell

สร้าง Java truststore โดย import public certificate ของ Root CA เข้าไป

keytool -keystore certificates/kafka.kaf01.truststore.jks -alias CA -import -file root/root-ca.pem -keypass changeme -storepass changeme

เราสามารถ list certificate ใน keystore และ trust store ด้วยคำสั่ง keytool -list

keytool -list -v -keystore certificates/kafka.kaf01.keystore.jks -storepass changeme

เราสามารถนำ keystore และ trust store ไปติดตั้งใน server เช่นในกรณี Apache Kafka

ไฟล์ server.properties

# Broker security settings
ssl.truststore.location=/etc/ssl/private/kafka.kaf01.truststore.jks
ssl.truststore.password=changeme
ssl.keystore.location=/etc/ssl/private/kafka.kaf01.keystore.jks
ssl.keystore.password=changeme
ssl.key.password=changeme

สามารถใช้ openssl ในการ verify certificate ที่ติดตั้งได้ เช่นในกรณีของ Apache Kafka (ใช้ listener SASL_SSL://:9093)

openssl s_client -connect kaf01:9093 -servername kaf01 -CAfile root/root-ca.pem

อ้างอิง

[1] Introducing CFSSL — CloudFlare’s PKI toolkit
[2] Private CA with CFSSL

--

--

Nont Banditwong

Cloud Engineering Specialist, Software Developer, System Engineer, Photographer and Learner