بکاپ وردپرس روی سی‌پنل و تست بازگردانی؛ چرا بکاپِ آزموده‌نشده بکاپ نیست

امید بداق مشکلات سایت بدون دیدگاه ۲۲ دقیقه مطالعه

این مقاله عضو خوشهٔ «امنیت هاست و وردپرس» است و لایه‌ای را باز می‌کند که بقیهٔ مقاله‌های این خوشه روی آن می‌نشینند. امن‌سازی، احتمال آلودگی را پایین می‌آورد؛ بکاپ، هزینهٔ آلودگی را. دومی تنها چیزی است که وقتی اولی شکست خورد — و یک‌بار حتماً می‌خورد — میان تو و بازسازی از صفر می‌ایستد.

ادعای اصلی‌ام ناخوشایند ولی ساده است: بکاپ، تا لحظه‌ای که روی محیطی دورریختنی بازگردانی‌اش نکرده باشی و نتیجه را با چشم خودت ندیده باشی، بکاپ نیست؛ فقط یک فایل فشرده با تاریخِ امروز است. تفاوت این دو را در کارِ عملی بارها دیده‌ام: در یکی، فاجعه به یک روزِ کاریِ شلوغ تبدیل می‌شود؛ در دیگری، سایت برای همیشه می‌رود.

پاسخ کوتاه: امروز، به همین ترتیب

اگر فقط بیست دقیقه وقت داری، به همین ترتیب جلو برو:

  1. با دستور tar -tzf فهرستِ فایل‌های داخلِ بکاپِ آخرت را ببین. تا این فهرست را نبینی، نمی‌دانی بکاپت چه دارد.
  2. از دیتابیس یک خروجی تازه با mysqldump بگیر و پرچم‌های --single-transaction و --default-character-set=utf8mb4 در آن باشد.
  3. با sha256sum یک اثرِ انگشتی برای آرشیو بساز و همان را در کنارش نگه دار.
  4. یک نسخه را بیرون از حسابِ هاستت بگذار؛ جایی که با رمزِ پنلِ هاست حذف‌شدنی نیست.
  5. همین آرشیو را روی یک دامنهٔ یک‌بارمصرف بازگردانی کن.
  6. بعد از بازگردانی، شش چیز را چک کن: ورود، یک صفحهٔ درون‌خطی، ارسال فرم، بازشدن یک پیوندِ یکتای عمیق، شمارش رکوردها، و نبودِ هشدارِ محتوای آمیخته.
  7. تاریخِ همین تست را در یک فایل یادداشت بنویس. بکاپی که تاریخِ تستِ معتبر ندارد، در روزِ حادثه بهت نمی‌خورد.

بکاپ کامل حساب در سی‌پنل چه چیزهایی را واقعاً داخل دارد

سه خروجی با هم اشتباه می‌شوند و هر سه حرفهٔ دیگری می‌کنند. اول، بکاپ کامل حساب. این خروجی معمولاً در سطح مدیر سرور ساخته می‌شود، به‌شکل آرشیوی با نامِ cpmove-USER.tar.gz یا NAME.tar.gz، و در بسیاری از پلن‌های اشتراکی دکمهٔ «دریافت بکاپ کامل حساب» در پنل خاموش است؛ اگر آن را می‌بینی، یک دلیل خوب برای خوشحالی است و اگر نمی‌بینی فاجعه نیست، چون مسیرِ دستی را داری.

چیزی که در نسخهٔ استانداردِ این آرشیو هست: تمامِ پوشهٔ خانه، یعنی public_html با فایل‌های سایت، صندوق‌های پستی زیر ~/mail، تعریفِ حساب‌های پستی و فیلترها زیر ~/etc، فایلِ cron، و یک پوشهٔ mysql که داخلش خروجیِ هر دیتابیس به‌صورت sql.gz هست. بخشِ تنظیماتِ حساب هم معمولاً همراهش می‌آید: دامنه‌های فرعی و اضافی، مسیرهای نمادین، و قانونِ بازنویسیِ آدرس.

تنها راهِ صادقانه برای فهمیدنِ اینکه چه چیزی داخلِ آرشیو است، دیدنِ همان فهرست است:

tar -tzf cpmove-USER.tar.gz | awk -F/ '{print $2}' | sort | uniq -c | sort -rn | head
tar -tzf cpmove-USER.tar.gz | grep -E '/(mysql|mail|etc|dns)/' | head -20

خروجیِ اول، فهرستِ بالادستی می‌دهد و می‌فهمی کدام پوشه در آرشیو هست. خروجیِ دوم، سه نشانۀ مهم را جدا می‌کند. اگر پوشۀ dns را در فهرست ندیدی، یعنی ناحیۀ DNS در این آرشیو نیست و باید جدا نگهش داری. حدس زدن در این مورد مجاز نیست؛ بعضی پیکربندی‌ها ناحیه را بیرون از خانهٔ کاربر نگه می‌دارند و بعضی نه.

سه خروجی، سه سطحِ بازیابی

جدول زیر را نه به‌عنوان تئوری، به‌عنوان سندی بخوان که در روزِ حادثه مشخص می‌کند چه چیزی قابل برگشت است و چه چیزی نه:

| خروجی | فایل سایت | دیتابیس | صندوق و حساب پستی | cron و تنظیمات حساب | ناحیۀ DNS | بازگردانی روی حسابِ تازه | |—|—|—|—|—|—|—| | بکاپ کاملِ حساب در سطح مدیر سرور | بله | بله، به‌صورت sql.gz | فایل صندوق بله؛ بازسازیِ خودکارِ حساب‌ها فقط روی همان سرور | بله، در قالب فایلِ تنظیمات | بستگی به پیکربندی؛ با tar -tzf اثباتش کن | خودکار، اگر مقصد همان سرور باشد | | بکاپ پوشۀ خانه از پنل | بله | نه | فایل صندوق بله، تعریفِ حساب به‌صورتِ قابل‌بازگردانی نه | نه | نه | دستی، فایل‌به‌فایل | | tar از فایل‌ها و mysqldump از دیتابیس، دستی | فقط آنچه خودت انتخاب کرده‌ای | بله، کامل و قابل‌کنترل | اصلاً | اصلاً | اصلاً | دستی، ولی قابل‌اتکا روی هر میزبان |

سطرِ میانی را جدی بگیر: خروجیِ «پوشۀ خانه» دیتابیس را داخلش ندارد. یعنی سایتی که تمامِ محتوا، تنظیمات، کاربران و سفارش‌هایش در دیتابیس است، با آن آرشیو برنمی‌گردد. این یکی از رایج‌ترین اشتباه‌هاست که در عمل، یک حادثۀ کوچک را به از دست رفتنِ سایت تبدیل می‌کند.

چیزی که در هیچ‌کدام از این سه نیست

سه دسته را از اول حساب کن، چون با هیچ نوع بکاپِ سی‌پنلی برنمی‌گردند:

هرچه بیرون از حسابِ توست. دامنهٔ ثبت‌شده، سرویس DNS بیرونی، ایمیلِ میزبانی‌شده روی سرویسی جدا، صندوق‌هایی که فقط روی گوشی همگام می‌شوند، و دادهٔ هر سرویس بیرونی که با سایتت کار می‌کند. اگر مسیرِ پستی دامنه روی Remote Mail Exchanger تنظیم شده باشد، روی این سرور اصلاً صندوقی وجود ندارد که بکاپش را بگیری؛ و این را با دو دستورِ دو دقیقه‌ای می‌فهمی:

du -sh ~/mail ~/public_html/wp-content/uploads
ls ~/etc

اگر ~/mail خالی بود، ایمیل تو جای دیگری است و بکاپِ هاست هرگز آن را نجات نمی‌دهد.

ناحیۀ DNS. ناحیۀ DNS تعیین می‌کند که ترافیک و ایمیل اصلاً به کجا می‌رسند. اگر ناحیه را روی سرویسِ میزبانی‌شدهٔ بیرونی نگه داشته‌ای، بکاپِ حسابِ هاست هیچ نقشی در آن ندارد. اگر هم روی همان سرور است، در بکاپِ پوشۀ خانه نیست. در هر دو حالت، کاری که باید بکنی این است: فهرستِ رکوردها را به‌صورت متنِ تاریخ‌دار در جای دیگری نگه دار. دو دقیقه کار است و تنها نسخه‌ای است که به آن تضمین داری.

گواهی امنیتی. بعد از بازگردانی روی حسابی تازه، اغلب باید گواهی را دوباره صادر یا نصب کنی. این را جزو مراحلِ بازگردانی بنویس، نه غافلگیریِ بعد از آن؛ و بعد از بازگردانی حتماً بررسی کن که سایت روی https باز می‌شود و مسیرِ اجبارِ http به https هم کار می‌کند.

قانون 3-2-1 روی هاست اشتراکی، آن‌طور که شدنی است

قانون می‌گوید 3 نسخه، روی 2 رسانهٔ مختلف، و یکی خارج از مکان. روی هاست اشتراکی، دو نکته این قانون را عوض می‌کنند و اگر عوضشان نکنی، نسخهٔ کاغذی‌اش گمراه‌کننده است.

نکتۀ اول: نسخه‌ای که روی همان سرور میزبانی است، برای تو یک نسخهٔ قابل‌کنترل نیست. تو نه دسترسی به سخت‌افزار داری، نه به نسخه‌های قدیمیِ خودِ سرور، نه این حق که بگویی ساعتِ 3 بامداد چه اتفاقی افتاده است. این نسخه مفید است، ولی شمارۀ اولِ قانون نیست؛ باید دو نسخه داشته باشی که خودت نگاهشان می‌داری و خودت حذفشان می‌کنی.

نکتۀ دوم: «خارج از مکان» در عمل یعنی خارج از دامنۀ اعتبارِ یک حساب. بکاپی که در ~/backups روی همان حساب است و با همان رمزِ پنل و همان FTP نوشته و پاک می‌شود، از همان شکست‌هایی می‌خورد که سایت می‌خورد. حسابی که رمزِ آن لو رفته باشد، بکاپِ داخلِ حسابِ آن هم پاک می‌شود. پس حداقلِ واقعی این است: یک نسخه روی فضای میزبان، یک نسخه روی فضای بیرونی با اعتباری جدا، و یک نسخه که بیرونی‌بودنش را با اثرِ انگشتی ثابت می‌کنی.

چرا «همان حساب» جای نگه‌داری بکاپ نیست

شش شکستِ واقعی که فقط بیرونِ حساب را لازم می‌کنند، نه داخلش:

سقفِ فضا و سقفِ تعداد فایل. بکاپِ داخلِ حساب، سهمِ دیسک و inode تو را می‌خورد. نتیجۀ آن معمولاً عجیب‌تر از کمبودِ جا است: ایمیل نوشته نمی‌شود، فرم ارسال نمی‌شود، و سایت با خطای نوشتن می‌ایستد. یعنی خودِ بکاپ، سایت را می‌شکند.

تعلیقِ حساب. صورتحسابی که پرداخت نمی‌شود، اخطاری که بی‌پاسخ می‌ماند، یا گزارشِ هرزنامه‌ای که از حسابِ تو رفته. در تعلیق، دسترسی بسته می‌شود و همه‌چیز داخل حساب قفل می‌ماند؛ بکاپِ داخلِ حساب هم با تو قفل می‌ماند.

عملیاتِ گروهیِ اشتباه. یک حذفِ بازگشتی با مسیرِ غلط، یک جایگزینیِ گروهی، یا یک بازگردانیِ نصفه. این‌ها حمله نیستند؛ خودت هستی، و تو فقط به بکاپِ خارج از حساب دسترسی خواهد داشت.

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

میرفتنِ سرور. روی هاست اشتراکی، زنده‌ماندنِ داده در یک فاجعه به سیاستِ نسخه‌گیریِ خودِ سرویس‌دهنده بستگی دارد، و تو هیچ کنترلی رویش نداری و نباید هم داشته باشی.

تغییرِ سرویس‌دهنده. مهاجرتِ ناموفق، یا روزی که تصمیم می‌گیری بروی. در آن روز، تنها چیزی که ارزش دارد یک آرشیوِ خواناست، نه دسترسی به پنلِ حسابِ قدیمی.

قاعده‌ای که از این شش مورد می‌آید: نسخه‌ای را که با اعتبارِ پنلِ هاست قابل حذف است، «نسخۀ موقتِ کاری» بنام، نه بکاپ.

حجم، تعداد نسخه و مدتِ نگهداری

سؤالِ عملی این است: چند نسخه و چقدر قدیمی. پاسخش به دو متغیر بستگی دارد، نه به سلیقه: سرعتِ تغییرِ سایتت، و بازۀ زمانیِ میانِ آلوده‌شدن و فهمیدنِ آلودگی.

نردبانی که برای یک سایتِ معرفی یا فروشگاهِ کوچک جواب می‌دهد:

  1. 7 نسخۀ روزانه.
  2. 4 نسخۀ هفتگی.
  3. 6 نسخۀ ماهانه.
  4. هر نسخه‌ای که درست پیش از یک به‌روزرسانیِ افزونه یا قالب گرفته شده، تا وقتی که آن به‌روزرسانی خودش را ثابت کند؛ معمولاً دو هفته.

مهم‌ترین بندِ این فهرست، بندِ آخر است، و دلیلش ساده است: بدترین موقعیتِ ممکن، بازگرداندنِ نسخه‌ای است که همان افزونۀ تازه در آن نصب است و همان افزونه همان چیزی است که سایت را شکسته یا آلوده کرده.

دربارۀ بازۀ کشف: اگر تشخیصِ تو هفتگی و با چشم باشد، ممکن است آلودگی چند هفته در سایت نشسته باشد. در نتیجه نگه‌داشتنِ 10 نسخۀ روزانه کافی نیست؛ یک نسخۀ سالمِ قدیمیِ واقعی لازم است. اگر بکاپِ ماهانه‌ات را بعد از 60 روز دور می‌ریزی، عملاً فقط به نسخه‌هایی دسترسی داری که همه‌شان داخلِ پنجرۀ آلودگی‌اند. این تنها دلیلی است که نسخۀ ماهانه را در قانون 3-2-1 جدی می‌گیرد.

حجم را هم منطقی کم کن. چیزی که هر روز عوض می‌شود دیتابیس و فایل‌های بارگذاری‌شده است؛ چیزی که ماه‌ها دست‌نخورده می‌ماند، هسته، قالب و افزونه‌ها هستند. پس این ترکیب کار می‌کند: یک آرشیوِ پایهٔ طولانی‌مدت از کد، به‌علاوهٔ خروجیِ روزانۀ دیتابیس و یک بکاپِ افزوده از مسیر بارگذاری. ولی یک تله دارد: اگر ترکیبِ «کدام دیتابیس با کدام آرشیوی پایه» را جایی ننویسی، در روزِ حادثه دو تکه داری که با هم بالا نمی‌آیند. پس کنارِ هر آرشیو، یک فایلِ فهرست بگذار با تاریخ، حجم، و اثرِ انگشتیِ هر دو فایل.

بکاپ خودکار با cron، و اجرا با کاربرِ همان حساب نه روت

اسکریپتِ شبانه باید با کاربرِ خودِ حساب اجرا شود. دلیلش سه تاست: فایل‌هایی که روت می‌سازد را حساب نمی‌تواند حذف کند، پس چرخۀ حذفِ نسخه‌های قدیمی بی‌صدا شکست می‌خورد و دیسک پر می‌شود؛ نسخۀ حاویِ رمزِ دیتابیس که روت ساخته، مالکیت و دسترسی‌ای دارد که بعداً خودت را گیر می‌اندازد؛ و در صورت آلودگی، مهاجمِ سطحِ حساب دستش به فایل‌های روت نمی‌رسد ولی به فایل‌های قابل‌نوشتِ خودش می‌رسد.

یک اسکریپتِ قابل‌اتکا، همین شکل است:

#!/bin/sh
set -eu
HOME_DIR=/home/USER
BK_DIR=$HOME_DIR/offsite-staging
STAMP=$(date +%F)
mkdir -p "$BK_DIR"

/usr/bin/mysqldump --defaults-extra-file="$HOME_DIR/.mydump.cnf" \
  --single-transaction --quick --routines --triggers --events \
  --default-character-set=utf8mb4 DB_NAME \
  | gzip -9 > "$BK_DIR/db-$STAMP.sql.gz"

/usr/bin/tar -czf "$BK_DIR/files-$STAMP.tar.gz" \
  --exclude='public_html/wp-content/cache' \
  --exclude='public_html/wp-content/uploads/ltscache' \
  --exclude='public_html/wp-content/litespeed' \
  --exclude='public_html/.git' \
  --exclude='public_html/wp-content/backup-*' \
  -C "$HOME_DIR" public_html

cd "$BK_DIR"
sha256sum "db-$STAMP.sql.gz" "files-$STAMP.tar.gz" > "sums-$STAMP.txt"
stat -c '%s' "db-$STAMP.sql.gz" "files-$STAMP.tar.gz" >> "sums-$STAMP.txt"

find "$BK_DIR" -name 'db-*.sql.gz' -mtime +14 -delete
find "$BK_DIR" -name 'files-*.tar.gz' -mtime +30 -delete

چند نکته که اسکریپت را از نصفه‌کاره نجات می‌دهد:

رمز دیتابیس را در خطِ فرمان نگذار؛ در بخشِ [client] یک فایلِ ~/.mydump.cnf با دسترسی 600 بنویس، چون آرگومانِ خطِ فرمان در فهرستِ فرایندِ بقیه دیده می‌شود.

--single-transaction فقط برای میزهای InnoDB سازگار است. اگر میزی با موتورِ قدیمی داری، آن میز در حالتِ ناسازگار کپی می‌شود و تو نمی‌فهمی؛ یک‌بار بررسی کن:

SELECT table_name, engine FROM information_schema.tables
WHERE table_schema = 'DB_NAME' AND engine <> 'InnoDB';

خروجیِ خالی یعنی این خطِ بکاپ از نظرِ سازگاری تمیز است. اگر چیزی برگشت، یا موتور را عوض کن یا برای همان میزها قفلِ موقتِ جدا در نظر بگیر.

بیرون‌گذاشتنِ کشِ LiteSpeed و پوشۀ کشِ افزونه‌ها از آرشیو، حجم را چشمگیر پایین می‌آورد و هیچ ارزشِ بازیابی ندارد، چون بازسازی‌شدنی‌اند. دقت کن که الگویِ استثناها باید با نامِ عضوها داخلِ آرشیو بخورد، چون تطبیق روی همان نام‌ها انجام می‌شود؛ اگر --exclude تو چیزی را حذف نکرد، مقصر همین تفاوتِ نوشتارِ مسیر است.

خطِ cron را این‌طور بنویس، با مسیرِ مطلق و با لاگِ جدا:

  # crontab -e با کاربرِ همان حساب، نه با روت
17 3 * * * /bin/sh /home/USER/bin/nightly-backup.sh >> /home/USER/backup.log 2>&1
47 3 * * * /bin/sh -c 'ls /home/USER/offsite-staging/db-$(date +\%F).sql.gz >/dev/null 2>&1 || echo "MISSING $(date +\%F)" >> /home/USER/backup.log'

دو تلهٔ این بخش را جدی بگیر. اول: در خطِ cron، هر نویسهٔ ٪ باید با \ فرار کند، وگرنه با معنیِ «خطِ نو» تفسیر می‌شود و فرمان نصفه اجرا می‌شود. دوم: محیطِ cron با محیطِ شلِ تعاملی یکی نیست؛ PATH کوتاه‌تر است و متغیرِ خانه ممکن است تنظیم نباشد. پس نامِ کاملِ باینری و مسیرِ مطلق بنویس. مسیرِ درستِ mysqldump را در همان شل با یک دستورِ جستجو پیدا کن و همان را در اسکریپت ثابت کن.

دلیلِ خطِ دوم، مهم‌ترین درسِ این بخش است: شایع‌ترین علتِ فاجعه این نیست که بکاپ نگرفته شده، بلکه این است که بکاپی که «باید می‌بود» آن روز ساخته نشده بوده و هیچ‌کس نفهمیده. یک کارِ خودکارِ دوخطی که فقط بودِ خروجی را راستی‌آزمایی می‌کند، از نیمی از حادثه‌ها جلوگیری می‌کند.

دو فایلی که بازگردانی بی‌صدا برمی‌گرداند

بازگردانی، «فایل‌ها را می‌ریزد» و بعد می‌رود. دو فایل هست که همین ریختن، تغییراتِ تو را در آن‌ها پاک می‌کند و نشانهٔ خطا هم نمی‌دهد.

اول، wp-config.php. داخلش نامِ دیتابیس، کاربر، رمز، پیشوندِ جدول، ABSPATH، هشت کلیدِ نمک، و هر ثابتِ دستی است: DISALLOW_FILE_EDIT، WP_DEBUG، تنظیمِ WP_MEMORY_LIMIT، مسیرِ کش، و ثابت‌های کوکی. نسخۀ داخلِ بکاپ، رمز و نامِ دیتابیسِ روزِ بکاپ را دارد. اگر مقصدت نامِ دیتابیس یا کاربرِ دیگری دارد، اولین چیزی که می‌بینی پیامِ خطای اتصالِ دیتابیس است و دلیالش را در جای دیگری می‌گردی.

دوم، .htaccess و هر قانونِ دستی‌ای که در آن گذاشته‌ای: بستنِ اجرای PHP در مسیر بارگذاری، اجبارِ https، بازنویسیِ مسیرِ ورود، استثناهایِ مسیرِ بازگشتِ درگاه پرداخت، فهرستِ مسدودِ IP، و دستورهای کشی که افزونۀ کش خودش می‌نویسد. وقتی این فایل از بکاپ برمی‌گردد، ماه‌ها تنظیم‌کاریِ دستی ناپدید می‌شود و خرابی‌اش شبیۀ «فرم کار نمی‌کند» یا «سفارش پرداخت می‌شود ولی تأیید نمی‌رسد» ظاهر می‌شود، نه شبیه بازگردانی.

روشِ عملی که این دو فایل را از تخریبِ بی‌صدا نجات می‌دهد:

  # درست پیش از بازگردانی
cp public_html/wp-config.php /home/USER/keep/wp-config.before
cp public_html/.htaccess    /home/USER/keep/htaccess.before

  # درست پس از بازگردانی
diff -u /home/USER/keep/wp-config.before public_html/wp-config.php
diff -u /home/USER/keep/htaccess.before  public_html/.htaccess

هر اختلافی که diff نشان می‌دهد، یک تصمیم است، نه یک خطا. ثابت‌های امنیتی و کلیدهای نمک و مسیرِ دست‌ساز را دوباره بگذار؛ و چون کلیدهای نمک را دوباره تولید می‌کنی، نشست‌های قدیمی هم بی‌اعتبار می‌شوند که در بازگردانیِ پس از آلودگی دقیقاً همان چیزی است که می‌خواهی.

یک نکتۀ ترتیبی هم همین‌جا لازم است: اگر پس از بازگردانی با صفحۀ سفید یا خطای PHP روبه‌رو شدی، قبل از هر حدسِ دیگری، فایلِ PHP مشکوک را با php -l بسنج. خطای نگارشِ درجا، نشانهٔ فایلِ نیمه‌نقل‌شده است و بکاپ را بی‌دلیل متهم نکن.

ایمیل، ناحیۀ DNS و گواهی: سه لایه‌ای که بکاپِ فایل برنمی‌گرداند

حتی در بهترین حالت، بازگردانیِ فایل و دیتابیس، سه لایه را برنمی‌گرداند و باید دستی بازسازی شوند:

حساب‌های پستی. فایلِ صندوق‌ها چیزی است که داخل آرشیو می‌ماند؛ تعریفِ حساب، رمز، سهمیه و فیلتر، چیزی است که در یک حسابِ تازه باید دوباره ساخته شود. اگر بکاپِ کاملِ حساب نداری، فهرستِ دستیِ حساب‌ها را داشته باش، وگرنه در روزِ حادثه نمی‌دانی چه کسی باید ایمیل بگیرد.

ناحیۀ DNS و با آن SPF و DKIM و DMARC. رکوردهای احرازِ پستی در ناحیۀ DNS زندگی می‌کنند، نه در فایل‌های وردپرس. یعنی اگر ناحیه را از دست بدهی، بکاپِ کاملِ وردپرس، ایمیلِ تو را قابل‌ارسالِ معتبر نمی‌کند. متنِ رکوردها را جداگانه در یک فایلِ تاریخ‌دار نگه دار؛ روش و معنای هر سه را در مقالۀ مربوط به احرازِ پستی باز کرده‌ام.

گواهی و مسیرهای HTTPS. بعد از بازگردانی، گواهی را دوباره صادر یا نصب کن، بعد مسیرِ اجبار را تست کن، و بعد بررسی کن که دامنۀ کوکی روی نشانیِ درست قفل باشد. یک بازگردانی که در آن کوکی روی دامنۀ آزمایشی مانده باشد، کاربرِ عادی را بیرون می‌اندازد و نشانه‌اش فقط این است که «رمز درست را می‌زنم و دوباره به صفحۀ ورود برمی‌گردم».

تست بازگردانی روی یک دامنهٔ یک‌بارمصرف

تستِ واقعی یعنی: از همان آرشیویی که امروز تولید شده، روی محیطی که به سایتِ زنده ربطی ندارد، یک سایتِ بالاآمده بسازی. سه راه داری: زیردامنۀ آزمایشی روی حسابی که برای این کار نگه داشته‌ای، دامنهٔ ارزان و یک‌بارمصرف روی همان سرور، یا محیط محلی. هر سه را در مقالۀ محیطِ آزمایشی روی سی‌پنل مقایسه کرده‌ام؛ برای تستِ بکاپ، محیط محلی صادقانه‌ترین است، چون هیچ‌وقت تصادفاً به فایل‌های زنده دست نمی‌زنی.

دستورِ کار، به همین ترتیب:

mkdir -p /home/TESTUSER/restore-2026-11-09
tar -xzf files-2026-11-09.tar.gz -C /home/TESTUSER/restore-2026-11-09
gunzip -c db-2026-11-09.sql.gz > /home/TESTUSER/restore-2026-11-09/db.sql
mysql TEST_DB < /home/TESTUSER/restore-2026-11-09/db.sql

cd /home/TESTUSER/restore-2026-11-09/public_html
wp option update siteurl  https://TEST-HOST
wp option update home     https://TEST-HOST
wp search-replace 'https://OLD-HOST' 'https://TEST-HOST' --all-tables --skip-columns=guid --dry-run
wp search-replace 'https://OLD-HOST' 'https://TEST-HOST' --all-tables --skip-columns=guid
wp rewrite flush --hard

سه احتیاط که این مراحل را از خطرناک به بی‌خطر می‌آورد:

جایگزینیِ آدرس را با ابزاری انجام بده که دادهٔ سریال‌شده را می‌فهمد. REPLACE خام روی متنِ ستون‌ها، آرایه‌های سریال‌شده را می‌شکند و نتیجۀ آن ناپدید شدنِ ابزارک‌ها، فهرستِ منو و تنظیماتِ قالب است، بدون هیچ پیامی. اگر ابزارِ خطِ فرمان در دسترس نیست، افزونه‌ای که جایگزینیِ آگاه از سریال‌سازی انجام می‌دهد انتخاب کن؛ و همیشه اول --dry-run.

محیطِ تست را از ایندکس بیرون نگه دار: هم robots.txt و هم تگِ noindex، و اگر می‌توانی با رمزِ سطحِ وب‌سرور ببند. دامنهٔ یک‌بارمصرفی که ایندکس شود، محتوای تکراریِ سایتِ تو را روی وب می‌پراکند و بعداً تمیزکردنش کارِ تو است.

دیتابیس را با همان table_prefix بیاور که در wp-config.php نوشته‌ای؛ اگر در مقصد پیشوند را عوض کردی، یا فایل را عوض کن یا میزها را بازنام‌گذاری کن. این جفتِ ناهم‌خوان، در نگاهِ اول شبیۀ «بکاپ خراب است» به نظر می‌رسد.

شش چیزی که بعد از بازگردانی باید تأیید شود

«باز شد» با «درست برگشت» یکی نیست. این شش را یکی‌یکی چک کن، و نتیجه را با تاریخ بنویس. هر کدام نشانهٔ شکستِ خودش را دارد:

یک: ورود. رمزِ درست از دیتابیسِ بازگردانده‌شده باید کار کند. اگر نشست بی‌درنگ بسته می‌شود، کوکی یا دامنه غلط است. اگر پس از بازگردانی نتوانستی وارد شوی، رمز را در دیتابیس تغییر نده؛ اول table_prefix و سپس siteurl را راستی‌آزمایی کن.

دو: یک صفحۀ درون‌خطی. نه صفحۀ اول؛ یک صفحۀ میانی که تصویر دارد. صفحۀ اول کش می‌خورد، ولی صفحۀ درون‌خطی مسیر بارگذاری، دیتابیس و بازنویسی را با هم تست می‌کند. نشانهٔ شکست: تصویرِ شکسته یعنی فایل‌ها نیامده‌اند، متنِ بدونِ استایل یعنی مسیرِ فایل‌های قالب غلط است.

سه: یک فرم، تا انتها. ارسالِ فرم را فقط با نگاه به پاسخِ مرورگر قضاوت نکن. روی محیطِ تست، پذیرفته شدنِ پیام به معنی رسیدنش نیست. راهِ صادقانه: اول بررسی کن که ورودی در دیتابیس ثبت شده باشد، اگر افزونۀ تو ورودی نگه می‌دارد؛ و هم‌زمان یک ارسالِ واقعی را به صندوقی بسنج که دسترسی داری. اگر نمی‌رسد، دلیلش معمولاً سقفِ روزانۀ ارسال یا مسیرِ SMTP است، نه خودِ فرم.

چهار: پیوندِ یکتای عمیق. یک آدرسِ درون‌خطی را مستقیم باز کن:

curl -s -o /dev/null -w '%{http_code}\n' https://TEST-HOST/namaune-nevishate-derونی/

اگر 404 گرفتی، بازنویسیِ مسیرها تازه نشده یا .htaccess از جای درست خوانده نمی‌شود. این یکی از رایج‌ترین نشانه‌های بازگردانیِ نیمه است و به‌راحتی «بکاپ ناقص» تفسیر می‌شود.

پنج: شمارشِ رکوردها. با چشم دیدنِ صفحه‌ها، حجمِ داده را اثبات نمی‌کند:

SELECT post_type, COUNT(*) FROM wp_posts GROUP BY post_type;
SELECT COUNT(*) FROM wp_comments;
SELECT COUNT(*) FROM wp_users;

همین سه شمارش را روی سایتِ زنده هم بگیر و دو طرف را مقایسه کن. اختلافِ عدد یعنی یا دامپ ناقص بوده یا واردکردن وسط کار قطع شده. نسخۀ دیتابیسی که شمارشش با مبدأ نمی‌خواند، در روزِ حادثه یک فاجعۀ دوم است.

شش: نبودِ هشدارِ محتوای آمیخته. اگر دامپ آدرسِ قدیمی را همه‌جا جایگزین نکرده باشد، بخشی از منابع روی http می‌مانند:

