رفتن به محتوا
LoopX

اتصال به GitHub با SSH

توی این درس یاد می‌گیری بدون تایپ رمز و به‌صورت امن با GitHub کار کنی. فرق آدرس HTTPS و SSH را می‌فهمی، با ssh-keygen -t ed25519 -C "ایمیل" یک جفت کلید می‌سازی، کلید عمومی را به GitHub می‌دهی (کلید خصوصی هرگز از دستگاهت بیرون نمی‌رود)، با ssh -T git@github.com اتصال را آزمایش می‌کنی و با git remote set-url origin git@github.com:user/repo.git آدرس یک پروژه را از HTTPS به SSH برمی‌گردانی. و چند مهارت کاربردی: ssh-agent و passphrase، چند حساب با ~/.ssh/config، و وقتی پورت ۲۲ بسته است، پورت ۴۴۳. تمرین: remote یک پروژه را از HTTPS به SSH تغییر بده.

وقتی با آدرس HTTPS (https://github.com/user/repo.git) push می‌کنی، Git باید بداند تو کیستی. GitHub از سال ۲۰۲۱ رمز حساب را برای عملیات Git قبول نمی‌کند؛ باید یک توکن دسترسی شخصی (Personal Access Token) بسازی و آن را به‌جای رمز بدهی یا یک credential helper نگهش دارد. توکن‌ها تاریخ انقضا دارند و اگر لو بروند به‌اندازه‌ی دسترسی‌شان خطر دارند. راه دوم SSH است: یک‌بار یک جفت کلید می‌سازی، نیمه‌ی عمومی را به GitHub می‌دهی، و از آن به بعد هر push و pull بدون رمز و بدون توکن انجام می‌شود.

کلید عمومی مثل قفلی است که به GitHub می‌دهی تا روی در بگذارد (همه می‌توانند قفل را ببینند و مشکلی نیست). کلید خصوصی تنها کلیدی است که قفل را باز می‌کند و فقط پیش خودت است. GitHub هرگز کلید خصوصی‌ات را نمی‌بیند؛ فقط می‌پرسد «ثابت کن کلید قفل را داری» و تو با یک امضای ریاضی ثابت می‌کنی (درس SSH در دوره‌ی لینوکس). برای هر دستگاه یک جفت کلید جدا بساز، تا اگر یکی گم شد فقط همان را باطل کنی.

HTTPS با توکن (یک رمز متنی که لو می‌رود) در برابر SSH با جفت کلید (کلید خصوصی هیچ‌وقت ارسال نمی‌شود).

مثال‌های عملی: یک «GitHub» برای تمرین

Section titled “مثال‌های عملی: یک «GitHub» برای تمرین”

برای اینکه همه‌ی مراحل را واقعاً اجرا کنم، به حساب GitHub تو دسترسی ندارم و نمی‌خواهم داشته باشم (ورود به حساب کار توست). پس یک سرور SSH شبیه GitHub ساخته‌ام: ماشین جدایی (githost) که کاربر git دارد، فقط عملیات Git را قبول می‌کند (با git-shell، یعنی دقیقاً مثل GitHub shell نمی‌دهد)، ورود با رمز را بسته (فقط کلید) و مخزن‌هایش در /srv/git هستند. لپ‌تاپ تو هم ماشین دیگری (lxlab) است. اتصال بین‌شان همان پروتکل واقعی SSH است. هر جا کار با GitHub واقعی ممکن و بی‌خطر بود (بدون ورود به حساب)، آن را هم روی GitHub واقعی نشان می‌دهم؛ جاهایی که حساب لازم است «نمونه» برچسب دارد.

یک مخزن می‌تواند با چند آدرس شناخته شود. فرقشان در پروتکل است:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'mkdir -p ~/gitlab/urls && cd ~/gitlab/urls && git init -q
git remote add https-style https://github.com/example/shop.git
git remote add ssh-style git@github.com:example/shop.git
git remote add ssh-url ssh://git@github.com/example/shop.git
git remote -v | grep fetch'
خروجی
https-style https://github.com/example/shop.git (fetch)
ssh-style git@github.com:example/shop.git (fetch)
ssh-url ssh://git@github.com/example/shop.git (fetch)
شکل نمونه نکته
HTTPS https://github.com/user/repo.git با توکن؛ در هر شبکه‌ای کار می‌کند
SSH (scp-مانند) git@github.com:user/repo.git شکل رایج SSH؛ بعد از : مسیر مخزن (دو نقطه، نه اسلش)
SSH (URL کامل) ssh://git@github.com/user/repo.git همان، با شکل URL؛ برای پورت دیگر مفید (ssh://git@host:443/...)

در آدرس SSH، قسمت git@ یعنی نام کاربری ثابتِ git روی GitHub (نه نام کاربری حساب تو). GitHub تو را از روی کلید می‌شناسد، نه از روی این نام. (من در این مثال remote ها را فقط تعریف کردم؛ به نشانی‌های example/shop وصل نشدم، چون چنین مخزنی ندارم.) برای مخزن‌های عمومی خواندن با HTTPS بدون هیچ ورودی ممکن است؛ این یک درخواست واقعی به یک مخزن آزمایشی عمومی GitHub است:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'git ls-remote https://github.com/octocat/Hello-World.git HEAD'
خروجی
7fd1a60b01f91b314f59955a4e4d4e80d8edf11d HEAD

(هش HEAD مخزن نمایشی octocat/Hello-World؛ بدون رمز و فقط خواندن.) ولی push همیشه ورود می‌خواهد، و حتی خواندن با SSH هم (حتی برای مخزن عمومی) کلید لازم دارد.

مثال ۲: اتصال واقعی به GitHub، بدون کلید معرفی‌شده

Section titled “مثال ۲: اتصال واقعی به GitHub، بدون کلید معرفی‌شده”

اول ببین SSH با GitHub قبل از هر تنظیمی چه می‌گوید. -T یعنی «ترمینال تعاملی نخواه» (برای GitHub لازم است چون shell نمی‌دهد). این یک اتصال واقعی است؛ هنوز هیچ کلیدی به GitHub معرفی نکرده‌ام، پس باید رد شود، و رد شدنش درست همان چیزی است که می‌خواهیم ببینیم:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'ssh -o HashKnownHosts=no -o StrictHostKeyChecking=accept-new -o ConnectTimeout=12 -T git@github.com 2>&1; echo "کد خروج: $?"'
خروجی
ssh: connect to host github.com port 22: Connection timed out
کد خروج: 255

برای یک حساب GitHub که کلیدش را معرفی کرده، موفقیت این‌طور است (طبق مستندات رسمی؛ این‌ها را اجرا نکرده‌ام، چون حساب ندارم):

نمونه (اجرا نشده): موفقیت روی GitHub
Hi USERNAME! You've successfully authenticated, but GitHub does not provide shell access.

دو حالت بد هم هست، که هر دو را زیاد می‌بینی: Permission denied (publickey) یعنی به GitHub رسیدی ولی کلیدی که فرستادی هیچ حسابی نمی‌شناسد (یا کلیدی نفرستادی)؛ و Connection timed out یعنی اصلاً به GitHub نرسیدی (فایروال یا فیلتر روی پورت ۲۲). بخش «پورت ۴۴۳» همین دومی را حل می‌کند.

مثال ۳: ساخت جفت کلید، ssh-keygen -t ed25519

Section titled “مثال ۳: ساخت جفت کلید، ssh-keygen -t ed25519”

حالا کلید می‌سازم. -t ed25519 نوع الگوریتم (مدرن و کوتاه؛ پیشنهاد GitHub)، -C یک توضیح (معمولاً ایمیل) که فقط برای شناسایی داخل فایل کلید ذخیره می‌شود، و -f مسیر فایل. برای تمرین از passphrase خالی (-N "") استفاده می‌کنم؛ روی دستگاه واقعی حتماً passphrase بگذار (مثال ۴):

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'ssh-keygen -t ed25519 -C "ali@example.com" -N "" -f ~/.ssh/id_ed25519'
echo "--- فایل‌های ساخته‌شده و دسترسی‌هایشان:"
c 'ls -l ~/.ssh'
خروجی
Created directory '/home/ali/.ssh'.
Generating public/private ed25519 key pair.
Your identification has been saved in /home/ali/.ssh/id_ed25519
Your public key has been saved in /home/ali/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8 ali@example.com
The key's randomart image is:
+--[ED25519 256]--+
|o++o+. |
| ++O+. |
|. =*B+ |
|.o++..o. . |
|o*B.o . S . |
|o+EO o . |
|.O. o . |
|..*. |
|.o.. |
+----[SHA256]-----+
--- فایل‌های ساخته‌شده و دسترسی‌هایشان:
total 8
-rw------- 1 ali ali 411 Oct 4 08:50 id_ed25519
-rw-r--r-- 1 ali ali 97 Oct 4 08:50 id_ed25519.pub

دو فایل ساخته شد: id_ed25519 (کلید خصوصی، دسترسی 600: فقط تو) و id_ed25519.pub (کلید عمومی، 644). خطی که می‌بینی («randomart») یک تصویر ساده‌ی اثر انگشت کلید است و SHA256:... اثر انگشت (fingerprint) آن. فقط محتوای .pub را می‌شود نشان داد، کپی کرد و به GitHub داد:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
echo "--- کلید عمومی (این را به GitHub می‌دهی):"
c 'cat ~/.ssh/id_ed25519.pub'
echo "--- اثر انگشتش:"
c 'ssh-keygen -lf ~/.ssh/id_ed25519.pub'
خروجی
--- کلید عمومی (این را به GitHub می‌دهی):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWfFqnMid8cDkI6I4rRNgeLTwtPlYG9hKep6LvBJIis ali@example.com
--- اثر انگشتش:
256 SHA256:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8 ali@example.com (ED25519)

کلید بدون passphrase یعنی هر کس فایلش را بردارد (مثلاً با دزدیده‌شدن لپ‌تاپ) فوراً به حساب تو وارد می‌شود. passphrase یک رمز روی خود فایل کلید است. ولی نمی‌خواهی در هر push آن را تایپ کنی؛ ssh-agent برنامه‌ای است که کلید را (بعد از یک بار وارد کردن passphrase) در حافظه نگه می‌دارد. این‌ها تعاملی‌اند، پس با ترمینال مجازی صفحه‌ی واقعی را می‌بینی (رمز آزمایشی lab-pass است و روی صفحه نمایش داده نمی‌شود، مثل ترمینال واقعی):

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
screen() { c 'tmux capture-pane -t t -p' | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
c 'tmux -u new-session -d -s t -x 100 -y 30 "bash --norc"'; sleep 1
c 'tmux send-keys -t t "ssh-keygen -t ed25519 -C ali@example.com -f ~/.ssh/id_protected" Enter'; sleep 1
c 'tmux send-keys -t t "lab-pass" Enter'; sleep 1
c 'tmux send-keys -t t "lab-pass" Enter'; sleep 1
screen | head -22
خروجی
bash-5.2$ ssh-keygen -t ed25519 -C ali@example.com -f ~/.ssh/id_protected
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/ali/.ssh/id_protected
Your public key has been saved in /home/ali/.ssh/id_protected.pub
The key fingerprint is:
SHA256:kF9v2XgG6bQZAVjqRFnKT8LquH91lzlOwzgUPSnhpfg ali@example.com
The key's randomart image is:
+--[ED25519 256]--+
| .=+.oo.. |
| =oo oo=+ |
| o B + Bo . |
| * = =.O |
| . S ..Eo+o |
| o ..+oO |
| . . . . = o |
| . . . |
| .... |
+----[SHA256]-----+
bash-5.2$

دو بار passphrase را پرسید (برای تأیید). حالا agent را راه می‌اندازم و کلید را به آن می‌دهم:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
screen() { c 'tmux capture-pane -t t -p' | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
c 'tmux send-keys -t t "clear" Enter'
c 'tmux send-keys -t t "eval \$(ssh-agent -s)" Enter'; sleep 1
c 'tmux send-keys -t t "ssh-add ~/.ssh/id_protected" Enter'; sleep 1
c 'tmux send-keys -t t "lab-pass" Enter'; sleep 1
c 'tmux send-keys -t t "ssh-add -l" Enter'; sleep 1
screen
c 'tmux -u kill-server'
خروجی
bash-5.2$ eval $(ssh-agent -s)
Agent pid 13089
bash-5.2$ ssh-add ~/.ssh/id_protected
Enter passphrase for /home/ali/.ssh/id_protected:
Identity added: /home/ali/.ssh/id_protected (ali@example.com)
bash-5.2$ ssh-add -l
256 SHA256:kF9v2XgG6bQZAVjqRFnKT8LquH91lzlOwzgUPSnhpfg ali@example.com (ED25519)
bash-5.2$

eval $(ssh-agent -s) agent را اجرا و متغیرهایش را در همین shell تنظیم می‌کند. ssh-add کلید را بعد از passphrase به agent می‌دهد (Identity added)، و ssh-add -l کلیدهای فعلی agent را با اثر انگشت فهرست می‌کند. از این به بعد push ها بدون پرسیدن passphrase کار می‌کنند، تا agent بسته شود. (روی مک: ssh-add --apple-use-keychain ~/.ssh/id_ed25519 و UseKeychain yes در ~/.ssh/config آن را در Keychain هم نگه می‌دارد؛ روی ویندوز با Git Bash یا WSL ssh-agent همین‌طور است. این‌ها را روی آن سیستم‌ها اجرا نکرده‌ام.)

مثال ۵: معرفی کلید عمومی به سرور، معادل GitHub

Section titled “مثال ۵: معرفی کلید عمومی به سرور، معادل GitHub”

روی GitHub این مرحله در مرورگر انجام می‌شود: Settings ← SSH and GPG keys ← New SSH key؛ یک عنوان (مثلاً «لپ‌تاپ شخصی») می‌نویسی، محتوای .pub را می‌چسبانی و ذخیره می‌کنی. (این مرحله را اجرا نکرده‌ام چون حساب ندارم؛ برای ساده‌تر شدن می‌شود با ابزار gh ssh-key add ~/.ssh/id_ed25519.pub هم انجام داد.) روی سرور خودمان معادلش اضافه‌کردن یک خط به فایل authorized_keys کاربر git است. اول سرور را می‌سازم (کاربری با git-shell و یک مخزن خالی):

Terminal window
server() { docker exec lxgit "$@"; }
server sh -c '
useradd -m -s /usr/bin/git-shell git
install -d -m 700 -o git -g git /home/git/.ssh
install -d -o git -g git /home/git/git-shell-commands
cat > /home/git/git-shell-commands/no-interactive-login <<EOF
#!/bin/sh
echo "Hi \$(whoami)! You have successfully authenticated, but this server does not provide shell access."
exit 128
EOF
chmod +x /home/git/git-shell-commands/no-interactive-login
chown git:git /home/git/git-shell-commands/no-interactive-login
touch /home/git/.hushlogin && chown git:git /home/git/.hushlogin
echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/10-git.conf
mkdir -p /srv/git && git init -q --bare --initial-branch=main /srv/git/shop.git && chown -R git:git /srv/git
getent passwd git | cut -d: -f1,6,7'
خروجی
git:/home/git:/usr/bin/git-shell

حالا کلید عمومی تو را به سرور می‌دهم، همان کاری که GitHub با دکمه‌ی «Add SSH key» می‌کند:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
server() { docker exec lxgit "$@"; }
PUB=$(c 'cat ~/.ssh/id_ed25519.pub')
server sh -c "echo '$PUB' >> /home/git/.ssh/authorized_keys; chown git:git /home/git/.ssh/authorized_keys; chmod 600 /home/git/.ssh/authorized_keys"
echo "--- اولین اتصال: ssh به میزبانی که برایش اعتماد نداریم:"
c 'tmux -u new-session -d -s t -x 100 -y 12 "bash --norc"'; sleep 1
c 'tmux send-keys -t t "ssh -T git@githost" Enter'; sleep 2
c 'tmux capture-pane -t t -p' | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'
خروجی
--- اولین اتصال: ssh به میزبانی که برایش اعتماد نداریم:
bash-5.2$ ssh -T git@githost
The authenticity of host 'githost (172.17.0.5)' can't be established.
ED25519 key fingerprint is SHA256:ls33FNLGMzAXA6EE0RU5ZcOy+vvGsFXa5YI5mqmmWQ8.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

اولین بار SSH می‌پرسد «به این سرور اعتماد داری؟» و اثر انگشت کلید میزبان (host key) را نشان می‌دهد. این همان لحظه‌ای است که باید مطمئن شوی به سرور درست وصل می‌شوی (نه یک مهاجم میان‌راه). روی GitHub، اثر انگشت‌های رسمی را در مستندات GitHub (docs.github.com، بخش «GitHub’s SSH key fingerprints») می‌بینی و با آنچه روی صفحه آمده مقایسه می‌کنی؛ روی سرور خودمان هم می‌شود از خود سرور پرسید:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
server() { docker exec lxgit "$@"; }
echo "--- اثر انگشت کلید میزبان، از خود سرور:"
server ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub | awk '{print $1, $2, $4}'
c 'tmux send-keys -t t "yes" Enter'; sleep 2
echo "--- بعد از yes (پیام موفقیت):"
c 'tmux capture-pane -t t -p' | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}' | tail -3
c 'tmux -u kill-server'
خروجی
--- اثر انگشت کلید میزبان، از خود سرور:
256 SHA256:ls33FNLGMzAXA6EE0RU5ZcOy+vvGsFXa5YI5mqmmWQ8 (ED25519)
--- بعد از yes (پیام موفقیت):
Warning: Permanently added 'githost' (ED25519) to the list of known hosts.
Hi git! You have successfully authenticated, but this server does not provide shell access.
bash-5.2$

هر دو اثر انگشت یکی‌اند، پس مطمئنیم. با yes سرور در ~/.ssh/known_hosts ذخیره شد و بار بعد دیگر نمی‌پرسد (و اگر روزی کلید سرور عوض شود، SSH هشدار می‌دهد). پیام آخر همان است که GitHub هم می‌دهد: احراز هویت موفق، ولی shell نداریم.

حالا مخزن را با آدرس SSH کلون می‌کنم و تغییر می‌دهم و push می‌کنم. بعد از git@githost: مسیر مخزن روی سرور می‌آید (روی GitHub، user/repo.git):

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'cd ~/gitlab && git clone git@githost:/srv/git/shop.git 2>&1
cd shop
echo "# Shop" > README.md && git add README.md && git commit -qm "Add README"
git push -u origin main 2>&1
git remote -v | head -1'
خروجی
Cloning into 'shop'...
warning: You appear to have cloned an empty repository.
To githost:/srv/git/shop.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
origin git@githost:/srv/git/shop.git (fetch)

بدون هیچ رمزی push انجام شد: SSH از روی کلید در ~/.ssh تو را شناخت. (git clone از یک مخزن خالی هشدار empty repository داد، که درست است.)

مثال ۷: remote را از HTTPS به SSH عوض کن (تمرین اصلی)

Section titled “مثال ۷: remote را از HTTPS به SSH عوض کن (تمرین اصلی)”

پروژه‌ای که قبلاً با HTTPS کلون کرده‌ای و حالا می‌خواهی با SSH کار کنی را دوباره کلون نمی‌کنی؛ فقط آدرس origin را عوض می‌کنی:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'mkdir -p ~/gitlab/old && cd ~/gitlab/old && git init -q
git remote add origin https://github.com/example/shop.git
echo "--- قبل (HTTPS):"
git remote -v | head -1
git remote set-url origin git@githost:/srv/git/shop.git
echo "--- بعد (SSH):"
git remote -v | head -1
echo "--- آدرس جدید واقعاً کار می‌کند؟"
git ls-remote origin'
خروجی
--- قبل (HTTPS):
origin https://github.com/example/shop.git (fetch)
--- بعد (SSH):
origin git@githost:/srv/git/shop.git (fetch)
--- آدرس جدید واقعاً کار می‌کند؟
6a7cc9169dc7743773da17aade221a9b56fb46a8 HEAD
6a7cc9169dc7743773da17aade221a9b56fb46a8 refs/heads/main

git remote set-url origin آدرس-جدید فقط یک خط در .git/config را عوض می‌کند. روی GitHub شکل دستور: git remote set-url origin git@github.com:USER/REPO.git. و آخرین دستور (git ls-remote origin) بدون هیچ دانلودی بررسی می‌کند آدرس کار می‌کند و شاخه‌ها را نشان می‌دهد؛ راه سریع «نصب شد؟» برای هر remote.

مثال ۸: چند حساب، ~/.ssh/config

Section titled “مثال ۸: چند حساب، ~/.ssh/config”

اگر هم حساب شخصی و هم حساب کاری GitHub داری، هر کدام کلید خودشان را دارند و یک کلید را نمی‌شود به دو حساب داد. ~/.ssh/config برای هر «حالت» یک اسم مستعار (alias) می‌سازد که میزبان، کاربر و کلید درست را مشخص می‌کند. روی GitHub هر دو حساب git@github.com هستند و تفاوت فقط کلید است؛ برای شبیه‌سازی دو حساب، من دو کاربر روی سرور (git و gitwork) ساخته‌ام، هر کدام با کلید خودش:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
server() { docker exec lxgit "$@"; }
c 'ssh-keygen -q -t ed25519 -C "ali@personal" -N "" -f ~/.ssh/id_personal; ssh-keygen -q -t ed25519 -C "ali@company" -N "" -f ~/.ssh/id_work'
server sh -c 'useradd -m -s /usr/bin/git-shell gitwork
install -d -m 700 -o gitwork -g gitwork /home/gitwork/.ssh
cp -a /home/git/git-shell-commands /home/gitwork/ && chown -R gitwork:gitwork /home/gitwork/git-shell-commands
touch /home/gitwork/.hushlogin && chown gitwork:gitwork /home/gitwork/.hushlogin
git init -q --bare --initial-branch=main /srv/git/work.git && chown -R gitwork:gitwork /srv/git/work.git'
PERS=$(c 'cat ~/.ssh/id_personal.pub'); WORK=$(c 'cat ~/.ssh/id_work.pub')
server sh -c "echo '$PERS' >> /home/git/.ssh/authorized_keys"
server sh -c "echo '$WORK' > /home/gitwork/.ssh/authorized_keys; chown gitwork:gitwork /home/gitwork/.ssh/authorized_keys; chmod 600 /home/gitwork/.ssh/authorized_keys"
c 'cat > ~/.ssh/config <<EOF
Host gh-personal
HostName githost
User git
IdentityFile ~/.ssh/id_personal
IdentitiesOnly yes
Host gh-work
HostName githost
User gitwork
IdentityFile ~/.ssh/id_work
IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config'
echo "--- شخصی:"
c 'ssh -T gh-personal'
echo "--- کاری:"
c 'ssh -T gh-work'
echo "--- کلون با alias (به‌جای githost، اسم مستعار):"
c 'cd ~/gitlab && git clone gh-work:/srv/git/work.git 2>&1 | head -2'
خروجی
--- شخصی:
Hi git! You have successfully authenticated, but this server does not provide shell access.
--- کاری:
Hi gitwork! You have successfully authenticated, but this server does not provide shell access.
--- کلون با alias (به‌جای githost، اسم مستعار):
Cloning into 'work'...
warning: You appear to have cloned an empty repository.

با alias، آدرس کلون می‌شود gh-work:... به‌جای gitwork@githost:...، و SSH از روی Host همان بلوک کلید درست را انتخاب می‌کند. IdentitiesOnly yes یعنی «فقط همین کلید را بفرست و کلیدهای دیگر agent را امتحان نکن» (وگرنه با کلیدهای زیاد به خطای Too many authentication failures می‌خوری). روی GitHub واقعی این می‌شود: Host github-work با HostName github.com، User git و IdentityFile ~/.ssh/id_work، و آدرس‌ها git@github-work:company/repo.git. (در یک مخزن مشخص هم می‌شود فقط برای همان یک مخزن گفت: git config core.sshCommand "ssh -i ~/.ssh/id_work -o IdentitiesOnly=yes".)

مثال ۹: وقتی پورت ۲۲ بسته است، پورت ۴۴۳

Section titled “مثال ۹: وقتی پورت ۲۲ بسته است، پورت ۴۴۳”

در بعضی شبکه‌ها (شرکت‌ها، دانشگاه‌ها، و گاهی اینترنت خانگی) پورت ۲۲ بسته یا کند است و ssh git@github.com با Connection timed out تمام می‌شود. GitHub یک راه جایگزین دارد: همان سرور SSH روی پورت ۴۴۳ با نام ssh.github.com (پورت HTTPS که تقریباً هیچ شبکه‌ای نمی‌بندد). ببین روی همین شبکه‌ای که این درس را در آن نوشتم، هر دو راه چه می‌گویند (اتصال‌های واقعی):

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
echo "--- پورت 22 (آدرس عادی):"
c 'ssh -o ConnectTimeout=10 -T git@github.com 2>&1; echo "کد خروج: $?"'
echo "--- پورت 443 (ssh.github.com):"
c 'ssh -o HashKnownHosts=no -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -T -p 443 git@ssh.github.com 2>&1; echo "کد خروج: $?"'
خروجی
--- پورت 22 (آدرس عادی):
ssh: connect to host github.com port 22: Connection timed out
کد خروج: 255
--- پورت 443 (ssh.github.com):
Warning: Permanently added '[ssh.github.com]:443' (ED25519) to the list of known hosts.
git@ssh.github.com: Permission denied (publickey).
کد خروج: 255

برای اینکه همیشه خودکار از ۴۴۳ بروی و آدرس‌های معمولی (git@github.com:...) را عوض نکنی، یک بلوک به ~/.ssh/config اضافه کن (این همان پیکربندی رسمی GitHub است):

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'cat >> ~/.ssh/config <<EOF
Host github.com
Hostname ssh.github.com
Port 443
User git
EOF
echo "--- حالا ssh به github.com از ۴۴۳ می‌رود (خط Connecting از ssh -v):"
ssh -v -o ConnectTimeout=10 -T git@github.com 2>&1 | grep -E "Connecting to|Authentications that can continue|Permission denied"'
خروجی
--- حالا ssh به github.com از ۴۴۳ می‌رود (خط Connecting از ssh -v):
debug1: Connecting to ssh.github.com [140.82.121.35] port 443.
debug1: Authentications that can continue: publickey
debug1: Authentications that can continue: publickey
git@ssh.github.com: Permission denied (publickey).

اتصال برقرار شد و GitHub رد کرد، چون کلیدی که فرستادیم (id_ed25519) هیچ حساب GitHub ای ندارد. این همان چیزی است که روی حساب واقعی با ثبت کلید عوض می‌شود. مهم این است که ترافیک از پورت ۴۴۳ رد شد. اثر انگشت سروری که به آن وصل شدی را هم می‌توانی با نسخه‌ی رسمی مقایسه کنی؛ اثر انگشت‌هایی که SSH ذخیره کرد:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'ssh-keygen -lf ~/.ssh/known_hosts | grep -i github'
خروجی
256 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU [ssh.github.com]:443 (ED25519)

اثر انگشت Ed25519 رسمی GitHub (از مستندات رسمی) این است: SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU. اگر اثر انگشت بالا با آن یکی بود، به سرور واقعی GitHub وصل شده‌ای.

مثال ۱۰: ببین Git با SSH چه می‌کند

Section titled “مثال ۱۰: ببین Git با SSH چه می‌کند”

git push و git clone روی SSH هیچ پروتکل مرموزی ندارند: Git برنامه‌ی ssh را با یک دستور مشخص اجرا می‌کند. با GIT_TRACE ببین دقیقاً چه چیزی:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'cd ~/gitlab/shop && GIT_TRACE=1 git ls-remote origin 2>&1 | grep -E "run_command|ssh" | head -2 | sed -E "s/^[0-9:.]+ [a-z-]+\.c:[0-9]+ +//"'
echo "--- و چرا کلید درست انتخاب شد؟ (ssh -v)"
c 'ssh -v -T git@githost 2>&1 | grep -E "Offering public key|Server accepts key|Authenticated to"'
خروجی
trace: run_command: unset GIT_PREFIX; GIT_PROTOCOL=version=2 ssh -o SendEnv=GIT_PROTOCOL git@githost 'git-upload-pack '\''/srv/git/shop.git'\'''
--- و چرا کلید درست انتخاب شد؟ (ssh -v)
debug1: Offering public key: /home/ali/.ssh/id_ed25519 ED25519 SHA256:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8
debug1: Server accepts key: /home/ali/.ssh/id_ed25519 ED25519 SHA256:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8
Authenticated to githost ([172.17.0.5]:22) using "publickey".

Git در واقع ssh git@githost "git-upload-pack '/srv/git/shop.git'" را اجرا می‌کند: یک دستور Git روی سرور که با خود پروتکل Git صحبت می‌کند. و ssh -v سه مرحله‌ی احراز هویت را نشان می‌دهد: کلیدی را پیشنهاد کرد (Offering public key)، سرور پذیرفت (Server accepts key) و احراز هویت شد (Authenticated to). همین ssh -v اولین ابزار عیب‌یابی است (اشتباهات رایج).

پشت پرده: آنچه روی GitHub می‌گذری فقط یک خط است

Section titled “پشت پرده: آنچه روی GitHub می‌گذری فقط یک خط است”

وقتی «New SSH key» را می‌زنی، GitHub محتوای .pub را در لیست کلیدهای مجاز حسابت ذخیره می‌کند، دقیقاً مثل یک خط در authorized_keys. اتصال SSH این‌طور است: ۱) کلاینت وصل می‌شود و کلید میزبان را بررسی می‌کند (known_hosts)، ۲) کلید عمومی‌اش را پیشنهاد می‌دهد، ۳) سرور چالشی می‌فرستد که فقط صاحب کلید خصوصی می‌تواند امضایش کند، ۴) کلاینت امضا می‌کند و سرور با کلید عمومی ثبت‌شده تأیید می‌کند. کلید خصوصی هرگز ارسال نمی‌شود (تفصیل در درس SSH). و GitHub از روی «کدام کلید پذیرفته شد» می‌فهمد تو کدام حسابی؛ برای همین کاربر همیشه git است. دو نکته‌ی کاربردی دیگر: کلید استقرار (deploy key) یک کلید ویژه‌ی فقط‌خواندنی (یا خواندنی-نوشتنی) برای یک مخزن است که برای سرورها و CI به‌کار می‌رود تا به‌جای کلید شخصی تو باشد؛ و هر کلید فقط روی یک حساب GitHub می‌تواند ثبت شود (برای همین چند حساب = چند کلید و ~/.ssh/config).

