Cái bẫy mà hầu hết dev đều dính phải
Bạn vừa deploy xong một web app lên Fedora Server. Nginx đã start, firewall đã mở port 80, file config trông hoàn toàn bình thường. Vậy mà truy cập vào thì… 502 Bad Gateway. Check log nginx thấy connect() to unix:/run/myapp.sock failed (13: Permission denied). Bạn chạy ls -la, quyền 755, thậm chí thử chmod 777 cho nhanh — vẫn không ăn thua.
Thủ phạm thực sự? SELinux — thứ mà 90% người mới dùng Fedora/RHEL đều muốn tắt ngay lập tức khi gặp lần đầu.
Mình dùng Fedora làm máy development chính đã 2 năm. Tốc độ cập nhật package nhanh, kernel mới liên tục — mình thích điều đó. Nhưng SELinux vẫn là thứ mình phải “chiến đấu” nhiều nhất mỗi khi setup môi trường mới. Bài này gom lại những gì mình học được sau nhiều lần bị nó chặn đứng.
SELinux hoạt động theo kiểu gì mà khó chịu vậy?
Linux truyền thống kiểm soát quyền truy cập theo mô hình DAC (Discretionary Access Control) — dựa vào user/group/permission bits. Bạn là owner file? Bạn muốn làm gì thì làm.
SELinux bổ sung lớp MAC (Mandatory Access Control) bên trên. Khác biệt cốt lõi: nó không quan tâm bạn là root hay không — nó quan tâm đến security context (nhãn bảo mật) gắn trên mỗi process và file.
Mỗi file, process, port đều mang context dạng:
user:role:type:level
Chạy ls -Z để thấy context thực tế:
ls -Z /var/www/html/
# Output:
system_u:object_r:httpd_sys_content_t:s0 index.html
Nginx process chạy với type httpd_t. Nó chỉ được phép đọc file có type httpd_sys_content_t. Khi bạn copy file từ /home sang /var/www/html, file đó giữ nguyên context cũ — thường là user_home_t hoặc tmp_t. Nginx bị từ chối dù permission bit là 777. Đây là nguyên nhân phổ biến nhất của lỗi SELinux permission denied khi deploy web app.
Ba cách giải quyết — từ tệ đến tốt
Cách 1: Tắt SELinux (đừng làm trên production)
Google “SELinux permission denied” và câu trả lời đầu tiên bạn thấy thường là:
setenforce 0
# Hoặc tệ hơn — disable hoàn toàn:
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
Nhanh. Hiệu quả ngay. Và sai hoàn toàn.
Tắt SELinux giống tắt chuông báo cháy cho đỡ ồn. Fedora và toàn bộ RHEL-based distros bật SELinux mặc định vì nó ngăn được tấn công privilege escalation — kiểu tấn công mà attacker khai thác lỗ hổng trong Nginx hoặc PHP để leo quyền ra ngoài process. Tắt đi là bỏ lớp bảo vệ đó hoàn toàn.
Cách 2: Permissive mode để debug
Permissive mode cho phép mọi thứ chạy nhưng vẫn ghi log các vi phạm — rất hữu ích để tìm hiểu app cần quyền gì trước khi viết policy:
# Chuyển Permissive tạm thời (mất sau reboot)
setenforce 0
# Kiểm tra mode hiện tại
getenforce
# Kết quả: Permissive hoặc Enforcing
# Xem denial logs
ausearch -m avc -ts recent
# Hoặc theo dõi realtime:
tail -f /var/log/audit/audit.log | grep denied
Dùng Permissive chỉ trong giai đoạn debug. Để lâu trên server production là chấp nhận rủi ro bảo mật không cần thiết.
Cách 3: Fix đúng context — nên làm đầu tiên
Khi file mang sai context — trường hợp phổ biến nhất là copy từ home directory vào /var/www:
# Xem context hiện tại của file
ls -Z /var/www/html/myapp/
# Khôi phục về context mặc định của thư mục đó
restorecon -Rv /var/www/html/myapp/
# Hoặc set thủ công
chcon -R -t httpd_sys_content_t /var/www/html/myapp/
Cạm bẫy hay gặp: chcon chỉ thay đổi tạm thời — bị reset sau khi SELinux relabel toàn bộ filesystem (ví dụ sau touch /.autorelabel rồi reboot). Để thay đổi tồn tại vĩnh viễn, dùng semanage fcontext:
# Thêm rule persistent cho thư mục ngoài /var/www
semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
# Apply rule vừa thêm
restorecon -Rv /srv/myapp/
Tạo Custom Policy cho ứng dụng phức tạp
Phần này hầu hết tutorial tiếng Việt đều bỏ qua — vì nó đòi hỏi hiểu cụ thể app của bạn cần làm gì. Nhưng đây lại là thứ bạn cần ngay khi deploy ứng dụng thực tế: kết nối port không standard, ghi vào thư mục ngoài convention, hay chạy background job với nhiều system call phức tạp.
Bước 1: Thu thập đủ denial logs
Chuyển Permissive, chạy app và test tất cả tính năng. Quan trọng: test hết — upload file, gọi API, chạy cron job — để SELinux ghi đủ các quyền bị block. Thiếu một tính năng là sau khi load policy lại bị chặn tiếp.
setenforce 0
# Chạy app, test hết các tính năng...
ausearch -m avc -ts today > /tmp/denials.log
Bước 2: Dùng audit2allow sinh policy
# Cài tools nếu chưa có
dnf install -y policycoreutils-python-utils
# Tạo policy module từ denial log
ausearch -m avc -ts today | audit2allow -M myapp_policy
# Tạo ra 2 file:
# myapp_policy.te — Type Enforcement source (đọc được)
# myapp_policy.pp — Policy Package compiled
Đọc qua file .te trước khi làm gì tiếp:
cat myapp_policy.te
# Ví dụ output:
# allow httpd_t unreserved_port_t:tcp_socket name_connect;
# allow httpd_t var_t:file { read write create };
Dòng thứ hai — allow httpd_t var_t:file { read write create } — là đèn đỏ. Type var_t quá rộng, bao gồm cả /var/lib, /var/log của toàn hệ thống. Nên thu hẹp xuống type cụ thể hơn, biên dịch lại bằng checkmodule + semodule_package thay vì load thẳng.
Bước 3: Load policy và bật lại Enforcing
semodule -i myapp_policy.pp
setenforce 1
# Kiểm tra đã load
semodule -l | grep myapp
Cho phép app bind port tùy chỉnh
App muốn bind port 8080 mà bị block? SELinux quản lý cả port — mỗi port được gán một type cụ thể và chỉ process có type phù hợp mới được bind:
# Xem port nào đã được gán type gì
semanage port -l | grep http
# Thêm port 8080 vào http_port_t
semanage port -a -t http_port_t -p tcp 8080
# Xác nhận
semanage port -l | grep 8080
Booleans — bật/tắt tính năng mà không cần viết policy
Fedora đi kèm hơn 300 boolean switch cho các tình huống phổ biến. Trước khi ngồi viết policy từ đầu, kiểm tra boolean trước — thường có sẵn đúng cái bạn cần:
# Xem boolean liên quan đến httpd/nginx
getsebool -a | grep httpd
# Cho phép nginx kết nối ra ngoài (ví dụ: proxy đến backend)
setsebool -P httpd_can_network_connect on
# Cho phép httpd đọc home directory
setsebool -P httpd_enable_homedirs on
# -P để persist sau reboot
Workflow debug khi gặp vấn đề SELinux
Quy trình mình hay dùng, theo thứ tự từ đơn giản đến phức tạp:
- Chạy
sestatus— xác nhận đang Enforcing - Chạy
ausearch -m avc -ts recent— tìm denial gần nhất, đọc type nào đang bị block - Thử fix đơn giản trước:
restoreconnếu là vấn đề context, boolean nếu là tình huống phổ biến,semanage portnếu là vấn đề port - Nếu không đủ → Permissive mode → thu thập đủ denials →
audit2allow - Đọc kỹ file .te, thu hẹp permission quá rộng trước khi compile
- Load policy, test toàn bộ tính năng, bật lại Enforcing
Một số lệnh tra cứu nhanh
# Xem context của process đang chạy
ps auxZ | grep nginx
# Xem toàn bộ port đã được map
semanage port -l
# Tìm denial theo tên process cụ thể
ausearch -m avc -c nginx
# Liệt kê custom policy đã load
semodule -l | grep -v ^base
Lần đầu mình thực sự thấy SELinux “đáng công” là khi một lỗ hổng trong app Node.js bị khai thác — attacker có shell trong process, nhưng không leo được ra ngoài vì SELinux chặn mọi system call nằm ngoài policy. Server vẫn chạy bình thường, chỉ audit log ghi lại hàng chục denial attempt. Từ đó mình không còn nhìn SELinux như thứ cản trở công việc nữa.