curl -s https://TEST-HOST/ | grep -o 'src="http://' | wc -l
curl -s https://TEST-HOST/ | grep -o 'href="http://TEST-HOST' | head

خروجیِ صفر تمیز است. هر عددی غیر از صفر یعنی هنوز آدرسِ کهنه در محتوا یا در تنظیماتِ قالب مانده، و در مرورگرِ واقعی با هشدار و با مسدود شدنِ منبع مواجه می‌شوی.

یک هفتمی هم اضافه کن که کمتر کسی چک می‌کند: تاریخ‌ها و منطقۀ زمانی. اگر تنظیمِ زمان در مقصد فرق کند، تاریخِ نوشته‌ها و نمایشِ شمسی جابه‌جا می‌شود و این خرابی در تستِ سریع از چشم در می‌رود.

خودِ آرشیو را با اثرِ انگشتی بسنج، قبل و بعد از انتقال

اثرِ انگشتی تنها چیزی است که ثابت می‌کند فایلی که منتقل کرده‌ای همان فایلی است که ساخته‌ای. یک انتقالِ نیمه، یک دیسکِ پر، یا اسکریپتی که وسط کار مرده باشد، آرشیوِ نصفه تحویلت می‌دهد که tar هم تا لحظۀ استخراج نمی‌گوید چیزی غلط است.

cd /home/USER/offsite-staging
sha256sum files-2026-11-09.tar.gz | tee files-2026-11-09.tar.gz.sha256
  # در مقصد، پس از کپی
sha256sum -c files-2026-11-09.tar.gz.sha256
tar -tzf files-2026-11-09.tar.gz > /dev/null && echo ARCHIVE_OK

سه نکتۀ دقیق:

مقایسه را بین دو آرشیوی که جدا ساخته شده‌اند نکن. خروجیِ gzip برای محتوایِ یکسان، بایت‌به‌بایت بازتولیدشدنی نیست، چون زمان و گاه نامِ فایل در سرِ آرشیو می‌نشیند. پس اثرِ انگشتیِ «همان فایلی که ساخته‌ای» را نگه دار و همان را در مقصد بسنج، نه همتایش را.

tar -tzf روی آرشیوی که فشرده‌سازی‌اش قطع شده، معمولاً بخشی از فهرست را نشان می‌دهد و بعد بی‌صدا می‌ایستد. به‌همین‌دلیل خطِ آخر را با بازگشتِ مقدارِ موفقیت نوشته‌ام.

اثرِ انگشتی را هم بیرون از حسابِ هاست نگه دار. اگر آرشیو و فایلِ فهرستش هم‌جا باشند، مهاجمی که به هر دو دسترسی دارد می‌تواند جفت را با هم عوض کند و چیزی که باقی می‌ماند یک دروغِ مرتب است.

اگر بکاپ آلوده باشد: چطور تاریخِ امن را انتخاب کنی

این بخشی است که بیشترِ راهنماهای بکاپ از آن می‌گذرند، و دقیقاً همان‌جا است که سایت‌ها دوباره می‌میرند. فرض کن سایت آلوده شده و نسخه‌های تو موجودند. بازگردانیِ اولین نسخه‌ای که سالم به نظر می‌رسد، سه اشتباهِ رایج دارد.

اشتباهِ اول: اعتماد به تاریخِ تغییرِ فایل. دستِ مهاجم تاریخِ فایل را هم عوض می‌کند، و بسیاری آسیب‌ها ماه‌ها بی‌صدا می‌مانند. تاریخِ تغییر، کفِ سنِ آلودگی است نه واقعیتش. راهِ درست: پنجرۀ آلودگی را از شواهدِ مستقل بساز، نه از فایل‌ها. سه شاهدِ قابل‌اتکا:

  1. نخستین درخواستِ مشکوک در لاگِ دسترسی به مسیری که در سایتت وجود ندارد.
  2. نخستین پیامِ ارسالیِ بی‌محتوا یا نخستین شکایتِ میزبان، با تاریخِ دقیق.
  3. نخستین باری که فهرستِ اثرِ انگشتیِ هفتگی، که در مقالۀ امن‌سازی ساختم، اختلافِ بی‌دلیل نشان داد.

قدمِ بعد: نسخه‌ای را انتخاب کن که یک دوره قبل از نخستین شاهد است، نه «یک روز قبل». اگر شاهدِ اولت 14 آبان است، نسخۀ 13 آبان داخلِ پنجره است، چون آلودگی ممکن است سه هفته زودتر نشسته باشد.

اشتباهِ دوم: بازگرداندنِ آرشیوِ آلوده بدونِ اسکن. آرشیوِ انتخابی را در یک مسیرِ قرنطینه، بیرون از public_html باز کن و همان بررسی‌های ساختاریافته را رویش اجرا کن، پیش از آن‌که چیزی را روی حسابِ فعال برگردانی:

mkdir -p /home/USER/quarantine
tar -xzf files-2026-10-14.tar.gz -C /home/USER/quarantine
grep -rlE 'eval\(base64_decode|gzinflate\(' /home/USER/quarantine/public_html/wp-content | head
gunzip -c db-2026-10-14.sql.gz | grep -c 'iframe'
gunzip -c db-2026-10-14.sql.gz | grep -c "s:13:\"administrator\""