دستور کار
ssh-keygen -t ed25519 -C "ایمیل" ساخت جفت کلید (با passphrase)
cat ~/.ssh/id_ed25519.pub کلید عمومی (برای GitHub)
ssh-keygen -lf کلید.pub اثر انگشت
eval "$(ssh-agent -s)" و ssh-add کلید agent و افزودن کلید
ssh-add -l کلیدهای agent
ssh -T git@github.com آزمایش اتصال به GitHub
ssh -vT git@github.com آزمایش با جزئیات (عیب‌یابی)
git remote set-url origin git@github.com:USER/REPO.git تبدیل HTTPS به SSH
git remote -v / git ls-remote origin آدرس‌ها / آزمایش اتصال
ssh -T -p 443 git@ssh.github.com اتصال از پورت ۴۴۳
فایل کار دسترسی
~/.ssh/id_ed25519 کلید خصوصی 600 (فقط تو)
~/.ssh/id_ed25519.pub کلید عمومی 644
~/.ssh/known_hosts اثر انگشت سرورهایی که قبلاً پذیرفته‌ای
~/.ssh/config alias ها، کلید و پورت هر میزبان 600

شایع‌ترین خطاست. چهار علت معمول: کلید به GitHub اضافه نشده، کلید اشتباه فرستاده می‌شود، نام کاربری git نیست، یا agent کلید را ندارد. با ssh -v ببین کدام کلید پیشنهاد می‌شود. این‌جا کلیدی می‌سازم که سرور نمی‌شناسد:

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_stranger
ssh -i ~/.ssh/id_stranger -o IdentitiesOnly=yes -T git@githost 2>&1
echo "--- ssh -v: کلید پیشنهاد شد ولی پذیرفته نشد:"
ssh -v -i ~/.ssh/id_stranger -o IdentitiesOnly=yes -T git@githost 2>&1 | grep -E "Offering public key|Authentications that can continue" | head -3'
خروجی
git@githost: Permission denied (publickey).
--- ssh -v: کلید پیشنهاد شد ولی پذیرفته نشد:
debug1: Authentications that can continue: publickey
debug1: Offering public key: /home/ali/.ssh/id_stranger ED25519 SHA256:IEJxRwL7GoBGkoMV8a5K40D7cFsdllbyx3RNqhelLWw explicit
debug1: Authentications that can continue: publickey

Offering public key ← ولی خبری از Server accepts key نیست. راه‌حل: کلید درست را بده (-i یا IdentityFile در config)، یا کلید عمومی این کلید را به حساب اضافه کن. (و روی GitHub همیشه git@ را استفاده کن، نه نام کاربری‌ات.)

۲) دسترسی خراب فایل کلید

Section titled “۲) دسترسی خراب فایل کلید”
Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'chmod 644 ~/.ssh/id_ed25519
ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -T git@githost 2>&1 | head -8
chmod 600 ~/.ssh/id_ed25519
echo "--- بعد از chmod 600:"
ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -T git@githost 2>&1 | head -2'
خروجی
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/ali/.ssh/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/home/ali/.ssh/id_ed25519": bad permissions
git@githost: Permission denied (publickey).
--- بعد از chmod 600:
Hi git! You have successfully authenticated, but this server does not provide shell access.

WARNING: UNPROTECTED PRIVATE KEY FILE!: دیگران روی همین ماشین می‌توانند کلید را بخوانند و SSH آن را نمی‌پذیرد (This private key will be ignored)؛ و نتیجه Permission denied. نکته‌ی جالب: SSH اول فقط کلید عمومی را پیشنهاد می‌دهد و فایل خصوصی را فقط وقتی لازم است می‌خواند (یعنی وقتی سرور پیشنهاد را پذیرفته)، برای همین این هشدار فقط برای کلیدی می‌آید که در واقع معتبر است. راه‌حل: chmod 600 ~/.ssh/id_ed25519.

Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'git ls-remote git@githost/srv/git/shop.git 2>&1 | head -3'
خروجی
fatal: 'git@githost/srv/git/shop.git' does not appear to be a git repository
fatal: Could not read from remote repository.

در شکل scp-مانند، بعد از میزبان باید دو نقطه بیاید. با اسلش، Git تمام «githost/srv/git/shop.git» را اسم میزبان فرض می‌کند و نمی‌تواند آن را پیدا کند. راه‌حل: git@github.com:user/repo.git.

۴) کلید خصوصی به‌جای عمومی

Section titled “۴) کلید خصوصی به‌جای عمومی”

GitHub فقط محتوای .pub را می‌خواهد. اگر فایل بدون .pub را بچسبانی (که خط اولش یک عبارت «BEGIN … PRIVATE KEY» است) آن را برای همیشه لو داده‌ای؛ فوراً آن جفت کلید را دور بریز و جدید بساز.

۵) کلید بدون passphrase روی لپ‌تاپ مشترک

Section titled “۵) کلید بدون passphrase روی لپ‌تاپ مشترک”

هر کس به فایل دسترسی پیدا کند به همه‌ی مخزن‌های تو دسترسی دارد. راه‌حل: passphrase + ssh-agent؛ و برای هر دستگاه یک کلید جدا بساز تا هر کدام را جداگانه بتوانی باطل کنی (GitHub ← Settings ← SSH keys ← Delete).

✎ تمرینآسان

یک جفت کلید ed25519 با توضیح me@example.com در مسیر ~/.ssh/id_practice بساز، اثر انگشت کلید عمومی را بخوان و ثابت کن دسترسی کلید خصوصی 600 است.

