Least Privilege trong thực tế: câu hỏi không phải là “có cấp sudo hay không?”
Một engineer cần vào server để làm việc. Cấp full sudo thì nhanh. Không cấp thì có thể họ không làm được gì. Nhưng có lẽ đó chưa phải cách đặt câu hỏi đúng.
Có một bài toán mình gặp trong công việc Infrastructure: một engineer bên ngoài cần SSH vào một nhóm server để kiểm tra application, xem log, inspect container và đôi khi restart một service.
Yêu cầu hoàn toàn hợp lý. Vấn đề nằm ở chỗ họ có cần quyền root trên toàn bộ server hay không.
Cách nhanh nhất chắc ai làm Infrastructure cũng biết: tạo account, cấp sudo đủ rộng, rồi dựa vào quy trình và trách nhiệm của người sử dụng để kiểm soát. Vận hành rất tiện. Nhưng nếu credential bị lộ, hoặc chỉ đơn giản là một command được chạy nhầm, blast radius cũng rộng tương ứng.
Mình bắt đầu bài toán bằng một câu khác:
Người này thật sự cần làm những operation nào để hoàn thành công việc?
Câu hỏi đó dẫn thiết kế sang một hướng khác.
Bastion Host không chỉ là một chiếc cầu SSH
Nhiều nơi dùng Bastion như một máy trung gian: login vào đó rồi SSH tiếp. Cách dùng này không sai, nhưng theo mình nó bỏ phí một phần giá trị của Bastion.
Nếu access path được thiết kế rõ ràng — user → Bastion → approved target — Bastion trở thành một control point. Authentication tập trung hơn, source path rõ hơn, network policy dễ hiểu hơn và số endpoint phải expose trực tiếp cũng giảm đi.
Điều mình thích ở mô hình này không phải vì nó “security” hơn trên giấy. Nó làm cho câu chuyện access trở nên dễ giải thích: ai đi vào từ đâu, đi đến đâu và được phép làm gì.
SSH key-only giải quyết một vấn đề, không phải mọi vấn đề
Với access kiểu này, mình ưu tiên SSH key thay cho password login. Nó tránh được một số rủi ro quen thuộc như password reuse và giúp revoke quyền theo từng user rõ ràng hơn.
Nhưng mình cũng không xem key-based authentication như một chiếc khiên thần kỳ. Private key vẫn là credential. Nếu người dùng bảo vệ nó kém, hệ thống vẫn có vấn đề.
Security control tốt thường không nằm ở một tính năng đơn lẻ, mà ở cách nhiều lớp nhỏ bổ sung cho nhau.
Từ “sudo” sang “approved operations”
Đây là chỗ mình thấy Least Privilege trở nên thực tế nhất.
Nếu công việc hằng ngày chỉ cần xem container đang chạy, đọc log, inspect trạng thái và restart đúng service được phê duyệt, tại sao account đó phải có khả năng sửa firewall, tạo user mới, thay SSH configuration hay cài package tùy ý?
Khi nhìn theo operation, sudoers không còn là câu hỏi nhị phân “có sudo / không sudo”. Nó trở thành một cách ánh xạ nhu cầu vận hành thành technical permissions.
Và mình nhận ra một điều: read-only operation và change operation không nên được đối xử giống nhau.
Xem trạng thái, đọc log hay inspect metadata thường có mức rủi ro khác với restart một service. Restart là thay đổi trạng thái hệ thống. Khi tách hai nhóm này, policy dễ review hơn và cuộc trao đổi với application owner cũng rõ ràng hơn rất nhiều.
Least Privilege quá chặt cũng có thể là một thiết kế tệ
Security đôi khi dễ rơi vào một cực khác: khóa mọi thứ thật chặt rồi để Infrastructure team trở thành người phải thực hiện thay tất cả.
Theo mình, đó không phải mục tiêu.
Nếu một engineer được giao trách nhiệm vận hành application nhưng mỗi lần xem log hay restart service đều phải mở ticket chờ một team khác, security control đang bắt đầu tạo operational friction. Và khi friction đủ lớn, con người thường tìm workaround.
Điểm cân bằng mình muốn là: người dùng có đủ quyền để tự làm những việc nằm trong scope của họ, nhưng capability ngoài scope thì không được cấp chỉ vì “sau này có thể cần”. Nếu nhu cầu thay đổi, policy được review và mở rộng có chủ đích.
Một thay đổi nhỏ trong cách đặt câu hỏi
Trước đây câu hỏi có thể là:
“Có cho user này sudo không?”
Bây giờ mình thích hỏi:
“User này cần thực hiện những operation nào, trên target nào, và operation nào thật sự làm thay đổi hệ thống?”
Hai câu nghe gần giống nhau, nhưng dẫn đến hai thiết kế rất khác.
Với mình, đó cũng là ý nghĩa thực dụng nhất của Least Privilege: không phải cố cấp ít quyền nhất có thể, mà là cấp đúng capability cần thiết cho công việc, và hiểu rõ tại sao capability đó tồn tại.
Bastion Host khi ấy không chỉ là một Jump Server. Nó trở thành một phần của access model — đủ rõ để vận hành, đủ giới hạn để giảm blast radius, và đủ minh bạch để sau này có thể audit.
Từ requirement đến một file sudoers cụ thể
Để ý tưởng này không chỉ dừng ở principle, có thể hình dung một ví dụ nhỏ trên Ubuntu. Thay vì cấp full sudo, mình tách các operation thành nhóm read và change, rồi ánh xạ chúng vào hai role khác nhau. Operator chủ yếu quan sát; DevOps có thêm một số operation thay đổi trạng thái đã được phê duyệt.
# /etc/sudoers.d/devops-operator
#
# Ví dụ Least Privilege:
# - DevOps: xem Docker + restart container + quản lý Docker daemon
# - Operator: chỉ quan sát, không được restart
Cmnd_Alias DOCKER_READ = \
/usr/bin/docker ps, \
/usr/bin/docker logs *, \
/usr/bin/docker inspect *, \
/usr/bin/docker stats *
Cmnd_Alias DOCKER_CHANGE = \
/usr/bin/docker restart *
Cmnd_Alias DOCKER_SERVICE_READ = \
/usr/bin/systemctl status docker, \
/usr/bin/journalctl -u docker *, \
/usr/bin/journalctl -u docker.service *
Cmnd_Alias DOCKER_SERVICE_CHANGE = \
/usr/bin/systemctl restart docker
Cmnd_Alias SYSTEM_READ = \
/usr/bin/ss *, \
/usr/bin/df *, \
/usr/bin/du *, \
/usr/bin/free *, \
/usr/bin/ps *, \
/usr/bin/ls *
# DevOps: quan sát + các operation thay đổi đã được approve
%devops ALL=(root) NOPASSWD: \
DOCKER_READ, \
DOCKER_CHANGE, \
DOCKER_SERVICE_READ, \
DOCKER_SERVICE_CHANGE, \
SYSTEM_READ
# Operator: chỉ quan sát
%operator ALL=(root) NOPASSWD: \
DOCKER_READ, \
DOCKER_SERVICE_READ, \
SYSTEM_READĐiểm mình muốn minh họa ở đây không phải là copy nguyên file này vào production. Điều đáng chú ý là không có một rule full sudo cho role. Mỗi nhóm quyền đều bắt đầu từ operation mà công việc thực sự cần, và DevOps với Operator không mặc nhiên có cùng capability.
Một chi tiết cần để ý: wildcard * trong sudoers vẫn phải được review cẩn thận. Khi requirement cần chặt hơn, có thể giới hạn tiếp theo container, service, argument hoặc approved path cụ thể thay vì mở rộng command không cần thiết.
Trước khi áp dụng, mình cũng muốn syntax được kiểm tra như một phần của change:
Và sau đó xác nhận capability thực tế của account:
Một file trong /etc/sudoers.d/ có thể khá nhỏ, nhưng nó thể hiện rất rõ tư duy phía sau access model: quyền này tồn tại để phục vụ công việc nào? Nếu không trả lời được câu hỏi đó, có lẽ permission đang rộng hơn mức cần thiết.
Ghi lại từ những trải nghiệm thực tế của mình trong quá trình làm Infrastructure — những câu chuyện vẫn còn tiếp tục.
Đọc trước ← High Availability không bắt đầu từ thiết bị thứ haiĐọc tiếp → Migration không bắt đầu ở Cutover