سه خطِ آخر برای دیتابیس است و ضرورتش را جدی بگیر: آلودگی‌های امروزی، محتوای تزریق‌شده، کاربرِ مدیرِ تازه و کلیدِ گزینۀ مخرب را در دیتابیس نگه می‌دارند. یعنی بازگرداندنِ فایلِ پاک با دیتابیسِ همان نسخه، آلودگی را برمی‌گرداند؛ و برعکسش هم درست است: پاک‌سازیِ فایل‌ها بدونِ دیتابیس درمان نیست.

اشتباهِ سوم: بازگرداندنِ همان درِ ورود. نسخۀ بکاپ، همان نسخۀ آسیب‌پذیرِ افزونه را برمی‌گرداند، همان کلیدهای نمکِ قدیمی را، همان رمزِ مدیرِ دیتابیس را و همان فایلِ تنظیماتِ ضعیف را. نتیجه: مهاجم چند ساعت بعد برمی‌گردد و تو نتیجه می‌گیری «بکاپ بد بود». حلقه را این ترتیب می‌شکند:

  1. هسته، قالب و هر افزونه را از منبعِ رسمیِ خودش بازسازی کن؛ نه از بکاپ.
  2. محتوا و داده را از دیتابیسِ پیش از پنجره برگردان.
  3. رمزِ حسابِ مدیر، رمزِ پنل، رمزِ دیتابیس و کاربرِ دیتابیس را عوض کن.
  4. کلیدهای نمک را تازه تولید کن تا نشست‌های دزدیده‌شده بی‌اعتبار شوند.
  5. cron حساب را بخوان؛ یک خطِ زمان‌بندیِ ناشناس که فایلِ داخلِ خانه را اجرا می‌کند، توضیحِ اصلیِ «پاک کردم و برگشت» است.
  6. فهرستِ اثرِ انگشتیِ پایه را از نو بساز تا هفتۀ بعدی مقایسه‌ات معنا داشته باشد.

و هشدارِ ساختاری: اگر نسخه‌ها داخلِ همان حساب بمانند، آلودگی می‌تواند رویشان بنشیند. تنها نسخه‌ای که در روزِ آلودگی بهت می‌خورد، نسخه‌ای است که مهاجمِ سطحِ حساب نتوانسته به آن بنویسد. ترتیبِ کاملِ بازگردانی پس از هک را در مقالۀ جدا در همین خوشه آورده‌ام؛ این‌جا نکته‌اش این بود که کدام تاریخ انتخاب شود و چرا.

چرخۀ ماهانه در بیست دقیقه

بکاپ یک عادت است، نه یک نصبِ یک‌باره. چرخه‌ای که در عمل نگه‌داشتنش شدنی است:

  1. crontab -l را نگاه کن: خطِ بکاپ هنوز هست و مسیرها عوض نشده‌اند.
  2. ls -lt روی پوشۀ خروجی: تازه‌ترین فایل باید دیروز باشد.
  3. یک sha256sum -c روی آخرین نسخۀ بیرونی.
  4. df -h و شمارشِ inode: اگر سهمِ دیسک به 80 درصد رسیده، چرخۀ حذف را بازنویسی کن.
  5. du -sh ~/mail: این عدد، حجمِ ایمیلِ از دست رفتنی‌ات را بهت یادآوری می‌کند.
  6. یک تستِ کوتاهِ بازگردانی: این‌بار لازم نیست کل سایت را بالا بیاوری؛ استخراجِ یک آرشیو و بازکردنِ سه مسیرِ بالا، دو دقیقه وقت می‌گیرد.
  7. یادداشتِ تاریخِ تست در فایلِ چرخه. سطرِ بدونِ تاریخ، در جلسۀ بعدیِ بررسی قابلِ استناد نیست.

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

کجا این روش کافی نیست

سه موقعیت که روشِ بالاتر پیش‌نیاز است، نه پاسخ:

دسترسی روت داری. آن‌وقت موضوعِ دیگری است: نسخۀ منطقی از ابزارِ خودِ موتور، نسخۀ فایل‌سیستمی، و کپیِ افزوده. بکاپِ سطحِ پنل، زیرساختِ سرور را پوشش نمی‌دهد.

دادهٔ مالی یا دادهٔ شخصی نگه می‌داری. در این حالت مسئله، بازگردانی نیست؛ نگهداری، محدود کردنِ دسترسی و حذفِ منظم است. یک آرشیوِ قدیمیِ پر از آدرسِ پستی و تلفن، خودش یک ریسک است، نه فقط یک بیمه‌نامه.

چند سایت روی یک حساب. یک بکاپِ ناقصِ یکی از سایت‌ها، تو را در روزِ حادثه مجبور می‌کند بین از دست دادنِ یکی و از دست دادنِ همه انتخاب کنی. برای هر سایت، مسیر، دیتابیس و نسخۀ جدا.

جمعِ همه‌اش در یک جمله: معیارِ بکاپ بودن، نه حجمِ آرشیو است نه تازگیِ تاریخش، بلکه یک بازگردانیِ تاریخ‌دار و آزموده‌شده است. تا آن را نداشته باشی، هر کاری که در این مقاله خواندی روی یک ستونِ چوبی ایستاده است.

مقالبی بخوانی

من متخصص سئو که عاشق اینه به کسب‌وکارها کمک کنه تو گوگل دیده بشن!از وردپرس گرفته تا سئو، هرچیزی که برای رشد آنلاین نیاز داری میتونم کمکت کنم.

دیدگاه خود را بنویسید