Tự Tay Ký Toàn Bộ Certificate Bằng cfssl

K
Kai··9 min read

Bài 3 để lại cho ta sáu máy trống và một workstation đủ công cụ. Hôm nay là bước "Kubernetes" thực sự đầu tiên, và cũng là phần kubeadm giấu kín nhất: ký toàn bộ certificate cho cluster. Ở Bài 2 ta đã vẽ ra sơ đồ ai trình cert gì cho ai; bài này biến sơ đồ đó thành các file .pem cụ thể.

Ta dùng cfssl của CloudFlare thay cho openssl thuần, vì với một lượng cert lớn thì cfssl gọn hơn nhiều: mỗi cert mô tả bằng một file JSON ngắn, ký bằng một lệnh. Mọi thứ làm trên workstation, trong thư mục ~/k8s-scratch/pki, rồi các bài sau mới đẩy từng file lên đúng máy cần nó.

Những gì ta sắp tạo

   Kubernetes CA ──┬── kube-apiserver        (server, SAN đầy đủ)
                   ├── apiserver-kubelet-client
                   ├── admin                 (O=system:masters)
                   ├── kube-controller-manager
                   ├── kube-scheduler
                   ├── kube-proxy
                   ├── worker-0  (CN=system:node:worker-0, O=system:nodes)
                   ├── worker-1  (CN=system:node:worker-1, O=system:nodes)
                   └── service-account       (cặp khóa ký token)

   etcd CA ────────┬── etcd                  (server+peer, SAN 3 controller)
                   └── apiserver-etcd-client

   front-proxy CA ─── front-proxy-client

Chuẩn bị thư mục và file cấu hình ký

cfssl cần một file cấu hình mô tả cách ký: cert dùng để làm gì (server auth, client auth) và hạn bao lâu. Ta dùng một "profile" tên kubernetes cho tất cả, hạn một năm (8760h):

mkdir -p ~/k8s-scratch/pki && cd ~/k8s-scratch/pki

