Nginx מחזיר 403? כך תתקן בעיות SELinux בשרת
נתקלת בשגיאת 403 ב-Nginx למרות הרשאות קבצים תקינות? המדריך יסביר כיצד לאתר חסימות SELinux בלוגים, להגדיר תוויות נכונות בעזרת semanage ו-restorecon, ולהשאיר את המערכת מאובטחת.
מדוע Nginx מחזיר שגיאת 403 על קובץ שהרשאותיו תקינות
כאשר Nginx מחזיר שגיאת 403 על קובץ שביטי ההרשאות שלו תקינים, כמעט תמיד מדובר ב-SELinux (Security-Enhanced Linux) שחוסם את פעולת הקריאה. SELinux בודק מערכת חוקים נוספת לאחר אישור ההרשאות הסטנדרטיות, ושרת האינטרנט מורשה לקרוא רק קבצים הנושאים תווית (label) של תוכן אינטרנט. הקובץ שלך נושא תווית שונה, לכן פעולת ה-open נכשלת ול-Nginx אין מה לשלוח.
בדוק את התווית, לא רק את מצב ההרשאות (mode):
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmlהנקודה המודפסת לאחר drwxr-xr-x מציינת שהקובץ נושא תווית SELinux. default_t הוא הערך שניתן לנתיב כאשר המדיניות אינה מכירה אותו, ושום חוק בכללי שרת האינטרנט אינו מתיר קריאה מסוג זה. לוג השגיאות מציג שגיאת Unix רגילה, וזו הסיבה שהדבר נראה כבאג בהרשאות:
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"ה-kernel מחזיר 13: Permission denied עבור שני סוגי הסירובים, הרגיל וזה של SELinux. לכן, המשימה הראשונה היא לגלות איזו שכבה סירבה לבקשה. אל תתחיל עם setenforce 0.
החלק במודל שדרוש לך
SELinux הוא מנגנון בקרת גישה כפויה, המכונה בדרך כלל MAC. כל תהליך רץ בתוך domain, כגון httpd_t עבור שרת האינטרנט. לכל קובץ ולכל פורט רשת מוצמד type, כגון httpd_sys_content_t. המדיניות היא רשימה של שילובים מותרים בין domain, type ופעולה, וכל מה שאינו מופיע ברשימה זו נחסם. המנגנון פועל לאחר בדיקת ה-Unix הקלאסית, לכן הרשאות הגישה ב-drwxr-xr-x עדיין חייבות לאפשר את הגישה תחילה. שתי השכבות חייבות לאשר את הפעולה.
הקשר (context) מלא מורכב מארבעה שדות המופרדים בנקודתיים, כמו system_u:system_r:httpd_t:s0: משתמש SELinux, התפקיד (role), ה-type, והרמה (level). בשרת, רוב הזמן תתמקד בשדה השלישי, ה-type. שני פקודות מציגות את הערכים בזמן אמת:
ps -eZ | grep nginx
id -Zתהליכי ה-worker של nginx מציגים הקשר שמסתיים ב-httpd_t. ה-shell של המשתמש שלך מציג unconfined_u:unconfined_r:unconfined_t:s0, מכיוון שמדיניות ה-targeted המוגדרת כברירת מחדל מגבילה שירותים אך אינה מתערבת במשתמשים אינטראקטיביים. כדאי לדעת זאת, כיוון ש-SELinux אינו מחליף את הרצת שירותים תחת משתמשים בעלי הרשאות מינימליות. הוא מגביל את טווח הגישה של שירות במקרה שמישהו פורץ אליו.
שלושת המצבים, ואילו הפצות כוללות SELinux
sestatus
getenforceהמצב Enforcing חוסם פעולות ומתעד אותן. המצב Permissive מאפשר הכל ומתעד את מה שהיה נחסם. המצב Disabled אינו טוען מדיניות כלל. הפקודה getenforce מציגה את המצב הנוכחי. הפקודה sestatus מציגה גם את המצב מתוך /etc/selinux/config, שהוא המצב שיופעל לאחר אתחול המערכת.
ההפצות Rocky Linux, AlmaLinux, Fedora ו-RHEL מגיעות עם SELinux במצב Enforcing ומדיניות targeted. לעומתן, Ubuntu ו-Debian משתמשות ב-AppArmor, שמבצע תפקיד דומה אך באמצעות מנגנון שונה (החלק האחרון עוסק בכך). לכן, ייתכן שאותו יישום יותקן בהצלחה בשרת אחד, אך יחזיר שגיאת 403 בשרת אחר.
התקינו את הכלים לפני שתזדקקו להם
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found בהפצה מינימלית משמעותו ש-policycoreutils-python-utils חסר: חבילה זו מכילה את semanage ואת audit2allow. setroubleshoot-server מוסיף את sealert וכותב סיכום בשפה פשוטה של כל חסימה לתוך ה-journal. התקינו את שניהם בשרת חדש, כיוון שהרגע שבו תזדקקו להם הוא בדיוק הרגע שבו משהו כבר אינו תקין.
כיצד לקרוא סירוב של SELinux ביומן ה-audit
כל סירוב נרשם על ידי ה-audit daemon כהודעת AVC (ראשי תיבות של access vector cache):
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0ארבעה שדות מספרים את הסיפור המלא. comm הוא התוכנית שנחסמה. scontext הוא ה-source context, הדומיין שבו התהליך רץ. tcontext הוא ה-target context, התווית (label) על האובייקט שהתהליך ניסה לגשת אליו. tclass הוא סוג האובייקט, במקרה זה קובץ. בקריאה משולבת: התהליך ב-httpd_t ניסה לקרוא קובץ בעל התווית default_t, ו-permissive=0 מציין שהבקשה אכן נחסמה ולא רק תועדה.
אם ausearch אינו מציג דבר, ייתכן שה-audit daemon אינו פועל. במקרה כזה, הסירובים מגיעים למאגר ה-ring buffer של הליבה:
sudo journalctl -k | grep -i avcכעת הפכו את הרישום למשפט בשפה ברורה:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why קורא את אותם רישומים ומציין את הסיבה שהוא מזהה: משתנה בוליאני כבוי, תווית שאינה תואמת למדיניות, או היעדר חוק כלשהו. sealert סורק את כל היומן ומדפיס פקודה מוצעת עבור כל סירוב. התייחסו להצעה כאל רמז בלבד. הניסוח משתנה בין גרסאות, ו-sealert לעיתים מציע מודול מדיניות מותאם אישית, בעוד שהפתרון הנכון הוא תיקון תווית בשורה אחת.
דבר נוסף שחשוב לדעת: המדיניות מכילה חוקי dontaudit שמסתירים סירובים שנחשבים לבלתי מזיקים, כך שתוכנית יכולה להתנהג בצורה שגויה בעוד היומן נשאר ריק. ניתן לבטל את ההסתרה שלהם למשך זמן הבדיקה:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -Bתיקון נתיב עם תיוג שגוי באמצעות semanage fcontext ו-restorecon
שתי פקודות, והסדר ביניהן קריטי. semanage fcontext -a מתעד מה צריך להיות התיוג של נתיב מסוים. restorecon מחילה את ברירת המחדל המתועדת הזו על הקבצים שבדיסק.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlהנתיב הוא ביטוי רגולרי (regex). (/.*)? מכסה את הספרייה עצמה ואת כל מה שתחתיה, וזה בדיוק מה שנדרש עבור document root. בדקו מה ישתנה לפני ביצוע השינוי: sudo restorecon -Rvn /data/www מדפיסה את התיוגים המתוכננים, כיוון ש--n מציין שאין לבצע פעולה בפועל. לאחר restorecon אמיתי, התיוג יהיה httpd_sys_content_t ושגיאת 403 תיעלם ללא צורך באתחול השירות.
השתמשו ב-chcon רק לצורך בדיקה. chcon -t httpd_sys_content_t index.html מגדירה את התיוג ישירות, אך עדכון חבילות או תיוג מחדש מלא (full relabel) באמצעות restorecon יאפסו אותו, כיוון שהמדיניות עדיין קובעת שהנתיב צריך להיות בעל תיוג אחר. semanage fcontext היא הגרסה ששורדת שינויים כאלה. הציגו את הרשומות הקיימות באמצעות sudo semanage fcontext -l | grep '^/data'.
תוכן שהשירות חייב לכתוב אליו זקוק לסוג (type) שונה. השתמשו ב-httpd_sys_rw_content_t עבור ספריית העלאות או מטמון (cache), והגבילו זאת לנתיבים אלו בלבד: אתר לקריאה בלבד תחת סוג בעל הרשאות כתיבה מעניק גישה רחבה יותר ממה שהיישום צריך.
מדוע התיוג היה שגוי מלכתחילה? כמעט תמיד בגלל האופן שבו הקבצים הגיעו ליעדם. mv שומרת על התיוג הקיים של הקובץ, לכן אתר שהועבר מ-/root מגיע עם תיוג admin_home_t ונשאר כך. פקודת cp פשוטה מעניקה לקובץ החדש את תיוג ברירת המחדל של ספריית היעד, וזה בדרך כלל מה שנדרש, בעוד ש-cp -a ו-rsync -X מעתיקות את תיוגי המקור יחד עם הקובץ. פקודת git clone לתוך ספריית שורש חדשה יוצרת default_t. כאשר דף נטען כראוי מ-/usr/share/nginx/html אך נכשל מהספרייה שלכם, זו הסיבה.
תיקון סוג של התנהגות באמצעות בוליאני
חלק מהכשלים אינם נובעים מבעיית תיוג (label). Reverse proxy על שרת Rocky או AlmaLinux חדש מחזיר שגיאת 502, ויומן השגיאות מציג:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamה־upstream שלכם תקין. ברירת המחדל של httpd_t אינה מאפשרת לשרת לפתוח חיבורי רשת יוצאים, ולכן הקריאה connect() נדחית עוד לפני שהיא מגיעה לממשק ה-loopback. מתג אחד שולט בכל ההתנהגות הזו:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P הוא הדגל הקובע: הוא כותב את הערך לדיסק. ללא -P השינוי יאבד באתחול הבא, מה שיוביל לשירות שעובד עד שהמכונה עוברת reboot. אשרו את השינוי באמצעות semanage boolean -l | grep httpd_can_network_connect, שמדפיס את הערך הנוכחי לצד הערך השמור.
העדיפו שימוש בבוליאני על פני כתיבת חוק ידני בכל פעם שקיים כזה. בוליאנים מגיעים כחלק ממדיניות ההפצה, לכן הם מתוחזקים, מתועדים וקלים לאיתור עבור האדם הבא שיעבוד על המערכת. getsebool -a מציג רשימה של כל הבוליאנים הקיימים במערכת.
הגדרת שירות להאזנה בפורט לא סטנדרטי
גם פורטים מסומנים בתוויות. אם תעבירו את Nginx לפורט 8081, הוא יסרב לעלות:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t רשאי לבצע bind לפורטים בעלי תווית http_port_t, והפורט 8081 אינו נמנה עמם. הוסיפו אותו:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081בדקו תחילה את הרשימה. כמה פורטים גבוהים כבר מורשים, כולל 8008 ו-8443, והוספה כפולה של פורט תיכשל עם ValueError: Port tcp/8081 already defined. אם הפורט כבר שייך לסוג אחר, שנו אותו בעזרת semanage port -m -t http_port_t -p tcp 8081 במקום להוסיף אותו שוב.
אותה פקודה היא זו שמאפשרת לפורט SSH שהועבר לעבוד. Bind to port 2222 on 0.0.0.0 failed: Permission denied בתוך journalctl -u sshd משמעו שהפורט 2222 חסר ב-ssh_port_t, לכן הריצו את sudo semanage port -a -t ssh_port_t -p tcp 2222 לפני שתבצעו restart ל-daemon ותסגרו את ה-session שלכם. זהו השלב שאנשים מדלגים עליו כשהם עוקבים אחר מדריך כללי ל-הקשחת SSH ב-VPS על גבי הפצה ממשפחת Red Hat. SELinux אינו firewall, לכן עדיין יש לפתוח את הפורט: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload כאן, או להשתמש ב-ufw על גבי הפצה של Debian או Ubuntu.
כאשר אין ערך בוליאני או תווית לשינוי
מצב זה נדיר בשרת תקין, וזהו המקום שבו משתמשים גורמים לנזק. ניתן להשתמש ב-audit2allow כדי לבנות מודול מדיניות מתוך סירובי הגישה (denials) המופיעים בלוג:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppקראו את התיעוד של nginx_local.te לפני ההתקנה. שני הרגלים יסייעו לשמור על בטיחות התהליך. סננו את הקלט כך שיכלול רק את התוכנית שאתם מתקנים באמצעות -c, שכן העברת שבוע שלם של סירובי גישה לא קשורים ל-audit2allow תאשר את כולם בבת אחת. לעולם אל תתקינו מודול שנבנה מתוך סירוב שאינכם יכולים להסביר: כלל המאפשר ל-httpd_t לקרוא כל קובץ במערכת הוא קל ליצירה, אך קשה מאוד לאיתור חודשים לאחר מכן. הסירו מודול באמצעות sudo semodule -r nginx_local.
מצב Permissive הוא כלי אבחון, לא פתרון
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1מצב Permissive מאפשר את הגישה ומתעד אותה. הערך האמיתי שלו הוא ביכולת לקבל תמונה מלאה. במצב Enforcing, השירות נעצר בסירוב הראשון, לכן אתם מתקנים אותו, מאתחלים, ונתקלים בסירוב הבא. במצב Permissive, ההרצה נמשכת והלוג אוסף את כל הסירובים במעבר אחד, ולאחר מכן אתם חוזרים למצב הקודם ומתקנים את כולם יחד.
setenforce אינו משנה את /etc/selinux/config, לכן אתחול מחזיר את השרת למצב Enforcing. זוהי רשת ביטחון, וזו גם הסיבה לכך ש"תיקון" שהתבסס על setenforce 0 צץ מחדש ברגע הכי פחות מתאים. אם שירות מסוים זקוק למרחב פעולה בזמן שאתם עובדים עליו, סמנו את הדומיין הספציפי במקום את כל המכונה: sudo semanage permissive -a httpd_t משאיר את כל שאר המערכת במצב Enforcing, ו־sudo semanage permissive -d httpd_t מבטל זאת.
מדוע השבתת SELinux עולה ביוקר יותר מתיקון התוויות
הגדרת SELINUX=disabled בתוך /etc/selinux/config מחליפה תיקון של שורת תווית אחת בהחלשת השרת לצמיתות. ההבדל מתגלה ביום שבו יישום אינטרנט נפרץ. במצב enforcing, הקוד של התוקף רץ בתוך httpd_t, ולכן הוא עשוי לקרוא תוכן אינטרנט, בעוד שקריאת /etc/shadow או כתיבת יחידת systemd נחסמות על ידי המדיניות, ללא קשר למה שמשתמש ה-Unix היה מאפשר. ללא מדיניות טעונה, אותו קוד מקבל את כל ההרשאות שיש לחשבון השירות.
להשבתה יש גם מחיר שמשלמים מאוחר יותר. כל עוד לא טעונה מדיניות, קבצים חדשים נוצרים ללא תווית, כך שמערכת הקבצים יוצאת מסנכרון עם המדיניות. הפעלה מחדש של SELinux דורשת אז תיוג מחדש (relabel) מלא, אחרת שירותים רבים יקרסו בבת אחת:
sudo fixfiles -F onboot
sudo rebootפעולה זו כותבת את /.autorelabel ומבצעת תיוג מחדש לכל מערכת הקבצים במהלך האתחול הבא. בדיסק גדול זה לוקח זמן רב והקונסולה נראית תקועה, לכן יש להתחיל זאת רק כשניתן להמתין. ב-Rocky Linux וב-AlmaLinux 9 קובץ התצורה כבר לא מכבה את רכיב ה-kernel בעצמו, והדרך המתועדת להשבתה מלאה של SELinux היא באמצעות ארגומנט kernel (sudo grubby --update-kernel ALL --args selinux=0). הכרת הפקודה הזו מסייעת כאשר יורשים שרת של מישהו אחר. זהו אינו הפתרון לשגיאת 403.
הוספת תווית נוספת למכולות
במארח ממשפחת Red Hat, תהליכי מכולות רצים בתוך container_t ומורשים לקרוא רק קבצים בעלי התווית container_file_t. מיפוי מסוג bind mount מהמארח נכשל עם השגיאה Permission denied בתוך המכולה, בעוד ש־ls -l במארח נראה תקין לחלוטין. הסיומת :Z מורה לסביבת הריצה לבצע תיווי מחדש (relabel) למיפוי:
docker run -d -v /data/appdata:/var/lib/app:Z myimageהתווית :Z מגדירה את הספרייה עבור מכולה זו בלבד. התווית :z מגדירה אותה לשיתוף בין מכולות. אם תפנו את :Z לספרייה ששירותים אחרים משתמשים בה, המערכת תבצע תיווי מחדש רקורסיבי לספרייה זו, מה שיגרום לתקלות בשירותים האחרים; לכן, הקצו למכולות נתיבים משלהן. כל שאר ההיבטים של ההגדרה זהים לכל image אחר, כפי שמוסבר ב-הרצת Docker על גבי VPS.
Ubuntu ו-Debian מספקות לכם את AppArmor
אותה מטרה, תכנון שונה. AppArmor מגביל תוכנית לפי הנתיב לקובץ ההרצה שלה, תוך שימוש בפרופיל תחת /etc/apparmor.d/, במקום לתייג קבצים על הדיסק. אין צורך בתיוג מחדש ואין restorecon. התחילו כאן:
sudo aa-status
sudo journalctl -k | grep -i apparmorסירוב מופיע כ-apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". תהליך העבודה זהה במבנהו: קראו את התיעוד על הסירוב, מצאו את הפרופיל, ושנו את הכלל. sudo apt install apparmor-utils מספק לכם את aa-complain (מצב מאפשר עבור פרופיל בודד) ואת aa-enforce כדי להחזיר את המצב לקדמותו. Ubuntu מגבילה קבוצה נבחרת של שירותים ארוזים ומשאירה את השאר ללא הגבלה, לכן קראו את aa-status כדי לראות מה פעיל באמת במקום להניח הנחות.
הרגל אחד משותף לשתי המערכות. כאשר שירות מדווח על Permission denied עבור משהו שנראה תקין, קראו את לוג האבטחה לפני שאתם נוגעים בהרשאות. לעיתים רחוקות הבעיה היא בהרשאות עצמן.
FAQ
מדוע Nginx מחזיר שגיאת 403 למרות שהרשאות הקבצים תקינות?
הסיבה היא ש־SELinux מנע את הקריאה, ולא הרשאות הגישה של מערכת הקבצים. שרת האינטרנט רץ בתוך ה-domain של httpd_t ומורשה לקרוא רק קבצים שתוייגו כתוכן אינטרנט, לכן קובץ שתוייג כ-default_t או admin_home_t ייחסם ו-Nginx לא יוכל להגיש אותו. ניתן לאמת זאת באמצעות sudo ausearch -m AVC -ts recent, שמציג scontext שמסתיים ב-httpd_t, כאשר tcontext מכיל את ה-type השגוי. לאחר מכן, יש לתעד את התיוג הנכון ולהחיל אותו: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" ולאחריו sudo restorecon -Rv /data/www.
האם בטוח להריץ setenforce 0 כדי לגרום לשירות לעבוד?
setenforce 0 הוא שלב אבחוני בלבד, לא פתרון. השתמשו בו כדי לשחזר את הבעיה פעם אחת, כך שהלוג יאסוף את כל החסימות במעבר אחד, קראו אותן בעזרת sudo ausearch -m AVC -ts recent, ולאחר מכן הריצו sudo setenforce 1 ותקנו את הגורמים. שרת שנותר במצב permissive מתעד כל חסימה אך לא מונע אותן, כך שאתם נשארים עם רעש במערכת ומאבדים את ההגנה. אם שירות מסוים זקוק למרחב פעולה בזמן שאתם עובדים, הריצו sudo semanage permissive -a httpd_t כדי ששאר המכונה תישאר במצב enforcing.
כיצד מריצים שירות על פורט לא סטנדרטי כאשר SELinux במצב enforcing?
יש להוסיף את הפורט ל-type שהשירות מורשה להאזין לו. עבור שרת אינטרנט בפורט 8081: sudo semanage port -a -t http_port_t -p tcp 8081. עבור SSH בפורט 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. בדקו תחילה את הרשימה הנוכחית בעזרת sudo semanage port -l | grep -w http_port_t, כיוון שפורט שכבר מופיע ברשימה יגרום לשגיאה ValueError: Port tcp/8081 already defined. ללא שלב זה, ה-daemon יקרוס בעת העלייה עם השגיאה bind() ... Permission denied, גם אם אין תהליך אחר שתופס את הפורט.
האם ב-Ubuntu יש SELinux?
לא. Ubuntu ו-Debian משתמשות ב-AppArmor, אשר אוכף פרופיל הקשור לנתיב של קובץ הרצה ולא לתיוג קבצים. בדקו זאת בעזרת sudo aa-status וחפשו שורות apparmor="DENIED" בתוך sudo journalctl -k. Ubuntu מגבילה קבוצה נבחרת של שירותים ארוזים, לכן תוכנות רבות רצות ללא הגבלה (unconfined) כברירת מחדל. Rocky Linux ו-AlmaLinux הן הפצות שבהן תפגשו SELinux במצב enforcing מההתקנה הראשונית, יחד עם Fedora ו-RHEL.