توی این درس یاد میگیری بدون تایپ رمز و بهصورت امن با 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 تغییر بده.
مسئله: هر بار رمز؟
Section titled “مسئله: هر بار رمز؟”وقتی با آدرس HTTPS (https://github.com/user/repo.git) push میکنی، Git باید بداند تو کیستی. GitHub از سال ۲۰۲۱ رمز حساب را برای عملیات Git قبول نمیکند؛ باید یک توکن دسترسی شخصی (Personal Access Token) بسازی و آن را بهجای رمز بدهی یا یک credential helper نگهش دارد. توکنها تاریخ انقضا دارند و اگر لو بروند بهاندازهی دسترسیشان خطر دارند. راه دوم SSH است: یکبار یک جفت کلید میسازی، نیمهی عمومی را به GitHub میدهی، و از آن به بعد هر push و pull بدون رمز و بدون توکن انجام میشود.
تشبیه: قفل و کلید
Section titled “تشبیه: قفل و کلید”کلید عمومی مثل قفلی است که به GitHub میدهی تا روی در بگذارد (همه میتوانند قفل را ببینند و مشکلی نیست). کلید خصوصی تنها کلیدی است که قفل را باز میکند و فقط پیش خودت است. GitHub هرگز کلید خصوصیات را نمیبیند؛ فقط میپرسد «ثابت کن کلید قفل را داری» و تو با یک امضای ریاضی ثابت میکنی (درس SSH در دورهی لینوکس). برای هر دستگاه یک جفت کلید جدا بساز، تا اگر یکی گم شد فقط همان را باطل کنی.
مثالهای عملی: یک «GitHub» برای تمرین
Section titled “مثالهای عملی: یک «GitHub» برای تمرین”برای اینکه همهی مراحل را واقعاً اجرا کنم، به حساب GitHub تو دسترسی ندارم و نمیخواهم داشته باشم (ورود به حساب کار توست). پس یک سرور SSH شبیه GitHub ساختهام: ماشین جدایی (githost) که کاربر git دارد، فقط عملیات Git را قبول میکند (با git-shell، یعنی دقیقاً مثل GitHub shell نمیدهد)، ورود با رمز را بسته (فقط کلید) و مخزنهایش در /srv/git هستند. لپتاپ تو هم ماشین دیگری (lxlab) است. اتصال بینشان همان پروتکل واقعی SSH است. هر جا کار با GitHub واقعی ممکن و بیخطر بود (بدون ورود به حساب)، آن را هم روی GitHub واقعی نشان میدهم؛ جاهایی که حساب لازم است «نمونه» برچسب دارد.
مثال ۱: دو شکل آدرس
Section titled “مثال ۱: دو شکل آدرس”یک مخزن میتواند با چند آدرس شناخته شود. فرقشان در پروتکل است:
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }c 'mkdir -p ~/gitlab/urls && cd ~/gitlab/urls && git init -qgit remote add https-style https://github.com/example/shop.gitgit remote add ssh-style git@github.com:example/shop.gitgit remote add ssh-url ssh://git@github.com/example/shop.gitgit 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 است:
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 معرفی نکردهام، پس باید رد شود، و رد شدنش درست همان چیزی است که میخواهیم ببینیم:
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 که کلیدش را معرفی کرده، موفقیت اینطور است (طبق مستندات رسمی؛ اینها را اجرا نکردهام، چون حساب ندارم):
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 بگذار (مثال ۴):
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_ed25519Your public key has been saved in /home/ali/.ssh/id_ed25519.pubThe key fingerprint is:SHA256:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8 ali@example.comThe 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 داد:
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 و ssh-agent
Section titled “مثال ۴: passphrase و ssh-agent”کلید بدون passphrase یعنی هر کس فایلش را بردارد (مثلاً با دزدیدهشدن لپتاپ) فوراً به حساب تو وارد میشود. passphrase یک رمز روی خود فایل کلید است. ولی نمیخواهی در هر push آن را تایپ کنی؛ ssh-agent برنامهای است که کلید را (بعد از یک بار وارد کردن passphrase) در حافظه نگه میدارد. اینها تعاملیاند، پس با ترمینال مجازی صفحهی واقعی را میبینی (رمز آزمایشی lab-pass است و روی صفحه نمایش داده نمیشود، مثل ترمینال واقعی):
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 1c 'tmux send-keys -t t "ssh-keygen -t ed25519 -C ali@example.com -f ~/.ssh/id_protected" Enter'; sleep 1c 'tmux send-keys -t t "lab-pass" Enter'; sleep 1c 'tmux send-keys -t t "lab-pass" Enter'; sleep 1screen | head -22bash-5.2$ ssh-keygen -t ed25519 -C ali@example.com -f ~/.ssh/id_protectedGenerating 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_protectedYour public key has been saved in /home/ali/.ssh/id_protected.pubThe key fingerprint is:SHA256:kF9v2XgG6bQZAVjqRFnKT8LquH91lzlOwzgUPSnhpfg ali@example.comThe 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 را راه میاندازم و کلید را به آن میدهم:
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 1c 'tmux send-keys -t t "ssh-add ~/.ssh/id_protected" Enter'; sleep 1c 'tmux send-keys -t t "lab-pass" Enter'; sleep 1c 'tmux send-keys -t t "ssh-add -l" Enter'; sleep 1screenc 'tmux -u kill-server'bash-5.2$ eval $(ssh-agent -s)Agent pid 13089bash-5.2$ ssh-add ~/.ssh/id_protectedEnter passphrase for /home/ali/.ssh/id_protected:Identity added: /home/ali/.ssh/id_protected (ali@example.com)bash-5.2$ ssh-add -l256 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 و یک مخزن خالی):
server() { docker exec lxgit "$@"; }server sh -c 'useradd -m -s /usr/bin/git-shell gitinstall -d -m 700 -o git -g git /home/git/.sshinstall -d -o git -g git /home/git/git-shell-commandscat > /home/git/git-shell-commands/no-interactive-login <<EOF#!/bin/shecho "Hi \$(whoami)! You have successfully authenticated, but this server does not provide shell access."exit 128EOFchmod +x /home/git/git-shell-commands/no-interactive-loginchown git:git /home/git/git-shell-commands/no-interactive-logintouch /home/git/.hushlogin && chown git:git /home/git/.hushloginecho "PasswordAuthentication no" > /etc/ssh/sshd_config.d/10-git.confmkdir -p /srv/git && git init -q --bare --initial-branch=main /srv/git/shop.git && chown -R git:git /srv/gitgetent passwd git | cut -d: -f1,6,7'git:/home/git:/usr/bin/git-shellحالا کلید عمومی تو را به سرور میدهم، همان کاری که GitHub با دکمهی «Add SSH key» میکند:
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 1c 'tmux send-keys -t t "ssh -T git@githost" Enter'; sleep 2c 'tmux capture-pane -t t -p' | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'--- اولین اتصال: ssh به میزبانی که برایش اعتماد نداریم:bash-5.2$ ssh -T git@githostThe 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») میبینی و با آنچه روی صفحه آمده مقایسه میکنی؛ روی سرور خودمان هم میشود از خود سرور پرسید:
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 2echo "--- بعد از yes (پیام موفقیت):"c 'tmux capture-pane -t t -p' | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}' | tail -3c '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 نداریم.
مثال ۶: clone و push با SSH
Section titled “مثال ۶: clone و push با SSH”حالا مخزن را با آدرس SSH کلون میکنم و تغییر میدهم و push میکنم. بعد از git@githost: مسیر مخزن روی سرور میآید (روی GitHub، user/repo.git):
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }c 'cd ~/gitlab && git clone git@githost:/srv/git/shop.git 2>&1cd shopecho "# Shop" > README.md && git add README.md && git commit -qm "Add README"git push -u origin main 2>&1git 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 -> mainbranch '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 را عوض میکنی:
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }c 'mkdir -p ~/gitlab/old && cd ~/gitlab/old && git init -qgit remote add origin https://github.com/example/shop.gitecho "--- قبل (HTTPS):"git remote -v | head -1git remote set-url origin git@githost:/srv/git/shop.gitecho "--- بعد (SSH):"git remote -v | head -1echo "--- آدرس جدید واقعاً کار میکند؟"git ls-remote origin'--- قبل (HTTPS):origin https://github.com/example/shop.git (fetch)--- بعد (SSH):origin git@githost:/srv/git/shop.git (fetch)--- آدرس جدید واقعاً کار میکند؟6a7cc9169dc7743773da17aade221a9b56fb46a8 HEAD6a7cc9169dc7743773da17aade221a9b56fb46a8 refs/heads/maingit 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) ساختهام، هر کدام با کلید خودش:
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 gitworkinstall -d -m 700 -o gitwork -g gitwork /home/gitwork/.sshcp -a /home/git/git-shell-commands /home/gitwork/ && chown -R gitwork:gitwork /home/gitwork/git-shell-commandstouch /home/gitwork/.hushlogin && chown gitwork:gitwork /home/gitwork/.hushlogingit 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 <<EOFHost gh-personal HostName githost User git IdentityFile ~/.ssh/id_personal IdentitiesOnly yes
Host gh-work HostName githost User gitwork IdentityFile ~/.ssh/id_work IdentitiesOnly yesEOFchmod 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 که تقریباً هیچ شبکهای نمیبندد). ببین روی همین شبکهای که این درس را در آن نوشتم، هر دو راه چه میگویند (اتصالهای واقعی):
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 است):
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 gitEOFecho "--- حالا 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: publickeydebug1: Authentications that can continue: publickeygit@ssh.github.com: Permission denied (publickey).اتصال برقرار شد و GitHub رد کرد، چون کلیدی که فرستادیم (id_ed25519) هیچ حساب GitHub ای ندارد. این همان چیزی است که روی حساب واقعی با ثبت کلید عوض میشود. مهم این است که ترافیک از پورت ۴۴۳ رد شد. اثر انگشت سروری که به آن وصل شدی را هم میتوانی با نسخهی رسمی مقایسه کنی؛ اثر انگشتهایی که SSH ذخیره کرد:
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 ببین دقیقاً چه چیزی:
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:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8debug1: Server accepts key: /home/ali/.ssh/id_ed25519 ED25519 SHA256:LegIlcMsEL26zUxMU94MciFcUupjKzHR4e0m8zp6EG8Authenticated 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).
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) Permission denied (publickey)
Section titled “۱) Permission denied (publickey)”شایعترین خطاست. چهار علت معمول: کلید به GitHub اضافه نشده، کلید اشتباه فرستاده میشود، نام کاربری git نیست، یا agent کلید را ندارد. با ssh -v ببین کدام کلید پیشنهاد میشود. اینجا کلیدی میسازم که سرور نمیشناسد:
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }c 'ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_strangerssh -i ~/.ssh/id_stranger -o IdentitiesOnly=yes -T git@githost 2>&1echo "--- 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: publickeydebug1: Offering public key: /home/ali/.ssh/id_stranger ED25519 SHA256:IEJxRwL7GoBGkoMV8a5K40D7cFsdllbyx3RNqhelLWw explicitdebug1: Authentications that can continue: publickeyOffering public key ← ولی خبری از Server accepts key نیست. راهحل: کلید درست را بده (-i یا IdentityFile در config)، یا کلید عمومی این کلید را به حساب اضافه کن. (و روی GitHub همیشه git@ را استفاده کن، نه نام کاربریات.)
۲) دسترسی خراب فایل کلید
Section titled “۲) دسترسی خراب فایل کلید”c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }c 'chmod 644 ~/.ssh/id_ed25519ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -T git@githost 2>&1 | head -8chmod 600 ~/.ssh/id_ed25519echo "--- بعد از 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 permissionsgit@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.
۳) بهجای : از / در آدرس
Section titled “۳) بهجای : از / در آدرس”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 repositoryfatal: 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 است.
دیدن جواب
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_practicessh-keygen -lf ~/.ssh/id_practice.pubstat -c "%a %n" ~/.ssh/id_practice ~/.ssh/id_practice.pub'256 SHA256:hQMIQMotmJrMr0PE60J2nKkpz0xIz4gBpE9V1MpzP64 me@example.com (ED25519)600 /home/ali/.ssh/id_practice644 /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 بفرست.
دیدن جواب
c() { docker exec -u ali -e TERM=xterm lxlab bash -c "$*"; }c 'mkdir -p ~/gitlab/convert && cd ~/gitlab/convert && git init -qgit remote add origin https://github.com/example/shop.gitecho "قبل: $(git remote get-url origin)"git remote set-url origin git@githost:/srv/git/shop.gitecho "بعد: $(git remote get-url origin)"git pull -q origin main 2>&1echo "update" >> README.md && git commit -qam "Update README over SSH"git push 2>&1 | tail -2git log --oneline | head -2'قبل: https://github.com/example/shop.gitبعد: git@githost:/srv/git/shop.gitupstream, see 'push.autoSetupRemote' in 'git help config'.
df9ab3a Update README over SSH6a7cc91 Add READMEدو حساب شبیهسازی کن: برای کاربر git (شخصی) و gitwork (کاری) دو alias در ~/.ssh/config بساز (در مثال ۸ ساخته شد). فقط با ssh -v ثابت کن alias کاری کلید id_work را پیشنهاد میکند، نه کلید شخصی؛ بعد یک commit به مخزن work.git بفرست.
دیدن جواب
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 -> mainbranch 'main' set up to track 'origin/main'.آزمونک
Section titled “آزمونک”کدام فایل را به GitHub (در Settings ← SSH keys) میدهی؟
فقط کلید عمومی (.pub). کلید خصوصی را هرگز به کسی نده؛ اگر لو برود باید کلید را باطل و جفت جدید بسازی.
در آدرس git@github.com:user/repo.git قسمت «git@» چیست؟
همهی حسابها git@github.com هستند؛ کلید تعیین میکند کدام حساب.
ssh -T git@github.com با «Permission denied (publickey)» تمام شد. یعنی چه؟
Connection timed out یعنی اصلاً به سرور نرسیدهای.
ssh روی پورت ۲۲ به github.com «Connection timed out» میدهد. راهحل رسمی؟
پورت ۴۴۳ همان پورت HTTPS است و تقریباً هیچ شبکهای نمیبندد.
remote یک پروژهی کلونشده با HTTPS را چطور به SSH تغییر میدهی؟
فقط آدرس عوض میشود؛ تاریخچه و شاخهها دست نمیخورند.
ssh-agent برای چیست؟
passphrase از سرقت فایل کلید محافظت میکند و agent راحتی را برمیگرداند.
جمعبندی
Section titled “جمعبندی”- 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_ed25519 | agent و افزودن کلید |
ssh -T git@github.com | آزمایش اتصال |
ssh -vT git@github.com | آزمایش با جزئیات |
git remote set-url origin git@github.com:USER/REPO.git | HTTPS ← SSH |
git ls-remote origin | آزمایش remote |
Host github.com / Hostname ssh.github.com / Port 443 | SSH از پورت ۴۴۳ (در ~/.ssh/config) |
chmod 600 ~/.ssh/id_ed25519 | دسترسی درست کلید خصوصی |