cat > ca-config.json <<'EOF'
{
  "signing": {
    "default": { "expiry": "8760h" },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

Để profile gộp cả server auth lẫn client auth cho gọn. Trên production người ta thường tách hai loại để cert chỉ làm đúng một vai, nhưng ở đây gộp lại đỡ rườm rà mà vẫn đúng.

Bước 1 — Ba Certificate Authority

Mỗi CA là một cặp khóa tự ký. cfssl gencert -initca nhận một CSR mô tả CN của CA rồi sinh ra <tên>.pem (cert) và <tên>-key.pem (private key). Tạo ba file CSR cho ba CA:

cat > ca-csr.json <<'EOF'
{ "CN": "kubernetes-ca", "key": {"algo":"rsa","size":2048},
  "names": [{"O":"Kubernetes","OU":"CA","C":"VN"}] }
EOF
cat > etcd-ca-csr.json <<'EOF'
{ "CN": "etcd-ca", "key": {"algo":"rsa","size":2048},
  "names": [{"O":"etcd","OU":"CA","C":"VN"}] }
EOF
cat > front-proxy-ca-csr.json <<'EOF'
{ "CN": "front-proxy-ca", "key": {"algo":"rsa","size":2048},
  "names": [{"O":"front-proxy","OU":"CA","C":"VN"}] }
EOF

Rồi sinh cả ba:

cfssl gencert -initca ca-csr.json          | cfssljson -bare ca
cfssl gencert -initca etcd-ca-csr.json     | cfssljson -bare etcd-ca
cfssl gencert -initca front-proxy-ca-csr.json | cfssljson -bare front-proxy-ca
2026/05/23 20:37:43 [INFO] generating a new CA key and certificate from CSR
2026/05/23 20:37:43 [INFO] generate received request
2026/05/23 20:37:43 [INFO] generating key: rsa-2048
2026/05/23 20:37:43 [INFO] signed certificate with serial number 6831289342662456212...
...

cfssljson -bare ca tách kết quả JSON của cfssl thành hai file ca.pemca-key.pem. Sau bước này có ba cặp CA. Ba private key *-ca-key.pem này là thứ nhạy cảm nhất trong cả cluster — ai có chúng thì ký được cert giả mạo bất kỳ danh tính nào.

Bước 2 — Cert cho các client của control plane

Bốn cert đầu đều là client cert, không cần SAN, chỉ cần CN và O đúng để api-server đọc ra danh tính (nhớ Bài 2: CN thành user, O thành group). Định nghĩa một hàm ngắn để khỏi lặp tham số ký:

GEN() { cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes "$@"; }

admin là danh tính ta dùng để quản trị cluster, đặt O=system:masters để có toàn quyền:

cat > admin-csr.json <<'EOF'
{ "CN":"admin","key":{"algo":"rsa","size":2048},
  "names":[{"O":"system:masters","OU":"k8s-scratch","C":"VN"}] }
EOF
GEN admin-csr.json | cfssljson -bare admin

Ba thành phần control plane, mỗi cái một danh tính có sẵn binding RBAC trong Kubernetes — CN phải đặt đúng chuỗi system:kube-...:

cat > kube-controller-manager-csr.json <<'EOF'
{ "CN":"system:kube-controller-manager","key":{"algo":"rsa","size":2048},
  "names":[{"O":"system:kube-controller-manager","C":"VN"}] }
EOF
GEN kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager

cat > kube-scheduler-csr.json <<'EOF'
{ "CN":"system:kube-scheduler","key":{"algo":"rsa","size":2048},
  "names":[{"O":"system:kube-scheduler","C":"VN"}] }
EOF
GEN kube-scheduler-csr.json | cfssljson -bare kube-scheduler

cat > kube-proxy-csr.json <<'EOF'
{ "CN":"system:kube-proxy","key":{"algo":"rsa","size":2048},
  "names":[{"O":"system:node-proxier","C":"VN"}] }
EOF
GEN kube-proxy-csr.json | cfssljson -bare kube-proxy

Khi ký các cert này, cfssl in một cảnh báo This certificate lacks a "hosts" field. Với client cert thì cảnh báo đó vô hại — trường hosts (SAN) chỉ cần cho server cert, còn client cert được nhận diện bằng CN/O, không bằng địa chỉ. Cứ bỏ qua.

Bước 3 — Cert kubelet cho từng worker

Đây là chỗ CN/O quan trọng nhất, như đã nói ở Bài 2. Mỗi worker cần một cert riêng với CN=system:node:<tên-node>O=system:nodes. Prefix system:node: bật Node authorizer, giới hạn mỗi kubelet chỉ chạm tới tài nguyên thuộc node của nó. Cert kubelet vừa làm client (gọi lên api-server) vừa làm server (api-server gọi xuống để lấy logs/exec), nên cần cả SAN gồm hostname và IP của node.

Một hàm nhỏ, gọi cho từng worker:

gen_kubelet() {  # $1=host  $2=ip
  local host=$1 ip=$2
  cat > ${host}-csr.json <<EOF
{ "CN":"system:node:${host}","key":{"algo":"rsa","size":2048},
  "hosts":["${host}","${ip}"],
  "names":[{"O":"system:nodes","C":"VN"}] }
EOF
  GEN -hostname=${host},${ip} ${host}-csr.json | cfssljson -bare ${host}
}
gen_kubelet worker-0 10.0.1.20
gen_kubelet worker-1 10.0.1.21

Kiểm tra lại cho chắc CN và SAN đúng — bước verify này đáng làm, vì sai một ký tự ở CN là kubelet sẽ bị từ chối khi join:

openssl x509 -in worker-0.pem -noout -subject -ext subjectAltName
subject=C = VN, O = system:nodes, CN = system:node:worker-0
X509v3 Subject Alternative Name:
    DNS:worker-0, IP Address:10.0.1.20

Bước 4 — Server cert cho kube-apiserver

Cert này cần trường SAN đầy đủ nhất, vì api-server bị gọi tới từ rất nhiều địa chỉ khác nhau (nhắc lại Bài 2). Danh sách SAN gồm:

  • 127.0.0.1localhost — gọi nội bộ trên chính máy controller.
  • IP từng controller 10.0.1.11/12/13 — gọi trực tiếp giữa các node.
  • IP private của load balancer 10.0.1.10Elastic IP của nó — vì kubectl từ máy bạn đi qua load balancer.
  • Các tên DNS kubernetes, kubernetes.default, ... .svc.cluster.local — tên Service nội bộ mà pod dùng để gọi api-server.
  • 10.32.0.1 — ClusterIP đầu dải Service, chính là địa chỉ ảo của Service kubernetes.

Vì sao có Elastic IP ở đây. kubectl trên laptop gọi api-server qua public IP của lb-0. Public IP tự cấp của EC2 đổi mỗi lần stop/start, mà SAN thì cố định trong cert — nếu để IP nhảy, kubectl sẽ báo x509: certificate is valid for ... not .... Nên ở đây ta gán cho lb-0 một Elastic IP cố định và đưa nó vào SAN. Nếu bạn chưa làm ở Bài 3, cấp ngay: aws ec2 allocate-address --domain vpc rồi aws ec2 associate-address --instance-id <lb-0> --allocation-id <eip>. Trong ví dụ này Elastic IP là 203.0.113.10.

APISERVER_SANS="10.32.0.1,10.0.1.10,203.0.113.10,10.0.1.11,10.0.1.12,10.0.1.13,127.0.0.1,localhost,lb-0,controller-0,controller-1,controller-2,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.default.svc.cluster.local"

cat > kube-apiserver-csr.json <<'EOF'
{ "CN":"kube-apiserver","key":{"algo":"rsa","size":2048},
  "names":[{"O":"Kubernetes","C":"VN"}] }
EOF
GEN -hostname="${APISERVER_SANS}" kube-apiserver-csr.json | cfssljson -bare kube-apiserver

Xác nhận SAN đã đủ:

openssl x509 -in kube-apiserver.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name:
    DNS:localhost, DNS:lb-0, DNS:controller-0, DNS:controller-1, DNS:controller-2,
    DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc,
    DNS:kubernetes.default.svc.cluster, DNS:kubernetes.default.svc.cluster.local,
    IP Address:10.32.0.1, IP Address:10.0.1.10, IP Address:203.0.113.10,
    IP Address:10.0.1.11, IP Address:10.0.1.12, IP Address:10.0.1.13, IP Address:127.0.0.1

Bước 5 — Hai client cert của api-server và cặp khóa service-account

api-server cũng là client trong hai cuộc hội thoại: nó gọi xuống kubelet, và nó đọc/ghi etcd. Cert gọi kubelet đặt O=system:masters để được phép gọi mọi kubelet:

cat > apiserver-kubelet-client-csr.json <<'EOF'
{ "CN":"kube-apiserver-kubelet-client","key":{"algo":"rsa","size":2048},
  "names":[{"O":"system:masters","C":"VN"}] }
EOF
GEN apiserver-kubelet-client-csr.json | cfssljson -bare apiserver-kubelet-client

Còn cặp khóa service-account — như đã nói ở Bài 2, nó dùng để ký và xác minh token của ServiceAccount. Ở đây ta sinh nó như một cert ký bởi CA cho tiện; thứ ta thực sự dùng về sau là phần khóa: controller-manager dùng private key để ký token, api-server dùng public key để xác minh.

cat > service-account-csr.json <<'EOF'
{ "CN":"service-accounts","key":{"algo":"rsa","size":2048},
  "names":[{"O":"Kubernetes","C":"VN"}] }
EOF
GEN service-account-csr.json | cfssljson -bare service-account

Bước 6 — Cert ký bởi etcd CA và front-proxy CA

etcd có CA riêng. Cert server của etcd cần SAN phủ cả ba controller (vì etcd chạy stacked trên cả ba và chúng nói chuyện peer-to-peer) cùng 127.0.0.1. Ta dùng một cert chung cho cả vai server lẫn peer cho gọn:

ETCD_GEN() { cfssl gencert -ca=etcd-ca.pem -ca-key=etcd-ca-key.pem -config=ca-config.json -profile=kubernetes "$@"; }

ETCD_SANS="127.0.0.1,localhost,controller-0,controller-1,controller-2,10.0.1.11,10.0.1.12,10.0.1.13"
cat > etcd-csr.json <<'EOF'
{ "CN":"etcd","key":{"algo":"rsa","size":2048}, "names":[{"O":"etcd","C":"VN"}] }
EOF
ETCD_GEN -hostname="${ETCD_SANS}" etcd-csr.json | cfssljson -bare etcd

# api-server → etcd (client)
cat > apiserver-etcd-client-csr.json <<'EOF'
{ "CN":"kube-apiserver-etcd-client","key":{"algo":"rsa","size":2048}, "names":[{"O":"etcd","C":"VN"}] }
EOF
ETCD_GEN apiserver-etcd-client-csr.json | cfssljson -bare apiserver-etcd-client

front-proxy CA ký một cert client duy nhất, dùng cho aggregation layer (ta cấu hình cờ tương ứng ở Bài 7, dùng đến khi mở rộng API về sau):

cat > front-proxy-client-csr.json <<'EOF'
{ "CN":"front-proxy-client","key":{"algo":"rsa","size":2048}, "names":[{"O":"front-proxy","C":"VN"}] }
EOF
cfssl gencert -ca=front-proxy-ca.pem -ca-key=front-proxy-ca-key.pem \
  -config=ca-config.json -profile=kubernetes front-proxy-client-csr.json | cfssljson -bare front-proxy-client

Verify chuỗi tin cậy

Trước khi đóng lại, kiểm tra mọi cert thực sự được ký bởi đúng CA của nó. openssl verify báo OK nghĩa là chuỗi tin cậy khớp:

openssl verify -CAfile etcd-ca.pem etcd.pem apiserver-etcd-client.pem
openssl verify -CAfile front-proxy-ca.pem front-proxy-client.pem
openssl verify -CAfile ca.pem kube-apiserver.pem admin.pem worker-0.pem kube-proxy.pem
etcd.pem: OK
apiserver-etcd-client.pem: OK
front-proxy-client.pem: OK
kube-apiserver.pem: OK
admin.pem: OK
worker-0.pem: OK
kube-proxy.pem: OK

Bản kiểm kê đầy đủ những danh tính ta vừa tạo:

ca                         CN=kubernetes-ca           (CA)
etcd-ca                    CN=etcd-ca                 (CA)
front-proxy-ca             CN=front-proxy-ca          (CA)
kube-apiserver             CN=kube-apiserver          O=Kubernetes
apiserver-kubelet-client   CN=kube-apiserver-kubelet-client  O=system:masters
apiserver-etcd-client      CN=kube-apiserver-etcd-client     O=etcd
service-account            CN=service-accounts        O=Kubernetes
etcd                       CN=etcd                    O=etcd
admin                      CN=admin                   O=system:masters
kube-controller-manager    CN=system:kube-controller-manager
kube-scheduler             CN=system:kube-scheduler
kube-proxy                 CN=system:kube-proxy       O=system:node-proxier
worker-0                   CN=system:node:worker-0    O=system:nodes
worker-1                   CN=system:node:worker-1    O=system:nodes
front-proxy-client         CN=front-proxy-client      O=front-proxy

Mười lăm cặp cert/key, tất cả đang nằm trên workstation trong ~/k8s-scratch/pki. Ta chưa đẩy lên node nào — việc đó làm dần ở các bài bootstrap, khi mỗi thành phần cần đúng cert của nó.

🧹 Một lưu ý về bảo mật, không phải dọn dẹp

Bài này không tạo tài nguyên cloud nên không có gì để dọn. Nhưng thư mục pki giờ chứa các private key — đặc biệt là ca-key.pem, etcd-ca-key.pem, front-proxy-ca-key.pem. Giữ chúng trên workstation, đừng commit lên Git, đừng chia sẻ. Trong series này ta để chung một thư mục cho tiện học; trên thực tế, private key của CA nên được cất ở nơi tách biệt và hạn chế truy cập.

Tổng kết

Ta đã có đủ giấy tờ tùy thân cho mọi thành phần trong cluster: ba CA và mười hai cert lá, mỗi cái đúng CN/O/SAN như sơ đồ PKI ở Bài 2, và đã verify chuỗi tin cậy. Đây là phần mất công nhất nhưng cũng là phần làm bạn hiểu rõ nhất vì sao về sau mỗi thành phần lại tin được thành phần khác.

Có cert rồi nhưng chưa thành phần nào dùng được ngay, vì chúng cần được gói lại cùng địa chỉ api-server và CA vào một dạng file mà các binary Kubernetes đọc: kubeconfig. Bài 5 sẽ sinh kubeconfig cho admin, controller-manager, scheduler, kube-proxy và từng kubelet, đồng thời tạo file cấu hình mã hóa Secret (encryption at rest) — chuẩn bị nốt phần cấu hình trước khi bật etcd ở Bài 6.

Related Posts