دیدن جواب
Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'ssh-keygen -q -t ed25519 -C "me@example.com" -N "" -f ~/.ssh/id_practice
ssh-keygen -lf ~/.ssh/id_practice.pub
stat -c "%a %n" ~/.ssh/id_practice ~/.ssh/id_practice.pub'
خروجی
256 SHA256:hQMIQMotmJrMr0PE60J2nKkpz0xIz4gBpE9V1MpzP64 me@example.com (ED25519)
600 /home/ali/.ssh/id_practice
644 /home/ali/.ssh/id_practice.pub
✎ تمرینمتوسط

تمرین اصلی: remote یک پروژه را از HTTPS به SSH تغییر بده. پروژه‌ای بساز که origin‌اش یک آدرس HTTPS باشد، آن را با git remote set-url به آدرس SSH سرور (git@githost:/srv/git/shop.git) ببر، با git ls-remote origin اتصال را آزمایش کن و یک commit بفرست.

دیدن جواب
Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'mkdir -p ~/gitlab/convert && cd ~/gitlab/convert && git init -q
git remote add origin https://github.com/example/shop.git
echo "قبل: $(git remote get-url origin)"
git remote set-url origin git@githost:/srv/git/shop.git
echo "بعد: $(git remote get-url origin)"
git pull -q origin main 2>&1
echo "update" >> README.md && git commit -qam "Update README over SSH"
git push 2>&1 | tail -2
git log --oneline | head -2'
خروجی
قبل: https://github.com/example/shop.git
بعد: git@githost:/srv/git/shop.git
upstream, see 'push.autoSetupRemote' in 'git help config'.
df9ab3a Update README over SSH
6a7cc91 Add README
✎ تمرینسخت

