“Permission denied” nhưng chmod 777 rồi mà vẫn không chạy
Bạn đã gặp: app chạy bằng user www-data, file permission 755, nhưng Nginx vẫn báo “Permission denied” khi đọc static file. Bạn thử chmod 777 — vẫn fail. Đồng nghiệp cũ bảo “tắt SELinux đi”. Bạn gõ setenforce 0, app chạy. Vé incident đóng.
Sáu tháng sau, ai đó exploit một plugin WordPress cũ và đọc được /etc/shadow. SELinux lẽ ra đã chặn, nhưng nó đã bị tắt từ incident trước.
Đây không phải chuyện hiếm. Đây là pattern “tắt MAC vì không hiểu” lặp lại từ máy ảo lab tới production.
Bài này không dạy bạn viết policy SELinux dài 300 dòng. Nó dạy bạn: đọc audit log, hiểu nguyên nhân, và viết policy tối thiểu để không phải tắt MAC.
DAC vs MAC: hai lớp kiểm soát
Unix truyền thống dùng DAC (Discretionary Access Control): chủ file quyết định rwx cho user/group/other. Vấn đề: process chạy với toàn bộ quyền của user gọi nó. Nginx worker chạy dưới www-data, nếu bị exploit, attacker có toàn bộ quyền của www-data (đọc /etc/passwd, mở socket, chạy shell…).
MAC (Mandatory Access Control) thêm một lớp policy toàn hệ thống, không phụ thuộc vào owner file. Ngay cả root cũng bị giới hạn bởi MAC (trừ khi policy cho phép).
- SELinux: Red Hat family (RHEL, Fedora, Rocky), policy dựa trên label/context gán cho mọi object (file, process, port, socket).
- AppArmor: Debian/Ubuntu family, policy dựa trên path, gắn profile vào binary.
Hai hệ thống khác nhau về cú pháp nhưng giống về tư duy: process chỉ được làm những gì policy cho phép, không hơn.
SELinux: đọc audit log trước khi setenforce 0
Ba trạng thái
getenforce
# Enforcing → đang chặn và log
# Permissive → chỉ log, không chặn (dùng để debug)
# Disabled → tắt hoàn toàn
Quy tắc: nếu phải debug, chuyển sang Permissive (ghi log đầy đủ rồi sinh policy từ log), không disable.
Context và label: “ps -Z” là bạn thân
ps -eZ | grep nginx
ls -Z /var/www/html/
Output điển hình:
system_u:system_r:httpd_t:s0 /var/www/html/index.html
Ba phần của context: user (system_u), role (system_r), type (httpd_t). 90% vấn đề ops nằm ở type.
Pattern kinh điển: app cần đọc file ngoài docroot
# App đọc file ở /opt/myapp/static, SELinux chặn
sudo ausearch -m avc -ts recent | tail
Audit log sẽ có dòng:
type=AVC msg=audit(...): avc: denied { read } for pid=1234 comm="nginx"
name="static" dev="sda1" ino=54321
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0 tclass=file
Dịch: process có type httpd_t (Nginx) bị từ chối read file có type default_t. Giải pháp:
# Gán label đúng cho thư mục
sudo semanage fcontext -a -t httpd_sys_content_t "/opt/myapp/static(/.*)?"
sudo restorecon -Rv /opt/myapp/static
audit2allow: sinh policy từ log (dùng cẩn thận)
# Xem proposal từ audit log, không apply ngay
sudo ausearch -m avc -ts recent | audit2allow
audit2allow gợi ý policy module. Đừng pipe thẳng vào semodule -i mà chưa đọc. Nhiều dòng AVC là triệu chứng, không phải root cause. Ví dụ: process sai domain vì khởi động sai cách, policy gợi ý có thể quá rộng.
Boolean: công tắc không cần viết policy
Nhiều hành vi phổ biến được kiểm soát qua boolean:
# Xem boolean liên quan tới httpd
getsebool -a | grep httpd
# Cho phép Nginx kết nối tới upstream TCP
sudo setsebool -P httpd_can_network_connect on
# Cho phép gửi email từ web app
sudo setsebool -P httpd_can_sendmail on
-P = persistent qua reboot. Boolean là cách an toàn nhất để điều chỉnh SELinux mà không viết policy tay.
AppArmor: profile dựa trên path
Kiểm tra trạng thái
sudo aa-status
# Liệt kê profile đang loaded, enforcing/complain mode
Complain mode = Permissive trong SELinux: chỉ log, không chặn.
Đọc log AppArmor
sudo journalctl -k --grep audit | tail
# Hoặc
sudo dmesg | grep -i apparmor
Output điển hình:
apparmor="DENIED" operation="open" profile="/usr/sbin/nginx"
name="/opt/myapp/static/" pid=1234 comm="nginx"
requested_mask="r" denied_mask="r"
Fix bằng aa-logprof
# Scan log, đề xuất cập nhật profile
sudo aa-logprof
aa-logprof tương tự audit2allow nhưng tương tác: nó hỏi bạn có muốn allow/deny từng truy cập. Đọc kỹ từng prompt, không “Allow All” mù.
Profile tối thiểu cho app custom
# Tạo profile skeleton
sudo aa-genprof /opt/myapp/bin/server
File profile điển hình tại /etc/apparmor.d/opt.myapp.bin.server:
/opt/myapp/bin/server {
#include <abstractions/base>
#include <abstractions/nameservice>
/opt/myapp/bin/server mr,
/opt/myapp/config/** r,
/opt/myapp/static/** r,
/var/log/myapp/*.log w,
/var/run/myapp.pid w,
network tcp,
}
Permission: r read, w write, m mmap/execute, rw read-write, mr map+read (binary).
Container và MAC: một lớp nữa
Khi chạy container, MAC của host vẫn áp dụng cho process trong container (qua namespace). docker/podman thường tự gán label SELinux (container_t), nhưng volume mount (-v /host/path:/container/path) là nơi policy host can thiệp.
# Mount volume với label SELinux phù hợp
docker run -v /host/data:/data:Z myapp # :Z = relabel riêng cho container này
docker run -v /host/data:/data:z myapp # :z = share label giữa các container
Cảnh báo: :Z relabel file trên host, có thể làm file đó không đọc được bởi service khác trên host nếu chạy nhầm.
“Nhưng tôi đang chạy trên cloud, có cần MAC không?”
Có. Cloud provider bảo vệ hạ tầng (network, hypervisor), không bảo vệ cấu hình sai bên trong VM. Một container escape, một app bị RCE, một library có CVE — tất cả đều là attack surface mà MAC giảm thiểu.
Bốn tình huống MAC cứu bạn:
- App bị RCE, attacker spawn shell: SELinux ngăn
httpd_tchạy/bin/bash. - File upload malicious PHP: AppArmor chặn process web đọc
/etc/shadow. - Container escape: MAC profile giới hạn syscall process container được gọi trên host.
- Developer vô tình chạy
rm -rf /trong CI: MAC profile CI runner không có quyền write ngoài workspace.
Playbook: debug MAC 5 phút
# 1. Xác định MAC đang chạy không
getenforce 2>/dev/null || aa-status --enabled 2>/dev/null
# 2. Mode hiện tại
getenforce # SELinux
sudo aa-status # AppArmor
# 3. Log gần đây
sudo ausearch -m avc -ts recent 2>/dev/null | tail
sudo journalctl -k --grep apparmor --since "10 minutes ago" 2>/dev/null | tail
# 4. Boolean/quyền liên quan (SELinux)
getsebool -a | grep -i <service>
# 5. Nếu test, tạm chuyển permissive/complain
sudo setenforce 0 # SELinux → Permissive (tạm)
sudo aa-complain /usr/sbin/nginx # AppArmor → complain cho profile này
# 6. Test app, xem log mới, fix, BẬT LẠI
sudo setenforce 1
sudo aa-enforce /usr/sbin/nginx
Liên hệ bài trước / sau
- Trước: Phần 12: Sao lưu và playbook incident
- Sau: Phần 14: TLS certificates và Let’s Encrypt
- Liên quan: Phần 4: User và permission, Phần 9: Firewall, Phần 10: SSH hardening
Câu hỏi hay gặp
1. “SELinux hay AppArmor, chọn cái nào?”
Bạn không chọn — distro chọn cho bạn. RHEL → SELinux, Ubuntu → AppArmor. Học cái distro bạn đang chạy, đừng tranh luận “cái nào tốt hơn” trong lúc incident.
2. “Tôi nên để Enforcing hay Permissive?”
Enforcing trên production. Permissive chỉ trong quá trình debug và có deadline bật lại. Mọi “tạm thời” kéo dài hơn 1 sprint là nợ kỹ thuật.
3. “Làm sao biết policy nào đang áp dụng cho một process?”
SELinux: ps -eZ | grep <process>. AppArmor: sudo aa-status | grep <process>. Cả hai cho biết label/profile hiện tại.
4. “Có nên tắt SELinux trên container host không?”
Không. Container runtime (Docker, Podman, containerd) tích hợp sẵn với SELinux/AppArmor. Tắt MAC trên container host là mở đường cho container escape không bị phát hiện.
Đọc thêm
- SELinux User’s and Administrator’s Guide (Red Hat)
- Ubuntu AppArmor documentation
- SELinux Coloring Book — giải thích trực quan
Bài tiếp theo: Phần 14: TLS certificates trên Linux, Let’s Encrypt và debug, từ certbot tới “certificate verify failed”.