Nếu bạn đang chạy server Ubuntu và dự định nâng cấp lên 26.04 LTS, có một thay đổi rất dễ bị bỏ qua: lệnh sudo mặc định giờ là sudo-rs, bản viết lại bằng Rust, chứ không còn là sudo truyền thống (bản của Todd Miller, sudo.ws) nữa. Phần lớn thời gian bạn sẽ không thấy khác biệt gì. Nhưng nếu bạn có script deploy, Ansible playbook, file sudoers tự viết hoặc CI pipeline phụ thuộc vào vài flag và option ít dùng, thì bản nâng cấp có thể làm hỏng chúng một cách âm thầm. Tuần này trên HN có thảo luận khá sôi nổi về chủ đề này. Mình sẽ chia sẻ cách audit hệ thống trước khi nâng cấp, theo đúng quy trình team mình đang dùng.
sudo-rs khác sudo truyền thống ở đâu?
sudo-rs được thiết kế để thay thế trực tiếp, nhưng chủ động không hỗ trợ toàn bộ tính năng của sudo gốc. Lý do khá hợp lý: sudo gốc có hơn 30 năm lịch sử, hàng trăm option trong sudoers, và mỗi tính năng ít dùng đều là thêm bề mặt tấn công. Một số khác biệt bạn nên biết:
- Không có plugin system và không hỗ trợ sudoers lưu trên LDAP.
- Nhiều
Defaultstrong sudoers không được hỗ trợ, ví dụ các option liên quan đến gửi mail (mail_badpass,mailto),insults, và một số option về log/IO. - Luôn dùng pseudo-terminal (pty), trong khi sudo gốc cho phép tắt qua
use_pty. - Một số flag và cách xử lý biến môi trường có chi tiết khác nhau, đặc biệt là với
-E/--preserve-envvàenv_keep.
Vấn đề nằm ở chỗ: khi sudo-rs gặp directive nó không hiểu trong sudoers, tùy loại directive mà nó có thể báo lỗi parse hoặc bỏ qua. Cả hai trường hợp đều nguy hiểm. Báo lỗi thì có thể khóa luôn quyền root của bạn, còn bỏ qua thì policy bạn tưởng đang áp dụng thực ra không còn tác dụng.
Luôn kiểm tra thực tế trên máy của bạn:
# Đang dùng bản sudo nào? sudo --version | head -1 readlink -f "$(command -v sudo)" # Xem các implementation có sẵn update-alternatives --display sudo 2>/dev/null ls -l /usr/bin/sudo* /usr/lib/cargo/bin/sudo 2>/dev/null Trên Ubuntu từ 25.10 trở đi, sudo gốc vẫn được cài kèm dưới tên sudo.ws, và bạn có thể chuyển lại bằng update-alternatives --config sudo. Đây là đường lui, nhưng đừng xem nó là giải pháp lâu dài.
Quy trình audit trước khi nâng cấp
Đây là flow team mình áp dụng cho mỗi nhóm server:
flowchart TD A[Inventory: sudoers + scripts + playbooks] --> B[Scan option và flag không hỗ trợ] B --> C{Có vấn đề?} C -- Không --> D[Test trên container ubuntu:26.04] C -- Có --> E[Sửa config / script] E --> D D --> F{Pass?} F -- Có --> G[Rollout canary 1-2 server] F -- Không --> E G --> H[Rollout toàn bộ] Bước quan trọng nhất là bước đầu tiên: bạn cần biết chỗ nào đang gọi sudo. Ngoài /etc/sudoers và /etc/sudoers.d/, hãy tìm cả trong repo infra, systemd unit, cron job và Dockerfile.
Script tự động scan sudoers
Đoạn Python dưới đây đọc toàn bộ sudoers và đánh dấu các directive có rủi ro. Danh sách RISKY_DEFAULTS chỉ là điểm khởi đầu. Hãy đối chiếu với tài liệu của bản sudo-rs bạn đang chạy (man sudoers trên chính máy 26.04), vì danh sách option được hỗ trợ thay đổi theo từng release.
#!/usr/bin/env python3 # audit_sudoers.py - chạy với: sudo python3 audit_sudoers.py import re from pathlib import Path RISKY_DEFAULTS = { "mail_badpass", "mail_always", "mail_no_user", "mailto", "mailerpath", "insults", "log_input", "log_output", "iolog_dir", "use_pty", "lecture", "lecture_file", "requiretty", "syslog", "logfile", } RISKY_PATTERNS = [ (re.compile(r"^\s*#?@?includedir\b"), "includedir: kiểm tra cú pháp được hỗ trợ"), (re.compile(r"\bNOEXEC\b"), "tag NOEXEC: hành vi có thể khác"), (re.compile(r"\bLDAP\b", re.I), "LDAP: sudo-rs không hỗ trợ"), ] def scan(path: Path): issues = [] for no, line in enumerate(path.read_text().splitlines(), 1): s = line.strip() if s.startswith("Defaults"): opts = re.split(r"[\s,]+", s.split(None, 1)[-1]) for opt in opts: name = opt.lstrip("!").split("=")[0].split("+")[0] if name in RISKY_DEFAULTS: issues.append((no, f"Defaults {name}")) for pat, msg in RISKY_PATTERNS: if pat.search(s): issues.append((no, msg)) return issues files = [Path("/etc/sudoers"), *sorted(Path("/etc/sudoers.d").glob("*"))] for f in files: if f.is_file(): for no, msg in scan(f): print(f"{f}:{no}: {msg}") Kết quả có dạng /etc/sudoers.d/90-deploy:3: Defaults log_output. Mỗi dòng là một điểm bạn cần quyết định: bỏ đi, thay bằng cơ chế khác (ví dụ chuyển audit log sang auditd thay vì log_output), hoặc giữ lại và chấp nhận dùng sudo.ws cho máy đó.
Với script shell, một lệnh grep đơn giản đã bắt được phần lớn trường hợp:
# Tìm các cách gọi sudo có flag "nhạy cảm" trong repo infra grep -rnE 'sudo\s+(-[a-zA-Z]*[EAHbS]|--preserve-env|--askpass|-u\s+#)' \ --include='*.sh' --include='*.yml' --include='*.yaml' \ --include='Dockerfile*' --include='*.service' . # Script nào đọc output của sudo -l? (format có thể khác) grep -rn 'sudo -l' --include='*.sh' --include='*.py' . Đặc biệt chú ý đến sudo -l. Mình đã thấy nhiều script parse output của lệnh này để kiểm tra quyền, và chỉ cần format output lệch một chút là logic sai ngay.
Test thật trong container trước khi chạm vào server
Scan tĩnh chỉ tìm được những gì bạn đã biết. Cách chắc chắn nhất là chạy chính script của bạn trên Ubuntu 26.04 trong container, với chính file sudoers của bạn:
cat > Dockerfile.sudotest <<'EOF' FROM ubuntu:26.04 RUN apt-get update && apt-get install -y sudo python3 \ && useradd -m deploy COPY sudoers.d/ /etc/sudoers.d/ COPY scripts/ /opt/scripts/ RUN chmod 0440 /etc/sudoers.d/* USER deploy EOF docker build -f Dockerfile.sudotest -t sudotest . # 1. sudoers có parse được không? docker run --rm --user root sudotest visudo -c # 2. Chạy script deploy thật docker run --rm sudotest bash -x /opt/scripts/deploy.sh visudo -c là bước bắt buộc. Nếu nó fail trong container, nghĩa là sau khi nâng cấp server bạn sẽ không dùng được sudo nữa. Khi đó chỉ còn cách vào bằng console hoặc recovery mode, và với VPS ở xa thì đó là một buổi tối rất tệ.
Nên đưa bước này vào CI để mọi thay đổi sudoers đều được test trên cả hai implementation:
flowchart LR PR[Pull Request sửa sudoers] --> CI[CI job] CI --> T1[ubuntu:24.04 + sudo gốc] CI --> T2[ubuntu:26.04 + sudo-rs] T1 --> M{Cả hai pass?} T2 --> M M -- Có --> Merge[Merge] M -- Không --> Fix[Sửa lại] Trong giai đoạn chuyển tiếp, khi fleet của bạn có cả 24.04 lẫn 26.04, test song song như vậy giúp bạn không phải đoán.
Những bẫy mình đã gặp
Một vài bài học thực tế:
- Đừng sửa sudoers trực tiếp trên server đang chạy. Luôn dùng
visudo -f /etc/sudoers.d/xxxvà luôn mở sẵn một session root khác trước khi thử. - Cloud-init và golden image. Nếu bạn build AMI hoặc image bằng Packer, sudoers được tạo trong lúc build. Hãy kiểm tra template đó, không chỉ server đang chạy.
- Biến môi trường. Script dựa vào
sudo -Eđể giữHTTP_PROXYhoặcPATHnên được viết lại theo kiểu tường minh:sudo env HTTP_PROXY="$HTTP_PROXY" cmd. Viết như vậy rõ ràng hơn và không phụ thuộc vào implementation. - Đừng vội
update-alternativesvề sudo.ws cho cả fleet. Làm vậy thì vấn đề vẫn nằm đó, chỉ bị dời sang lần nâng cấp sau.
Kết luận
Việc Ubuntu chuyển sang sudo-rs là một bước đúng về mặt bảo mật, nhờ memory safety và codebase gọn hơn. Cái giá phải trả là bạn cần dọn dẹp những config đã tích tụ qua nhiều năm. Checklist hành động:
- Chạy
sudo --versionvàupdate-alternatives --display sudođể biết bạn đang dùng gì. - Scan
/etc/sudoers*bằng script ở trên và đối chiếu vớiman sudoerstrên máy 26.04. -
greprepo infra để tìmsudo -E,sudo -lvà các flag ít dùng. - Build container
ubuntu:26.04, chạyvisudo -cvà chạy script deploy thật. - Đưa test sudoers lên CI cho cả 24.04 và 26.04.
- Rollout theo canary, luôn có sẵn session root dự phòng.
- Coi
sudo.wschỉ là đường lui tạm thời, không phải đích đến.
Chỉ mất khoảng nửa ngày để audit, nhưng có thể giúp bạn tránh được một sự cố mất quyền root trên production. Làm ngay bây giờ, trước khi do-release-upgrade tự nhắc bạn.


