สร้าง Certificate Authority ไว้ใช้งานใน lab test environment ด้วย CloudFlare’s PKI/TLS toolkit (CFSSL)
ในการติดตั้ง 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