DEV Community

EME GUG
EME GUG

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'])}")
Enter fullscreen mode Exit fullscreen mode

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:

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:

# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Enter fullscreen mode Exit fullscreen mode

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à đủ:

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
#!/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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)