0

Kernel CVEs Are Leaking Faster Than You Patch: A Practical Kernel Update & Reboot Strategy for Linux Servers

Tuần này trên Hacker News có một tin khiến nhiều sysadmin giật mình: vài lỗ hổng Linux kernel bị vô tình gửi lên mailing list công khai trước khi có bản vá chính thức. Nghĩa là trong vài giờ đến vài ngày, attacker có thể đọc chi tiết lỗ hổng và viết exploit, còn server của bạn vẫn chạy kernel cũ. Thực tế còn tệ hơn thế. Rất nhiều team ở Việt Nam mà mình từng làm cùng có thói quen apt upgrade đều đặn nhưng không bao giờ reboot. Package kernel mới đã được cài, nhưng kernel đang chạy vẫn là bản từ 200 ngày trước. Bài này chia sẻ cách mình xử lý vấn đề đó: phát hiện server nào đang chạy kernel lỗi thời, quyết định khi nào dùng livepatch và khi nào reboot, rồi tự động hóa rolling reboot mà không gây downtime.

1. Cài kernel mới khác với chạy kernel mới

Đây là điểm nhiều người hiểu sai nhất. Khi apt hay dnf cài kernel mới, file vmlinuz mới nằm trong /boot, nhưng memory vẫn đang chạy kernel cũ cho tới lần boot tiếp theo. Userspace cũng vậy: update openssl xong mà không restart nginx thì process vẫn giữ thư viện cũ đã bị xóa trong memory.

Cách kiểm tra nhanh trên một máy:

# Kernel đang chạy
uname -r

# Kernel mới nhất đã cài (Debian/Ubuntu)
ls -1 /boot/vmlinuz-* | sort -V | tail -n1

# Ubuntu tạo file này khi có package yêu cầu reboot
cat /var/run/reboot-required 2>/dev/null && cat /var/run/reboot-required.pkgs

# RHEL/Rocky/Alma: dùng dnf-utils
needs-restarting -r; echo "exit=$?"   # exit 1 = cần reboot

# Tool tốt nhất cho Debian/Ubuntu: needrestart (>= 3.6)
sudo apt install -y needrestart
sudo needrestart -b -r l

Output batch mode của needrestart rất dễ parse:

NEEDRESTART-VER: 3.6
NEEDRESTART-KCUR: 6.8.0-45-generic
NEEDRESTART-KEXP: 6.8.0-52-generic
NEEDRESTART-KSTA: 3
NEEDRESTART-SVC: nginx.service

Với KSTA: 1 là kernel đã mới nhất, 2 là có bản update cùng ABI, 3 là có version mới hơn và phải reboot. Ngoài ra bạn có thể xem /sys/devices/system/cpu/vulnerabilities/ để biết kernel đang chạy đã mitigate các lỗi CPU như Spectre hay Retbleed chưa.

2. Quét cả fleet bằng một script Python nhỏ

Kiểm tra từng máy bằng tay thì được 5 server, đến 50 server là bỏ cuộc. Script dưới đây SSH song song vào danh sách host, chạy needrestart và in ra máy nào đang chạy kernel cũ. Chỉ dùng stdlib, chạy được với Python 3.10+.

#!/usr/bin/env python3
import subprocess
from concurrent.futures import ThreadPoolExecutor

HOSTS = [h.strip() for h in open("hosts.txt") if h.strip()]

