توی این درس یاد میگیری همهی آنچه در دوره دیدی را روی یک سرور تازه کنار هم بگذاری. سرور تازه معمولاً فقط یک کاربر root و یک رمز دارد و پورت ۲۲ را به روی همهی اینترنت باز دارد؛ رباتها طی چند دقیقه شروع به حدس رمز میکنند. قدمبهقدم: بهروزرسانی سیستم، ساختن کاربر sudo، ورود با کلید SSH و بستن ورود با رمز و root (sudo nano /etc/ssh/sshd_config، sudo systemctl restart ssh)، فایروال ufw، بهروزرسانی خودکار و fail2ban (sudo apt install unattended-upgrades fail2ban)؛ و در پایان هر دفاع را از یک ماشین دیگر آزمایش میکنی. تمرین: یک VPS یا ماشین مجازی تازه را از صفر ایمن کن.
مسئله: یک سرور تازه، پر از درِ باز
Section titled “مسئله: یک سرور تازه، پر از درِ باز”وقتی یک VPS اجاره میکنی، ارائهدهنده یک آدرس IP، یک کاربر root و یک رمز میدهد. از همان لحظه آن IP روی اینترنت است. رباتهایی که ۲۴ ساعته IP ها را اسکن میکنند معمولاً در کمتر از چند ساعت اولین تلاش ورود را با root و رمزهای رایج میزنند. اگر سرور با تنظیمات پیشفرض بماند، فقط وقت و شانس است که کی نفوذ کنند. کار تو این است که قبل از هر چیز دیگر سرور را سخت کنی.
تشبیه: ساختمان تازهساز
Section titled “تشبیه: ساختمان تازهساز”سرور تازه مثل ساختمان تازهساز است که در ورودیاش با یک قفل ساده باز مانده. امنیتش چند لایه است: قفل بهتر در (کلید بهجای رمز)، نگهبان در ورودی که فقط راههای لازم را باز میگذارد (فایروال)، دوربین و بازرسی که مزاحم تکراری را بیرون میاندازد (fail2ban)، و نگهداری منظم که نقصهای ساختمان را وصله میکند (بهروزرسانی خودکار). هیچ لایهای بهتنهایی کافی نیست؛ ولی با هم، حملهی ساده را بیاثر میکنند.
چکلیست پروژه
Section titled “چکلیست پروژه”مثالهای عملی (قدمبهقدم)
Section titled “مثالهای عملی (قدمبهقدم)”برای اینکه واقعاً «یک سرور تازه» باشد، برای این درس یک ماشین systemdدار تمیز (server) از نو میسازم که هیچکدام از تنظیمات امنیتی را ندارد؛ و لپتاپ تو یک ماشین جدا (client) است که از بیرون به آن وصل میشود. چون دو ماشین جدا هستند، دستورها را از بیرون با docker exec میزنم؛ مثل درس فایروال: server یعنی «روی سرور اجرا کن» و client یعنی «روی لپتاپ». روی سرور واقعی خودت server را نمینویسی و همان دستورها را (با sudo) در ssh میزنی.
قدم ۰: وضع تازهی سرور
Section titled “قدم ۰: وضع تازهی سرور”server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')CIP=$(client hostname -I | awk '{print $1}')echo "آدرس سرور: $SIP | آدرس لپتاپ: $CIP"echo "--- من root هستم (مثل اولین ورود به VPS) روی:"server sh -c 'hostname; . /etc/os-release; echo "$PRETTY_NAME"'echo "--- تنظیمهای ssh در حالت پیشفرض:"server sshd -T | grep -E '^(port|permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries) 'echo "--- فایروال:"server ufw statusecho "--- پورتهای باز:"server sh -c "ss -tlnp | awk 'NR>1 {print \$4}' | sort -u | tr '\n' ' '"; echoآدرس سرور: 172.17.0.5 | آدرس لپتاپ: 172.17.0.2--- من root هستم (مثل اولین ورود به VPS) روی:serverUbuntu 24.04.5 LTS--- تنظیمهای ssh در حالت پیشفرض:port 22maxauthtries 6permitrootlogin without-passwordpubkeyauthentication yespasswordauthentication yes--- فایروال:Status: inactive--- پورتهای باز:0.0.0.0:22 [::]:22وضع پیشفرض: ورود با رمز مجاز (passwordauthentication yes)، ورود root با کلید مجاز، فایروال خاموش و پورت ۲۲ باز. حالا چکلیست را اجرا میکنیم. (permitrootlogin without-password همان prohibit-password است: root با رمز نه، ولی با کلید بله.)
قدم ۱: بهروزرسانی سیستم
Section titled “قدم ۱: بهروزرسانی سیستم”اولین کار روی هر سرور تازه: بستهها را به آخرین وصلهها برسان (درس apt). اول ببین چه چیزی بهروز میشود (-s = شبیهسازی)، بعد واقعاً:
server() { docker exec lxfresh "$@"; }echo "--- شبیهسازی:"server sh -c 'apt-get update -qq; apt-get -s upgrade | grep -E "^(Inst|[0-9]+ upgraded)"'echo "--- اجرای واقعی:"server sh -c 'DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq 2>&1 | tail -3'--- شبیهسازی:2 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Inst libaudit-common [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [all])Inst libaudit1 [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [arm64])--- اجرای واقعی:Setting up libaudit1:arm64 (1:3.1.2-2.1ubuntu0.1) ...Processing triggers for man-db (2.12.0-4build2) ...Processing triggers for libc-bin (2.39-0ubuntu8.9) ...روی یک سرور واقعی ممکن است بعد از بهروزرسانی هسته لازم شود ریبوت کنی (/var/run/reboot-required). در سرور تازه بهترین زمان ریبوت همین الآن است.
قدم ۲: ساخت کاربر ادمین با sudo
Section titled “قدم ۲: ساخت کاربر ادمین با sudo”با root کار روزمره نکن (درس root و sudo). یک کاربر شخصی (اینجا ops) با گروه sudo میسازیم: هر کار مدیریتی با نام صاحبش ثبت میشود و میشود دسترسی هر نفر را جدا بست. باید رمز هم داشته باشد، چون sudo رمز خود او را میخواهد (یک اشتباه رایج: ساختن کاربر بدون رمز و بعد گیرکردن پشت sudo):
server() { docker exec lxfresh "$@"; }PW=lab-passserver adduser --disabled-password --gecos "" ops > /dev/nullserver usermod -aG sudo opsserver sh -c "echo ops:$PW | chpasswd"server id opsecho "--- ops چه اجازههایی دارد؟"server sudo -l -U ops | tail -2uid=1002(ops) gid=1002(ops) groups=1002(ops),27(sudo),100(users)--- ops چه اجازههایی دارد؟User ops may run the following commands on server: (ALL : ALL) ALL--disabled-password یعنی هنوز رمز نداشت و chpasswd رمز آزمایشی گذاشت. (روی سرور واقعی از sudo passwd ops استفاده کن و رمز قوی بگذار؛ رمز را هرگز داخل دستور نمینویسی.) sudo -l -U ops نشان میدهد ops با رمز خودش هر دستوری را میتواند با sudo بزند.
قدم ۳: کلید SSH، از لپتاپ
Section titled “قدم ۳: کلید SSH، از لپتاپ”روی لپتاپ (کلاینت) یک جفتکلید میسازم (بدون passphrase فقط برای این آزمایش؛ روی لپتاپ واقعی حتماً passphrase بگذار) و کلید عمومی را روی کاربر ops سرور میگذارم. چون ssh-copy-id رمز میپرسد، از ترمینال مجازی (tmux) استفاده میکنم تا صفحهی واقعی را ببینی:
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')PW=lab-passcs() { client tmux capture-pane -t c -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }client ssh-keygen -q -t ed25519 -N "" -C ali@laptop -f /home/ali/.ssh/lx_projecho "--- کلید عمومی (این را میشود به همه نشان داد):"client cat /home/ali/.ssh/lx_proj.pub | cut -c1-70client tmux -u new-session -d -s c -x 100 -y 30 "bash --norc"; sleep 1client tmux send-keys -t c "ssh-copy-id -i ~/.ssh/lx_proj.pub -o StrictHostKeyChecking=accept-new ops@$SIP" Enter; sleep 3client tmux send-keys -t c "$PW" Enter; sleep 3cs | grep -E 'Number of key|password:'echo "--- ورود با کلید (بدون رمز):"client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes ops@$SIP 'whoami; hostname'--- کلید عمومی (این را میشود به همه نشان داد):ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFY91u7PspvLHYxDZTdFX2hsgxR9OjIkACops@172.17.0.5's password:Number of key(s) added: 1--- ورود با کلید (بدون رمز):opsserverNumber of key(s) added: 1 و ورود بعدی بدون رمز. (جزئیات: درس SSH.) ولی هنوز ورود با رمز هم مجاز است؛ تا آن را نبندیم، کلید فقط یک راه اضافه است، نه یک قفل.
قدم ۴: امنکردن sshd
Section titled “قدم ۴: امنکردن sshd”حالا ورود با رمز و ورود مستقیم root را میبندیم. این قدم خطرناکترین قدم پروژه است: اگر اشتباه کنی خودت را بیرون میاندازی. پس یک نشست باز را نگه میدارم (همان ssh با کلید) و با آن فایل تنظیمات را ویرایش میکنم (sudo nano + رمز sudo)، و بعد در یک نشست دوم آزمایش میکنم. تنظیمات را در یک فایل جدا در sshd_config.d مینویسم (بهتر از ویرایش فایل اصلی /etc/ssh/sshd_config؛ هر دو کار میکنند):
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')PW=lab-passcs() { client tmux capture-pane -t c -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }client tmux send-keys -t c "clear" Enterclient tmux send-keys -t c "ssh -i ~/.ssh/lx_proj ops@$SIP" Enter; sleep 3client tmux send-keys -t c "sudo nano /etc/ssh/sshd_config.d/10-hardening.conf" Enter; sleep 2client tmux send-keys -t c "$PW" Enter; sleep 2client tmux send-keys -t c -l "PermitRootLogin no"; client tmux send-keys -t c Enterclient tmux send-keys -t c -l "PasswordAuthentication no"; client tmux send-keys -t c Enterclient tmux send-keys -t c -l "MaxAuthTries 3"; client tmux send-keys -t c Enter; sleep 1cs | head -5 GNU nano 7.2 /etc/ssh/sshd_config.d/10-hardening.conf *PermitRootLogin noPasswordAuthentication noMaxAuthTries 3سه خط نوشتم:
| تنظیم | معنی |
|---|---|
PermitRootLogin no |
ورود مستقیم با root ممنوع (حتی با کلید) |
PasswordAuthentication no |
ورود با رمز ممنوع؛ فقط کلید |
MaxAuthTries 3 |
بعد از ۳ تلاش ناموفق در یک اتصال، قطع کن |
ذخیره میکنم (Ctrl+O، Enter، Ctrl+X)، قبل از اعمال بررسی نحو را میزنم و بعد sshd را ریستارت میکنم:
client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }cs() { client tmux capture-pane -t c -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }client tmux send-keys -t c C-o; sleep 1; client tmux send-keys -t c Enter; sleep 1; client tmux send-keys -t c C-x; sleep 1client tmux send-keys -t c "clear" Enterclient tmux send-keys -t c "sudo sshd -t && sudo systemctl restart ssh && echo 'sshd درست بود و ریستارت شد'" Enter; sleep 3csops@server:~$ sudo sshd -t && sudo systemctl restart ssh && echo 'sshd درست بود و ریستارت شد'sshd درست بود و ریستارت شدops@server:~$sshd -t بدون خروجی یعنی نحو درست است؛ فقط بعد از آن ریستارت. نشست فعلی با ریستارت قطع نشد (اتصالهای برقرار میمانند). حالا آزمایش در یک نشست دوم (نشست اول را باز میگذارم تا اگر چیزی خراب بود راه برگشت داشته باشم):
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')echo "--- ۱) ورود با کلید (باید کار کند):"client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes ops@$SIP 'echo ورود با کلید کار کرد'echo "--- ۲) ورود با رمز (باید رد شود):"client ssh -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1 ops@$SIP true < /dev/null 2>&1echo "--- ۳) ورود root، حتی اگر کلید معتبر باشد:"server sh -c 'mkdir -p /root/.ssh; cat /home/ops/.ssh/authorized_keys > /root/.ssh/authorized_keys; chmod 700 /root/.ssh; chmod 600 /root/.ssh/authorized_keys'client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes root@$SIP true 2>&1echo "--- لاگ سرور چه میگوید؟"server journalctl -u ssh --no-pager -o cat | grep -E 'ROOT LOGIN' | tail -1 | cut -c1-80server rm -rf /root/.sshecho "--- تنظیم مؤثر:"server sshd -T | grep -E '^(permitrootlogin|passwordauthentication|maxauthtries) '--- ۱) ورود با کلید (باید کار کند):ورود با کلید کار کرد--- ۲) ورود با رمز (باید رد شود):ops@172.17.0.5: Permission denied (publickey).--- ۳) ورود root، حتی اگر کلید معتبر باشد:root@172.17.0.5: Permission denied (publickey).--- لاگ سرور چه میگوید؟ROOT LOGIN REFUSED FROM 172.17.0.2 port 53262 [preauth]--- تنظیم مؤثر:maxauthtries 3permitrootlogin nopasswordauthentication noهر سه آزمایش درست درآمد: کلید کار میکند، رمز رد میشود (Permission denied (publickey)) و ورود root رد میشود و در لاگ سرور هم ثبت میشود (ROOT LOGIN REFUSED)، حتی وقتی کلید معتبر روی root گذاشتم. (آن فایل authorized_keys برای root را فقط برای آزمایش گذاشتم و پاک کردم.)
قدم ۵: فایروال
Section titled “قدم ۵: فایروال”حالا فقط پورتهایی که لازم است: SSH و (برای وبسایتها) HTTP. اول سیاست پیشفرض و اول SSH، بعد روشنکردن (درس فایروال). برای آزمایش از بیرون، یک وبسرور واقعی (nginx) روی پورت ۸۰ و یک سرویس تستی فراموششده روی ۸۰۹۹ بالا میآورم:
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')server systemctl start nginxserver sh -c 'python3 -m http.server 8099 --bind 0.0.0.0 > /dev/null 2>&1 &'sleep 1echo "--- قبل از فایروال، از لپتاپ:"for p in 22 80 8099; do client nc -zv -w 2 $SIP $p 2>&1; doneserver ufw default deny incomingserver ufw default allow outgoingserver ufw allow OpenSSHserver ufw allow 80/tcpserver ufw --force enableserver ufw status verboseecho "--- بعد از فایروال، از لپتاپ:"for p in 22 80 8099; do client nc -zv -w 2 $SIP $p 2>&1; done--- قبل از فایروال، از لپتاپ:Connection to 172.17.0.5 22 port [tcp/ssh] succeeded!Connection to 172.17.0.5 80 port [tcp/http] succeeded!Connection to 172.17.0.5 8099 port [tcp/*] succeeded!Default incoming policy changed to 'deny'(be sure to update your rules accordingly)Default outgoing policy changed to 'allow'(be sure to update your rules accordingly)Rules updatedRules updated (v6)Rules updatedRules updated (v6)Firewall is active and enabled on system startupStatus: activeLogging: on (low)Default: deny (incoming), allow (outgoing), deny (routed)New profiles: skip
To Action From-- ------ ----22/tcp (OpenSSH) ALLOW IN Anywhere80/tcp ALLOW IN Anywhere22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)80/tcp (v6) ALLOW IN Anywhere (v6)
--- بعد از فایروال، از لپتاپ:Connection to 172.17.0.5 22 port [tcp/ssh] succeeded!Connection to 172.17.0.5 80 port [tcp/http] succeeded!nc: connect to 172.17.0.5 port 8099 (tcp) timed out: Operation now in progressسرویس فراموششدهی ۸۰۹۹ که قبلاً از بیرون باز بود، حالا timed out است: فایروال همان کاری را کرد که باید، بدون اینکه خود سرویس را دست بزنیم. اگر سرویسهای بیشتری داری (مثلاً HTTPS) فقط همانها را اضافه کن (ufw allow 443/tcp).
قدم ۶: بهروزرسانی خودکار و fail2ban
Section titled “قدم ۶: بهروزرسانی خودکار و fail2ban”دو بستهی بعدی را با یک دستور نصب میکنم: sudo apt install unattended-upgrades fail2ban. unattended-upgrades هر روز وصلههای امنیتی را خودکار نصب میکند. fail2ban لاگها را میخواند و IP هایی را که چند بار شکست خوردهاند موقتاً میبندد:
server() { docker exec lxfresh "$@"; }server sh -c 'DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unattended-upgrades fail2ban 2>&1 | tail -2'echo "--- فعالسازی بهروزرسانی خودکار (فایل 20auto-upgrades):"server cat /etc/apt/apt.conf.d/20auto-upgradesecho "--- منابع مجاز برای نصب خودکار (خطهای // کامنتاند):"server sh -c "grep -E '^\s*(//)?\s*\"\\\$\{distro_id\}' /etc/apt/apt.conf.d/50unattended-upgrades"echo "--- شبیهسازی یک اجرا:"server sh -c 'unattended-upgrade --dry-run 2>&1 | tail -2; echo "(بدون خروجی یعنی چیزی برای بهروزرسانی نبود)"'echo "--- و timer روزانه:"server systemctl list-timers 'apt-daily*' --no-pager | cut -c1-110Processing triggers for man-db (2.12.0-4build2) ...Processing triggers for libc-bin (2.39-0ubuntu8.9) ...--- فعالسازی بهروزرسانی خودکار (فایل 20auto-upgrades):APT::Periodic::Update-Package-Lists "1";APT::Periodic::Unattended-Upgrade "1";--- منابع مجاز برای نصب خودکار (خطهای // کامنتاند): "${distro_id}:${distro_codename}"; "${distro_id}:${distro_codename}-security"; "${distro_id}ESMApps:${distro_codename}-apps-security"; "${distro_id}ESM:${distro_codename}-infra-security";// "${distro_id}:${distro_codename}-updates";// "${distro_id}:${distro_codename}-proposed";// "${distro_id}:${distro_codename}-backports";--- شبیهسازی یک اجرا:(بدون خروجی یعنی چیزی برای بهروزرسانی نبود)--- و timer روزانه:NEXT LEFT LAST PASSED UNIT ACTIVATESSun 2026-10-04 23:38:09 UTC 15h - - apt-daily.timer apt-daily.serviceMon 2026-10-05 06:19:05 UTC 22h - - apt-daily-upgrade.timer apt-daily-upgrade.service
2 timers listed.Pass --all to see loaded but inactive timers, too.فایل 20auto-upgrades دو خط دارد: Update-Package-Lists "1" (هر روز فهرست را تازه کن) و Unattended-Upgrade "1" (و وصلههای امنیتی را نصب کن). فایل 50unattended-upgrades تعیین میکند از کدام منبعها خودکار نصب شود: پیشفرض فقط نسخهی اصلی، مخزن -security و ESM (وصلههای امنیتی طولانیمدت)؛ سه خط آخر (-updates، -proposed، -backports) با // کامنت شدهاند، یعنی بهروزرسانیهای معمولی و آزمایشی خودکار نصب نمیشوند. اجرای روزانه را هم timer های apt-daily و apt-daily-upgrade راه میاندازند. حالا fail2ban: برای sshd همین الآن روشن است (پیشفرض Ubuntu)، من فقط سختگیرانهترش میکنم (۳ شکست در ۱۰ دقیقه، ۱۰ دقیقه بن):
server() { docker exec lxfresh "$@"; }server sh -c 'cat > /etc/fail2ban/jail.d/10-ssh.local <<EOC[sshd]enabled = truemaxretry = 3findtime = 10mbantime = 10mEOCsystemctl enable --now fail2ban > /dev/null 2>&1systemctl restart fail2ban'sleep 3echo "--- وضعیت زندان sshd:"server fail2ban-client status sshd--- وضعیت زندان sshd:Status for the jail: sshd|- Filter| |- Currently failed: 0| |- Total failed: 3| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd`- Actions |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 172.17.0.2صبر کن! Currently banned: 1، در حالی که هنوز حملهای نکردهایم: لپتاپ خودمان بن شده. جواب در لاگ خود fail2ban است (/var/log/fail2ban.log). هنگام شروع، journal ده دقیقهی اخیر (findtime) را میخواند و تلاشهای ناموفق آزمایش قدم ۴ (ورود با رمز و ورود root که عمداً رد شدند) را هم شمرد:
server() { docker exec lxfresh "$@"; }server sh -c "grep -E 'Found|Ban ' /var/log/fail2ban.log | sed 's/^[^]]*\]: //'"INFO [sshd] Found 172.17.0.2 - 2026-10-04 07:50:55INFO [sshd] Found 172.17.0.2 - 2026-10-04 07:50:55INFO [sshd] Found 172.17.0.2 - 2026-10-04 07:50:55NOTICE [sshd] Ban 172.17.0.2سه بار Found و بعد Ban: fail2ban نمیداند «آزمایش» و «حمله» فرقی دارند؛ هر شکستِ یک IP میشمارد. این درس یک قاعدهی عملی دارد: بعد از روشنکردن fail2ban، تستهای خودت هم تو را بن میکنند (در قدم ۸ همین را دوباره میبینی).
قدم ۷: آزمایش fail2ban از بیرون (و یک عیب واقعی)
Section titled “قدم ۷: آزمایش fail2ban از بیرون (و یک عیب واقعی)”fail2ban میگوید لپتاپ بن است. پس اگر درست کار کند، پورت ۲۲ باید از لپتاپ بسته باشد. همیشه از بیرون امتحان کن، نه فقط از روی گزارش خود ابزار:
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')echo "--- fail2ban میگوید:"server fail2ban-client status sshd | tail -3echo "--- پورت 22 از لپتاپ:"client nc -zv -w 3 $SIP 22 2>&1--- fail2ban میگوید: |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 172.17.0.2--- پورت 22 از لپتاپ:Connection to 172.17.0.5 22 port [tcp/ssh] succeeded!عجیب است: بن ثبت شده ولی پورت هنوز باز است! یعنی fail2ban بن را «ثبت کرد» ولی نتوانست «اجرا» کند. راهش لاگ است. نگاه کن fail2ban موقع بنکردن چه خطایی دیده:
server() { docker exec lxfresh "$@"; }echo "--- خطاهای بن در لاگ fail2ban:"server sh -c "grep -m1 'nft: not found' /var/log/fail2ban.log | sed 's/^[^]]*\]: //'"server sh -c "grep -m1 'returned 127' /var/log/fail2ban.log | sed 's/^[^]]*\]: //'"server sh -c "grep -m1 'HINT on 127' /var/log/fail2ban.log | sed 's/^[^]]*\]: //' | cut -c1-70"echo "--- آیا ابزار nft هست؟"server sh -c 'which nft || echo "nft نصب نیست"'--- خطاهای بن در لاگ fail2ban:ERROR ffffb8be2420 -- stderr: '/bin/sh: 1: nft: not found'ERROR ffffb8be2420 -- returned 127INFO HINT on 127: "Command not found". Make sure that all commands--- آیا ابزار nft هست؟nft نصب نیستnft: not found و returned 127 (کد ۱۲۷ یعنی «دستور پیدا نشد»): fail2ban برای بستن یک IP دستور nft (فایروال nftables) را اجرا میکند، ولی این ماشین کمحجم آن را ندارد (روی بیشتر سرورها نصب است). علت از لاگ پیدا شد. درستش میکنم: اول بن ناقص قبلی را برمیدارم، nftables را نصب و fail2ban را ریستارت میکنم؛ و این بار حملهی واقعی میزنم: چهار بار ورود با یک کاربر وجودنداشته (کاری که رباتها همیشه میکنند):
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')CIP=$(client hostname -I | awk '{print $1}')server fail2ban-client set sshd unbanip $CIP > /dev/nullserver sh -c 'DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nftables > /dev/null 2>&1; systemctl restart fail2ban'sleep 3for i in 1 2 3 4; do client ssh -o BatchMode=yes -o ConnectTimeout=3 nosuchuser@$SIP true 2>&1 | tail -1; donesleep 3echo "--- وضعیت:"server fail2ban-client status sshd | tail -3echo "--- پورت 22 از لپتاپ:"client nc -zv -w 3 $SIP 22 2>&1echo "--- قاعدهی nftables که fail2ban ساخت:"server sh -c 'nft list set inet f2b-table addr-set-sshd | grep elements'nosuchuser@172.17.0.5: Permission denied (publickey).nosuchuser@172.17.0.5: Permission denied (publickey).nosuchuser@172.17.0.5: Permission denied (publickey).nosuchuser@172.17.0.5: Permission denied (publickey).--- وضعیت: |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 172.17.0.2--- پورت 22 از لپتاپ:nc: connect to 172.17.0.5 port 22 (tcp) failed: Connection refused--- قاعدهی nftables که fail2ban ساخت: elements = { 172.17.0.2 }حالا Connection refused: IP لپتاپ واقعاً بسته شد و در مجموعهی addr-set-sshd است. هر چهار تلاش به Permission denied رسیدند (تلاشها پشتسرهم و در کسری از ثانیه بودند و fail2ban لاگ را با کمی تأخیر میخواند)، ولی بعد از آن پورت برای این IP بسته است. بعد از bantime (۱۰ دقیقه) خودکار باز میشود؛ برای رفع بن زودتر:
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')CIP=$(client hostname -I | awk '{print $1}')server fail2ban-client set sshd unbanip $CIPclient nc -zv -w 3 $SIP 22 2>&1echo "--- ورود قانونی با کلید دوباره کار میکند:"client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes ops@$SIP 'echo خوش آمدی $(whoami)'1Connection to 172.17.0.5 22 port [tcp/ssh] succeeded!--- ورود قانونی با کلید دوباره کار میکند:خوش آمدی opsعدد 1 که unbanip چاپ کرد یعنی یک IP از بن درآمد.
نکتهی مهم: اگر خودت چند بار اشتباه بزنی بن میشوی (همین بلا سر لپتاپ ما با تستهای قدم ۴ آمد). اگر IP ثابت داری (دفتر) آن را در ignoreip در jail.local بگذار، و راه برگشت (کنسول ارائهدهنده) را آماده نگه دار. و چون ورود با رمز بسته است، رباتها اصلاً تا مرحلهی «حدس رمز» نمیرسند؛ fail2ban فقط تلاشهای بیفایده را زودتر قطع میکند و لاگ را تمیز نگه میدارد.
چرا بعد از روشنکردن fail2ban ممکن است IP خودت بن شود، حتی اگر حملهای نکرده باشی؟
fail2ban هنگام شروع، لاگ ده دقیقهی اخیر (findtime) را میخواند. تستهای ناموفق قدم ۴ سه شکست از لپتاپ بودند و همان کافی بود. پس بعد از روشنکردن آن، آزمایشهایت را با احتیاط بزن و راه برگشت (کنسول ارائهدهنده) داشته باش.
قدم ۸: آزمایش نهایی از بیرون
Section titled “قدم ۸: آزمایش نهایی از بیرون”آخرین قدم همان است که هر چکلیست خوبی دارد: از بیرون ثابت کن آنچه ساختهای واقعاً کار میکند. یک اسکریپت چک روی لپتاپ بنویس و نتیجه را ببین:
#!/bin/bash# آزمایش نهایی امنیت سرور (روی لپتاپ اجرا میشود)SIP=$1; KEY=$2ok() { echo " ✓ $1"; }bad() { echo " ✗ $1"; }echo "۱) پورتهای لازم باز:"for p in 22 80; do nc -z -w 2 $SIP $p 2>/dev/null && ok "پورت $p باز" || bad "پورت $p بسته"; doneecho "۲) پورتهای غیرلازم بسته:"for p in 23 3306 8099; do nc -z -w 2 $SIP $p 2>/dev/null && bad "پورت $p باز است!" || ok "پورت $p بسته"; doneecho "۳) ورود با کلید:"ssh -i $KEY -o BatchMode=yes -o ConnectTimeout=4 ops@$SIP true 2>/dev/null && ok "کار میکند" || bad "کار نمیکند"echo "۴) ورود با رمز رد شود:"ssh -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1 -o ConnectTimeout=4 -o BatchMode=no ops@$SIP true < /dev/null 2>&1 | grep -q 'Permission denied (publickey)' && ok "رد شد (فقط کلید)" || bad "ورود با رمز هنوز ممکن است"echo "۵) ورود root رد شود:"ssh -i $KEY -o BatchMode=yes -o ConnectTimeout=4 root@$SIP true 2>/dev/null && bad "root وارد شد!" || ok "root رد شد"client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }server() { docker exec lxfresh "$@"; }SIP=$(server hostname -I | awk '{print $1}')docker cp audit.sh lxlab:/tmp/audit.shclient bash /tmp/audit.sh $SIP /home/ali/.ssh/lx_projecho "--- و سمت سرور:"server sh -c 'echo "ufw: $(ufw status | head -1)"; echo "fail2ban: $(systemctl is-active fail2ban)"; echo "بهروزرسانی خودکار: $(grep Unattended /etc/apt/apt.conf.d/20auto-upgrades)"'۱) پورتهای لازم باز: ✓ پورت 22 باز ✓ پورت 80 باز۲) پورتهای غیرلازم بسته: ✓ پورت 23 بسته ✓ پورت 3306 بسته ✓ پورت 8099 بسته۳) ورود با کلید: ✓ کار میکند۴) ورود با رمز رد شود: ✓ رد شد (فقط کلید)۵) ورود root رد شود: ✓ root رد شد--- و سمت سرور:ufw: Status: activefail2ban: activeبهروزرسانی خودکار: APT::Periodic::Unattended-Upgrade "1";همهی بررسیها سبز است. حالا این یک الگوی تکرارپذیر دارد: هر سرور تازه، همین هشت قدم، و همین اسکریپت آزمایش.
پشت پرده: هر لایه جلوی چه چیزی را میگیرد؟
Section titled “پشت پرده: هر لایه جلوی چه چیزی را میگیرد؟”| لایه | چه حملهای را میبندد | اگر نباشد |
|---|---|---|
| بهروزرسانی خودکار | سوءاستفاده از آسیبپذیریهای شناختهشدهی نرمافزار | هر آسیبپذیری تا نصب دستی وصله، باز میماند |
کاربر sudo بهجای root |
اشتباه یا نفوذ با دسترسی کامل؛ بینامبودن اعمال | همه با root کار میکنند؛ هیچ ردپایی نیست |
| کلید SSH، بدون رمز | حدس رمز (brute force)، افشای رمز | رمز ضعیف = نفوذ |
بستن root |
هدفگرفتن حسابی که همه میدانند وجود دارد | حملهی مستقیم به root |
| فایروال | سرویسهای فراموششده یا آسیبپذیر روی پورتهای باز | هر پورت باز یک در است |
fail2ban |
اسکنها و تلاشهای تکراری؛ پر شدن لاگها | سرور و لاگ زیر بار رباتها |
این الگو را دفاع در عمق (defense in depth) میگویند: هر لایه فرض میکند لایهی قبلی ممکن است شکسته شود. و دقت کن چیزی که نکردیم: پورت ssh را عوض نکردیم (فقط نویز لاگ را کم میکند، امنیت واقعی نمیدهد)، رمز پیچیدهی جایگزین نخواستیم (کلید قویتر است)، و چیزی را غیرفعال نکردیم که هنوز لازم است.
برای بعد از این پروژه (در محدودهی این دوره نیست): پشتیبانگیری خودکار (درسهای آرشیو و cron)، مانیتورینگ و هشدار (دیسک، حافظه، سرویسها)، احراز هویت دو مرحلهای برای SSH، محدودکردن AllowUsers در sshd، و بازبینی دورهای لاگها و کاربرها.
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
sudo apt update && sudo apt upgrade |
بهروزرسانی |
sudo adduser نام / sudo usermod -aG sudo نام |
کاربر ادمین |
ssh-keygen -t ed25519 -C "..." / ssh-copy-id user@host |
کلید و نصب آن |
sudo nano /etc/ssh/sshd_config یا /etc/ssh/sshd_config.d/نام.conf |
تنظیمات sshd |
sudo sshd -t |
بررسی نحو قبل از ریستارت |
sudo systemctl restart ssh |
اعمال تنظیمات |
sudo ufw default deny incoming / allow OpenSSH / enable |
فایروال |
sudo apt install unattended-upgrades fail2ban |
بهروزرسانی خودکار و دفاع از brute force |
sudo fail2ban-client status sshd |
وضعیت و IP های بنشده |
sudo fail2ban-client set sshd unbanip IP |
رفع بن |
sudo unattended-upgrade --dry-run |
شبیهسازی بهروزرسانی خودکار |
تنظیم sshd |
مقدار توصیهشده |
|---|---|
PermitRootLogin |
no |
PasswordAuthentication |
no (بعد از آزمایش ورود با کلید) |
MaxAuthTries |
3 |
PubkeyAuthentication |
yes (پیشفرض) |
AllowUsers |
فقط کاربرهای لازم (اختیاری) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) بستن رمز قبل از آزمایش ورود با کلید
Section titled “۱) بستن رمز قبل از آزمایش ورود با کلید”اگر PasswordAuthentication no را بگذاری و کلیدت درست نصب نشده باشد، دیگر راه ورودی نیست. راهحل: قاعدهی طلایی قدم ۴: نشست باز + نشست دوم + sshd -t.
۲) ساختن کاربر sudo بدون رمز
Section titled “۲) ساختن کاربر sudo بدون رمز”docker exec lxfresh sh -c 'adduser --disabled-password --gecos "" nopass > /dev/null; usermod -aG sudo nopass; su - nopass -c "sudo -n true" 2>&1 | head -2; userdel -r nopass 2>/dev/null'sudo: a password is requiredsudo: a password is required: کاربر در گروه sudo است ولی رمزی ندارد که sudo بپرسد. راهحل: حتماً رمز بگذار (sudo passwd نام)؛ NOPASSWD را برای آدمها استفاده نکن.
۳) روشنکردن فایروال بدون SSH
Section titled “۳) روشنکردن فایروال بدون SSH”درس فایروال: ufw allow OpenSSH قبل از enable.
۴) ban شدن خودت با fail2ban
Section titled “۴) ban شدن خودت با fail2ban”چند تلاش اشتباه از IP خودت = بن. راهحل: ignoreip = IP-ثابت-تو در jail.local؛ و کنسول ارائهدهنده را برای رفع بن آماده نگه دار.
۵) باور اینکه «بن ثبت شد پس کار میکند»
Section titled “۵) باور اینکه «بن ثبت شد پس کار میکند»”قدم ۷: Currently banned: 1 ولی پورت باز. همیشه از بیرون آزمایش کن و لاگ را بخوان (sudo tail /var/log/fail2ban.log).
۶) حذف known_hosts یا ignore کردن هشدار host key
Section titled “۶) حذف known_hosts یا ignore کردن هشدار host key”اگر سرور را از نو ساختی و کلید میزبان عوض شد، هشدار REMOTE HOST IDENTIFICATION HAS CHANGED میآید (درس SSH). علتش را بررسی کن؛ هرگز بیفکر StrictHostKeyChecking=no را برای همیشه نگذار.
وضع نهایی سرور را بخوان: چه کسی روی چه پورتهایی گوش میدهد، و کدام قاعدههای فایروال فعالاند.
دیدن جواب
server() { docker exec lxfresh "$@"; }echo "--- گوشدهندهها:"server sh -c "ss -tlnp | awk 'NR>1 {print \$4, \$6}' | cut -c1-60"echo "--- فایروال:"server sh -c "ufw status | grep -v '(v6)'"--- گوشدهندهها:0.0.0.0:8099 users:(("python3",pid=592,fd=3))0.0.0.0:22 users:(("sshd",pid=508,fd=3),("systemd",pid=1,fd=0.0.0.0:80 users:(("nginx",pid=584,fd=5),("nginx",pid=583,fd[::]:22 users:(("sshd",pid=508,fd=4),("systemd",pid=1,fd=62)[::]:80 users:(("nginx",pid=584,fd=6),("nginx",pid=583,fd=6)--- فایروال:Status: active
To Action From-- ------ ----OpenSSH ALLOW Anywhere80/tcp ALLOW Anywhereتمرین اصلی: یک VPS یا ماشین مجازی تازه را از صفر ایمن کن. (روی ماشین واقعی خودت همین هشت قدم را بزن.) برای همین سرور آزمایشی: یک کاربر ادمین دوم (dev2) با کلید خودش بساز و با AllowUsers ops dev2 در sshd ورود را به این دو نفر محدود کن؛ ثابت کن کاربر ali (که روی سرور هست ولی مجاز نیست) حتی با کلید معتبر وارد نمیشود.
دیدن جواب
server() { docker exec lxfresh "$@"; }client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }SIP=$(server hostname -I | awk '{print $1}')server adduser --disabled-password --gecos "" dev2 > /dev/nullPUB=$(client cat /home/ali/.ssh/lx_proj.pub)for u in dev2 ali; do server sh -c "install -d -m 700 -o $u -g $u /home/$u/.ssh && echo '$PUB' >> /home/$u/.ssh/authorized_keys && chown $u:$u /home/$u/.ssh/authorized_keys && chmod 600 /home/$u/.ssh/authorized_keys"doneserver sh -c 'echo "AllowUsers ops dev2" > /etc/ssh/sshd_config.d/20-allow.conf'server sh -c 'sshd -t && systemctl reload ssh && echo "sshd درست بود"'for u in ops dev2 ali; do printf "%-5s: " $u; client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes -o ConnectTimeout=4 $u@$SIP 'echo وارد شد' 2>&1 | tail -1doneecho "--- لاگ سرور برای ali:"server journalctl -u ssh --no-pager -o cat | grep -E 'not allowed|AllowUsers' | tail -1 | cut -c1-110server sh -c 'rm -f /etc/ssh/sshd_config.d/20-allow.conf; systemctl reload ssh; userdel -r dev2 2>/dev/null; rm -rf /home/ali/.ssh/authorized_keys'sshd درست بودops : وارد شدdev2 : وارد شدali : ali@172.17.0.5: Permission denied (publickey).--- لاگ سرور برای ali:User ali from 172.17.0.2 not allowed because not listed in AllowUsersیک بررسی خودکار برای «سلامت امنیتی سرور» روی خود سرور بنویس که اینها را چک کند و خلاصه چاپ کند: PasswordAuthentication و PermitRootLogin در sshd -T، فعالبودن ufw، fail2ban و timer بهروزرسانی خودکار، و تعداد IP هایی که الآن بن شدهاند.
دیدن جواب
docker exec lxfresh sh -c 'chk() { if [ "$2" = "$3" ]; then echo "✓ $1 ($2)"; else echo "✗ $1: $2 (انتظار $3)"; fi; }chk "ورود با رمز" "$(sshd -T | awk "/^passwordauthentication/ {print \$2}")" "no"chk "ورود root" "$(sshd -T | awk "/^permitrootlogin/ {print \$2}")" "no"chk "ufw" "$(ufw status | awk "/^Status/ {print \$2}")" "active"chk "fail2ban" "$(systemctl is-active fail2ban)" "active"chk "timer بهروزرسانی" "$(systemctl is-active apt-daily-upgrade.timer)" "active"echo "IP های بنشدهی فعلی: $(fail2ban-client status sshd | awk "/Currently banned/ {print \$NF}")"'✓ ورود با رمز (no)✓ ورود root (no)✓ ufw (active)✓ fail2ban (active)✓ timer بهروزرسانی (active)IP های بنشدهی فعلی: 0آزمونک
Section titled “آزمونک”چرا با root کار روزمره نمیکنیم و یک کاربر sudo میسازیم؟
همچنین میشود دسترسی هر نفر را جدا گرفت.
پیش از بستن ورود با رمز در sshd مهمترین کار؟
اگر کلید درست کار نکند و نشستها را بسته باشی، فقط کنسول ارائهدهنده میماند.
PermitRootLogin no چه میکند؟
در لاگ ROOT LOGIN REFUSED ثبت میشود.
unattended-upgrades چه میکند؟
تنظیمش در /etc/apt/apt.conf.d/20auto-upgrades و 50unattended-upgrades.
fail2ban چه کاری انجام میدهد؟
با maxretry، findtime و bantime تنظیم میشود.
fail2ban میگوید «Currently banned: 1» ولی مهاجم هنوز وصل میشود. اول کجا را نگاه میکنی؟
علتها را از لاگ بخوان و همیشه از بیرون آزمایش کن.
جمعبندی
Section titled “جمعبندی”- سرور تازه یعنی پورت ۲۲ باز، رمز، root؛ رباتها بهسرعت میرسند. هشت قدم: بهروزرسانی ← کاربر
sudo← کلید SSH ← امنکردنsshd← فایروال ← بهروزرسانی خودکار ←fail2ban← آزمایش از بیرون. sshd:PermitRootLogin no،PasswordAuthentication no،MaxAuthTries 3؛ همیشه نشست باز + نشست دوم وsudo sshd -tقبل ازsudo systemctl restart ssh.- فایروال:
deny incoming، فقطOpenSSHو80/tcpو… ، اول SSH بعد enable. sudo apt install unattended-upgrades fail2ban؛fail2ban-client status sshdوunbanip؛ وقتی بن کار نمیکند، لاگ (/var/log/fail2ban.log) علت را میگوید؛ و تستهای خودت هم شمرده میشوند.- دفاع در عمق: هر لایه جلوی نوعی حمله را میگیرد؛ و همیشه از بیرون آزمایش کن، نه فقط از داخل.
| دستور | کاری که میکند |
|---|---|
sudo apt update && sudo apt upgrade -y | بهروزرسانی |
sudo adduser ops && sudo usermod -aG sudo ops | کاربر ادمین |
ssh-keygen -t ed25519 && ssh-copy-id ops@host | کلید و نصب آن |
sudo nano /etc/ssh/sshd_config.d/10-hardening.conf | تنظیم sshd |
sudo sshd -t && sudo systemctl restart ssh | بررسی و اعمال |
sudo ufw default deny incoming; sudo ufw allow OpenSSH; sudo ufw enable | فایروال |
sudo apt install unattended-upgrades fail2ban | بهروزرسانی خودکار و fail2ban |
sudo fail2ban-client status sshd | IP های بنشده |
sudo fail2ban-client set sshd unbanip IP | رفع بن |
nc -zv -w 2 host 22 / ssh -o PubkeyAuthentication=no host | آزمایش از بیرون |