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

پروژه: راه‌اندازی یک سرور تازه

توی این درس یاد می‌گیری همه‌ی آنچه در دوره دیدی را روی یک سرور تازه کنار هم بگذاری. سرور تازه معمولاً فقط یک کاربر 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 “مثال‌های عملی (قدم‌به‌قدم)”

برای اینکه واقعاً «یک سرور تازه» باشد، برای این درس یک ماشین systemd‌دار تمیز (server) از نو می‌سازم که هیچ‌کدام از تنظیمات امنیتی را ندارد؛ و لپ‌تاپ تو یک ماشین جدا (client) است که از بیرون به آن وصل می‌شود. چون دو ماشین جدا هستند، دستورها را از بیرون با docker exec می‌زنم؛ مثل درس فایروال: server یعنی «روی سرور اجرا کن» و client یعنی «روی لپ‌تاپ». روی سرور واقعی خودت server را نمی‌نویسی و همان دستورها را (با sudo) در ssh می‌زنی.

Terminal window
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 status
echo "--- پورت‌های باز:"
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) روی:
server
Ubuntu 24.04.5 LTS
--- تنظیم‌های ssh در حالت پیش‌فرض:
port 22
maxauthtries 6
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication yes
--- فایروال:
Status: inactive
--- پورت‌های باز:
0.0.0.0:22 [::]:22

وضع پیش‌فرض: ورود با رمز مجاز (passwordauthentication yes)، ورود root با کلید مجاز، فایروال خاموش و پورت ۲۲ باز. حالا چک‌لیست را اجرا می‌کنیم. (permitrootlogin without-password همان prohibit-password است: root با رمز نه، ولی با کلید بله.)

قدم ۱: به‌روزرسانی سیستم

Section titled “قدم ۱: به‌روزرسانی سیستم”

اولین کار روی هر سرور تازه: بسته‌ها را به آخرین وصله‌ها برسان (درس apt). اول ببین چه چیزی به‌روز می‌شود (-s = شبیه‌سازی)، بعد واقعاً:

Terminal window
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):

Terminal window
server() { docker exec lxfresh "$@"; }
PW=lab-pass
server adduser --disabled-password --gecos "" ops > /dev/null
server usermod -aG sudo ops
server sh -c "echo ops:$PW | chpasswd"
server id ops
echo "--- ops چه اجازه‌هایی دارد؟"
server sudo -l -U ops | tail -2
خروجی
uid=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) استفاده می‌کنم تا صفحه‌ی واقعی را ببینی:

Terminal window
server() { docker exec lxfresh "$@"; }
client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }
SIP=$(server hostname -I | awk '{print $1}')
PW=lab-pass
cs() { 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_proj
echo "--- کلید عمومی (این را می‌شود به همه نشان داد):"
client cat /home/ali/.ssh/lx_proj.pub | cut -c1-70
client tmux -u new-session -d -s c -x 100 -y 30 "bash --norc"; sleep 1
client tmux send-keys -t c "ssh-copy-id -i ~/.ssh/lx_proj.pub -o StrictHostKeyChecking=accept-new ops@$SIP" Enter; sleep 3
client tmux send-keys -t c "$PW" Enter; sleep 3
cs | grep -E 'Number of key|password:'
echo "--- ورود با کلید (بدون رمز):"
client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes ops@$SIP 'whoami; hostname'
خروجی
--- کلید عمومی (این را می‌شود به همه نشان داد):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFY91u7PspvLHYxDZTdFX2hsgxR9OjIkAC
ops@172.17.0.5's password:
Number of key(s) added: 1
--- ورود با کلید (بدون رمز):
ops
server

Number of key(s) added: 1 و ورود بعدی بدون رمز. (جزئیات: درس SSH.) ولی هنوز ورود با رمز هم مجاز است؛ تا آن را نبندیم، کلید فقط یک راه اضافه است، نه یک قفل.

حالا ورود با رمز و ورود مستقیم root را می‌بندیم. این قدم خطرناک‌ترین قدم پروژه است: اگر اشتباه کنی خودت را بیرون می‌اندازی. پس یک نشست باز را نگه می‌دارم (همان ssh با کلید) و با آن فایل تنظیمات را ویرایش می‌کنم (sudo nano + رمز sudo)، و بعد در یک نشست دوم آزمایش می‌کنم. تنظیمات را در یک فایل جدا در sshd_config.d می‌نویسم (بهتر از ویرایش فایل اصلی /etc/ssh/sshd_config؛ هر دو کار می‌کنند):

Terminal window
server() { docker exec lxfresh "$@"; }
client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }
SIP=$(server hostname -I | awk '{print $1}')
PW=lab-pass
cs() { client tmux capture-pane -t c -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
client tmux send-keys -t c "clear" Enter
client tmux send-keys -t c "ssh -i ~/.ssh/lx_proj ops@$SIP" Enter; sleep 3
client tmux send-keys -t c "sudo nano /etc/ssh/sshd_config.d/10-hardening.conf" Enter; sleep 2
client tmux send-keys -t c "$PW" Enter; sleep 2
client tmux send-keys -t c -l "PermitRootLogin no"; client tmux send-keys -t c Enter
client tmux send-keys -t c -l "PasswordAuthentication no"; client tmux send-keys -t c Enter
client tmux send-keys -t c -l "MaxAuthTries 3"; client tmux send-keys -t c Enter; sleep 1
cs | head -5
خروجی
GNU nano 7.2 /etc/ssh/sshd_config.d/10-hardening.conf *
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3

سه خط نوشتم:

تنظیم معنی
PermitRootLogin no ورود مستقیم با root ممنوع (حتی با کلید)
PasswordAuthentication no ورود با رمز ممنوع؛ فقط کلید
MaxAuthTries 3 بعد از ۳ تلاش ناموفق در یک اتصال، قطع کن

ذخیره می‌کنم (Ctrl+O، Enter، Ctrl+X)، قبل از اعمال بررسی نحو را می‌زنم و بعد sshd را ریستارت می‌کنم:

Terminal window
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 1
client tmux send-keys -t c "clear" Enter
client tmux send-keys -t c "sudo sshd -t && sudo systemctl restart ssh && echo 'sshd درست بود و ریستارت شد'" Enter; sleep 3
cs
خروجی
ops@server:~$ sudo sshd -t && sudo systemctl restart ssh && echo 'sshd درست بود و ریستارت شد'
sshd درست بود و ریستارت شد
ops@server:~$

sshd -t بدون خروجی یعنی نحو درست است؛ فقط بعد از آن ریستارت. نشست فعلی با ریستارت قطع نشد (اتصال‌های برقرار می‌مانند). حالا آزمایش در یک نشست دوم (نشست اول را باز می‌گذارم تا اگر چیزی خراب بود راه برگشت داشته باشم):

Terminal window
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>&1
echo "--- ۳) ورود 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>&1
echo "--- لاگ سرور چه می‌گوید؟"
server journalctl -u ssh --no-pager -o cat | grep -E 'ROOT LOGIN' | tail -1 | cut -c1-80
server rm -rf /root/.ssh
echo "--- تنظیم مؤثر:"
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 3
permitrootlogin no
passwordauthentication no

هر سه آزمایش درست درآمد: کلید کار می‌کند، رمز رد می‌شود (Permission denied (publickey)) و ورود root رد می‌شود و در لاگ سرور هم ثبت می‌شود (ROOT LOGIN REFUSED)، حتی وقتی کلید معتبر روی root گذاشتم. (آن فایل authorized_keys برای root را فقط برای آزمایش گذاشتم و پاک کردم.)

حالا فقط پورت‌هایی که لازم است: SSH و (برای وب‌سایت‌ها) HTTP. اول سیاست پیش‌فرض و اول SSH، بعد روشن‌کردن (درس فایروال). برای آزمایش از بیرون، یک وب‌سرور واقعی (nginx) روی پورت ۸۰ و یک سرویس تستی فراموش‌شده روی ۸۰۹۹ بالا می‌آورم:

Terminal window
server() { docker exec lxfresh "$@"; }
client() { docker exec -u ali -e TERM=xterm lxlab "$@"; }
SIP=$(server hostname -I | awk '{print $1}')
server systemctl start nginx
server sh -c 'python3 -m http.server 8099 --bind 0.0.0.0 > /dev/null 2>&1 &'
sleep 1
echo "--- قبل از فایروال، از لپ‌تاپ:"
for p in 22 80 8099; do client nc -zv -w 2 $SIP $p 2>&1; done
server ufw default deny incoming
server ufw default allow outgoing
server ufw allow OpenSSH
server ufw allow 80/tcp
server ufw --force enable
server ufw status verbose
echo "--- بعد از فایروال، از لپ‌تاپ:"
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 updated
Rules updated (v6)
Rules updated
Rules updated (v6)
Firewall is active and enabled on system startup
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
22/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 هایی را که چند بار شکست خورده‌اند موقتاً می‌بندد:

Terminal window
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-upgrades
echo "--- منابع مجاز برای نصب خودکار (خط‌های // کامنت‌اند):"
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-110
خروجی
Processing 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 ACTIVATES
Sun 2026-10-04 23:38:09 UTC 15h - - apt-daily.timer apt-daily.service
Mon 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)، من فقط سخت‌گیرانه‌ترش می‌کنم (۳ شکست در ۱۰ دقیقه، ۱۰ دقیقه بن):

Terminal window
server() { docker exec lxfresh "$@"; }
server sh -c 'cat > /etc/fail2ban/jail.d/10-ssh.local <<EOC
[sshd]
enabled = true
maxretry = 3
findtime = 10m
bantime = 10m
EOC
systemctl enable --now fail2ban > /dev/null 2>&1
systemctl restart fail2ban'
sleep 3
echo "--- وضعیت زندان 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 که عمداً رد شدند) را هم شمرد:

Terminal window
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:55
INFO [sshd] Found 172.17.0.2 - 2026-10-04 07:50:55
INFO [sshd] Found 172.17.0.2 - 2026-10-04 07:50:55
NOTICE [sshd] Ban 172.17.0.2

سه بار Found و بعد Ban: fail2ban نمی‌داند «آزمایش» و «حمله» فرقی دارند؛ هر شکستِ یک IP می‌شمارد. این درس یک قاعده‌ی عملی دارد: بعد از روشن‌کردن fail2ban، تست‌های خودت هم تو را بن می‌کنند (در قدم ۸ همین را دوباره می‌بینی).

قدم ۷: آزمایش fail2ban از بیرون (و یک عیب واقعی)

Section titled “قدم ۷: آزمایش fail2ban از بیرون (و یک عیب واقعی)”

fail2ban می‌گوید لپ‌تاپ بن است. پس اگر درست کار کند، پورت ۲۲ باید از لپ‌تاپ بسته باشد. همیشه از بیرون امتحان کن، نه فقط از روی گزارش خود ابزار:

Terminal window
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 -3
echo "--- پورت 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 موقع بن‌کردن چه خطایی دیده:

Terminal window
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 127
INFO HINT on 127: "Command not found". Make sure that all commands
--- آیا ابزار nft هست؟
nft نصب نیست

nft: not found و returned 127 (کد ۱۲۷ یعنی «دستور پیدا نشد»): fail2ban برای بستن یک IP دستور nft (فایروال nftables) را اجرا می‌کند، ولی این ماشین کم‌حجم آن را ندارد (روی بیشتر سرورها نصب است). علت از لاگ پیدا شد. درستش می‌کنم: اول بن ناقص قبلی را برمی‌دارم، nftables را نصب و fail2ban را ریستارت می‌کنم؛ و این بار حمله‌ی واقعی می‌زنم: چهار بار ورود با یک کاربر وجودنداشته (کاری که ربات‌ها همیشه می‌کنند):

Terminal window
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/null
server sh -c 'DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nftables > /dev/null 2>&1; systemctl restart fail2ban'
sleep 3
for i in 1 2 3 4; do client ssh -o BatchMode=yes -o ConnectTimeout=3 nosuchuser@$SIP true 2>&1 | tail -1; done
sleep 3
echo "--- وضعیت:"
server fail2ban-client status sshd | tail -3
echo "--- پورت 22 از لپ‌تاپ:"
client nc -zv -w 3 $SIP 22 2>&1
echo "--- قاعده‌ی 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 (۱۰ دقیقه) خودکار باز می‌شود؛ برای رفع بن زودتر:

Terminal window
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
client nc -zv -w 3 $SIP 22 2>&1
echo "--- ورود قانونی با کلید دوباره کار می‌کند:"
client ssh -i /home/ali/.ssh/lx_proj -o BatchMode=yes ops@$SIP 'echo خوش آمدی $(whoami)'
خروجی
1
Connection to 172.17.0.5 22 port [tcp/ssh] succeeded!
--- ورود قانونی با کلید دوباره کار می‌کند:
خوش آمدی ops

عدد 1 که unbanip چاپ کرد یعنی یک IP از بن درآمد.

نکته‌ی مهم: اگر خودت چند بار اشتباه بزنی بن می‌شوی (همین بلا سر لپ‌تاپ ما با تست‌های قدم ۴ آمد). اگر IP ثابت داری (دفتر) آن را در ignoreip در jail.local بگذار، و راه برگشت (کنسول ارائه‌دهنده) را آماده نگه دار. و چون ورود با رمز بسته است، ربات‌ها اصلاً تا مرحله‌ی «حدس رمز» نمی‌رسند؛ fail2ban فقط تلاش‌های بی‌فایده را زودتر قطع می‌کند و لاگ را تمیز نگه می‌دارد.

⚡ بررسی سریع

چرا بعد از روشن‌کردن fail2ban ممکن است IP خودت بن شود، حتی اگر حمله‌ای نکرده باشی؟

قدم ۸: آزمایش نهایی از بیرون

Section titled “قدم ۸: آزمایش نهایی از بیرون”

آخرین قدم همان است که هر چک‌لیست خوبی دارد: از بیرون ثابت کن آنچه ساخته‌ای واقعاً کار می‌کند. یک اسکریپت چک روی لپ‌تاپ بنویس و نتیجه را ببین:

audit.sh
#!/bin/bash
# آزمایش نهایی امنیت سرور (روی لپ‌تاپ اجرا می‌شود)
SIP=$1; KEY=$2
ok() { 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 بسته"; done
echo "۲) پورت‌های غیرلازم بسته:"
for p in 23 3306 8099; do nc -z -w 2 $SIP $p 2>/dev/null && bad "پورت $p باز است!" || ok "پورت $p بسته"; done
echo "۳) ورود با کلید:"
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 رد شد"
Terminal window
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.sh
client bash /tmp/audit.sh $SIP /home/ali/.ssh/lx_proj
echo "--- و سمت سرور:"
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: active
fail2ban: 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، و بازبینی دوره‌ای لاگ‌ها و کاربرها.

دستور کار
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 “۱) بستن رمز قبل از آزمایش ورود با کلید”

اگر PasswordAuthentication no را بگذاری و کلیدت درست نصب نشده باشد، دیگر راه ورودی نیست. راه‌حل: قاعده‌ی طلایی قدم ۴: نشست باز + نشست دوم + sshd -t.

۲) ساختن کاربر sudo بدون رمز

Section titled “۲) ساختن کاربر sudo بدون رمز”
Terminal window
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 required

sudo: a password is required: کاربر در گروه sudo است ولی رمزی ندارد که sudo بپرسد. راه‌حل: حتماً رمز بگذار (sudo passwd نام)؛ NOPASSWD را برای آدم‌ها استفاده نکن.

۳) روشن‌کردن فایروال بدون SSH

Section titled “۳) روشن‌کردن فایروال بدون SSH”

درس فایروال: ufw allow OpenSSH قبل از enable.

چند تلاش اشتباه از 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 را برای همیشه نگذار.

✎ تمرینآسان

وضع نهایی سرور را بخوان: چه کسی روی چه پورت‌هایی گوش می‌دهد، و کدام قاعده‌های فایروال فعال‌اند.

دیدن جواب
Terminal window
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 Anywhere
80/tcp ALLOW Anywhere
✎ تمرینمتوسط

تمرین اصلی: یک VPS یا ماشین مجازی تازه را از صفر ایمن کن. (روی ماشین واقعی خودت همین هشت قدم را بزن.) برای همین سرور آزمایشی: یک کاربر ادمین دوم (dev2) با کلید خودش بساز و با AllowUsers ops dev2 در sshd ورود را به این دو نفر محدود کن؛ ثابت کن کاربر ali (که روی سرور هست ولی مجاز نیست) حتی با کلید معتبر وارد نمی‌شود.

دیدن جواب
Terminal window
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/null
PUB=$(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"
done
server 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 -1
done
echo "--- لاگ سرور برای ali:"
server journalctl -u ssh --no-pager -o cat | grep -E 'not allowed|AllowUsers' | tail -1 | cut -c1-110
server 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 هایی که الآن بن شده‌اند.

دیدن جواب
Terminal window
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
؟ آزمونک
  1. چرا با root کار روزمره نمی‌کنیم و یک کاربر sudo می‌سازیم؟

  2. پیش از بستن ورود با رمز در sshd مهم‌ترین کار؟

  3. PermitRootLogin no چه می‌کند؟

  4. unattended-upgrades چه می‌کند؟

  5. fail2ban چه کاری انجام می‌دهد؟

  6. fail2ban می‌گوید «Currently banned: 1» ولی مهاجم هنوز وصل می‌شود. اول کجا را نگاه می‌کنی؟

  • سرور تازه یعنی پورت ۲۲ باز، رمز، 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 sshdIP های بن‌شده
sudo fail2ban-client set sshd unbanip IPرفع بن
nc -zv -w 2 host 22 / ssh -o PubkeyAuthentication=no hostآزمایش از بیرون