def check(host: str) -> dict:
    cmd = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
               host, "sudo needrestart -b -r l"]
                   try:
                           out = subprocess.run(cmd, capture_output=True, text=True,
                                                        timeout=60).stdout
                                                            except subprocess.TimeoutExpired:
                                                                    return {"host": host, "error": "timeout"}
                                                                    
                                                                        info = {"host": host, "services": []}
                                                                            for line in out.splitlines():
                                                                                    key, _, val = line.partition(": ")
                                                                                            if key == "NEEDRESTART-KCUR":
                                                                                                        info["running"] = val
                                                                                                                elif key == "NEEDRESTART-KEXP":
                                                                                                                            info["expected"] = val
                                                                                                                                    elif key == "NEEDRESTART-KSTA":
                                                                                                                                                info["status"] = int(val)
                                                                                                                                                        elif key == "NEEDRESTART-SVC":
                                                                                                                                                                    info["services"].append(val)
                                                                                                                                                                        return info
                                                                                                                                                                        
                                                                                                                                                                        with ThreadPoolExecutor(max_workers=20) as pool:
                                                                                                                                                                            results = list(pool.map(check, HOSTS))
                                                                                                                                                                            
                                                                                                                                                                            for r in sorted(results, key=lambda x: -x.get("status", 0)):
                                                                                                                                                                                if "error" in r:
                                                                                                                                                                                        print(f"[??]   {r['host']}: {r['error']}")
                                                                                                                                                                                            elif r.get("status") == 3:
                                                                                                                                                                                                    print(f"[REBOOT] {r['host']}: {r['running']} -> {r['expected']}")
                                                                                                                                                                                                        elif r["services"]:
                                                                                                                                                                                                                print(f"[SVC]  {r['host']}: restart {', '.join(r['services'])}")
                                                                                                                                                                                                                ```
                                                                                                                                                                                                                
                                                                                                                                                                                                                Mình chạy script này bằng cron mỗi sáng thứ Hai, đẩy output vào channel chat của team. Thấy danh sách `[REBOOT]` dài ra là biết cần lên lịch maintenance.
                                                                                                                                                                                                                
                                                                                                                                                                                                                ## 3. Livepatch hay reboot?
                                                                                                                                                                                                                
                                                                                                                                                                                                                Livepatch cho phép vá function trong kernel đang chạy mà không cần reboot. Các lựa chọn phổ biến hiện nay:
                                                                                                                                                                                                                
                                                                                                                                                                                                                - **Ubuntu**: Canonical Livepatch, bật qua `sudo pro enable livepatch` (cần Ubuntu Pro, miễn phí cho tối đa 5 máy cá nhân).
                                                                                                                                                                                                                - **RHEL 8/9 và các bản rebuild**: `kpatch` với `dnf install kpatch kpatch-dnf`.
                                                                                                                                                                                                                - Kiểm tra trạng thái chung: `cat /sys/kernel/livepatch/*/enabled`.
                                                                                                                                                                                                                
                                                                                                                                                                                                                Nhưng livepatch **không thay thế được reboot**. Nó chỉ vá được những lỗi có thể sửa bằng cách thay function. Lỗi liên quan đến thay đổi data structure, init code hay một số mitigation phần cứng thì vẫn phải reboot. Nguyên tắc của mình như sau:
                                                                                                                                                                                                                
                                                                                                                                                                                                                ```mermaid
                                                                                                                                                                                                                flowchart TD
                                                                                                                                                                                                                    A[CVE kernel mới công bố] --> B{Có exploit public<br/>hoặc CVSS >= 7?}
                                                                                                                                                                                                                        B -- Không --> C[Gom vào đợt reboot định kỳ<br/>2-4 tuần/lần]
                                                                                                                                                                                                                            B -- Có --> D{Có livepatch?}
                                                                                                                                                                                                                                D -- Có --> E[Apply livepatch ngay]
                                                                                                                                                                                                                                    E --> F[Vẫn reboot trong đợt kế tiếp]
                                                                                                                                                                                                                                        D -- Không --> G[Rolling reboot khẩn cấp<br/>trong 24-48h]
                                                                                                                                                                                                                                            C --> H[Verify bằng needrestart]
                                                                                                                                                                                                                                                F --> H
                                                                                                                                                                                                                                                    G --> H
                                                                                                                                                                                                                                                    ```
                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                    Điểm mấu chốt là **reboot phải trở thành việc nhàm chán**. Nếu team sợ reboot thì đó là dấu hiệu hệ thống thiếu redundancy, và đó mới là vấn đề thật cần giải quyết.
                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                    Để không vô tình reboot giữa giờ cao điểm, cấu hình `unattended-upgrades` chỉ cài package chứ không tự reboot:
                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                    ```bash
                                                                                                                                                                                                                                                    # /etc/apt/apt.conf.d/50unattended-upgrades
                                                                                                                                                                                                                                                    Unattended-Upgrade::Automatic-Reboot "false";
                                                                                                                                                                                                                                                    Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
                                                                                                                                                                                                                                                    ```
                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                    ## 4. Rolling reboot không downtime
                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                    Với cluster Kubernetes, quy trình chuẩn là drain node, reboot, chờ node Ready, uncordon, rồi làm node tiếp theo. Nếu đã có kured (Kubernetes Reboot Daemon) thì nó tự làm việc này khi phát hiện `/var/run/reboot-required`. Còn nếu muốn kiểm soát bằng tay, script dưới đây là đủ:
                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                    ```mermaid
                                                                                                                                                                                                                                                    sequenceDiagram
                                                                                                                                                                                                                                                        participant S as Script
                                                                                                                                                                                                                                                            participant K as kube-apiserver
                                                                                                                                                                                                                                                                participant N as Node
                                                                                                                                                                                                                                                                    S->>K: kubectl drain node
                                                                                                                                                                                                                                                                        K->>N: Evict pods (tôn trọng PDB)
                                                                                                                                                                                                                                                                            S->>N: ssh sudo reboot
                                                                                                                                                                                                                                                                                N-->>K: Node NotReady
                                                                                                                                                                                                                                                                                    N-->>K: Boot kernel mới, Node Ready
                                                                                                                                                                                                                                                                                        S->>K: kubectl uncordon node
                                                                                                                                                                                                                                                                                            S->>S: Sang node tiếp theo
                                                                                                                                                                                                                                                                                            ```
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            ```bash
                                                                                                                                                                                                                                                                                            #!/usr/bin/env bash
                                                                                                                                                                                                                                                                                            set -euo pipefail
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            for node in $(kubectl get nodes -o name | cut -d/ -f2); do
                                                                                                                                                                                                                                                                                              kver=$(ssh "$node" uname -r)
                                                                                                                                                                                                                                                                                                if ! ssh "$node" test -f /var/run/reboot-required; then
                                                                                                                                                                                                                                                                                                    echo "[skip] $node ($kver)"; continue
                                                                                                                                                                                                                                                                                                      fi
                                                                                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                                                                                        echo "[drain] $node ($kver)"
                                                                                                                                                                                                                                                                                                          kubectl drain "$node" --ignore-daemonsets \
                                                                                                                                                                                                                                                                                                              --delete-emptydir-data --timeout=300s
                                                                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                                                                                ssh "$node" 'sudo systemctl reboot' || true
                                                                                                                                                                                                                                                                                                                  sleep 30
                                                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                                    kubectl wait --for=condition=Ready "node/$node" --timeout=600s
                                                                                                                                                                                                                                                                                                                      kubectl uncordon "$node"
                                                                                                                                                                                                                                                                                                                        echo "[done] $node -> $(ssh "$node" uname -r)"
                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                          sleep 60   # cho workload ổn định trước khi sang node khác
                                                                                                                                                                                                                                                                                                                          done
                                                                                                                                                                                                                                                                                                                          ```
                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                          Vài lưu ý rút ra từ kinh nghiệm thực tế:
                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                          - **PodDisruptionBudget** là bắt buộc. Thiếu PDB thì `drain` có thể evict cùng lúc cả 3 replica của service.
                                                                                                                                                                                                                                                                                                                          - Database stateful (PostgreSQL, Redis primary) nên **failover thủ công trước**, đừng để drain tự xử lý.
                                                                                                                                                                                                                                                                                                                          - Server VM đơn lẻ không có cluster thì ít nhất phải có load balancer phía trước và tối thiểu 2 instance. Nếu không, mỗi lần reboot là một lần downtime.
                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                          ## Kết luận
                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                          Kernel bug bị lộ sớm sẽ còn xảy ra. Từ năm 2024 kernel.org đã trở thành CNA và công bố hàng nghìn CVE mỗi năm, nên không thể đọc từng CVE rồi mới hành động. Thứ cứu bạn là **quy trình**, không phải phản xạ. Những việc nên làm ngay tuần này:
                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                          1. Cài `needrestart` (Debian/Ubuntu) hoặc `dnf-utils` (RHEL) trên mọi server, chạy `needrestart -b` để biết máy nào đang chạy kernel cũ.
                                                                                                                                                                                                                                                                                                                          2. Dựng script quét fleet như phần 2 và chạy bằng cron hàng tuần, gửi kết quả vào chat của team.
                                                                                                                                                                                                                                                                                                                          3. Bật livepatch cho các máy quan trọng để mua thêm thời gian khi có CVE nghiêm trọng, nhưng đừng coi nó là lý do để không reboot.
                                                                                                                                                                                                                                                                                                                          4. Đặt lịch rolling reboot định kỳ 2–4 tuần một lần, kèm PDB và health check. Một khi reboot đã thành thói quen thì patch khẩn cấp chỉ còn là chạy lại script.
                                                                                                                                                                                                                                                                                                                          5. Theo dõi kernel đang chạy (`uname -r`) bằng metric, ví dụ dùng `node_uname_info` của node_exporter, để dashboard cho thấy ngay node nào bị bỏ sót.
                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                          Server chạy kernel đã vá thì leak bao nhiêu lên mailing list cũng không ảnh hưởng tới bạn.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí