דלג לתוכן
Full Stack / מדריך

לשתף שרת פיתוח מקומי בכתובת HTTPS, עם SSH ו־nginx

במקום ngrok: מנהרת SSH הפוכה לשרת שלכם, nginx שמנתב כל תת־דומיין לפורט אחר, תעודת wildcard וסיסמה. כל שלב מוסבר: למה, איך, ומה אמורים לראות.

05.10.2026 · 30 דקות קריאה · רמת ביניים
מחשב עם שרת פיתוח מקומי מחובר במנהרת SSH לשרת, שמעביר את הבקשה לפורט הנכון, דרך מנעול HTTPS, עד לקישור שנפתח בטלפון.

איור: HomeRan

תוכן עניינים

האתר רץ אצלכם על localhost:3000, ומישהו צריך לראות אותו עכשיו: לקוח, מעצב או הטלפון שלכם בחוץ. שירותים כמו ngrok פותרים את זה בפקודה אחת, אבל התנועה עוברת דרך שרת של מישהו אחר, והכתובת משתנה בכל הפעלה בתוכנית החינמית. אם יש לכם שרת קטן עם כתובת ציבורית, אפשר לבנות את אותו דבר בעצמכם, עם כלים שכבר מותקנים עליו.

המדריך בנוי על הרעיון מהפוסט Self-hosted HTTP tunnels with SSH and nginx של וינסנט ברנה. כאן אני בונה גרסה פשוטה יותר: טווח פורטים קבוע וסיסמה, במקום פורטים אקראיים וקישורים חתומים שפגים. בכל שלב אסביר למה הוא קיים, איך עושים אותו, ומה אמורים לראות כשהוא עובד. בסוף אסביר מה ההבדל מהגרסה המקורית ומתי כדאי לעבור אליה.

איך זה עובד

שלושה חלקים, וכל אחד עושה דבר אחד:

  1. 01מנהרת SSH הפוכה

    המחשב שלכם פותח חיבור SSH לשרת ומבקש ממנו להאזין לפורט, למשל 8042. כל חיבור לפורט הזה בשרת עובר דרך המנהרה לשרת הפיתוח אצלכם.

  2. 02nginx על השרת

    מקבל בקשות HTTPS לכתובות כמו p8042.tunnel.example.com, קורא את מספר הפורט מתוך שם המארח ומעביר את הבקשה ל־127.0.0.1:8042.

  3. 03DNS ותעודה

    רשומת wildcard שמפנה כל תת־דומיין לשרת, ותעודת Let's Encrypt אחת שמכסה את כולם.

כך נראה המסלול של בקשה אחת, מהטלפון של הלקוח עד המחשב שלכם:

Text
browser  ->  https://p8042.tunnel.example.com
         ->  DNS: *.tunnel.example.com points to the server
         ->  nginx on the server (TLS, password, port 8042 from the name)
         ->  127.0.0.1:8042 on the server (the end of the tunnel)
         ->  SSH connection to your laptop
         ->  localhost:3000 (your dev server)

היתרון של המבנה הזה: אין שום תוכנה חדשה. SSH כבר מותקן בשני הצדדים, ו־nginx כבר רץ על רוב השרתים. אין גם צורך לפתוח פורטים בחומת האש מעבר ל־22 ול־443: הפורט של המנהרה מאזין רק ל־127.0.0.1 בשרת, ורק nginx מגיע אליו.

מה צריך לפני שמתחילים

  • שרת עם כתובת IP ציבורית, עם גישת sudo. הדוגמאות כתובות ל־Debian ול־Ubuntu; בהפצות אחרות שמות החבילות קצת שונים.
  • nginx מותקן על השרת. אם הוא עדיין לא שם: sudo apt install nginx.
  • דומיין שאתם שולטים ב־DNS שלו. בדוגמאות הדומיין הוא example.com, וכל המנהרות יהיו תחת tunnel.example.com. החליפו בדומיין שלכם בכל מקום.
  • פורטים 22 ו־443 פתוחים בחומת האש של השרת.

שלב 1: מנהרה הפוכה בפקודה אחת

למה מתחילים מכאן: זה הלב של כל הפתרון, ואפשר לבדוק אותו לבד, בלי DNS, בלי תעודה ובלי nginx. אם המנהרה לא עובדת, אין טעם להמשיך.

SSH יודע להעביר פורטים בשני כיוונים. -L (local) פותח פורט אצלכם ומעביר אותו לשרת. -R (remote), הכיוון שאנחנו צריכים, פותח פורט בשרת ומעביר כל חיבור אליו אליכם. זו מנהרה ״הפוכה״, כי החיבור נפתח מהמחשב שלכם החוצה, אבל התנועה זורמת פנימה. זה גם מה שמאפשר לה לעבוד מאחורי NAT או ראוטר ביתי: המחשב שלכם לא צריך כתובת ציבורית.

על המחשב שלכם, כשהאתר רץ על פורט 3000:

Shell
ssh -N -R 8042:localhost:3000 [email protected]
  • -R 8042:localhost:3000: השרת מאזין לפורט 8042, וכל חיבור אליו מגיע ל־localhost:3000 אצלכם. הסדר תמיד ״פורט בשרת : לאן להעביר אצלכם״.
  • -N: לא לפתוח shell. החיבור קיים רק בשביל המנהרה.

איך זה נראה: שום דבר. הפקודה לא מדפיסה כלום ולא מחזירה את שורת הפקודה, וזה בדיוק המצב התקין: המנהרה פתוחה כל עוד הפקודה רצה. Ctrl+C סוגר אותה.

עכשיו, בחלון אחר, התחברו לשרת ובדקו שהוא באמת מאזין:

Shell
sudo ss -tlnp | grep 8042
Text
LISTEN 0  128  127.0.0.1:8042  0.0.0.0:*  users:(("sshd",pid=41237,fd=9))
LISTEN 0  128      [::1]:8042     [::]:*  users:(("sshd",pid=41237,fd=8))

שימו לב לכתובת: 127.0.0.1, לא 0.0.0.0. כברירת מחדל sshd קושר פורטים של מנהרה הפוכה רק לכתובת המקומית של השרת (ההגדרה GatewayPorts no), וזה בדיוק מה שאנחנו רוצים: מבחוץ אי אפשר להגיע ל־8042 ישירות, רק תהליכים על השרת עצמו, כמו nginx.

ובדיקה אחרונה, עדיין על השרת:

Shell
curl -I http://127.0.0.1:8042
Text
HTTP/1.1 200 OK
Content-Type: text/html
...

אם חזרה תשובה מהאתר שלכם, המנהרה עובדת. אם קיבלתם Connection refused, בדקו שהפקודה במחשב שלכם עדיין רצה ושהאתר באמת מאזין על 3000.

אם המנהרה לא נפתחת בכלל, בדקו ש־sshd מרשה העברת פורטים:

Shell
sudo sshd -T | grep -E 'allowtcpforwarding|gatewayports'
Text
allowtcpforwarding yes
gatewayports no

אלה ערכי ברירת המחדל. אם allowtcpforwarding הוא no, מישהו כיבה אותו ב־/etc/ssh/sshd_config.

שלב 2: משתמש נפרד למנהרות

למה: בשלב 1 התחברתם עם המשתמש הרגיל שלכם, שיש לו shell ואולי גם sudo. המפתח שישמש את המנהרות יישב על המחשב, ייכנס לסקריפטים, ואולי גם למחשב של עמית. עדיף שגם אם הוא ידלוף, כל מה שאפשר לעשות איתו הוא לפתוח מנהרה, לא להיכנס לשרת.

איך: על השרת, צרו משתמש בלי סיסמה ובלי גישת התחברות רגילה:

Shell
sudo adduser --disabled-password --gecos "" tunnel
sudo mkdir -p /home/tunnel/.ssh
sudo touch /home/tunnel/.ssh/authorized_keys
sudo chown -R tunnel:tunnel /home/tunnel/.ssh
sudo chmod 700 /home/tunnel/.ssh
sudo chmod 600 /home/tunnel/.ssh/authorized_keys

ההרשאות חשובות: אם התיקייה או הקובץ פתוחים לאחרים, sshd מתעלם מהם בשקט, והחיבור נכשל בלי הסבר ברור.

על המחשב שלכם, צרו מפתח נפרד למנהרות והציגו את החלק הציבורי שלו:

Shell
ssh-keygen -t ed25519 -f ~/.ssh/tunnel -C laptop-tunnel
cat ~/.ssh/tunnel.pub

העתיקו את השורה שהודפסה לקובץ /home/tunnel/.ssh/authorized_keys בשרת, והוסיפו לפניה הגבלות. השורה המלאה נראית כך:

Text
restrict,port-forwarding,command="/bin/false" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... laptop-tunnel
  • restrict מבטל הכול: shell אינטראקטיבי, העברת סוכן, X11 והעברת פורטים.
  • port-forwarding מחזיר רק את העברת הפורטים.
  • command="/bin/false" מבטיח שגם אם מישהו ינסה לפתוח shell עם המפתח, הוא ייסגר מיד. עם -N לא מתבקשת פקודה בכלל, כך שהמנהרה לא מושפעת.

איך בודקים שזה עובד: נסו להתחבר רגיל עם המפתח החדש:

Shell
ssh -i ~/.ssh/tunnel [email protected]
Text
PTY allocation request failed on channel 0
Connection to server.example.com closed.

זו התוצאה הנכונה: החיבור נדחה מיד. ועכשיו המנהרה, עם אותו מפתח:

Shell
ssh -i ~/.ssh/tunnel -N -R 8042:localhost:3000 [email protected]

היא צריכה להיפתח כמו בשלב 1. כדי לא לכתוב -i בכל פעם, הוסיפו ל־~/.ssh/config במחשב שלכם:

Text
Host tunnel-server
    HostName server.example.com
    User tunnel
    IdentityFile ~/.ssh/tunnel

מעכשיו ssh -N -R 8042:localhost:3000 tunnel-server מספיק.

שלב 3: DNS לכל המנהרות

למה: כל מנהרה צריכה כתובת משלה, p<port>.tunnel.example.com, כדי ש־nginx ידע לאיזה פורט להעביר. אי אפשר להוסיף רשומת DNS לכל פורט מראש, ולכן משתמשים ברשומת wildcard: כוכבית שתופסת כל תת־דומיין שלא הוגדר במפורש.

איך: אצל ספק ה־DNS, הוסיפו שתי רשומות:

רשומהסוגערך
tunnel.example.comAכתובת ה־IP של השרת
*.tunnel.example.comCNAMEtunnel.example.com

הרשומה הראשונה מצביעה על השרת. השנייה אומרת ״כל מה שמתחת ל־tunnel, לך לאותו מקום״. היתרון ב־CNAME על פני A שני: אם השרת יחליף כתובת, משנים רק רשומה אחת.

אם הדומיין מאחורי Cloudflare, השאירו את שתי הרשומות במצב DNS only (ענן אפור, לא כתום). במצב Proxied, Cloudflare עומד באמצע, מציג לדפדפן תעודה משלו, וחיבורי WebSocket ארוכים עלולים להיסגר. כאן אנחנו רוצים שהדפדפן ידבר ישירות עם nginx.

איך זה נראה: אחרי כמה דקות, בדקו מהמחשב שלכם כתובת שלא הגדרתם במפורש:

Shell
dig +short p8042.tunnel.example.com
Text
tunnel.example.com.
203.0.113.10

השורה הראשונה היא ה־CNAME, השנייה היא כתובת השרת. אם לא חוזר כלום, הרשומות עוד לא התפשטו, או שיש טעות בשם.

שלב 4: תעודת wildcard עם certbot

למה: בלי תעודה, הדפדפן יציג אזהרה אדומה, ושום לקוח לא ילחץ ״המשך בכל זאת״. תעודה רגילה מכסה שם אחד, וכאן יש אינסוף שמות, אז צריך תעודת wildcard: *.tunnel.example.com.

הבעיה: Let's Encrypt מנפיקה תעודת wildcard רק אחרי אימות דרך DNS. באימות הרגיל (HTTP), היא בודקת קובץ על שרת אחד, אבל זה לא מוכיח שאתם שולטים בכל התת־דומיינים. באימות DNS, היא מבקשת שתוסיפו רשומת TXT בשם _acme-challenge.tunnel.example.com עם ערך שהיא בחרה, ובודקת שהרשומה הופיעה. רק מי ששולט ב־DNS יכול לעשות את זה.

אפשר להוסיף את הרשומה ידנית, אבל אז תצטרכו לחזור על זה בכל חידוש, כל 90 יום. במקום זה, certbot משתמש בתוסף שמדבר עם ה־API של ספק ה־DNS, מוסיף את הרשומה, מחכה לאימות ומוחק אותה. לתוסף הזה צריך הרשאה, וזה בדיוק התפקיד של הקובץ cloudflare.ini.

התקנה

Shell
sudo apt install certbot python3-certbot-dns-cloudflare

לספקים אחרים יש תוספים דומים (python3-certbot-dns-digitalocean, python3-certbot-dns-route53 ועוד), וכל אחד מקבל קובץ הרשאות משלו.

טוקן API ב־Cloudflare

למה טוקן ולא המפתח הגלובלי: ב־Cloudflare יש Global API Key, שנותן שליטה מלאה בכל החשבון: כל הדומיינים, החיוב וההגדרות. הקובץ שניצור יישב על השרת לתמיד. אם השרת ייפרץ, אתם רוצים שהנזק יהיה מוגבל לעריכת רשומות DNS של דומיין אחד, וזה מה שטוקן עם הרשאה מצומצמת נותן.

איך יוצרים אותו:

  1. בלוח הבקרה של Cloudflare: My Profile ואז API Tokens.
  2. Create Token, ובחרו בתבנית Edit zone DNS.
  3. תחת Zone Resources בחרו Include, Specific zone ואת הדומיין שלכם. אל תשאירו ״All zones״.
  4. Continue to summary ואז Create Token. העתיקו את הטוקן: Cloudflare מציג אותו פעם אחת בלבד.

הקובץ cloudflare.ini

על השרת, צרו את הקובץ:

Shell
sudo nano /etc/letsencrypt/cloudflare.ini

התוכן הוא הערה ושורה אחת עם הטוקן שהעתקתם:

Text
# Cloudflare API token for certbot: Zone.DNS edit on example.com only
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567

ואז נעלו אותו, כך שרק root יוכל לקרוא:

Shell
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

אם תשכחו את ה־chmod, certbot יעבוד, אבל ידפיס אזהרה בסגנון ״Unsafe permissions on credentials configuration file״. כדאי להתייחס אליה: היא אומרת שכל משתמש על השרת יכול לקרוא את הטוקן ולשנות את ה־DNS שלכם.

הנפקת התעודה

Shell
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d 'tunnel.example.com' -d '*.tunnel.example.com' \
  --deploy-hook 'systemctl reload nginx'
  • certonly: רק להנפיק תעודה, לא לגעת בהגדרות nginx. את ההגדרות נכתוב בעצמנו בשלב הבא.
  • שני ה־-d: התעודה תכסה גם את tunnel.example.com עצמו וגם כל תת־דומיין שלו. wildcard לא כולל את השם שמעליו.
  • המירכאות הבודדות סביב *.tunnel.example.com חשובות: בלעדיהן ה־shell עלול לפרש את הכוכבית כשמות קבצים.
  • --deploy-hook: אחרי כל חידוש מוצלח, nginx טוען את התעודה החדשה. בלי זה, certbot יחדש את הקובץ, אבל nginx ימשיך להגיש את הישנה עד שמישהו יטען אותו מחדש, ואחרי 90 יום הדפדפנים יתחילו להתריע.

איך זה נראה: אחרי כחצי דקה (התוסף מחכה שהרשומה תתפשט):

Text
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/tunnel.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/tunnel.example.com/privkey.pem
This certificate expires on 2027-01-03.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

שני הנתיבים האלה ייכנסו להגדרות nginx. שימו לב שהתיקייה נקראת tunnel.example.com, בלי הכוכבית.

כדי לוודא שהחידוש האוטומטי יעבוד, הריצו חידוש מדומה:

Shell
sudo certbot renew --dry-run

אם הוא מסתיים ב־״Congratulations, all simulated renewals succeeded״, אפשר לשכוח מהתעודה.

פרסומת

שלב 5: nginx קורא את הפורט מתוך שם המארח

למה: זה החלק שמחבר הכול. nginx מקבל בקשה ל־p8042.tunnel.example.com, וצריך להבין ממנה ״העבר ל־127.0.0.1:8042״. במקום לכתוב בלוק server לכל פורט, משתמשים ביכולת של nginx לקבל ביטוי רגולרי ב־server_name: קבוצה עם שם בתוך הביטוי הופכת למשתנה שאפשר להשתמש בו בהמשך.

איך: צרו את הקובץ /etc/nginx/sites-available/tunnel.conf:

nginx
# WebSocket: pass "Upgrade" through, otherwise close the connection normally
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    # p8000 ... p8099: the number becomes $port
    server_name ~^p(?<port>80[0-9][0-9])\.tunnel\.example\.com$;

    ssl_certificate     /etc/letsencrypt/live/tunnel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem;

    auth_basic           "Preview";
    auth_basic_user_file /etc/nginx/tunnel.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:$port;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 1h;
    }
}

שורה אחרי שורה:

  • map יוצר משתנה $connection_upgrade: אם הדפדפן ביקש לשדרג את החיבור ל־WebSocket, הערך הוא upgrade; אחרת close. הוא חייב לשבת מחוץ ל־server, וזה בסדר, כי הקבצים ב־sites-enabled נטענים בתוך הבלוק http. אם כבר יש לכם map עם אותו שם בקובץ אחר, nginx יתלונן על כפילות; מחקו אחד מהם.
  • listen 443 ssl ו־http2 on: HTTPS בלבד. אין צורך בבלוק לפורט 80, כי הקישורים שתשלחו יתחילו תמיד ב־https://.
  • server_name ~^p(?<port>80[0-9][0-9])...: ה־~ אומר ״זה ביטוי רגולרי״. (?<port>...) תופס את המספר לתוך המשתנה $port. הנקודות מסומנות \. כדי שיתאימו רק לנקודה אמיתית.
  • הטווח 80[0-9][0-9] מגביל את nginx לפורטים 8000-8099. זו החלטת אבטחה, לא נוחות: בלי הגבלה, כל מי שמגיע לכתובת יכול לבקש p5432.tunnel.example.com ולהגיע לשירות שמאזין על השרת עצמו, למשל מסד נתונים. בדקו עם sudo ss -tlnp שאף שירות אחר בשרת לא משתמש בטווח שבחרתם.
  • auth_basic: סיסמה על כל המנהרות. ההסבר המלא בשלב הבא.
  • proxy_pass http://127.0.0.1:$port: העברה לקצה המנהרה בשרת.
  • proxy_set_header Host $host: שרת הפיתוח יראה את השם p8042.tunnel.example.com, ולא 127.0.0.1. זה חשוב לקישורים שהאתר בונה, ולבדיקת השם ש־Vite עושה (שלב 8).
  • Upgrade ו־Connection: מעבירים חיבורי WebSocket. בלעדיהם האתר ייטען, אבל ה־HMR (רענון המודולים בזמן אמת) של Vite או Nuxt לא יעבוד, והדף לא יתעדכן כשאתם שומרים קובץ.
  • X-Forwarded-Proto https: מספר לשרת הפיתוח שהגולש הגיע ב־HTTPS, למקרה שהוא בונה כתובות מלאות או מחליט על cookies מאובטחים.
  • proxy_read_timeout 1h: ברירת המחדל היא 60 שניות. חיבור ה־HMR שקט רוב הזמן, ובלי זה nginx יסגור אותו כל דקה.

הפעילו את הקובץ, בדקו את התחביר וטענו מחדש:

Shell
sudo ln -s /etc/nginx/sites-available/tunnel.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

nginx -t תמיד לפני reload: אם יש טעות, השרת ממשיך לרוץ עם ההגדרות הישנות במקום ליפול. כרגע הוא עוד ייכשל, כי קובץ הסיסמאות לא קיים. זה השלב הבא.

שלב 6: קובץ הסיסמאות

למה: שרת פיתוח לא נבנה כדי לעמוד מול האינטרנט. הוא מציג הודעות שגיאה מפורטות, לפעמים קוד מקור דרך source maps, ולפעמים משתני סביבה. בוטים סורקים כל הזמן תת־דומיינים חדשים, במיוחד כאלה שמופיעים ברשומות התעודות הציבוריות. סיסמה אחת מספיקה כדי שרק מי שקיבל אותה מכם יגיע לאתר.

מה זה Basic Auth: כשהדפדפן מבקש דף מוגן, nginx מחזיר 401 עם הכותרת WWW-Authenticate. הדפדפן מציג חלון קטן של שם משתמש וסיסמה, ושולח אותם בכל בקשה אחרי זה. הם נשלחים בקידוד Base64, שהוא לא הצפנה, ולכן Basic Auth בטוח רק מעל HTTPS. אצלנו כל החיבור מוצפן, כך שזה בסדר.

nginx לא שומר סיסמאות בעצמו. הוא קורא קובץ בפורמט שנולד ב־Apache: שורה לכל משתמש, ״שם:גיבוב״. הכלי שיוצר את הקובץ הזה, htpasswd, מגיע בחבילה apache2-utils. השם מטעה: היא לא מתקינה את Apache, רק כמה כלי שורת פקודה קטנים.

איך:

Shell
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/tunnel.htpasswd preview
Text
New password:
Re-type new password:
Adding password for user preview
  • -c יוצר את הקובץ. רק בפעם הראשונה: על קובץ קיים הוא מוחק את כל מה שהיה בו.
  • preview הוא שם המשתמש. אפשר לבחור כל שם.

כדי להוסיף משתמש נוסף, למשל לקוח מסוים, בלי -c:

Shell
sudo htpasswd /etc/nginx/tunnel.htpasswd client-acme

וכדי להסיר משתמש: sudo htpasswd -D /etc/nginx/tunnel.htpasswd client-acme.

איך זה נראה בפנים:

Shell
sudo cat /etc/nginx/tunnel.htpasswd
Text
preview:$apr1$Qx3kq7Lb$2Uu1k9oW0vN8yJm4Hq6cX/
client-acme:$apr1$8Hn2Wd0p$Jf5sTzQe7RkL1vB3nY9aM.

הסיסמה עצמה לא שמורה, רק גיבוב שלה. $apr1$ מציין את שיטת הגיבוב. nginx צריך לקרוא את הקובץ, אבל אף אחד אחר לא:

Shell
sudo chown root:www-data /etc/nginx/tunnel.htpasswd
sudo chmod 640 /etc/nginx/tunnel.htpasswd

אם אתם לא רוצים להתקין חבילה בשביל כלי אחד, openssl יכול לייצר את אותה שורה:

Shell
printf 'preview:%s\n' "$(openssl passwd -apr1)" | sudo tee /etc/nginx/tunnel.htpasswd

עכשיו אפשר לטעון את nginx: sudo nginx -t && sudo systemctl reload nginx.

הבדיקה המלאה: כשהמנהרה משלב 2 רצה, מהמחשב שלכם:

Shell
curl -I https://p8042.tunnel.example.com
Text
HTTP/2 401
www-authenticate: Basic realm="Preview"

בלי סיסמה, 401. עם סיסמה:

Shell
curl -I -u preview https://p8042.tunnel.example.com
Text
Enter host password for user 'preview':
HTTP/2 200
content-type: text/html

200 אומר שכל השרשרת עובדת: DNS, תעודה, nginx, סיסמה, מנהרה ושרת הפיתוח. בדפדפן תראו חלון התחברות, ואחריו את האתר שלכם.

שלב 7: פקודה אחת ששולחת קישור

למה: כל ההגדרות עד כאן נעשות פעם אחת. מה שתעשו כל יום הוא לפתוח מנהרה, וכדאי שזו תהיה פקודה של מילה אחת.

איך: על המחשב שלכם, צרו את הקובץ ~/bin/share:

Shell
#!/usr/bin/env bash
# share <local-port> [remote-port]: expose a local dev server at https://p<port>.tunnel.example.com
set -euo pipefail

local_port=${1:?usage: share <local-port> [remote-port]}
remote_port=${2:-$((8000 + RANDOM % 100))}

echo "https://p${remote_port}.tunnel.example.com"
exec ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \
  -R "${remote_port}:localhost:${local_port}" tunnel-server
  • set -euo pipefail: הסקריפט עוצר בשגיאה הראשונה במקום להמשיך בשקט.
  • ${1:?usage: ...}: אם לא נתתם פורט, הסקריפט מדפיס את שורת השימוש ויוצא.
  • ${2:-$((8000 + RANDOM % 100))}: בלי פורט שני, נבחר פורט אקראי בטווח 8000-8099, אותו טווח ש־nginx מכיר.
  • exec ssh: הסקריפט מוחלף בתהליך ה־ssh, כך ש־Ctrl+C סוגר את המנהרה ישירות.
  • ExitOnForwardFailure=yes: אם הפורט כבר תפוס בשרת (מנהרה אחרת שלכם, או שירות אחר), ssh יוצא עם שגיאה. בלי האפשרות הזו הוא רק מדפיס אזהרה ונשאר מחובר בלי מנהרה, ואתם שולחים קישור שלא עובד.
  • ServerAliveInterval=30: סימן חיים כל 30 שניות, כדי שראוטר או חומת אש לא יסגרו חיבור שנראה להם שקט.
  • tunnel-server: השם מ־~/.ssh/config שהגדרנו בשלב 2, עם המשתמש והמפתח הנכונים.

הפכו אותו לבר הרצה, ובדקו ש־~/bin נמצא ב־PATH:

Shell
chmod +x ~/bin/share
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc   # or ~/.bashrc

איך זה נראה:

Shell
share 3000
Text
https://p8057.tunnel.example.com

והסמן נשאר מהבהב: המנהרה פתוחה. שלחו את הכתובת יחד עם שם המשתמש והסיסמה. כשמסיימים, Ctrl+C, והכתובת מחזירה מיד 502 Bad Gateway.

אם הפורט שנבחר תפוס:

Text
https://p8042.tunnel.example.com
Error: remote port forwarding failed for listen port 8042

מריצים שוב, או נותנים פורט מפורש: share 3000 8010.

שלב 8: Vite ושם המארח

למה: שרתי פיתוח חדשים בודקים את כותרת Host ודוחים שמות שהם לא מכירים. זו הגנה מפני DNS rebinding: מתקפה שבה אתר זר מפנה את הדומיין שלו לכתובת המקומית שלכם, וכך קוד באתר הזר קורא נתונים משרת הפיתוח שלכם. בגלל ש־nginx מעביר את השם p8042.tunnel.example.com, השרת רואה שם שהוא לא מכיר.

איך זה נראה: במקום האתר, הדפדפן מציג:

Text
Blocked request. This host ("p8042.tunnel.example.com") is not allowed.
To allow this host, add "p8042.tunnel.example.com" to `server.allowedHosts` in vite.config.js.

איך מתקנים: מוסיפים את הדומיין לרשימה המותרת:

TypeScript
// vite.config.ts
export default defineConfig({
  server: {
    allowedHosts: ['.tunnel.example.com'],
  },
})

הנקודה בתחילת הערך מתירה כל תת־דומיין, כך שלא צריך לעדכן את ההגדרה כשהפורט משתנה. ב־Nuxt אותה הגדרה נכנסת תחת vite:

TypeScript
// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    server: {
      allowedHosts: ['.tunnel.example.com'],
    },
  },
})

כשמשהו לא עובד

מה רואיםמה כנראה קרהמה בודקים
502 Bad Gatewayאין מנהרה על הפורט הזהש־share עדיין רץ, ושהפורט בכתובת זהה לפורט שהודפס
401 שוב ושובשם משתמש או סיסמה שגוייםהשורה ב־tunnel.htpasswd, ושלא דרסתם את הקובץ עם -c
שגיאת תעודה בדפדפןהרשומה ב־Cloudflare במצב Proxied, או שהתעודה לא כוללת את ה־wildcardענן אפור, ו־sudo certbot certificates
הדף של ברירת המחדל של nginxהפורט מחוץ לטווח 8000-8099, או שהקובץ לא ב־sites-enabledהכתובת, ו־ls /etc/nginx/sites-enabled
״Blocked request״Vite לא מכיר את השםallowedHosts (שלב 8)
האתר עולה, אבל לא מתעדכן בשמירהWebSocket לא עוברשורות ה־Upgrade, ה־Connection וה־proxy_read_timeout
Connection refused ב־curl על השרתשרת הפיתוח לא מאזין על הפורט המקומישהאתר רץ, ועל איזה פורט

מתי לעבור לקישורים חתומים

הגרסה כאן מתאימה כשאתם משתפים עם מעט אנשים שאתם סומכים עליהם. יש לה שתי חולשות שכדאי להכיר:

  • סיסמה אחת לכל המנהרות. מי שקיבל אותה פעם יכול להיכנס לכל מנהרה פעילה, גם אחרי שבוע. משתמש נפרד לכל לקוח (שלב 6) מקטין את הבעיה, אבל לא פותר אותה.
  • פורט שחוזר לשימוש. קישור ישן ל־p8042 יוביל מחר למה שירוץ אז על 8042.

הפוסט המקורי של ברנה פותר את שתיהן: sshd בוחר פורט פנוי לבד (-R 0:...), ו־nginx בודק עם המודול secure_link חתימה שמשלבת את הפורט, זמן תפוגה וסוד ששמור בשרת. כל קישור עובד רק למנהרה אחת ורק עד מועד מסוים. אם אתם שולחים קישורים ללקוחות, זה שווה את המורכבות הנוספת, וכדאי לקרוא את הפירוט אצלו.

מקור

המדריך מבוסס על הרעיון מ־Self-hosted HTTP tunnels with SSH and nginx מאת וינסנט ברנה. ההגדרות, הסקריפט, ההסברים וההגנה בסיסמה הם גרסה פשוטה שכתבתי, והיא שונה מזו שבמקור.

פרסומת

עוד משהו לקרוא

לכל הכתבות בנושא

מה מעניין אותך?