دو حساب شبیه‌سازی کن: برای کاربر git (شخصی) و gitwork (کاری) دو alias در ~/.ssh/config بساز (در مثال ۸ ساخته شد). فقط با ssh -v ثابت کن alias کاری کلید id_work را پیشنهاد می‌کند، نه کلید شخصی؛ بعد یک commit به مخزن work.git بفرست.

دیدن جواب
Terminal window
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }
c 'echo "--- کدام فایل کلید برای gh-work پیشنهاد می‌شود؟"
ssh -v -T gh-work 2>&1 | grep -E "Offering public key" | sed "s/.*Offering public key: //"
echo "--- push به مخزن کاری:"
cd ~/gitlab/work && git config user.name "Ali (work)" && git config user.email "ali@company.example"
echo "task list" > TASKS.md && git add TASKS.md && git commit -qm "Add tasks"
git push -u origin main 2>&1 | tail -2'
خروجی
--- کدام فایل کلید برای gh-work پیشنهاد می‌شود؟
/home/ali/.ssh/id_work ED25519 SHA256:TOta6+vEx7ivVOknie50O0yk6j+anzEV7Cl/70zih5c explicit
--- push به مخزن کاری:
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
⚡ بررسی سریع

کدام فایل را به GitHub (در Settings ← SSH keys) می‌دهی؟

