[{"data":1,"prerenderedAt":2694},["ShallowReactive",2],{"posts":3,"\u002Ffullstack\u002Fshare-local-dev-server-ssh-tunnel":227},[4,21,35,49,59,69,79,88,96,104,113,121,129,137,146,154,162,174,185,196,206,216],{"path":5,"title":6,"description":7,"type":8,"level":9,"minutes":10,"date":11,"cover":12,"coverAlt":13,"featured":14,"tags":15,"topic":20},"\u002Fai\u002Fopus-5-5-long-running-tasks","לעבוד עם Opus 5.5: משימה אחת, ריצה ארוכה","מה משתנה כשהמודל מחזיק משימה ארוכה לבד: איך מגדירים סוף ברור, נקודות עצירה ב־CLAUDE.md, רשימת משימות בקובץ וסוכני משנה.","הסבר","רמת ביניים",8,"2026-10-05","\u002Fimages\u002Fposts\u002Fopus-5-5-long-running-tasks.svg","כרטיס של משימה אחת יוצא לריצה ארוכה, עוצר בתמרור לאישור, מסמן משימות בקובץ, מתחלק לשלושה סוכני משנה ומגיע לדגל סיום עם וי.",false,[16,17,18,19],"AI","Claude Code","Agents","Opus 5.5","ai",{"path":22,"title":23,"description":24,"type":25,"level":9,"minutes":26,"date":11,"cover":27,"coverAlt":28,"featured":14,"tags":29,"topic":34},"\u002Ffullstack\u002Fshare-local-dev-server-ssh-tunnel","לשתף שרת פיתוח מקומי בכתובת HTTPS, עם SSH ו־nginx","במקום ngrok: מנהרת SSH הפוכה לשרת שלכם, nginx שמנתב כל תת־דומיין לפורט אחר, תעודת wildcard וסיסמה. כל שלב מוסבר: למה, איך, ומה אמורים לראות.","מדריך",30,"\u002Fimages\u002Fposts\u002Fshare-local-dev-server-ssh-tunnel.svg","מחשב עם שרת פיתוח מקומי מחובר במנהרת SSH לשרת, שמעביר את הבקשה לפורט הנכון, דרך מנעול HTTPS, עד לקישור שנפתח בטלפון.",[30,31,32,33],"SSH","nginx","HTTPS","DevOps","fullstack",{"path":36,"title":37,"description":38,"type":25,"level":9,"minutes":39,"date":40,"cover":41,"coverAlt":42,"featured":14,"tags":43,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-cloudflare-tunnel","גישה ל-Home Assistant מחוץ לבית עם Cloudflare Tunnel","מחברים את Home Assistant לכתובת משלכם בלי לפתוח פורט בנתב. מדריך ל-Home Assistant OS ול-Docker, עם הרשאות גישה ובדיקות מהטלפון.",14,"2026-10-04","\u002Fimages\u002Fposts\u002Fhome-assistant-cloudflare-tunnel.svg","טלפון מחוץ לבית מתחבר דרך Cloudflare Tunnel ומחבר מקומי ל-Home Assistant בבית.",[44,45,46,47],"Home Assistant","Cloudflare","Docker","בית חכם","smart-home",{"path":50,"title":51,"description":52,"type":25,"level":53,"minutes":10,"date":54,"cover":55,"coverAlt":56,"featured":14,"tags":57,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-backup-update-recovery","גיבוי שאפשר לשחזר, ועדכון שאפשר להתאושש ממנו","מגדירים עותק גיבוי מחוץ לשרת, שומרים מפתח הצפנה ובודקים שחזור מבודד לפני שמעדכנים את הבית הפעיל.","למתחילים","2026-09-29","\u002Fimages\u002Fposts\u002Fhome-assistant-backup-update-recovery.svg","שרת מקומי, ארכיון גיבוי ועותק חיצוני מחוברים למסלול שחזור.",[44,46,58],"גיבוי",{"path":60,"title":61,"description":62,"type":25,"level":53,"minutes":10,"date":63,"cover":64,"coverAlt":65,"featured":14,"tags":66,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-device-maintenance","חיישן נעלם? תחזוקה של מכשירים וסוללות","בונים רשימת תחזוקה קצרה, מפרידים בין סוללה חלשה למכשיר לא זמין ומאתרים בעיות דיווח בלי לאפס את הרשת.","2026-09-24","\u002Fimages\u002Fposts\u002Fhome-assistant-device-maintenance.svg","סוללה חלשה, רשת מכשירים עם צומת לא זמין וכלי תחזוקה.",[44,67,68],"חיישנים","Zigbee",{"path":70,"title":71,"description":72,"type":25,"level":53,"minutes":10,"date":73,"cover":74,"coverAlt":75,"featured":14,"tags":76,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-washing-machine-finished","מכונת הכביסה סיימה: התראה לפי צריכת חשמל","מודדים מחזור כביסה, מגדירים ספי הספק מתוך הנתונים ושולחים התראה רק אחרי ריצה שזוהתה, עם טיפול בדיווח חסר.","2026-09-19","\u002Fimages\u002Fposts\u002Fhome-assistant-washing-machine-finished.svg","מכונת כביסה מחוברת לגרף צריכת הספק ולהודעת סיום בטלפון.",[44,77,78,67],"אוטומציות","YAML",{"path":80,"title":81,"description":82,"type":25,"level":53,"minutes":83,"date":84,"cover":85,"coverAlt":86,"featured":14,"tags":87,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-smart-button-actions","כפתור חכם במקום עוד אפליקציה","מחברים כפתור פיזי לפעולה יומיומית, מזהים איך האינטגרציה מדווחת על לחיצות ובודקים תגובה בלי להסתמך על דגם מסוים.",7,"2026-09-14","\u002Fimages\u002Fposts\u002Fhome-assistant-smart-button-actions.svg","יד לוחצת על כפתור פיזי שמפעיל תאורה ותריס במסגרת סצנה.",[44,77,68],{"path":89,"title":90,"description":91,"type":25,"level":53,"minutes":83,"date":92,"cover":93,"coverAlt":94,"featured":14,"tags":95,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-helpers-house-modes","Helpers: נותנים לבית מצבים וזיכרון","מוסיפים מצב אורחים שנשלט מלוח הבקרה, מחברים אותו לתנאי באוטומציה ובודקים מה קורה אחרי אתחול.","2026-09-09","\u002Fimages\u002Fposts\u002Fhome-assistant-helpers-house-modes.svg","בורר מצבי בית מחובר למתגי עזר שמייצגים אורחים, יציאה והשהיה.",[44,77,78],{"path":97,"title":98,"description":99,"type":25,"level":53,"minutes":83,"date":100,"cover":101,"coverAlt":102,"featured":14,"tags":103,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-automation-debugging","האוטומציה לא עבדה: מוצאים את הסיבה","בודקים טריגרים, תנאים ופעולות לפי הסדר, קוראים Trace ומבחינים בין תקלה בחיישן לבין כלל שלא התקיים.","2026-09-04","\u002Fimages\u002Fposts\u002Fhome-assistant-automation-debugging.svg","מסלול אוטומציה מתפצל לתנאי ולפעולה, וזכוכית מגדלת מדגישה את נקודת הבדיקה.",[44,77,78],{"path":105,"title":106,"description":107,"type":25,"level":53,"minutes":83,"date":108,"cover":109,"coverAlt":110,"featured":14,"tags":111,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-light-manual-control","האור נכבה כשאתם עדיין בחדר? שיפור אוטומציית התאורה","משפרים תאורה לפי תנועה בעזרת כיבוי נפרד ומתג להשארת אור, ובודקים מה קורה בזמן קריאה, בהפעלה מחדש ובשחרור השליטה הידנית.","2026-08-30","\u002Fimages\u002Fposts\u002Fhome-assistant-light-manual-control.svg","חיישן תנועה, אדם בחדר ומנורה מופיעים לצד מתג שמאפשר להשהות כיבוי אוטומטי.",[44,47,77,112],"תאורה",{"path":114,"title":115,"description":116,"type":25,"level":53,"minutes":10,"date":117,"cover":118,"coverAlt":119,"featured":14,"tags":120,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-presence-detection","מי בבית? זיהוי נוכחות בלי כיבויים מפתיעים","מחברים את הטלפון לישות Person, בודקים זיהוי כניסה ויציאה ובונים התראת עזיבה שממתינה ולא מתייחסת למידע חסר כאל בית ריק.","2026-08-25","\u002Fimages\u002Fposts\u002Fhome-assistant-presence-detection.svg","שני אנשים מחוץ לבית, אזור נוכחות סביב הבית ושעון להשהיית פעולות יציאה.",[44,47,77],{"path":122,"title":123,"description":124,"type":25,"level":53,"minutes":83,"date":125,"cover":126,"coverAlt":127,"featured":14,"tags":128,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-useful-notifications","התראות שימושיות בלי להציף את הטלפון","מגדירים התראה כשהדלת נשארת פתוחה, בודקים הודעת ניסיון ומונעים הודעות חוזרות על כל פתיחה קצרה.","2026-08-20","\u002Fimages\u002Fposts\u002Fhome-assistant-useful-notifications.svg","דלת פתוחה מחוברת לשעון המתנה ולהתראה אחת בטלפון.",[44,47,77],{"path":130,"title":131,"description":132,"type":25,"level":53,"minutes":83,"date":133,"cover":134,"coverAlt":135,"featured":14,"tags":136,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-scenes-scripts","לילה טוב בלחיצה אחת: סצנות וסקריפטים","יוצרים סצנת ערב וסקריפט ״לילה טוב״ ב-Home Assistant, ומבינים מתי לשמור מצבים ומתי להריץ רצף פעולות.","2026-08-15","\u002Fimages\u002Fposts\u002Fhome-assistant-scenes-scripts.svg","כפתור אחד מפעיל רצף פעולות שמוביל לבית במצב לילה.",[44,47,77],{"path":138,"title":139,"description":140,"type":25,"level":53,"minutes":83,"date":141,"cover":142,"coverAlt":143,"featured":14,"tags":144,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-family-dashboard","לוח בקרה שבני הבית באמת ישתמשו בו","בונים לוח בקרה מובנה ב-Home Assistant עם חדרים, כפתורים ברורים ותצוגה נוחה בטלפון, בלי להתקין הרחבות.","2026-08-10","\u002Fimages\u002Fposts\u002Fhome-assistant-family-dashboard.svg","לוח בקרה גדול עם כפתורי תאורה וטמפרטורה מופיע לצד גרסה מותאמת לטלפון.",[44,47,145],"Dashboard",{"path":147,"title":148,"description":149,"type":25,"level":53,"minutes":83,"date":150,"cover":151,"coverAlt":152,"featured":14,"tags":153,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-organize-devices","עושים סדר בבית: חדרים, מכשירים וישויות","מסדרים את Home Assistant לפי חדרים, מבינים מה ההבדל בין מכשיר לישות ובוחרים שמות שקל למצוא בלי לשבור אוטומציות.","2026-08-05","\u002Fimages\u002Fposts\u002Fhome-assistant-organize-devices.svg","תוכנית חדרים מחוברת למכשיר אחד ולשלוש ישויות נפרדות.",[44,47,77],{"path":155,"title":156,"description":157,"type":25,"level":53,"minutes":10,"date":158,"cover":159,"coverAlt":160,"featured":14,"tags":161,"topic":48},"\u002Fsmart-home\u002Fhome-assistant-hallway-motion-light","תנועה במסדרון, אור שנדלק: אוטומציה ראשונה ב־Home Assistant","מדליקים תאורה כשמזוהה תנועה ומכבים אחרי שתי דקות ללא תנועה, עם YAML מלא והסבר על טריגרים ובדיקות.","2026-07-31","\u002Fimages\u002Fposts\u002Fhome-assistant-hallway-motion-light.svg","מסדרון ביתי עם חיישן תנועה ותאורת תקרה שנדלקת כשאדם נכנס",[44,77,112,78],{"path":163,"title":164,"description":165,"type":25,"level":9,"minutes":10,"date":166,"cover":167,"coverAlt":168,"featured":14,"tags":169,"topic":34},"\u002Ffullstack\u002Ffetch-http-errors-and-timeouts","למה Fetch לא זורק שגיאה כשהשרת מחזיר 404?","ההבדל בין כישלון רשת לתשובת HTTP, טיפול בגוף תשובה לא צפוי והגבלת זמן לבקשה, עם פונקציה שאפשר להריץ.","2026-07-26","\u002Fimages\u002Fposts\u002Ffetch-http-errors-and-timeouts.svg","בקשת Fetch מתפצלת לתשובת HTTP עם שגיאה ולכשל רשת או פסק זמן",[170,171,172,173],"JavaScript","Fetch","HTTP","Debugging",{"path":175,"title":176,"description":177,"type":8,"level":53,"minutes":10,"date":178,"cover":179,"coverAlt":180,"featured":14,"tags":181,"topic":20},"\u002Fai\u002Frag-answering-from-your-documents","RAG: איך נותנים למודל לענות מתוך המסמכים שלכם","מה מחפשים לפני שמבקשים מהמודל לכתוב, למה קטעי המקור חשובים, ואיך בודקים אם התשובה באמת נשענת עליהם.","2026-07-21","\u002Fimages\u002Fposts\u002Frag-answering-from-your-documents.svg","מסמכי מקור נבדקים בזכוכית מגדלת, קטע מתאים נבחר ועובר לכרטיס תשובה",[16,182,183,184],"RAG","חיפוש","מסמכים",{"path":186,"title":187,"description":188,"type":8,"level":53,"minutes":10,"date":189,"cover":190,"coverAlt":191,"featured":14,"tags":192,"topic":48},"\u002Fsmart-home\u002Fmatter-thread-zigbee-explained","Matter, Thread ו־Zigbee: מה ההבדל ומה צריך בבית?","שלושת השמות אינם מתארים את אותו סוג של דבר. הסבר על שכבות התקשורת, תפקידי הרכזת ובדיקת התאמה למערכת קיימת.","2026-07-16","\u002Fimages\u002Fposts\u002Fmatter-thread-zigbee-explained.svg","שתי קבוצות של מכשירי בית חכם, רשת Mesh ורכזת, עם שער שמקשר בין שכבות התקשורת",[193,194,68,195],"Matter","Thread","רשת ביתית",{"path":197,"title":198,"description":199,"type":25,"level":53,"minutes":200,"date":201,"cover":202,"coverAlt":203,"featured":14,"tags":204,"topic":34},"\u002Ffullstack\u002Fbrowser-to-server-save-form","מהדפדפן לשרת: בונים טופס שמירה ורואים מה קורה בדרך","טופס HTML, בקשת POST ושרת Node.js קטן. דוגמה מלאה שממחישה איך נתונים עוברים ואיפה בודקים אותם.",9,"2026-07-11","\u002Fimages\u002Fposts\u002Fbrowser-to-server-save-form.svg","טופס בדפדפן שולח מעטפת נתונים לשרת ומשם למסד הנתונים",[170,205,172,171],"Node.js",{"path":207,"title":208,"description":209,"type":8,"level":53,"minutes":83,"date":210,"cover":211,"coverAlt":212,"featured":213,"tags":214,"topic":20},"\u002Fai\u002Fai-agent-from-request-to-result","סוכן AI: מה קורה בין הבקשה לתוצאה","איך מודל שפה משתמש בכלים, מה ההבדל בין סוכן לתהליך קבוע ואיך מגדירים משימה שאפשר לבדוק.","2026-07-06","\u002Fimages\u002Fposts\u002Fai-agent-from-request-to-result.svg","בקשה עוברת דרך מנגנון בחירה לכלי חיפוש, מסמכים וביצוע, עד לתוצאה שנבדקה",true,[16,18,215,77],"Tool Calling",{"path":217,"title":218,"description":219,"type":25,"level":53,"minutes":220,"date":221,"cover":222,"coverAlt":223,"featured":14,"tags":224,"topic":48},"\u002Fsmart-home\u002Fsmart-home-first-steps-home-assistant","בית חכם מתחילים בצעד קטן: בחירת ציוד והתקנת Home Assistant","מה צריך לבית חכם ראשון, איך בוחרים בין Home Assistant OS ל-Container, ואיך מתקינים על Raspberry Pi, מחשב, מכונה וירטואלית או NAS.",24,"2026-07-01","\u002Fimages\u002Fposts\u002Fsmart-home-first-steps-home-assistant.svg","בית עם חיישן ותאורה, המחובר לשרת Home Assistant ולשתי אפשרויות התקנה: מערכת הפעלה וקונטיינר",[44,47,225,46,226,77],"התקנה","Raspberry Pi",{"id":228,"title":23,"body":229,"cover":27,"coverAlt":28,"date":11,"description":24,"draft":14,"extension":2685,"featured":14,"level":9,"meta":2686,"minutes":26,"navigation":213,"ogImage":2687,"path":22,"seo":2688,"sitemap":2689,"stem":2690,"tags":2691,"type":25,"updated":2692,"__hash__":2693},"posts\u002Ffullstack\u002Fshare-local-dev-server-ssh-tunnel.md",{"type":230,"value":231,"toc":2666},"minimark",[232,241,252,257,260,264,267,277,280,284,329,333,339,358,361,391,408,414,417,442,448,467,470,486,492,499,505,530,536,550,554,563,569,658,664,667,703,710,716,739,745,762,768,771,792,803,809,816,820,829,834,883,886,893,898,914,920,923,927,934,941,951,954,975,986,990,996,1001,1045,1049,1052,1067,1070,1076,1079,1094,1103,1107,1161,1194,1199,1205,1211,1214,1231,1234,1238,1255,1264,1515,1518,1666,1669,1713,1719,1729,1733,1738,1752,1763,1767,1799,1805,1823,1828,1844,1850,1855,1870,1876,1883,1910,1917,1954,1960,1966,1980,1986,1992,2011,2017,2023,2027,2032,2040,2191,2244,2254,2285,2289,2302,2308,2314,2317,2323,2329,2333,2345,2350,2356,2362,2414,2420,2473,2477,2608,2612,2615,2633,2651,2662],[233,234,235,236,240],"p",{},"האתר רץ אצלכם על ",[237,238,239],"code",{},"localhost:3000",", ומישהו צריך לראות אותו עכשיו: לקוח, מעצב או הטלפון שלכם בחוץ. שירותים כמו ngrok פותרים את זה בפקודה אחת, אבל התנועה עוברת דרך שרת של מישהו אחר, והכתובת משתנה בכל הפעלה בתוכנית החינמית. אם יש לכם שרת קטן עם כתובת ציבורית, אפשר לבנות את אותו דבר בעצמכם, עם כלים שכבר מותקנים עליו.",[233,242,243,244,251],{},"המדריך בנוי על הרעיון מהפוסט ",[245,246,250],"a",{"href":247,"rel":248},"https:\u002F\u002Fvincent.bernat.ch\u002Fen\u002Fblog\u002F2026-http-over-ssh",[249],"nofollow","Self-hosted HTTP tunnels with SSH and nginx"," של וינסנט ברנה. כאן אני בונה גרסה פשוטה יותר: טווח פורטים קבוע וסיסמה, במקום פורטים אקראיים וקישורים חתומים שפגים. בכל שלב אסביר למה הוא קיים, איך עושים אותו, ומה אמורים לראות כשהוא עובד. בסוף אסביר מה ההבדל מהגרסה המקורית ומתי כדאי לעבור אליה.",[253,254,256],"h2",{"id":255},"איך-זה-עובד","איך זה עובד",[233,258,259],{},"שלושה חלקים, וכל אחד עושה דבר אחד:",[261,262],"process",{":steps":263},"[{\"title\":\"מנהרת SSH הפוכה\",\"text\":\"המחשב שלכם פותח חיבור SSH לשרת ומבקש ממנו להאזין לפורט, למשל 8042. כל חיבור לפורט הזה בשרת עובר דרך המנהרה לשרת הפיתוח אצלכם.\"},{\"title\":\"nginx על השרת\",\"text\":\"מקבל בקשות HTTPS לכתובות כמו p8042.tunnel.example.com, קורא את מספר הפורט מתוך שם המארח ומעביר את הבקשה ל־127.0.0.1:8042.\"},{\"title\":\"DNS ותעודה\",\"text\":\"רשומת wildcard שמפנה כל תת־דומיין לשרת, ותעודת Let's Encrypt אחת שמכסה את כולם.\"}]",[233,265,266],{},"כך נראה המסלול של בקשה אחת, מהטלפון של הלקוח עד המחשב שלכם:",[268,269,275],"pre",{"className":270,"code":272,"language":273,"meta":274},[271],"language-text","browser  ->  https:\u002F\u002Fp8042.tunnel.example.com\n         ->  DNS: *.tunnel.example.com points to the server\n         ->  nginx on the server (TLS, password, port 8042 from the name)\n         ->  127.0.0.1:8042 on the server (the end of the tunnel)\n         ->  SSH connection to your laptop\n         ->  localhost:3000 (your dev server)\n","text","",[237,276,272],{"__ignoreMap":274},[233,278,279],{},"היתרון של המבנה הזה: אין שום תוכנה חדשה. SSH כבר מותקן בשני הצדדים, ו־nginx כבר רץ על רוב השרתים. אין גם צורך לפתוח פורטים בחומת האש מעבר ל־22 ול־443: הפורט של המנהרה מאזין רק ל־127.0.0.1 בשרת, ורק nginx מגיע אליו.",[253,281,283],{"id":282},"מה-צריך-לפני-שמתחילים","מה צריך לפני שמתחילים",[285,286,287,299,309,323],"ul",{},[288,289,290,294,295,298],"li",{},[291,292,293],"strong",{},"שרת עם כתובת IP ציבורית",", עם גישת ",[237,296,297],{},"sudo",". הדוגמאות כתובות ל־Debian ול־Ubuntu; בהפצות אחרות שמות החבילות קצת שונים.",[288,300,301,304,305,308],{},[291,302,303],{},"nginx מותקן על השרת",". אם הוא עדיין לא שם: ",[237,306,307],{},"sudo apt install nginx",".",[288,310,311,314,315,318,319,322],{},[291,312,313],{},"דומיין שאתם שולטים ב־DNS שלו",". בדוגמאות הדומיין הוא ",[237,316,317],{},"example.com",", וכל המנהרות יהיו תחת ",[237,320,321],{},"tunnel.example.com",". החליפו בדומיין שלכם בכל מקום.",[288,324,325,328],{},[291,326,327],{},"פורטים 22 ו־443 פתוחים"," בחומת האש של השרת.",[253,330,332],{"id":331},"שלב-1-מנהרה-הפוכה-בפקודה-אחת","שלב 1: מנהרה הפוכה בפקודה אחת",[233,334,335,338],{},[291,336,337],{},"למה מתחילים מכאן:"," זה הלב של כל הפתרון, ואפשר לבדוק אותו לבד, בלי DNS, בלי תעודה ובלי nginx. אם המנהרה לא עובדת, אין טעם להמשיך.",[233,340,341,342,345,346,349,350,353,354,357],{},"SSH יודע להעביר פורטים בשני כיוונים. ",[237,343,344],{},"-L"," (local) פותח פורט אצלכם ומעביר אותו לשרת. ",[237,347,348],{},"-R"," (remote), הכיוון שאנחנו צריכים, פותח פורט ",[291,351,352],{},"בשרת"," ומעביר כל חיבור אליו ",[291,355,356],{},"אליכם",". זו מנהרה ״הפוכה״, כי החיבור נפתח מהמחשב שלכם החוצה, אבל התנועה זורמת פנימה. זה גם מה שמאפשר לה לעבוד מאחורי NAT או ראוטר ביתי: המחשב שלכם לא צריך כתובת ציבורית.",[233,359,360],{},"על המחשב שלכם, כשהאתר רץ על פורט 3000:",[268,362,366],{"className":363,"code":364,"language":365,"meta":274,"style":274},"language-bash shiki shiki-themes github-dark-contrast","ssh -N -R 8042:localhost:3000 you@server.example.com\n","bash",[237,367,368],{"__ignoreMap":274},[369,370,373,377,381,384,388],"span",{"class":371,"line":372},"line",1,[369,374,376],{"class":375},"svObZ","ssh",[369,378,380],{"class":379},"sDLfK"," -N",[369,382,383],{"class":379}," -R",[369,385,387],{"class":386},"sU2Wk"," 8042:localhost:3000",[369,389,390],{"class":386}," you@server.example.com\n",[285,392,393,402],{},[288,394,395,398,399,401],{},[237,396,397],{},"-R 8042:localhost:3000",": השרת מאזין לפורט 8042, וכל חיבור אליו מגיע ל־",[237,400,239],{}," אצלכם. הסדר תמיד ״פורט בשרת : לאן להעביר אצלכם״.",[288,403,404,407],{},[237,405,406],{},"-N",": לא לפתוח shell. החיבור קיים רק בשביל המנהרה.",[233,409,410,413],{},[291,411,412],{},"איך זה נראה:"," שום דבר. הפקודה לא מדפיסה כלום ולא מחזירה את שורת הפקודה, וזה בדיוק המצב התקין: המנהרה פתוחה כל עוד הפקודה רצה. Ctrl+C סוגר אותה.",[233,415,416],{},"עכשיו, בחלון אחר, התחברו לשרת ובדקו שהוא באמת מאזין:",[268,418,420],{"className":363,"code":419,"language":365,"meta":274,"style":274},"sudo ss -tlnp | grep 8042\n",[237,421,422],{"__ignoreMap":274},[369,423,424,426,429,432,436,439],{"class":371,"line":372},[369,425,297],{"class":375},[369,427,428],{"class":386}," ss",[369,430,431],{"class":379}," -tlnp",[369,433,435],{"class":434},"snl16"," |",[369,437,438],{"class":375}," grep",[369,440,441],{"class":379}," 8042\n",[268,443,446],{"className":444,"code":445,"language":273,"meta":274},[271],"LISTEN 0  128  127.0.0.1:8042  0.0.0.0:*  users:((\"sshd\",pid=41237,fd=9))\nLISTEN 0  128      [::1]:8042     [::]:*  users:((\"sshd\",pid=41237,fd=8))\n",[237,447,445],{"__ignoreMap":274},[233,449,450,451,454,455,458,459,462,463,466],{},"שימו לב לכתובת: ",[237,452,453],{},"127.0.0.1",", לא ",[237,456,457],{},"0.0.0.0",". כברירת מחדל ",[237,460,461],{},"sshd"," קושר פורטים של מנהרה הפוכה רק לכתובת המקומית של השרת (ההגדרה ",[237,464,465],{},"GatewayPorts no","), וזה בדיוק מה שאנחנו רוצים: מבחוץ אי אפשר להגיע ל־8042 ישירות, רק תהליכים על השרת עצמו, כמו nginx.",[233,468,469],{},"ובדיקה אחרונה, עדיין על השרת:",[268,471,473],{"className":363,"code":472,"language":365,"meta":274,"style":274},"curl -I http:\u002F\u002F127.0.0.1:8042\n",[237,474,475],{"__ignoreMap":274},[369,476,477,480,483],{"class":371,"line":372},[369,478,479],{"class":375},"curl",[369,481,482],{"class":379}," -I",[369,484,485],{"class":386}," http:\u002F\u002F127.0.0.1:8042\n",[268,487,490],{"className":488,"code":489,"language":273,"meta":274},[271],"HTTP\u002F1.1 200 OK\nContent-Type: text\u002Fhtml\n...\n",[237,491,489],{"__ignoreMap":274},[233,493,494,495,498],{},"אם חזרה תשובה מהאתר שלכם, המנהרה עובדת. אם קיבלתם ",[237,496,497],{},"Connection refused",", בדקו שהפקודה במחשב שלכם עדיין רצה ושהאתר באמת מאזין על 3000.",[233,500,501,502,504],{},"אם המנהרה לא נפתחת בכלל, בדקו ש־",[237,503,461],{}," מרשה העברת פורטים:",[268,506,508],{"className":363,"code":507,"language":365,"meta":274,"style":274},"sudo sshd -T | grep -E 'allowtcpforwarding|gatewayports'\n",[237,509,510],{"__ignoreMap":274},[369,511,512,514,517,520,522,524,527],{"class":371,"line":372},[369,513,297],{"class":375},[369,515,516],{"class":386}," sshd",[369,518,519],{"class":379}," -T",[369,521,435],{"class":434},[369,523,438],{"class":375},[369,525,526],{"class":379}," -E",[369,528,529],{"class":386}," 'allowtcpforwarding|gatewayports'\n",[268,531,534],{"className":532,"code":533,"language":273,"meta":274},[271],"allowtcpforwarding yes\ngatewayports no\n",[237,535,533],{"__ignoreMap":274},[233,537,538,539,542,543,546,547,308],{},"אלה ערכי ברירת המחדל. אם ",[237,540,541],{},"allowtcpforwarding"," הוא ",[237,544,545],{},"no",", מישהו כיבה אותו ב־",[237,548,549],{},"\u002Fetc\u002Fssh\u002Fsshd_config",[253,551,553],{"id":552},"שלב-2-משתמש-נפרד-למנהרות","שלב 2: משתמש נפרד למנהרות",[233,555,556,559,560,562],{},[291,557,558],{},"למה:"," בשלב 1 התחברתם עם המשתמש הרגיל שלכם, שיש לו shell ואולי גם ",[237,561,297],{},". המפתח שישמש את המנהרות יישב על המחשב, ייכנס לסקריפטים, ואולי גם למחשב של עמית. עדיף שגם אם הוא ידלוף, כל מה שאפשר לעשות איתו הוא לפתוח מנהרה, לא להיכנס לשרת.",[233,564,565,568],{},[291,566,567],{},"איך:"," על השרת, צרו משתמש בלי סיסמה ובלי גישת התחברות רגילה:",[268,570,572],{"className":363,"code":571,"language":365,"meta":274,"style":274},"sudo adduser --disabled-password --gecos \"\" tunnel\nsudo mkdir -p \u002Fhome\u002Ftunnel\u002F.ssh\nsudo touch \u002Fhome\u002Ftunnel\u002F.ssh\u002Fauthorized_keys\nsudo chown -R tunnel:tunnel \u002Fhome\u002Ftunnel\u002F.ssh\nsudo chmod 700 \u002Fhome\u002Ftunnel\u002F.ssh\nsudo chmod 600 \u002Fhome\u002Ftunnel\u002F.ssh\u002Fauthorized_keys\n",[237,573,574,593,607,618,633,646],{"__ignoreMap":274},[369,575,576,578,581,584,587,590],{"class":371,"line":372},[369,577,297],{"class":375},[369,579,580],{"class":386}," adduser",[369,582,583],{"class":379}," --disabled-password",[369,585,586],{"class":379}," --gecos",[369,588,589],{"class":386}," \"\"",[369,591,592],{"class":386}," tunnel\n",[369,594,596,598,601,604],{"class":371,"line":595},2,[369,597,297],{"class":375},[369,599,600],{"class":386}," mkdir",[369,602,603],{"class":379}," -p",[369,605,606],{"class":386}," \u002Fhome\u002Ftunnel\u002F.ssh\n",[369,608,610,612,615],{"class":371,"line":609},3,[369,611,297],{"class":375},[369,613,614],{"class":386}," touch",[369,616,617],{"class":386}," \u002Fhome\u002Ftunnel\u002F.ssh\u002Fauthorized_keys\n",[369,619,621,623,626,628,631],{"class":371,"line":620},4,[369,622,297],{"class":375},[369,624,625],{"class":386}," chown",[369,627,383],{"class":379},[369,629,630],{"class":386}," tunnel:tunnel",[369,632,606],{"class":386},[369,634,636,638,641,644],{"class":371,"line":635},5,[369,637,297],{"class":375},[369,639,640],{"class":386}," chmod",[369,642,643],{"class":379}," 700",[369,645,606],{"class":386},[369,647,649,651,653,656],{"class":371,"line":648},6,[369,650,297],{"class":375},[369,652,640],{"class":386},[369,654,655],{"class":379}," 600",[369,657,617],{"class":386},[233,659,660,661,663],{},"ההרשאות חשובות: אם התיקייה או הקובץ פתוחים לאחרים, ",[237,662,461],{}," מתעלם מהם בשקט, והחיבור נכשל בלי הסבר ברור.",[233,665,666],{},"על המחשב שלכם, צרו מפתח נפרד למנהרות והציגו את החלק הציבורי שלו:",[268,668,670],{"className":363,"code":669,"language":365,"meta":274,"style":274},"ssh-keygen -t ed25519 -f ~\u002F.ssh\u002Ftunnel -C laptop-tunnel\ncat ~\u002F.ssh\u002Ftunnel.pub\n",[237,671,672,695],{"__ignoreMap":274},[369,673,674,677,680,683,686,689,692],{"class":371,"line":372},[369,675,676],{"class":375},"ssh-keygen",[369,678,679],{"class":379}," -t",[369,681,682],{"class":386}," ed25519",[369,684,685],{"class":379}," -f",[369,687,688],{"class":386}," ~\u002F.ssh\u002Ftunnel",[369,690,691],{"class":379}," -C",[369,693,694],{"class":386}," laptop-tunnel\n",[369,696,697,700],{"class":371,"line":595},[369,698,699],{"class":375},"cat",[369,701,702],{"class":386}," ~\u002F.ssh\u002Ftunnel.pub\n",[233,704,705,706,709],{},"העתיקו את השורה שהודפסה לקובץ ",[237,707,708],{},"\u002Fhome\u002Ftunnel\u002F.ssh\u002Fauthorized_keys"," בשרת, והוסיפו לפניה הגבלות. השורה המלאה נראית כך:",[268,711,714],{"className":712,"code":713,"language":273,"meta":274},[271],"restrict,port-forwarding,command=\"\u002Fbin\u002Ffalse\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... laptop-tunnel\n",[237,715,713],{"__ignoreMap":274},[285,717,718,724,730],{},[288,719,720,723],{},[237,721,722],{},"restrict"," מבטל הכול: shell אינטראקטיבי, העברת סוכן, X11 והעברת פורטים.",[288,725,726,729],{},[237,727,728],{},"port-forwarding"," מחזיר רק את העברת הפורטים.",[288,731,732,735,736,738],{},[237,733,734],{},"command=\"\u002Fbin\u002Ffalse\""," מבטיח שגם אם מישהו ינסה לפתוח shell עם המפתח, הוא ייסגר מיד. עם ",[237,737,406],{}," לא מתבקשת פקודה בכלל, כך שהמנהרה לא מושפעת.",[233,740,741,744],{},[291,742,743],{},"איך בודקים שזה עובד:"," נסו להתחבר רגיל עם המפתח החדש:",[268,746,748],{"className":363,"code":747,"language":365,"meta":274,"style":274},"ssh -i ~\u002F.ssh\u002Ftunnel tunnel@server.example.com\n",[237,749,750],{"__ignoreMap":274},[369,751,752,754,757,759],{"class":371,"line":372},[369,753,376],{"class":375},[369,755,756],{"class":379}," -i",[369,758,688],{"class":386},[369,760,761],{"class":386}," tunnel@server.example.com\n",[268,763,766],{"className":764,"code":765,"language":273,"meta":274},[271],"PTY allocation request failed on channel 0\nConnection to server.example.com closed.\n",[237,767,765],{"__ignoreMap":274},[233,769,770],{},"זו התוצאה הנכונה: החיבור נדחה מיד. ועכשיו המנהרה, עם אותו מפתח:",[268,772,774],{"className":363,"code":773,"language":365,"meta":274,"style":274},"ssh -i ~\u002F.ssh\u002Ftunnel -N -R 8042:localhost:3000 tunnel@server.example.com\n",[237,775,776],{"__ignoreMap":274},[369,777,778,780,782,784,786,788,790],{"class":371,"line":372},[369,779,376],{"class":375},[369,781,756],{"class":379},[369,783,688],{"class":386},[369,785,380],{"class":379},[369,787,383],{"class":379},[369,789,387],{"class":386},[369,791,761],{"class":386},[233,793,794,795,798,799,802],{},"היא צריכה להיפתח כמו בשלב 1. כדי לא לכתוב ",[237,796,797],{},"-i"," בכל פעם, הוסיפו ל־",[237,800,801],{},"~\u002F.ssh\u002Fconfig"," במחשב שלכם:",[268,804,807],{"className":805,"code":806,"language":273,"meta":274},[271],"Host tunnel-server\n    HostName server.example.com\n    User tunnel\n    IdentityFile ~\u002F.ssh\u002Ftunnel\n",[237,808,806],{"__ignoreMap":274},[233,810,811,812,815],{},"מעכשיו ",[237,813,814],{},"ssh -N -R 8042:localhost:3000 tunnel-server"," מספיק.",[253,817,819],{"id":818},"שלב-3-dns-לכל-המנהרות","שלב 3: DNS לכל המנהרות",[233,821,822,824,825,828],{},[291,823,558],{}," כל מנהרה צריכה כתובת משלה, ",[237,826,827],{},"p\u003Cport>.tunnel.example.com",", כדי ש־nginx ידע לאיזה פורט להעביר. אי אפשר להוסיף רשומת DNS לכל פורט מראש, ולכן משתמשים ברשומת wildcard: כוכבית שתופסת כל תת־דומיין שלא הוגדר במפורש.",[233,830,831,833],{},[291,832,567],{}," אצל ספק ה־DNS, הוסיפו שתי רשומות:",[835,836,837,853],"table",{},[838,839,840],"thead",{},[841,842,843,847,850],"tr",{},[844,845,846],"th",{},"רשומה",[844,848,849],{},"סוג",[844,851,852],{},"ערך",[854,855,856,869],"tbody",{},[841,857,858,863,866],{},[859,860,861],"td",{},[237,862,321],{},[859,864,865],{},"A",[859,867,868],{},"כתובת ה־IP של השרת",[841,870,871,876,879],{},[859,872,873],{},[237,874,875],{},"*.tunnel.example.com",[859,877,878],{},"CNAME",[859,880,881],{},[237,882,321],{},[233,884,885],{},"הרשומה הראשונה מצביעה על השרת. השנייה אומרת ״כל מה שמתחת ל־tunnel, לך לאותו מקום״. היתרון ב־CNAME על פני A שני: אם השרת יחליף כתובת, משנים רק רשומה אחת.",[233,887,888,889,892],{},"אם הדומיין מאחורי Cloudflare, השאירו את שתי הרשומות במצב ",[291,890,891],{},"DNS only"," (ענן אפור, לא כתום). במצב Proxied, Cloudflare עומד באמצע, מציג לדפדפן תעודה משלו, וחיבורי WebSocket ארוכים עלולים להיסגר. כאן אנחנו רוצים שהדפדפן ידבר ישירות עם nginx.",[233,894,895,897],{},[291,896,412],{}," אחרי כמה דקות, בדקו מהמחשב שלכם כתובת שלא הגדרתם במפורש:",[268,899,901],{"className":363,"code":900,"language":365,"meta":274,"style":274},"dig +short p8042.tunnel.example.com\n",[237,902,903],{"__ignoreMap":274},[369,904,905,908,911],{"class":371,"line":372},[369,906,907],{"class":375},"dig",[369,909,910],{"class":386}," +short",[369,912,913],{"class":386}," p8042.tunnel.example.com\n",[268,915,918],{"className":916,"code":917,"language":273,"meta":274},[271],"tunnel.example.com.\n203.0.113.10\n",[237,919,917],{"__ignoreMap":274},[233,921,922],{},"השורה הראשונה היא ה־CNAME, השנייה היא כתובת השרת. אם לא חוזר כלום, הרשומות עוד לא התפשטו, או שיש טעות בשם.",[253,924,926],{"id":925},"שלב-4-תעודת-wildcard-עם-certbot","שלב 4: תעודת wildcard עם certbot",[233,928,929,931,932,308],{},[291,930,558],{}," בלי תעודה, הדפדפן יציג אזהרה אדומה, ושום לקוח לא ילחץ ״המשך בכל זאת״. תעודה רגילה מכסה שם אחד, וכאן יש אינסוף שמות, אז צריך תעודת wildcard: ",[237,933,875],{},[233,935,936,937,940],{},"הבעיה: Let's Encrypt מנפיקה תעודת wildcard רק אחרי אימות דרך DNS. באימות הרגיל (HTTP), היא בודקת קובץ על שרת אחד, אבל זה לא מוכיח שאתם שולטים בכל התת־דומיינים. באימות DNS, היא מבקשת שתוסיפו רשומת TXT בשם ",[237,938,939],{},"_acme-challenge.tunnel.example.com"," עם ערך שהיא בחרה, ובודקת שהרשומה הופיעה. רק מי ששולט ב־DNS יכול לעשות את זה.",[233,942,943,944,947,948,308],{},"אפשר להוסיף את הרשומה ידנית, אבל אז תצטרכו לחזור על זה בכל חידוש, כל 90 יום. במקום זה, ",[237,945,946],{},"certbot"," משתמש בתוסף שמדבר עם ה־API של ספק ה־DNS, מוסיף את הרשומה, מחכה לאימות ומוחק אותה. לתוסף הזה צריך הרשאה, וזה בדיוק התפקיד של הקובץ ",[237,949,950],{},"cloudflare.ini",[952,953,225],"h3",{"id":225},[268,955,957],{"className":363,"code":956,"language":365,"meta":274,"style":274},"sudo apt install certbot python3-certbot-dns-cloudflare\n",[237,958,959],{"__ignoreMap":274},[369,960,961,963,966,969,972],{"class":371,"line":372},[369,962,297],{"class":375},[369,964,965],{"class":386}," apt",[369,967,968],{"class":386}," install",[369,970,971],{"class":386}," certbot",[369,973,974],{"class":386}," python3-certbot-dns-cloudflare\n",[233,976,977,978,981,982,985],{},"לספקים אחרים יש תוספים דומים (",[237,979,980],{},"python3-certbot-dns-digitalocean",", ",[237,983,984],{},"python3-certbot-dns-route53"," ועוד), וכל אחד מקבל קובץ הרשאות משלו.",[952,987,989],{"id":988},"טוקן-api-בcloudflare","טוקן API ב־Cloudflare",[233,991,992,995],{},[291,993,994],{},"למה טוקן ולא המפתח הגלובלי:"," ב־Cloudflare יש Global API Key, שנותן שליטה מלאה בכל החשבון: כל הדומיינים, החיוב וההגדרות. הקובץ שניצור יישב על השרת לתמיד. אם השרת ייפרץ, אתם רוצים שהנזק יהיה מוגבל לעריכת רשומות DNS של דומיין אחד, וזה מה שטוקן עם הרשאה מצומצמת נותן.",[233,997,998],{},[291,999,1000],{},"איך יוצרים אותו:",[1002,1003,1004,1014,1023,1037],"ol",{},[288,1005,1006,1007,1010,1011,308],{},"בלוח הבקרה של Cloudflare: ",[291,1008,1009],{},"My Profile"," ואז ",[291,1012,1013],{},"API Tokens",[288,1015,1016,1019,1020,308],{},[291,1017,1018],{},"Create Token",", ובחרו בתבנית ",[291,1021,1022],{},"Edit zone DNS",[288,1024,1025,1026,1029,1030,981,1033,1036],{},"תחת ",[291,1027,1028],{},"Zone Resources"," בחרו ",[237,1031,1032],{},"Include",[237,1034,1035],{},"Specific zone"," ואת הדומיין שלכם. אל תשאירו ״All zones״.",[288,1038,1039,1010,1042,1044],{},[291,1040,1041],{},"Continue to summary",[291,1043,1018],{},". העתיקו את הטוקן: Cloudflare מציג אותו פעם אחת בלבד.",[952,1046,1048],{"id":1047},"הקובץ-cloudflareini","הקובץ cloudflare.ini",[233,1050,1051],{},"על השרת, צרו את הקובץ:",[268,1053,1055],{"className":363,"code":1054,"language":365,"meta":274,"style":274},"sudo nano \u002Fetc\u002Fletsencrypt\u002Fcloudflare.ini\n",[237,1056,1057],{"__ignoreMap":274},[369,1058,1059,1061,1064],{"class":371,"line":372},[369,1060,297],{"class":375},[369,1062,1063],{"class":386}," nano",[369,1065,1066],{"class":386}," \u002Fetc\u002Fletsencrypt\u002Fcloudflare.ini\n",[233,1068,1069],{},"התוכן הוא הערה ושורה אחת עם הטוקן שהעתקתם:",[268,1071,1074],{"className":1072,"code":1073,"language":273,"meta":274},[271],"# Cloudflare API token for certbot: Zone.DNS edit on example.com only\ndns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567\n",[237,1075,1073],{"__ignoreMap":274},[233,1077,1078],{},"ואז נעלו אותו, כך שרק root יוכל לקרוא:",[268,1080,1082],{"className":363,"code":1081,"language":365,"meta":274,"style":274},"sudo chmod 600 \u002Fetc\u002Fletsencrypt\u002Fcloudflare.ini\n",[237,1083,1084],{"__ignoreMap":274},[369,1085,1086,1088,1090,1092],{"class":371,"line":372},[369,1087,297],{"class":375},[369,1089,640],{"class":386},[369,1091,655],{"class":379},[369,1093,1066],{"class":386},[233,1095,1096,1097,981,1100,1102],{},"אם תשכחו את ה־",[237,1098,1099],{},"chmod",[237,1101,946],{}," יעבוד, אבל ידפיס אזהרה בסגנון ״Unsafe permissions on credentials configuration file״. כדאי להתייחס אליה: היא אומרת שכל משתמש על השרת יכול לקרוא את הטוקן ולשנות את ה־DNS שלכם.",[952,1104,1106],{"id":1105},"הנפקת-התעודה","הנפקת התעודה",[268,1108,1110],{"className":363,"code":1109,"language":365,"meta":274,"style":274},"sudo certbot certonly --dns-cloudflare \\\n  --dns-cloudflare-credentials \u002Fetc\u002Fletsencrypt\u002Fcloudflare.ini \\\n  -d 'tunnel.example.com' -d '*.tunnel.example.com' \\\n  --deploy-hook 'systemctl reload nginx'\n",[237,1111,1112,1127,1137,1153],{"__ignoreMap":274},[369,1113,1114,1116,1118,1121,1124],{"class":371,"line":372},[369,1115,297],{"class":375},[369,1117,971],{"class":386},[369,1119,1120],{"class":386}," certonly",[369,1122,1123],{"class":379}," --dns-cloudflare",[369,1125,1126],{"class":379}," \\\n",[369,1128,1129,1132,1135],{"class":371,"line":595},[369,1130,1131],{"class":379},"  --dns-cloudflare-credentials",[369,1133,1134],{"class":386}," \u002Fetc\u002Fletsencrypt\u002Fcloudflare.ini",[369,1136,1126],{"class":379},[369,1138,1139,1142,1145,1148,1151],{"class":371,"line":609},[369,1140,1141],{"class":379},"  -d",[369,1143,1144],{"class":386}," 'tunnel.example.com'",[369,1146,1147],{"class":379}," -d",[369,1149,1150],{"class":386}," '*.tunnel.example.com'",[369,1152,1126],{"class":379},[369,1154,1155,1158],{"class":371,"line":620},[369,1156,1157],{"class":379},"  --deploy-hook",[369,1159,1160],{"class":386}," 'systemctl reload nginx'\n",[285,1162,1163,1169,1179,1185],{},[288,1164,1165,1168],{},[237,1166,1167],{},"certonly",": רק להנפיק תעודה, לא לגעת בהגדרות nginx. את ההגדרות נכתוב בעצמנו בשלב הבא.",[288,1170,1171,1172,1175,1176,1178],{},"שני ה־",[237,1173,1174],{},"-d",": התעודה תכסה גם את ",[237,1177,321],{}," עצמו וגם כל תת־דומיין שלו. wildcard לא כולל את השם שמעליו.",[288,1180,1181,1182,1184],{},"המירכאות הבודדות סביב ",[237,1183,875],{}," חשובות: בלעדיהן ה־shell עלול לפרש את הכוכבית כשמות קבצים.",[288,1186,1187,1190,1191,1193],{},[237,1188,1189],{},"--deploy-hook",": אחרי כל חידוש מוצלח, nginx טוען את התעודה החדשה. בלי זה, ",[237,1192,946],{}," יחדש את הקובץ, אבל nginx ימשיך להגיש את הישנה עד שמישהו יטען אותו מחדש, ואחרי 90 יום הדפדפנים יתחילו להתריע.",[233,1195,1196,1198],{},[291,1197,412],{}," אחרי כחצי דקה (התוסף מחכה שהרשומה תתפשט):",[268,1200,1203],{"className":1201,"code":1202,"language":273,"meta":274},[271],"Successfully received certificate.\nCertificate is saved at: \u002Fetc\u002Fletsencrypt\u002Flive\u002Ftunnel.example.com\u002Ffullchain.pem\nKey is saved at:         \u002Fetc\u002Fletsencrypt\u002Flive\u002Ftunnel.example.com\u002Fprivkey.pem\nThis certificate expires on 2027-01-03.\nCertbot has set up a scheduled task to automatically renew this certificate in the background.\n",[237,1204,1202],{"__ignoreMap":274},[233,1206,1207,1208,1210],{},"שני הנתיבים האלה ייכנסו להגדרות nginx. שימו לב שהתיקייה נקראת ",[237,1209,321],{},", בלי הכוכבית.",[233,1212,1213],{},"כדי לוודא שהחידוש האוטומטי יעבוד, הריצו חידוש מדומה:",[268,1215,1217],{"className":363,"code":1216,"language":365,"meta":274,"style":274},"sudo certbot renew --dry-run\n",[237,1218,1219],{"__ignoreMap":274},[369,1220,1221,1223,1225,1228],{"class":371,"line":372},[369,1222,297],{"class":375},[369,1224,971],{"class":386},[369,1226,1227],{"class":386}," renew",[369,1229,1230],{"class":379}," --dry-run\n",[233,1232,1233],{},"אם הוא מסתיים ב־״Congratulations, all simulated renewals succeeded״, אפשר לשכוח מהתעודה.",[253,1235,1237],{"id":1236},"שלב-5-nginx-קורא-את-הפורט-מתוך-שם-המארח","שלב 5: nginx קורא את הפורט מתוך שם המארח",[233,1239,1240,1242,1243,1246,1247,1250,1251,1254],{},[291,1241,558],{}," זה החלק שמחבר הכול. nginx מקבל בקשה ל־",[237,1244,1245],{},"p8042.tunnel.example.com",", וצריך להבין ממנה ״העבר ל־127.0.0.1:8042״. במקום לכתוב בלוק ",[237,1248,1249],{},"server"," לכל פורט, משתמשים ביכולת של nginx לקבל ביטוי רגולרי ב־",[237,1252,1253],{},"server_name",": קבוצה עם שם בתוך הביטוי הופכת למשתנה שאפשר להשתמש בו בהמשך.",[233,1256,1257,1259,1260,1263],{},[291,1258,567],{}," צרו את הקובץ ",[237,1261,1262],{},"\u002Fetc\u002Fnginx\u002Fsites-available\u002Ftunnel.conf",":",[268,1265,1268],{"className":1266,"code":1267,"language":31,"meta":274,"style":274},"language-nginx shiki shiki-themes github-dark-contrast","# WebSocket: pass \"Upgrade\" through, otherwise close the connection normally\nmap $http_upgrade $connection_upgrade {\n    default upgrade;\n    ''      close;\n}\n\nserver {\n    listen 443 ssl;\n    listen [::]:443 ssl;\n    http2 on;\n\n    # p8000 ... p8099: the number becomes $port\n    server_name ~^p(?\u003Cport>80[0-9][0-9])\\.tunnel\\.example\\.com$;\n\n    ssl_certificate     \u002Fetc\u002Fletsencrypt\u002Flive\u002Ftunnel.example.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Ftunnel.example.com\u002Fprivkey.pem;\n\n    auth_basic           \"Preview\";\n    auth_basic_user_file \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002F127.0.0.1:$port;\n        proxy_http_version 1.1;\n        proxy_set_header Host $host;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_set_header X-Forwarded-Proto https;\n        proxy_read_timeout 1h;\n    }\n}\n",[237,1269,1270,1276,1292,1300,1308,1313,1318,1325,1336,1343,1355,1360,1366,1378,1382,1391,1400,1405,1416,1425,1430,1442,1451,1462,1470,1478,1486,1494,1505,1511],{"__ignoreMap":274},[369,1271,1272],{"class":371,"line":372},[369,1273,1275],{"class":1274},"sQBQX","# WebSocket: pass \"Upgrade\" through, otherwise close the connection normally\n",[369,1277,1278,1281,1285,1289],{"class":371,"line":595},[369,1279,1280],{"class":434},"map",[369,1282,1284],{"class":1283},"s95oV"," $",[369,1286,1288],{"class":1287},"s9osk","http_upgrade",[369,1290,1291],{"class":1283}," $connection_upgrade {\n",[369,1293,1294,1297],{"class":371,"line":609},[369,1295,1296],{"class":379},"    default",[369,1298,1299],{"class":1283}," upgrade;\n",[369,1301,1302,1305],{"class":371,"line":620},[369,1303,1304],{"class":386},"    ''",[369,1306,1307],{"class":1283},"      close;\n",[369,1309,1310],{"class":371,"line":635},[369,1311,1312],{"class":1283},"}\n",[369,1314,1315],{"class":371,"line":648},[369,1316,1317],{"emptyLinePlaceholder":213},"\n",[369,1319,1320,1322],{"class":371,"line":83},[369,1321,1249],{"class":434},[369,1323,1324],{"class":1283}," {\n",[369,1326,1327,1330,1333],{"class":371,"line":10},[369,1328,1329],{"class":434},"    listen ",[369,1331,1332],{"class":379},"443",[369,1334,1335],{"class":1283}," ssl;\n",[369,1337,1338,1340],{"class":371,"line":200},[369,1339,1329],{"class":434},[369,1341,1342],{"class":1283},"[::]:443 ssl;\n",[369,1344,1346,1349,1352],{"class":371,"line":1345},10,[369,1347,1348],{"class":434},"    http2 ",[369,1350,1351],{"class":379},"on",[369,1353,1354],{"class":1283},";\n",[369,1356,1358],{"class":371,"line":1357},11,[369,1359,1317],{"emptyLinePlaceholder":213},[369,1361,1363],{"class":371,"line":1362},12,[369,1364,1365],{"class":1274},"    # p8000 ... p8099: the number becomes $port\n",[369,1367,1369,1372,1376],{"class":371,"line":1368},13,[369,1370,1371],{"class":434},"    server_name ~",[369,1373,1375],{"class":1374},"sns5M","^p(?\u003Cport>80[0-9][0-9])\\.tunnel\\.example\\.com$",[369,1377,1354],{"class":1283},[369,1379,1380],{"class":371,"line":39},[369,1381,1317],{"emptyLinePlaceholder":213},[369,1383,1385,1388],{"class":371,"line":1384},15,[369,1386,1387],{"class":434},"    ssl_certificate ",[369,1389,1390],{"class":1283},"    \u002Fetc\u002Fletsencrypt\u002Flive\u002Ftunnel.example.com\u002Ffullchain.pem;\n",[369,1392,1394,1397],{"class":371,"line":1393},16,[369,1395,1396],{"class":434},"    ssl_certificate_key ",[369,1398,1399],{"class":1283},"\u002Fetc\u002Fletsencrypt\u002Flive\u002Ftunnel.example.com\u002Fprivkey.pem;\n",[369,1401,1403],{"class":371,"line":1402},17,[369,1404,1317],{"emptyLinePlaceholder":213},[369,1406,1408,1411,1414],{"class":371,"line":1407},18,[369,1409,1410],{"class":434},"    auth_basic ",[369,1412,1413],{"class":386},"          \"Preview\"",[369,1415,1354],{"class":1283},[369,1417,1419,1422],{"class":371,"line":1418},19,[369,1420,1421],{"class":434},"    auth_basic_user_file ",[369,1423,1424],{"class":1283},"\u002Fetc\u002Fnginx\u002Ftunnel.htpasswd;\n",[369,1426,1428],{"class":371,"line":1427},20,[369,1429,1317],{"emptyLinePlaceholder":213},[369,1431,1433,1436,1439],{"class":371,"line":1432},21,[369,1434,1435],{"class":434},"    location",[369,1437,1438],{"class":375}," \u002F ",[369,1440,1441],{"class":1283},"{\n",[369,1443,1445,1448],{"class":371,"line":1444},22,[369,1446,1447],{"class":434},"        proxy_pass ",[369,1449,1450],{"class":1283},"http:\u002F\u002F127.0.0.1:$port;\n",[369,1452,1454,1457,1460],{"class":371,"line":1453},23,[369,1455,1456],{"class":434},"        proxy_http_version ",[369,1458,1459],{"class":379},"1.1",[369,1461,1354],{"class":1283},[369,1463,1464,1467],{"class":371,"line":220},[369,1465,1466],{"class":434},"        proxy_set_header ",[369,1468,1469],{"class":1283},"Host $host;\n",[369,1471,1473,1475],{"class":371,"line":1472},25,[369,1474,1466],{"class":434},[369,1476,1477],{"class":1283},"Upgrade $http_upgrade;\n",[369,1479,1481,1483],{"class":371,"line":1480},26,[369,1482,1466],{"class":434},[369,1484,1485],{"class":1283},"Connection $connection_upgrade;\n",[369,1487,1489,1491],{"class":371,"line":1488},27,[369,1490,1466],{"class":434},[369,1492,1493],{"class":1283},"X-Forwarded-Proto https;\n",[369,1495,1497,1500,1503],{"class":371,"line":1496},28,[369,1498,1499],{"class":434},"        proxy_read_timeout ",[369,1501,1502],{"class":379},"1h",[369,1504,1354],{"class":1283},[369,1506,1508],{"class":371,"line":1507},29,[369,1509,1510],{"class":1283},"    }\n",[369,1512,1513],{"class":371,"line":26},[369,1514,1312],{"class":1283},[233,1516,1517],{},"שורה אחרי שורה:",[285,1519,1520,1553,1568,1592,1609,1617,1625,1639,1650,1658],{},[288,1521,1522,1526,1527,1530,1531,1534,1535,1538,1539,1541,1542,1545,1546,1549,1550,1552],{},[291,1523,1524],{},[237,1525,1280],{}," יוצר משתנה ",[237,1528,1529],{},"$connection_upgrade",": אם הדפדפן ביקש לשדרג את החיבור ל־WebSocket, הערך הוא ",[237,1532,1533],{},"upgrade","; אחרת ",[237,1536,1537],{},"close",". הוא חייב לשבת מחוץ ל־",[237,1540,1249],{},", וזה בסדר, כי הקבצים ב־",[237,1543,1544],{},"sites-enabled"," נטענים בתוך הבלוק ",[237,1547,1548],{},"http",". אם כבר יש לכם ",[237,1551,1280],{}," עם אותו שם בקובץ אחר, nginx יתלונן על כפילות; מחקו אחד מהם.",[288,1554,1555,1560,1561,1564,1565,308],{},[291,1556,1557],{},[237,1558,1559],{},"listen 443 ssl"," ו־",[237,1562,1563],{},"http2 on",": HTTPS בלבד. אין צורך בבלוק לפורט 80, כי הקישורים שתשלחו יתחילו תמיד ב־",[237,1566,1567],{},"https:\u002F\u002F",[288,1569,1570,1575,1576,1579,1580,1583,1584,1587,1588,1591],{},[291,1571,1572],{},[237,1573,1574],{},"server_name ~^p(?\u003Cport>80[0-9][0-9])...",": ה־",[237,1577,1578],{},"~"," אומר ״זה ביטוי רגולרי״. ",[237,1581,1582],{},"(?\u003Cport>...)"," תופס את המספר לתוך המשתנה ",[237,1585,1586],{},"$port",". הנקודות מסומנות ",[237,1589,1590],{},"\\."," כדי שיתאימו רק לנקודה אמיתית.",[288,1593,1594,1600,1601,1604,1605,1608],{},[291,1595,1596,1597],{},"הטווח ",[237,1598,1599],{},"80[0-9][0-9]"," מגביל את nginx לפורטים 8000-8099. זו החלטת אבטחה, לא נוחות: בלי הגבלה, כל מי שמגיע לכתובת יכול לבקש ",[237,1602,1603],{},"p5432.tunnel.example.com"," ולהגיע לשירות שמאזין על השרת עצמו, למשל מסד נתונים. בדקו עם ",[237,1606,1607],{},"sudo ss -tlnp"," שאף שירות אחר בשרת לא משתמש בטווח שבחרתם.",[288,1610,1611,1616],{},[291,1612,1613],{},[237,1614,1615],{},"auth_basic",": סיסמה על כל המנהרות. ההסבר המלא בשלב הבא.",[288,1618,1619,1624],{},[291,1620,1621],{},[237,1622,1623],{},"proxy_pass http:\u002F\u002F127.0.0.1:$port",": העברה לקצה המנהרה בשרת.",[288,1626,1627,1632,1633,1635,1636,1638],{},[291,1628,1629],{},[237,1630,1631],{},"proxy_set_header Host $host",": שרת הפיתוח יראה את השם ",[237,1634,1245],{},", ולא ",[237,1637,453],{},". זה חשוב לקישורים שהאתר בונה, ולבדיקת השם ש־Vite עושה (שלב 8).",[288,1640,1641,1649],{},[291,1642,1643,1560,1646],{},[237,1644,1645],{},"Upgrade",[237,1647,1648],{},"Connection",": מעבירים חיבורי WebSocket. בלעדיהם האתר ייטען, אבל ה־HMR (רענון המודולים בזמן אמת) של Vite או Nuxt לא יעבוד, והדף לא יתעדכן כשאתם שומרים קובץ.",[288,1651,1652,1657],{},[291,1653,1654],{},[237,1655,1656],{},"X-Forwarded-Proto https",": מספר לשרת הפיתוח שהגולש הגיע ב־HTTPS, למקרה שהוא בונה כתובות מלאות או מחליט על cookies מאובטחים.",[288,1659,1660,1665],{},[291,1661,1662],{},[237,1663,1664],{},"proxy_read_timeout 1h",": ברירת המחדל היא 60 שניות. חיבור ה־HMR שקט רוב הזמן, ובלי זה nginx יסגור אותו כל דקה.",[233,1667,1668],{},"הפעילו את הקובץ, בדקו את התחביר וטענו מחדש:",[268,1670,1672],{"className":363,"code":1671,"language":365,"meta":274,"style":274},"sudo ln -s \u002Fetc\u002Fnginx\u002Fsites-available\u002Ftunnel.conf \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F\nsudo nginx -t && sudo systemctl reload nginx\n",[237,1673,1674,1690],{"__ignoreMap":274},[369,1675,1676,1678,1681,1684,1687],{"class":371,"line":372},[369,1677,297],{"class":375},[369,1679,1680],{"class":386}," ln",[369,1682,1683],{"class":379}," -s",[369,1685,1686],{"class":386}," \u002Fetc\u002Fnginx\u002Fsites-available\u002Ftunnel.conf",[369,1688,1689],{"class":386}," \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F\n",[369,1691,1692,1694,1697,1699,1702,1704,1707,1710],{"class":371,"line":595},[369,1693,297],{"class":375},[369,1695,1696],{"class":386}," nginx",[369,1698,679],{"class":379},[369,1700,1701],{"class":1283}," && ",[369,1703,297],{"class":375},[369,1705,1706],{"class":386}," systemctl",[369,1708,1709],{"class":386}," reload",[369,1711,1712],{"class":386}," nginx\n",[268,1714,1717],{"className":1715,"code":1716,"language":273,"meta":274},[271],"nginx: the configuration file \u002Fetc\u002Fnginx\u002Fnginx.conf syntax is ok\nnginx: configuration file \u002Fetc\u002Fnginx\u002Fnginx.conf test is successful\n",[237,1718,1716],{"__ignoreMap":274},[233,1720,1721,1724,1725,1728],{},[237,1722,1723],{},"nginx -t"," תמיד לפני ",[237,1726,1727],{},"reload",": אם יש טעות, השרת ממשיך לרוץ עם ההגדרות הישנות במקום ליפול. כרגע הוא עוד ייכשל, כי קובץ הסיסמאות לא קיים. זה השלב הבא.",[253,1730,1732],{"id":1731},"שלב-6-קובץ-הסיסמאות","שלב 6: קובץ הסיסמאות",[233,1734,1735,1737],{},[291,1736,558],{}," שרת פיתוח לא נבנה כדי לעמוד מול האינטרנט. הוא מציג הודעות שגיאה מפורטות, לפעמים קוד מקור דרך source maps, ולפעמים משתני סביבה. בוטים סורקים כל הזמן תת־דומיינים חדשים, במיוחד כאלה שמופיעים ברשומות התעודות הציבוריות. סיסמה אחת מספיקה כדי שרק מי שקיבל אותה מכם יגיע לאתר.",[233,1739,1740,1743,1744,1747,1748,1751],{},[291,1741,1742],{},"מה זה Basic Auth:"," כשהדפדפן מבקש דף מוגן, nginx מחזיר ",[237,1745,1746],{},"401"," עם הכותרת ",[237,1749,1750],{},"WWW-Authenticate",". הדפדפן מציג חלון קטן של שם משתמש וסיסמה, ושולח אותם בכל בקשה אחרי זה. הם נשלחים בקידוד Base64, שהוא לא הצפנה, ולכן Basic Auth בטוח רק מעל HTTPS. אצלנו כל החיבור מוצפן, כך שזה בסדר.",[233,1753,1754,1755,1758,1759,1762],{},"nginx לא שומר סיסמאות בעצמו. הוא קורא קובץ בפורמט שנולד ב־Apache: שורה לכל משתמש, ״שם:גיבוב״. הכלי שיוצר את הקובץ הזה, ",[237,1756,1757],{},"htpasswd",", מגיע בחבילה ",[237,1760,1761],{},"apache2-utils",". השם מטעה: היא לא מתקינה את Apache, רק כמה כלי שורת פקודה קטנים.",[233,1764,1765],{},[291,1766,567],{},[268,1768,1770],{"className":363,"code":1769,"language":365,"meta":274,"style":274},"sudo apt install apache2-utils\nsudo htpasswd -c \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd preview\n",[237,1771,1772,1783],{"__ignoreMap":274},[369,1773,1774,1776,1778,1780],{"class":371,"line":372},[369,1775,297],{"class":375},[369,1777,965],{"class":386},[369,1779,968],{"class":386},[369,1781,1782],{"class":386}," apache2-utils\n",[369,1784,1785,1787,1790,1793,1796],{"class":371,"line":595},[369,1786,297],{"class":375},[369,1788,1789],{"class":386}," htpasswd",[369,1791,1792],{"class":379}," -c",[369,1794,1795],{"class":386}," \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd",[369,1797,1798],{"class":386}," preview\n",[268,1800,1803],{"className":1801,"code":1802,"language":273,"meta":274},[271],"New password:\nRe-type new password:\nAdding password for user preview\n",[237,1804,1802],{"__ignoreMap":274},[285,1806,1807,1817],{},[288,1808,1809,1812,1813,1816],{},[237,1810,1811],{},"-c"," יוצר את הקובץ. ",[291,1814,1815],{},"רק בפעם הראשונה",": על קובץ קיים הוא מוחק את כל מה שהיה בו.",[288,1818,1819,1822],{},[237,1820,1821],{},"preview"," הוא שם המשתמש. אפשר לבחור כל שם.",[233,1824,1825,1826,1263],{},"כדי להוסיף משתמש נוסף, למשל לקוח מסוים, בלי ",[237,1827,1811],{},[268,1829,1831],{"className":363,"code":1830,"language":365,"meta":274,"style":274},"sudo htpasswd \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd client-acme\n",[237,1832,1833],{"__ignoreMap":274},[369,1834,1835,1837,1839,1841],{"class":371,"line":372},[369,1836,297],{"class":375},[369,1838,1789],{"class":386},[369,1840,1795],{"class":386},[369,1842,1843],{"class":386}," client-acme\n",[233,1845,1846,1847,308],{},"וכדי להסיר משתמש: ",[237,1848,1849],{},"sudo htpasswd -D \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd client-acme",[233,1851,1852],{},[291,1853,1854],{},"איך זה נראה בפנים:",[268,1856,1858],{"className":363,"code":1857,"language":365,"meta":274,"style":274},"sudo cat \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd\n",[237,1859,1860],{"__ignoreMap":274},[369,1861,1862,1864,1867],{"class":371,"line":372},[369,1863,297],{"class":375},[369,1865,1866],{"class":386}," cat",[369,1868,1869],{"class":386}," \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd\n",[268,1871,1874],{"className":1872,"code":1873,"language":273,"meta":274},[271],"preview:$apr1$Qx3kq7Lb$2Uu1k9oW0vN8yJm4Hq6cX\u002F\nclient-acme:$apr1$8Hn2Wd0p$Jf5sTzQe7RkL1vB3nY9aM.\n",[237,1875,1873],{"__ignoreMap":274},[233,1877,1878,1879,1882],{},"הסיסמה עצמה לא שמורה, רק גיבוב שלה. ",[237,1880,1881],{},"$apr1$"," מציין את שיטת הגיבוב. nginx צריך לקרוא את הקובץ, אבל אף אחד אחר לא:",[268,1884,1886],{"className":363,"code":1885,"language":365,"meta":274,"style":274},"sudo chown root:www-data \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd\nsudo chmod 640 \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd\n",[237,1887,1888,1899],{"__ignoreMap":274},[369,1889,1890,1892,1894,1897],{"class":371,"line":372},[369,1891,297],{"class":375},[369,1893,625],{"class":386},[369,1895,1896],{"class":386}," root:www-data",[369,1898,1869],{"class":386},[369,1900,1901,1903,1905,1908],{"class":371,"line":595},[369,1902,297],{"class":375},[369,1904,640],{"class":386},[369,1906,1907],{"class":379}," 640",[369,1909,1869],{"class":386},[233,1911,1912,1913,1916],{},"אם אתם לא רוצים להתקין חבילה בשביל כלי אחד, ",[237,1914,1915],{},"openssl"," יכול לייצר את אותה שורה:",[268,1918,1920],{"className":363,"code":1919,"language":365,"meta":274,"style":274},"printf 'preview:%s\\n' \"$(openssl passwd -apr1)\" | sudo tee \u002Fetc\u002Fnginx\u002Ftunnel.htpasswd\n",[237,1921,1922],{"__ignoreMap":274},[369,1923,1924,1927,1930,1933,1935,1938,1941,1944,1946,1949,1952],{"class":371,"line":372},[369,1925,1926],{"class":379},"printf",[369,1928,1929],{"class":386}," 'preview:%s\\n'",[369,1931,1932],{"class":386}," \"$(",[369,1934,1915],{"class":375},[369,1936,1937],{"class":386}," passwd ",[369,1939,1940],{"class":379},"-apr1",[369,1942,1943],{"class":386},")\"",[369,1945,435],{"class":434},[369,1947,1948],{"class":375}," sudo",[369,1950,1951],{"class":386}," tee",[369,1953,1869],{"class":386},[233,1955,1956,1957,308],{},"עכשיו אפשר לטעון את nginx: ",[237,1958,1959],{},"sudo nginx -t && sudo systemctl reload nginx",[233,1961,1962,1965],{},[291,1963,1964],{},"הבדיקה המלאה:"," כשהמנהרה משלב 2 רצה, מהמחשב שלכם:",[268,1967,1969],{"className":363,"code":1968,"language":365,"meta":274,"style":274},"curl -I https:\u002F\u002Fp8042.tunnel.example.com\n",[237,1970,1971],{"__ignoreMap":274},[369,1972,1973,1975,1977],{"class":371,"line":372},[369,1974,479],{"class":375},[369,1976,482],{"class":379},[369,1978,1979],{"class":386}," https:\u002F\u002Fp8042.tunnel.example.com\n",[268,1981,1984],{"className":1982,"code":1983,"language":273,"meta":274},[271],"HTTP\u002F2 401\nwww-authenticate: Basic realm=\"Preview\"\n",[237,1985,1983],{"__ignoreMap":274},[233,1987,1988,1989,1991],{},"בלי סיסמה, ",[237,1990,1746],{},". עם סיסמה:",[268,1993,1995],{"className":363,"code":1994,"language":365,"meta":274,"style":274},"curl -I -u preview https:\u002F\u002Fp8042.tunnel.example.com\n",[237,1996,1997],{"__ignoreMap":274},[369,1998,1999,2001,2003,2006,2009],{"class":371,"line":372},[369,2000,479],{"class":375},[369,2002,482],{"class":379},[369,2004,2005],{"class":379}," -u",[369,2007,2008],{"class":386}," preview",[369,2010,1979],{"class":386},[268,2012,2015],{"className":2013,"code":2014,"language":273,"meta":274},[271],"Enter host password for user 'preview':\nHTTP\u002F2 200\ncontent-type: text\u002Fhtml\n",[237,2016,2014],{"__ignoreMap":274},[233,2018,2019,2022],{},[237,2020,2021],{},"200"," אומר שכל השרשרת עובדת: DNS, תעודה, nginx, סיסמה, מנהרה ושרת הפיתוח. בדפדפן תראו חלון התחברות, ואחריו את האתר שלכם.",[253,2024,2026],{"id":2025},"שלב-7-פקודה-אחת-ששולחת-קישור","שלב 7: פקודה אחת ששולחת קישור",[233,2028,2029,2031],{},[291,2030,558],{}," כל ההגדרות עד כאן נעשות פעם אחת. מה שתעשו כל יום הוא לפתוח מנהרה, וכדאי שזו תהיה פקודה של מילה אחת.",[233,2033,2034,2036,2037,1263],{},[291,2035,567],{}," על המחשב שלכם, צרו את הקובץ ",[237,2038,2039],{},"~\u002Fbin\u002Fshare",[268,2041,2043],{"className":363,"code":2042,"language":365,"meta":274,"style":274},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# share \u003Clocal-port> [remote-port]: expose a local dev server at https:\u002F\u002Fp\u003Cport>.tunnel.example.com\nset -euo pipefail\n\nlocal_port=${1:?usage: share \u003Clocal-port> [remote-port]}\nremote_port=${2:-$((8000 + RANDOM % 100))}\n\necho \"https:\u002F\u002Fp${remote_port}.tunnel.example.com\"\nexec ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \\\n  -R \"${remote_port}:localhost:${local_port}\" tunnel-server\n",[237,2044,2045,2050,2055,2066,2070,2094,2127,2131,2144,2170],{"__ignoreMap":274},[369,2046,2047],{"class":371,"line":372},[369,2048,2049],{"class":1274},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[369,2051,2052],{"class":371,"line":595},[369,2053,2054],{"class":1274},"# share \u003Clocal-port> [remote-port]: expose a local dev server at https:\u002F\u002Fp\u003Cport>.tunnel.example.com\n",[369,2056,2057,2060,2063],{"class":371,"line":609},[369,2058,2059],{"class":379},"set",[369,2061,2062],{"class":379}," -euo",[369,2064,2065],{"class":386}," pipefail\n",[369,2067,2068],{"class":371,"line":620},[369,2069,1317],{"emptyLinePlaceholder":213},[369,2071,2072,2075,2078,2081,2084,2087,2089,2092],{"class":371,"line":635},[369,2073,2074],{"class":1283},"local_port",[369,2076,2077],{"class":434},"=",[369,2079,2080],{"class":1287},"${1",[369,2082,2083],{"class":434},":?",[369,2085,2086],{"class":1283},"usage",[369,2088,1263],{"class":434},[369,2090,2091],{"class":1283}," share \u003Clocal-port> [remote-port]",[369,2093,1312],{"class":1287},[369,2095,2096,2099,2101,2104,2107,2110,2113,2116,2119,2122,2125],{"class":371,"line":648},[369,2097,2098],{"class":1283},"remote_port",[369,2100,2077],{"class":434},[369,2102,2103],{"class":1287},"${2",[369,2105,2106],{"class":434},":-",[369,2108,2109],{"class":1283},"$((",[369,2111,2112],{"class":1287},"8000",[369,2114,2115],{"class":1283}," + RANDOM ",[369,2117,2118],{"class":434},"%",[369,2120,2121],{"class":1287}," 100",[369,2123,2124],{"class":1283},"))",[369,2126,1312],{"class":1287},[369,2128,2129],{"class":371,"line":83},[369,2130,1317],{"emptyLinePlaceholder":213},[369,2132,2133,2136,2139,2141],{"class":371,"line":10},[369,2134,2135],{"class":379},"echo",[369,2137,2138],{"class":386}," \"https:\u002F\u002Fp${",[369,2140,2098],{"class":1283},[369,2142,2143],{"class":386},"}.tunnel.example.com\"\n",[369,2145,2146,2149,2152,2154,2157,2160,2162,2165,2168],{"class":371,"line":200},[369,2147,2148],{"class":379},"exec",[369,2150,2151],{"class":386}," ssh",[369,2153,380],{"class":379},[369,2155,2156],{"class":379}," -o",[369,2158,2159],{"class":386}," ExitOnForwardFailure=yes",[369,2161,2156],{"class":379},[369,2163,2164],{"class":386}," ServerAliveInterval=",[369,2166,2167],{"class":379},"30",[369,2169,1126],{"class":379},[369,2171,2172,2175,2178,2180,2183,2185,2188],{"class":371,"line":1345},[369,2173,2174],{"class":379},"  -R",[369,2176,2177],{"class":386}," \"${",[369,2179,2098],{"class":1283},[369,2181,2182],{"class":386},"}:localhost:${",[369,2184,2074],{"class":1283},[369,2186,2187],{"class":386},"}\"",[369,2189,2190],{"class":386}," tunnel-server\n",[285,2192,2193,2199,2205,2211,2220,2229,2235],{},[288,2194,2195,2198],{},[237,2196,2197],{},"set -euo pipefail",": הסקריפט עוצר בשגיאה הראשונה במקום להמשיך בשקט.",[288,2200,2201,2204],{},[237,2202,2203],{},"${1:?usage: ...}",": אם לא נתתם פורט, הסקריפט מדפיס את שורת השימוש ויוצא.",[288,2206,2207,2210],{},[237,2208,2209],{},"${2:-$((8000 + RANDOM % 100))}",": בלי פורט שני, נבחר פורט אקראי בטווח 8000-8099, אותו טווח ש־nginx מכיר.",[288,2212,2213,2216,2217,2219],{},[237,2214,2215],{},"exec ssh",": הסקריפט מוחלף בתהליך ה־",[237,2218,376],{},", כך ש־Ctrl+C סוגר את המנהרה ישירות.",[288,2221,2222,2225,2226,2228],{},[237,2223,2224],{},"ExitOnForwardFailure=yes",": אם הפורט כבר תפוס בשרת (מנהרה אחרת שלכם, או שירות אחר), ",[237,2227,376],{}," יוצא עם שגיאה. בלי האפשרות הזו הוא רק מדפיס אזהרה ונשאר מחובר בלי מנהרה, ואתם שולחים קישור שלא עובד.",[288,2230,2231,2234],{},[237,2232,2233],{},"ServerAliveInterval=30",": סימן חיים כל 30 שניות, כדי שראוטר או חומת אש לא יסגרו חיבור שנראה להם שקט.",[288,2236,2237,2240,2241,2243],{},[237,2238,2239],{},"tunnel-server",": השם מ־",[237,2242,801],{}," שהגדרנו בשלב 2, עם המשתמש והמפתח הנכונים.",[233,2245,2246,2247,2250,2251,1263],{},"הפכו אותו לבר הרצה, ובדקו ש־",[237,2248,2249],{},"~\u002Fbin"," נמצא ב־",[237,2252,2253],{},"PATH",[268,2255,2257],{"className":363,"code":2256,"language":365,"meta":274,"style":274},"chmod +x ~\u002Fbin\u002Fshare\necho 'export PATH=\"$HOME\u002Fbin:$PATH\"' >> ~\u002F.zshrc   # or ~\u002F.bashrc\n",[237,2258,2259,2269],{"__ignoreMap":274},[369,2260,2261,2263,2266],{"class":371,"line":372},[369,2262,1099],{"class":375},[369,2264,2265],{"class":386}," +x",[369,2267,2268],{"class":386}," ~\u002Fbin\u002Fshare\n",[369,2270,2271,2273,2276,2279,2282],{"class":371,"line":595},[369,2272,2135],{"class":379},[369,2274,2275],{"class":386}," 'export PATH=\"$HOME\u002Fbin:$PATH\"'",[369,2277,2278],{"class":434}," >>",[369,2280,2281],{"class":386}," ~\u002F.zshrc",[369,2283,2284],{"class":1274},"   # or ~\u002F.bashrc\n",[233,2286,2287],{},[291,2288,412],{},[268,2290,2292],{"className":363,"code":2291,"language":365,"meta":274,"style":274},"share 3000\n",[237,2293,2294],{"__ignoreMap":274},[369,2295,2296,2299],{"class":371,"line":372},[369,2297,2298],{"class":375},"share",[369,2300,2301],{"class":379}," 3000\n",[268,2303,2306],{"className":2304,"code":2305,"language":273,"meta":274},[271],"https:\u002F\u002Fp8057.tunnel.example.com\n",[237,2307,2305],{"__ignoreMap":274},[233,2309,2310,2311,308],{},"והסמן נשאר מהבהב: המנהרה פתוחה. שלחו את הכתובת יחד עם שם המשתמש והסיסמה. כשמסיימים, Ctrl+C, והכתובת מחזירה מיד ",[237,2312,2313],{},"502 Bad Gateway",[233,2315,2316],{},"אם הפורט שנבחר תפוס:",[268,2318,2321],{"className":2319,"code":2320,"language":273,"meta":274},[271],"https:\u002F\u002Fp8042.tunnel.example.com\nError: remote port forwarding failed for listen port 8042\n",[237,2322,2320],{"__ignoreMap":274},[233,2324,2325,2326,308],{},"מריצים שוב, או נותנים פורט מפורש: ",[237,2327,2328],{},"share 3000 8010",[253,2330,2332],{"id":2331},"שלב-8-vite-ושם-המארח","שלב 8: Vite ושם המארח",[233,2334,2335,2337,2338,2341,2342,2344],{},[291,2336,558],{}," שרתי פיתוח חדשים בודקים את כותרת ",[237,2339,2340],{},"Host"," ודוחים שמות שהם לא מכירים. זו הגנה מפני DNS rebinding: מתקפה שבה אתר זר מפנה את הדומיין שלו לכתובת המקומית שלכם, וכך קוד באתר הזר קורא נתונים משרת הפיתוח שלכם. בגלל ש־nginx מעביר את השם ",[237,2343,1245],{},", השרת רואה שם שהוא לא מכיר.",[233,2346,2347,2349],{},[291,2348,412],{}," במקום האתר, הדפדפן מציג:",[268,2351,2354],{"className":2352,"code":2353,"language":273,"meta":274},[271],"Blocked request. This host (\"p8042.tunnel.example.com\") is not allowed.\nTo allow this host, add \"p8042.tunnel.example.com\" to `server.allowedHosts` in vite.config.js.\n",[237,2355,2353],{"__ignoreMap":274},[233,2357,2358,2361],{},[291,2359,2360],{},"איך מתקנים:"," מוסיפים את הדומיין לרשימה המותרת:",[268,2363,2367],{"className":2364,"code":2365,"language":2366,"meta":274,"style":274},"language-ts shiki shiki-themes github-dark-contrast","\u002F\u002F vite.config.ts\nexport default defineConfig({\n  server: {\n    allowedHosts: ['.tunnel.example.com'],\n  },\n})\n","ts",[237,2368,2369,2374,2388,2393,2404,2409],{"__ignoreMap":274},[369,2370,2371],{"class":371,"line":372},[369,2372,2373],{"class":1274},"\u002F\u002F vite.config.ts\n",[369,2375,2376,2379,2382,2385],{"class":371,"line":595},[369,2377,2378],{"class":434},"export",[369,2380,2381],{"class":434}," default",[369,2383,2384],{"class":375}," defineConfig",[369,2386,2387],{"class":1283},"({\n",[369,2389,2390],{"class":371,"line":609},[369,2391,2392],{"class":1283},"  server: {\n",[369,2394,2395,2398,2401],{"class":371,"line":620},[369,2396,2397],{"class":1283},"    allowedHosts: [",[369,2399,2400],{"class":386},"'.tunnel.example.com'",[369,2402,2403],{"class":1283},"],\n",[369,2405,2406],{"class":371,"line":635},[369,2407,2408],{"class":1283},"  },\n",[369,2410,2411],{"class":371,"line":648},[369,2412,2413],{"class":1283},"})\n",[233,2415,2416,2417,1263],{},"הנקודה בתחילת הערך מתירה כל תת־דומיין, כך שלא צריך לעדכן את ההגדרה כשהפורט משתנה. ב־Nuxt אותה הגדרה נכנסת תחת ",[237,2418,2419],{},"vite",[268,2421,2423],{"className":2364,"code":2422,"language":2366,"meta":274,"style":274},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  vite: {\n    server: {\n      allowedHosts: ['.tunnel.example.com'],\n    },\n  },\n})\n",[237,2424,2425,2430,2441,2446,2451,2460,2465,2469],{"__ignoreMap":274},[369,2426,2427],{"class":371,"line":372},[369,2428,2429],{"class":1274},"\u002F\u002F nuxt.config.ts\n",[369,2431,2432,2434,2436,2439],{"class":371,"line":595},[369,2433,2378],{"class":434},[369,2435,2381],{"class":434},[369,2437,2438],{"class":375}," defineNuxtConfig",[369,2440,2387],{"class":1283},[369,2442,2443],{"class":371,"line":609},[369,2444,2445],{"class":1283},"  vite: {\n",[369,2447,2448],{"class":371,"line":620},[369,2449,2450],{"class":1283},"    server: {\n",[369,2452,2453,2456,2458],{"class":371,"line":635},[369,2454,2455],{"class":1283},"      allowedHosts: [",[369,2457,2400],{"class":386},[369,2459,2403],{"class":1283},[369,2461,2462],{"class":371,"line":648},[369,2463,2464],{"class":1283},"    },\n",[369,2466,2467],{"class":371,"line":83},[369,2468,2408],{"class":1283},[369,2470,2471],{"class":371,"line":10},[369,2472,2413],{"class":1283},[253,2474,2476],{"id":2475},"כשמשהו-לא-עובד","כשמשהו לא עובד",[835,2478,2479,2492],{},[838,2480,2481],{},[841,2482,2483,2486,2489],{},[844,2484,2485],{},"מה רואים",[844,2487,2488],{},"מה כנראה קרה",[844,2490,2491],{},"מה בודקים",[854,2493,2494,2509,2528,2542,2558,2572,2592],{},[841,2495,2496,2500,2503],{},[859,2497,2498],{},[237,2499,2313],{},[859,2501,2502],{},"אין מנהרה על הפורט הזה",[859,2504,2505,2506,2508],{},"ש־",[237,2507,2298],{}," עדיין רץ, ושהפורט בכתובת זהה לפורט שהודפס",[841,2510,2511,2516,2519],{},[859,2512,2513,2515],{},[237,2514,1746],{}," שוב ושוב",[859,2517,2518],{},"שם משתמש או סיסמה שגויים",[859,2520,2521,2522,2525,2526],{},"השורה ב־",[237,2523,2524],{},"tunnel.htpasswd",", ושלא דרסתם את הקובץ עם ",[237,2527,1811],{},[841,2529,2530,2533,2536],{},[859,2531,2532],{},"שגיאת תעודה בדפדפן",[859,2534,2535],{},"הרשומה ב־Cloudflare במצב Proxied, או שהתעודה לא כוללת את ה־wildcard",[859,2537,2538,2539],{},"ענן אפור, ו־",[237,2540,2541],{},"sudo certbot certificates",[841,2543,2544,2547,2552],{},[859,2545,2546],{},"הדף של ברירת המחדל של nginx",[859,2548,2549,2550],{},"הפורט מחוץ לטווח 8000-8099, או שהקובץ לא ב־",[237,2551,1544],{},[859,2553,2554,2555],{},"הכתובת, ו־",[237,2556,2557],{},"ls \u002Fetc\u002Fnginx\u002Fsites-enabled",[841,2559,2560,2563,2566],{},[859,2561,2562],{},"״Blocked request״",[859,2564,2565],{},"Vite לא מכיר את השם",[859,2567,2568,2571],{},[237,2569,2570],{},"allowedHosts"," (שלב 8)",[841,2573,2574,2577,2580],{},[859,2575,2576],{},"האתר עולה, אבל לא מתעדכן בשמירה",[859,2578,2579],{},"WebSocket לא עובר",[859,2581,2582,2583,2585,2586,2588,2589],{},"שורות ה־",[237,2584,1645],{},", ה־",[237,2587,1648],{}," וה־",[237,2590,2591],{},"proxy_read_timeout",[841,2593,2594,2602,2605],{},[859,2595,2596,2598,2599,2601],{},[237,2597,497],{}," ב־",[237,2600,479],{}," על השרת",[859,2603,2604],{},"שרת הפיתוח לא מאזין על הפורט המקומי",[859,2606,2607],{},"שהאתר רץ, ועל איזה פורט",[253,2609,2611],{"id":2610},"מתי-לעבור-לקישורים-חתומים","מתי לעבור לקישורים חתומים",[233,2613,2614],{},"הגרסה כאן מתאימה כשאתם משתפים עם מעט אנשים שאתם סומכים עליהם. יש לה שתי חולשות שכדאי להכיר:",[285,2616,2617,2623],{},[288,2618,2619,2622],{},[291,2620,2621],{},"סיסמה אחת לכל המנהרות."," מי שקיבל אותה פעם יכול להיכנס לכל מנהרה פעילה, גם אחרי שבוע. משתמש נפרד לכל לקוח (שלב 6) מקטין את הבעיה, אבל לא פותר אותה.",[288,2624,2625,2628,2629,2632],{},[291,2626,2627],{},"פורט שחוזר לשימוש."," קישור ישן ל־",[237,2630,2631],{},"p8042"," יוביל מחר למה שירוץ אז על 8042.",[233,2634,2635,2636,2638,2639,2642,2643,2646,2647,308],{},"הפוסט המקורי של ברנה פותר את שתיהן: ",[237,2637,461],{}," בוחר פורט פנוי לבד (",[237,2640,2641],{},"-R 0:...","), ו־nginx בודק עם המודול ",[237,2644,2645],{},"secure_link"," חתימה שמשלבת את הפורט, זמן תפוגה וסוד ששמור בשרת. כל קישור עובד רק למנהרה אחת ורק עד מועד מסוים. אם אתם שולחים קישורים ללקוחות, זה שווה את המורכבות הנוספת, וכדאי לקרוא ",[245,2648,2650],{"href":247,"rel":2649},[249],"את הפירוט אצלו",[2652,2653,2655],"note",{"title":2654},"מקור",[233,2656,2657,2658,2661],{},"המדריך מבוסס על הרעיון מ־",[245,2659,250],{"href":247,"rel":2660},[249]," מאת וינסנט ברנה. ההגדרות, הסקריפט, ההסברים וההגנה בסיסמה הם גרסה פשוטה שכתבתי, והיא שונה מזו שבמקור.",[2663,2664,2665],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sQBQX, html code.shiki .sQBQX{--shiki-default:#959DA5}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}",{"title":274,"searchDepth":595,"depth":595,"links":2667},[2668,2669,2670,2671,2672,2673,2679,2680,2681,2682,2683,2684],{"id":255,"depth":595,"text":256},{"id":282,"depth":595,"text":283},{"id":331,"depth":595,"text":332},{"id":552,"depth":595,"text":553},{"id":818,"depth":595,"text":819},{"id":925,"depth":595,"text":926,"children":2674},[2675,2676,2677,2678],{"id":225,"depth":609,"text":225},{"id":988,"depth":609,"text":989},{"id":1047,"depth":609,"text":1048},{"id":1105,"depth":609,"text":1106},{"id":1236,"depth":595,"text":1237},{"id":1731,"depth":595,"text":1732},{"id":2025,"depth":595,"text":2026},{"id":2331,"depth":595,"text":2332},{"id":2475,"depth":595,"text":2476},{"id":2610,"depth":595,"text":2611},"md",{},"\u002Fimages\u002Fposts\u002Fshare-local-dev-server-ssh-tunnel.png",{"title":23,"description":24},{"loc":22},"fullstack\u002Fshare-local-dev-server-ssh-tunnel",[30,31,32,33],null,"889-YXabBNloc4FEn7djHiSRJQlwRDRhtpcg-keMe5U",1791190130272]