Lỗi Gặp Phải
Log lỗi của Nginx hiển thị nội dung tương tự như sau:
2024/01/15 10:23:41 [crit] 12345#0: *1 connect() to 127.0.0.1:8080 failed (13: Permission denied) while connecting to upstream, client: 203.0.113.1, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "example.com"
Nginx trả về lỗi 502 Bad Gateway cho trình duyệt. Khởi động lại Nginx hoặc dịch vụ backend không giải quyết được gì. Backend thực ra vẫn đang chạy bình thường — curl localhost:8080 vẫn trả về kết quả tốt. Đây là dấu hiệu điển hình của việc bị SELinux chặn.
Nguyên Nhân Gốc Rễ
SELinux áp dụng kiểm soát truy cập bắt buộc bên trên các quyền Linux thông thường. Trên các hệ thống dựa trên RHEL, SELinux mặc định chạy ở chế độ Enforcing — và chế độ này có những ràng buộc riêng về những gì Nginx được phép làm.
Khi Nginx hoạt động như một reverse proxy, nó cần được cấp quyền SELinux rõ ràng để thực hiện các kết nối mạng ra ngoài. Nếu không có quyền đó, kernel sẽ trả về EACCES (13: Permission denied), dù bạn đang chạy với quyền root và các quyền trên file trông hoàn toàn bình thường.
Vấn đề này thường xảy ra khi bạn:
- Proxy đến ứng dụng Node.js, Python, Ruby hoặc Java chạy trên cổng không chuẩn
- Proxy đến PHP-FPM qua Unix socket (
/run/php-fpm/www.sock) - Chuyển cấu hình hiện có sang máy chủ RHEL/CentOS/Rocky mới
- Chuyển backend từ cổng 80 hoặc 443 (được cho phép mặc định) sang các cổng như 8080, 3000 hoặc 5000
Xác Nhận Đây Là Lỗi SELinux
Đừng đoán mò. Hãy chạy các lệnh sau trước:
# Kiểm tra trạng thái SELinux
getenforce
# Kết quả cần là: Enforcing
# Kiểm tra log audit để tìm các lần bị từ chối
sudo ausearch -m avc -ts recent | grep nginx
# Hoặc dò thẳng trong file log audit
sudo grep 'nginx' /var/log/audit/audit.log | grep denied
Một lần bị từ chối trông như thế này:
type=AVC msg=audit(...): avc: denied { name_connect } for pid=12345 comm="nginx" dest=8080 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:http_cache_port_t:s0 tclass=tcp_socket permissive=0
Dòng denied { name_connect } chính là bằng chứng rõ ràng. SELinux đang chặn Nginx kết nối đến cổng upstream.
Cách Sửa 1: Bật httpd_can_network_connect (Khuyến Nghị)
Với hầu hết các cấu hình, một boolean này là đủ để giải quyết mọi thứ. Nó cho phép domain httpd_t — là domain mà Nginx chạy dưới đó — thực hiện kết nối TCP ra ngoài đến bất kỳ cổng nào:
# Bật vĩnh viễn (còn hiệu lực sau khi khởi động lại)
sudo setsebool -P httpd_can_network_connect 1
# Kiểm tra
getsebool httpd_can_network_connect
Cờ -P rất quan trọng. Bỏ qua nó và cài đặt sẽ bị đặt lại sau lần khởi động tiếp theo.
Sau đó tải lại Nginx và kiểm tra:
sudo nginx -t && sudo systemctl reload nginx
Cách Sửa 2: Cho Phép Cổng Cụ Thể Qua semanage
Thay vì mở toàn bộ kết nối ra ngoài, bạn có thể gán nhãn cho một cổng cụ thể để Nginx có thể kết nối đến. Cách này chặt chẽ hơn về mặt bảo mật.
# Cài đặt semanage nếu chưa có
sudo dnf install -y policycoreutils-python-utils
# Gán nhãn cổng backend của bạn (ví dụ: 3000) là http_port_t
sudo semanage port -a -t http_port_t -p tcp 3000
# Đã được gán nhãn khác? Dùng -m để sửa thay thế:
sudo semanage port -m -t http_port_t -p tcp 3000
# Xác nhận
sudo semanage port -l | grep http_port_t
Các cổng mặc định đã được gán nhãn http_port_t: 80, 443, 8008, 8009, 8080, 8443. Nếu backend của bạn đang chạy trên một trong số đó mà vẫn gặp lỗi, hãy chuyển sang Cách Sửa 1 — vấn đề nằm ở chỗ khác.
Cách Sửa 3: Unix Socket (PHP-FPM, Gunicorn, v.v.)
Proxy đến Unix socket thay vì cổng TCP?
upstream backend {
server unix:/run/myapp/app.sock;
}
File socket cần có đúng context SELinux. Kiểm tra giá trị hiện tại của nó:
ls -Z /run/myapp/app.sock
Bạn cần thấy httpd_var_run_t hoặc httpd_sock_t. Nếu thấy var_run_t hoặc giá trị khác, hãy gán lại nhãn:
# Gán nhãn một lần
sudo chcon -t httpd_sock_t /run/myapp/app.sock
# Vĩnh viễn — đặt context trên thư mục để còn hiệu lực sau khi khởi động lại
sudo semanage fcontext -a -t httpd_sock_t "/run/myapp(/.*)?"
sudo restorecon -Rv /run/myapp/
Riêng với PHP-FPM, hãy bật thêm boolean này:
sudo setsebool -P httpd_can_network_connect 1
Xác Nhận Đã Sửa Thành Công
# Tải lại Nginx
sudo systemctl reload nginx
# Kiểm tra các lần bị từ chối AVC mới (nên không có gì)
sudo ausearch -m avc -ts recent | grep nginx
# Truy cập endpoint
curl -I http://example.com/
# Theo dõi log lỗi theo thời gian thực
sudo tail -f /var/log/nginx/error.log
Không còn lỗi 502, không còn AVC denial trong log audit — vậy là xong.
Những Gì Không Nên Làm
Cách "sửa" phổ biến nhất trên Stack Overflow là lệnh này:
# ĐỪNG làm vậy trên môi trường production
sudo setenforce 0
Đúng là nó có tác dụng. Chính vì vậy mà nhiều người làm theo. Nhưng lệnh này vô hiệu hóa toàn bộ SELinux trên hệ thống — không chỉ riêng cho Nginx. Mọi dịch vụ khác cũng mất đi lớp bảo vệ đó. Hãy dùng các cách sửa có chọn lọc ở trên thay thế.
Chế độ Permissive hữu ích để xác nhận nguyên nhân. Chỉ cần bật lại enforcement sau khi kiểm tra:
# Tắt tạm thời để xác nhận đây là lỗi SELinux
sudo setenforce 0
curl -I http://example.com/ # Hoạt động ngay? Chắc chắn là lỗi SELinux rồi.
# Bật lại và áp dụng cách sửa thực sự
sudo setenforce 1
sudo setsebool -P httpd_can_network_connect 1
Mẹo Thêm
Nếu bạn đang vật lộn với quyền trên thư mục Unix socket — cụ thể là chế độ nào cho phép cả Nginx và ứng dụng của bạn truy cập socket mà không mở quá rộng — Unix Permissions Calculator trên ToolCraft cho phép bạn tính toán các giá trị chmod phù hợp một cách trực quan. Không cần nhẩm tính bát phân nữa.
Để debug SELinux sâu hơn, audit2why và audit2allow (cả hai đều có trong gói policycoreutils-python-utils) dịch các AVC denial thô sang ngôn ngữ dễ hiểu. audit2allow thậm chí có thể tạo module policy tùy chỉnh cho các trường hợp đặc biệt mà các boolean tiêu chuẩn không bao phủ:
# Giải thích bằng ngôn ngữ thông thường về những gì bị từ chối và tại sao
sudo ausearch -m avc -ts recent | audit2why
# Tạo module policy tùy chỉnh cho các trường hợp bất thường
sudo ausearch -m avc -ts recent | audit2allow -M mynginx
sudo semodule -i mynginx.pp