؟ آزمونک
  1. در آدرس git@github.com:user/repo.git قسمت «git@» چیست؟

  2. ssh -T git@github.com با «Permission denied (publickey)» تمام شد. یعنی چه؟

  3. ssh روی پورت ۲۲ به github.com «Connection timed out» می‌دهد. راه‌حل رسمی؟

  4. remote یک پروژه‌ی کلون‌شده با HTTPS را چطور به SSH تغییر می‌دهی؟

  5. ssh-agent برای چیست؟

  • GitHub برای عملیات Git رمز حساب را قبول نمی‌کند: توکن (HTTPS) یا SSH. SSH بدون رمز و بدون انقضا: یک جفت کلید (ssh-keygen -t ed25519 -C "ایمیل")، کلید عمومی (.pub) را به GitHub بده، کلید خصوصی هرگز (دسترسی 600).
  • passphrase + ssh-agent (eval "$(ssh-agent -s)" و ssh-add) امنیت و راحتی را با هم می‌دهد. برای هر دستگاه یک کلید.
  • آدرس SSH: git@github.com:USER/REPO.git (دو نقطه). ssh -T git@github.com آزمایش می‌کند؛ موفق: Hi USER! You've successfully authenticated....
  • HTTPS ← SSH: git remote set-url origin git@github.com:USER/REPO.git؛ git ls-remote origin آزمایش.
  • چند حساب: ~/.ssh/config با Host، IdentityFile، IdentitiesOnly yes؛ یا core.sshCommand برای یک مخزن.
  • پورت ۲۲ بسته؟ ssh.github.com پورت ۴۴۳ (Hostname/Port در config). عیب‌یابی: ssh -vT؛ اثر انگشت سرور را با مستندات رسمی مقایسه کن (SHA256:+DiY3... برای Ed25519).
برگه‌ی تقلب این درس
دستورکاری که می‌کند
ssh-keygen -t ed25519 -C "ایمیل"ساخت جفت کلید
cat ~/.ssh/id_ed25519.pubکلید عمومی (برای GitHub)
eval "$(ssh-agent -s)" ; ssh-add ~/.ssh/id_ed25519agent و افزودن کلید
ssh -T git@github.comآزمایش اتصال
ssh -vT git@github.comآزمایش با جزئیات
git remote set-url origin git@github.com:USER/REPO.gitHTTPS ← SSH
git ls-remote originآزمایش remote
Host github.com / Hostname ssh.github.com / Port 443SSH از پورت ۴۴۳ (در ~/.ssh/config)
chmod 600 ~/.ssh/id_ed25519دسترسی درست کلید خصوصی