الطلب الذي أجبتَ عنه صار الآن سجلًّا تحتفظ به
كل ما عدا هذا في هذه السلسلة يتّجه إلى الخارج: أخفِ المصدر، وشفِّر القرص، واختر الولاية القضائية، وامنع شخصًا آخر من الاحتفاظ بسجلّ عنك. أما هذا الدليل فيتّجه نحو الجهاز نفسه، لأن الخادم الذي يُجيب عن الطلبات يكتب مَن أرسلها — بشكل افتراضي، في عدّة أماكن في آنٍ واحد، بمدة احتفاظ لم يخترها أحد.
انظر إلى ما تحتفظ به صورة قياسية (stock image) بعد أسبوعين من النشر. يحتوي /var/log/nginx/access.log على سطر واحد لكل طلب: العنوان، والطابع الزمني، والمسار، والمُحيل (referrer)، وسلسلة وكيل المستخدم، يُدوَّر يوميًا ويُحتفَظ به أربعة عشر يومًا. يحتوي /var/log/auth.log على كل جلسة SSH، بما في ذلك عنوان المصدر لكل جلسة من جلساتك أنت. ويحمل سجلّ journald الأحداث نفسها مجدَّدًا، مضافًا إليها كل ما طبعته خدماتك إلى stderr. وإذا كان Docker يعمل، فلكل حاوية ملفّ سجلّ بصيغة JSON لا يحمل، في وضعه الافتراضي، أي حدّ لحجمه على الإطلاق. وإذا كان fail2ban يعمل، فله سجلّ بكل عنوان حجبه على الإطلاق، وقاعدة بيانات SQLite تقول الشيء نفسه، والعناوين في كليهما كاملة.
لا شيء من ذلك ضارّ بذاته، ومعظمه مفيد فعلاً — لمدة ساعة تقريبًا بعد أن يحدث خطأ ما. المشكلة في الشكل: دقّة كاملة، محفوظة لمدة طويلة، بالمصادفة لا بالقرار. السؤال الذي يستحق أن تطرحه عن كل واحد من هذه الملفّات ليس "هل هذا حسّاس؟" بل "ماذا كنتُ سأفعل فعلاً بالسطر الذي كتبته قبل ثلاثة أسابيع؟" بالنسبة للغالبية الساحقة من الأسطر على الغالبية الساحقة من الخوادم، الجواب هو لا شيء، والسجلّ الذي لن تقرأه أبدًا مسؤوليةٌ خالصة — تجاه اختراق، وتجاه نسخة احتياطية تعيش أطول من الجهاز، وتجاه أيًّا كان مَن سيطلبه يومًا ما.
تقليل البيانات هو الاسم الرسمي لهذا الحل، وهو أقلّ الأفكار إثارةً للجدل في حماية البيانات: اجمع ما تحتاجه المهمّة، واحتفظ به طالما احتاجته المهمّة، ثم توقّف. وما يلي يُطبِّق ذلك على جهاز تُشغّله فعلاً، على افتراض أنك أجريتَ بالفعل الساعة الأولى بعد النشر وأن هناك ما يستحق الحماية على الجهاز.
SP·02ستة سجلّات، اثنان منها يُسمّيان الأشخاص
قبل أن تغيّر أي شيء، اعرف الجرد. يكتب VPS صغير يعمل بـDebian أو Ubuntu ويُشغّل خدمة ويب عادةً ستة تدفّقات، وهي تتداخل أكثر مما يتوقّع الناس — فالحدث نفسه غالبًا ما ينتهي به المطاف في ثلاثة ملفّات بثلاث مدد احتفاظ مختلفة.
سجلّ nginx للوصول هو الذي يُسمّي زوّارك. سطر واحد لكل طلب، يحمل عنوان العميل، والمسار الدقيق، والمُحيل، وسلسلة وكيل مستخدم مفصّلة بما يكفي لتكون بصمة ضعيفة بمفردها. سجلّ nginx للأخطاء هو ما ينساه الناس: فهو يسجّل client: 203.0.113.9 عند كل مهلة upstream، وكل خطأ 403، وكل طلب مشوَّه — وخلافًا لسجلّ الوصول، صيغته ثابتة ولا يمكن تخصيصها بقالب.
سجلّ المصادقة — /var/log/auth.log في عائلة Debian، و/var/log/secure في عائلة RHEL — يُسمّي أنت. كل سطر قبول بمفتاح عام (publickey) يحمل عنوان مصدرك وبصمة المفتاح الذي فتح الجلسة. أي شخص يقرأ شهرًا منه يعرف من أي الشبكات تُدير خادمك، وفي أي ساعات، وكم مفتاحًا مختلفًا تملك. وعلى جهاز مملوك بهوية مجهولة، غالبًا ما يكون ذلك الملفّ أكثر كشفًا من أي شيء ولّده زوّارك.
سجلّ journald يحمل نسخة من معظم ما سبق، مضافًا إليه المخرجان القياسيان stdout وstderr لكل وحدة. وفي التثبيت الافتراضي، يُسمح له بالنموّ حتى 10% من نظام الملفّات، بحدّ أقصى 4 GB، قبل أن يبدأ بإسقاط أقدم المُدخلات. وعلى أي قرص بسعة 40 GB أو أكثر، ذلك السقف هو الـ4 GB كاملة، وهي عند الأحجام التي يُنتجها خادم صغير أشهر عديدة.
سجلّات التطبيق هي الورقة المجهولة. إطار عمل في وضع debug يسجّل عناوين URL كاملة بما فيها سلاسل الاستعلام، وسلاسل الاستعلام تحمل بشكل روتيني رموز الجلسات، وروابط إعادة تعيين كلمة المرور، وعبارات البحث. ويمكن ضبط PHP-FPM ليكتب سجلّ وصول خاصًا به، يُكرِّر سطر الطلب الذي كتبه nginx بالفعل، من ملفّ لا تلمسه إعدادات nginx إطلاقًا.
سجلّات الحاويات هي الأكثر هدوءًا. مُشغِّل json-file الافتراضي في Docker لا يُدوِّر شيئًا ما لم تضبطه بنفسك، لذا يحتفظ /var/lib/docker/containers/*/*-json.log بكل ما قالته الحاوية منذ إنشائها. وهذه طريقة شائعة لاكتشاف أن سياسة احتفاظ "14 يومًا" تحمل في الواقع أحد عشر شهرًا، وهي من فئة المفاجأة نفسها التي تخصّ قواعد جدار الحماية التي يكتبها Docker خلف ufw.
من بين الستة، اثنان يحملان مُعرِّفات عن بشر تستحق التقليل المتعمَّد: سجلّ nginx للوصول (زوّارك) وسجلّ المصادقة (أنت). أما البقية فتحتاج غالبًا إلى حدّ للحجم وساعة أقصر فحسب.
SP·03قلِّم وقت الكتابة، لا وقت التدوير
الغريزة الأولى هي أن تستمر في التسجيل بشكل طبيعي ثم تُنظِّف لاحقًا — مهمّة cron ليلية تُعيد كتابة ملفّ الأمس بعد حذف العناوين منه. لا تبنِ ذلك. فمهمّة التنظيف تعني أن العناوين الخام كانت موجودة فعلاً على القرص لمدة تصل إلى يوم كامل، وخلال ذلك اليوم التقطتها تشغيلةُ نسخك الاحتياطي في لقطة، وربما نسختها لقطةُ مزوّدك على مستوى الكتل، وبقيت في أيًّا كان ما فعله نظام الملفّات بالكتل القديمة. والأسوأ أن هذه المهمّة جزء متحرّك: تفشل بصمت في الأسبوع الذي يمتلئ فيه القرص، ولا شيء يُخبرك أن ملفّ الأمس لا يزال كاملاً.
التقليل الوحيد الذي يمكنك الاعتماد عليه فعلاً هو ما يحدث قبل كتابة السطر. في nginx، هذا يعني كتلة map تُقيَّم وقت الكتابة في السجلّ، تُعيد كتابة العنوان في متغيّر جديد، وlog_format يستخدم المتغيّر الجديد بدلاً من $remote_addr. العنوان الكامل لا يُسلسَل أبدًا. لا شيء لتنظيفه لاحقًا، ولا شيء لجدولته، ولا شيء يمكن أن يُخطئ في اليوم الذي لا تُراقب فيه.
# /etc/nginx/conf.d/00-privacy-log.conf -- http context, loaded before the sites
map $remote_addr $ip_trunc {
# IPv4: keep the /24, zero the host part
~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0";
# IPv6: keep the first two groups, drop the rest
~(?<v6>[^:]+:[^:]+): "${v6}::";
# anything the two patterns cannot parse -- including compressed forms
# like ::1 -- falls through here, i.e. fails closed rather than open
default "0.0.0.0";
}
log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
'$body_bytes_sent "$http_referer" "$http_user_agent" '
'rid=$request_id rt=$request_time';
ثلاثة تفاصيل تحسم ما إذا كان هذا يعمل فعلاً. أولًا، يجب أن تكون الـmap ضمن سياق http — فداخل كتلة server يرفض nginx البدء. ووضعها في conf.d/ باسم يُرتَّب أبجديًا مبكرًا هو أبسط طريقة للتأكد من ذلك.
ثانيًا، إذا كنتَ خلف Cloudflare أو أي حافة أخرى، تحقّق مما يحتويه $remote_addr. تستبدله وحدة realip بعنوان العميل الحقيقي في وقت مبكر من معالجة الطلب، وتحتفظ بعنوان الاتصال في $realip_remote_addr. وهذا الترتيب في صالحك: فالـmap تعمل وقت الكتابة في السجلّ، لذا تُقلِّم الزائر الحقيقي لا الحافة. لكن هذا يعني أيضًا أنك إن كنت لم تضبط realip، فأنت تُقلِّم عنوان Cloudflare ولا تتعلّم شيئًا، بينما يبقى العنوان الحقيقي في ترويسة.
ثالثًا، دقِّق بقية سلسلة الصيغة. تقليم $remote_addr لا يُحقّق شيئًا على الإطلاق إذا كان السطر لا يزال ينتهي بـ"$http_x_forwarded_for" أو يحمل $http_cf_connecting_ip — والعديد جدًا من الصيغ القياسية وصيغ لوحات التحكّم تتضمّن واحدًا من هذين. العنوان الكامل موجود في ترويسات الطلب؛ ولا يبقى خارج الملفّ إلا إذا تركتَ كل ترويسة تحمله خارج الصيغة.
لاحظ ما حلّ مكانه: $request_id، سلسلة عشوائية سداسية عشرية من 32 محرفًا يُولِّدها nginx لكل طلب. وهذا هو المحور الذي يقوم عليه القسم التالي.
ما يكسره التقليم، والمهامّ الثلاث التي يؤدّيها السجلّ فعلاً
يعترض أحدهم دائمًا بأن السجلّات المُجهَّلة عديمة الفائدة، وهو مُحقّ جزئيًا — لأن "السجلّات" في الحقيقة ثلاث مهامّ غير مترابطة تلبس اسم ملفّ واحد، ومهمّة واحدة فقط منها تحتاج العنوان.
المهمّة الأولى: احجب مَن يُغرقك بالطلبات الآن. هذه تحتاج العنوان الكامل، وتحتاجه خلال ثوانٍ. لا تحتاجه غدًا. fail2ban هو الأداة المعتادة لهذا، وهو فعلاً لا يستطيع العمل من ملفّ مُقلَّم — فحجب 203.0.113.0 يحجب مُضيفًا بريئًا واحدًا ويترك المهاجم متّصلاً. لكن هذه المهمّة يكفيها ملفّ يعيش يومًا واحدًا، أو الاستغناء عن الملفّ كليًا: فـlimit_req وlimit_conn الخاصّان بـnginx يحتفظان بحالتهما في الذاكرة المشتركة، ويتصرّفان خلال ميكروثوانٍ بدلاً من فاصل فحص fail2ban الدوري، ولا يكتبان شيئًا على القرص.
المهمّة الثانية: معرفة سبب عودة ذلك الطلب بخطأ 500. هذه تحتاج ربطًا (correlation)، لا هوية. معرّف طلب (request ID) يمرّ من nginx إلى التطبيق يربط سطر الوصول، وخطأ upstream، وتتبّع مكدّس التطبيق (stack trace) بحدث واحد — وهذا هو ما كنتَ تحاول فعله فعلاً حين لجأتَ إلى العنوان. عمليًا، المعرّف أفضل: فهو ينجو من عميل على شبكة جوّالة يتغيّر عنوانه في منتصف الجلسة، ولا يفسد حين يتشارك أربعة زوّار عنوان CGNAT واحدًا.
المهمّة الثالثة: فهم حركة المرور عبر الزمن. الحجم، ومزيج رموز الحالة، وأي المسارات أكثر ازدحامًا، وهل الزاحف (crawler) خارج عن السيطرة. عنوان مُقلَّم مناسب تمامًا هنا، و/24 الذي ينجو من التقليم يكفي لمعرفة أن شبكة واحدة مسؤولة عن 40% من طلباتك.
إذن، التصميم ليس "سجِّل أقلّ" بل افصل بحسب الساعة: تدفّق بدقّة كاملة يعيش يومًا واحدًا ويُغذّي أدوات الحجب، وتدفّق مُقلَّم يعيش بقدر ما تريد للإحصاءات. يكتب nginx كليهما في اللحظة نفسها، فلا توجد خطوة معالجة بينهما ولا نافذة زمنية يبقى فيها الشيء الخطأ على القرص أطول مما ينبغي.
# inside the server block, or in a snippet included by it
access_log /var/log/nginx/access.log privacy; # truncated, keep for weeks
access_log /var/log/nginx/security.log secip; # full address, keep for a day
# static assets are pure noise in a privacy log -- drop them entirely
location ~* \.(?:css|js|svg|png|jpe?g|webp|woff2?|ico)$ {
access_log off;
expires 30d;
}
صيغة secip سطر واحد مقارنةً بالصيغة الأخرى — $remote_addr، والطابع الزمني، والطلب، والحالة، لا شيء آخر. إنه الملفّ الوحيد على الجهاز الذي يُسمح فيه لعنوان زائر كامل أن يستقرّ، وهذا يجعل مدة الاحتفاظ به قرارًا واحدًا في مكان واحد بدلاً من خاصية عليك التفكير فيها عبر ستة ملفّات.
journald وauth.log، والأثر الذي يقود إليك أنت
خصوصية الزائر هي الجزء الذي يُكتَب عنه. أما أثر المسؤول فهو الجزء المهمّ على جهاز غايتُه كلّها أن يبقى اسمك غير مرتبط به، وهو يكمن كلّه تقريبًا في مكانين.
يسجّل /var/log/auth.log سطر Accepted publickey لكل جلسة تفتحها، مع عنوان مصدرك وبصمة مفتاحك. وعلى مدى شهر، هو جدول بعادات عملك وقائمة بالشبكات التي تستخدمها. إذا كنتَ تتصل دائمًا عبر النفق نفسه، فذلك عنوان واحد متكرّر ومُملّ نسبيًا. أما إذا كنتَ تتصل من أينما وجدتَ نفسك، فهو سجلّ رحلات.
يحمل سجلّ journald الأحداث نفسها مضافًا إليها كل ما طبعته وحداتك، وإعداداته الافتراضية سخيّة: SystemMaxUse= يساوي 10% من نظام الملفّات، وMaxRetentionSec= غير مضبوط إطلاقًا، ما يعني عدم وجود حدّ زمني على الإطلاق — بل حدّ للحجم فقط. وعلى خادم هادئ، يحتفظ هذا المزيج بأشهر.
هناك مقايضة حقيقية هنا وتستحق أن تُذكَر بصراحة لا أن يُلوَّح بها فحسب. السجلّات التي تصفك أنت هي نفسها السجلّات التي تُخبرك كيف دخل أحدهم. اضبط Storage=volatile ويعيش السجلّ في الذاكرة العشوائية (RAM) فقط، فيختفي عند إعادة التشغيل — خاص فعلاً، وعديم الفائدة فعلاً في الصباح الذي تجد فيه عملية لم تُشغّلها أنت، لأن أول إعادة تشغيل يجريها مهاجمك تمحو الدليل. وبالنسبة لمعظم الناس، الوسط المعقول هو تخزين دائم بحدّ أقصى صارم وساعة قصيرة: طويلة بما يكفي للتحقيق في حادثة تلاحظها خلال أسبوع، وقصيرة بما يكفي حتى لا يكون الملفّ مذكّرات يومية.
ملاحظتان تخصّان التنفيذ تُوقِعان الناس في الخطأ. تختلف صور Debian وUbuntu في ما إذا كان rsyslog مثبَّتًا؛ فإذا كان /var/log/auth.log موجودًا على جهازك فإن rsyslog هو من يكتبه، وحدود journald لا تحكم ذلك الملفّ على الإطلاق — بل هذا عمل logrotate. أما ForwardToSyslog= فهو ما يُغذّي rsyslog من السجلّ، لذا فإن تعطيله على جهاز يملك الاثنين معًا يمنعك من الاحتفاظ بنسختين من كل شيء تحت سياستَي احتفاظ مختلفتين.
سياسة احتفاظك هي أيًّا كان ما تقوله نسخك الاحتياطية
هذه هي النقطة التي تنقض كل العمل الدقيق أعلاه، وهي غير مرئية ما لم تبحث عنها عمدًا.
لنقل إن logrotate يحتفظ بأربعة عشر يومًا من سجلّات nginx، وأنت راضٍ عن ذلك. أضِف الآن النسخة الاحتياطية خارج الموقع التي أصبتَ حين أعددتها: تشغيلة يومية بـBorg أو restic، بمدة احتفاظ تبلغ سبع أرشيفات يومية، وأربعة أسبوعية، وستة شهرية. كل واحد من تلك الأرشيفات يحتوي /var/log كما كان عليه يوم تشغيله. أقدم أرشيف شهري عمره ستة أشهر ويحمل الأربعة عشر يومًا من السجلّات التي كانت سارية حينها. مدة احتفاظك الفعلية بالسجلّات ليست أربعة عشر يومًا. إنها ستة أشهر، في مستودع مشفّر لا يمكنك تنفيذ grep عليه دون استعادته، على جهاز ثانٍ، في بلد مختلف.
هناك حلّان صادقان بالضبط لا ثالث لهما. استبعد أدلّة السجلّات من النسخة الاحتياطية — فهي الشيء الوحيد على الخادم الذي تستطيع دائمًا تقريبًا إعادة بنائه أو الاستغناء عنه، واستعادة تُغفل /var/log ليست استعادة أسوأ. أو ضمّنها عمدًا واقبل أن احتفاظك الحقيقي هو احتفاظ المستودع، وفي هذه الحالة قل ذلك صراحةً في أي سياسة تنشرها، لأن البديل سياسة مُعلَنة تناقضها بنيتك التحتية نفسها.
# exclude logs from the backup, and prove it took
borg create --stats \
--exclude '/var/log' \
--exclude '/var/lib/docker/containers' \
::'{hostname}-{now:%Y-%m-%d}' /
# the check that matters: can you still find an address in the newest archive?
borg list ::"$(borg list --short --last 1)" | grep -c '^var/log/' || echo "clean"
وبما أنك في هذا الإطار الذهني، إليك أمرين آخرين قريبين من المشكلة نفسها. لقطات (snapshots) مزوّدك ليست لقطاتك أنت. تلتقط اللقطة على مستوى المُراقِب الوهمي (hypervisor) القرص كما كان، بما في ذلك سجلّات دوّرتها بعيدًا منذ ذلك الحين، وهي تعيش على تخزين المزوّد بموجب مدة احتفاظ المزوّد — وهذا سبب إضافي يؤكّد أن العناوين ما كان ينبغي أن تُكتَب كاملة من الأساس، لا سببًا لفعل شيء ذكي بعد وقوع الأمر.
ولا تلجأ إلى shred على VPS. الكتابة فوق ملفّ على قرص افتراضي رقيق التوفير (thin-provisioned)، فوق نظام ملفّات نسخ-عند-الكتابة (copy-on-write)، فوق قرص SSD يُعيد تخطيط الكتل لتوزيع البِلى (wear-levelling)، لا يكتب فوق الخلايا الفيزيائية التي حملته بشكل موثوق. الحذف الآمن على تخزين مُستأجَر ومُفترَض هو مسرحية لا أكثر. التقليل الذي ينجح هو ما فعلته وقت الكتابة؛ وكل ما يأتي بعد ذلك مجرّد محاولة لا يمكنك التحقّق منها. تشفير القرص يُغيّر هذه المعادلة — لكنه يُغيّرها قبل كتابة البيانات، وهو الدرس نفسه مجدَّدًا.
النُّسَخ التي لا تتحكّم بها
التقليل على جهازك أنت طبقة واحدة من طبقات عدّة، والوضوح بشأن البقية هو ما يمنع هذا من أن يتحوّل إلى شعور زائف بالاكتمال.
شبكة استضافتك ترى سجلّ التدفّق (flow record). المصدر، والوجهة، والمنافذ، والبايتات، والتوقيت — لكل اتصال يدخل الجهاز ويخرج منه، سواء سجّلتَ شيئًا أم لا. لا يُغيّر أي إعداد على الخادم ذلك. وهذا جزء كبير من سبب كون الولاية القضائية التي يقع فيها الجهاز متغيّرًا حقيقيًا لا مسألة تسويقية؛ فـما تُلزَم به الشبكة قانونًا يختلف اختلافًا هائلاً من بلد إلى آخر.
شبكة توصيل المحتوى (CDN) أو الحافة الخاصة بك تُسجِّل عند الحافة. إذا كان Cloudflare يُنهي TLS نيابةً عنك، فهو يملك سطر الطلب وعنوان العميل قبل أن يتدخّل خادمك، بجدول احتفاظ خاص به، وخاضع لإجراءاته القانونية الخاصة. تقليم سجلّك عند المصدر (origin) لا يصل إليه رجوعًا. تشغيل حافة تملكها أنت هو النسخة من هذا التي تستطيع فعلاً ضبط إعداداتها — وقواعد التسجيل في هذا الدليل تنطبق على جهاز الحافة أولًا، لأنه المكان الذي تصل إليه العناوين غير المُقلَّمة.
أدوات تتبّع الأخطاء والتحليلات تُرسله خارج الجهاز نيابةً عنك. يُرفِق Sentry ومعظم نظرائه عنوان IP الخاص بالعميل بكل حدث افتراضيًا؛ والإعداد يُسمّى عادةً شيئًا شبيهًا بـsend_default_pii، ويستحق أن تتحقّق منه بدلاً من افتراض حاله. وأي أداة تحليلات مُستضافة هي، بحكم بنائها، نسخة من طرف ثالث لسجلّ الوصول الذي أمضيتَ للتوّ بعد ظهر كامل تُقلِّمه.
البريد هو الأكثر تسريبًا على الإطلاق. إذا كان أي شيء على الجهاز يُرسل بريدًا، فإن الترويسات تحمل المضيف المُرسِل وعنوانه، وكل مُرحِّل (relay) على المسار يحتفظ بنسخة من المغلّف (envelope) مع طوابعه الزمنية. ونموذج تواصل يُرسل إليك بريدًا هو سجلّ لا تُديره أنت.
لا شيء من هذا يجعل العمل المحلّي عديم الجدوى — فالنسخة المحلّية هي التي تُصادَر مع الجهاز، أو تُسرَّب في اختراق، أو تُسلِّمها أنت بنفسك. إنها ببساطة الطبقة الوحيدة التي تتحكّم بها بالكامل، والخطأ هو معاملتها وكأنها الصورة كاملةً.
SP·08التقليل بالتصميم، لا الحذف عند الإشعار
يستحق الأمر دقّة هنا، لأن الاثنين كثيرًا ما يُخلَط بينهما، والفرق بينهما هو الفرق كلّه بين ممارسة هندسية معيارية وشيء لا ينبغي أن تفعله.
أن تقرّر مسبقًا ماذا تجمع خدمتك وإلى متى تحتفظ به هو ممارسة عادية وموثَّقة ومُشجَّع عليها. بموجب اللائحة العامة لحماية البيانات (GDPR)، هذان مبدآن من المبادئ الجوهرية — تقليل البيانات وتحديد مدة الاحتفاظ — ومدة احتفاظ أقصر بالسجلّات ضابطٌ يطلبه المدقّقون، لا ضابط يعترضون عليه. لا يوجد التزام عام على مُشغِّل موقع أو زبون استضافة في الاتحاد الأوروبي بالاحتفاظ بسجلّات حركة المرور؛ فالتوجيه القاضي بالاحتفاظ الشامل الذي أوحى يومًا بعكس ذلك أبطلته محكمة العدل الأوروبية عام 2014، والقوانين الوطنية التي نجت منه تُلزم في معظمها مزوّدي الاتصالات، لا مَن يُشغّل خادم ويب. والولايات المتحدة أيضًا ليس فيها تفويض عام بالاحتفاظ يخصّ مُشغِّلي المواقع.
تدمير سجلّات محدّدة بعد أن تصلك إشعار بشأنها فعلٌ مختلف تمامًا. طلب حفظ، أو أمر تجميد بيانات لأغراض التقاضي، أو أمر قضائي، أو تحقيق من الشرطة، كل ذلك يُغيّر ما يجوز لك فعله بالبيانات الموجودة في تلك اللحظة، و"سياسة الاحتفاظ عندي حذفتها" ليست دفاعًا إذا كنتَ قد عجّلتَ الحذف بسبب ذلك الإشعار. لا شيء في هذا الدليل يخصّ ذلك. سياسة الاحتفاظ شيء تضبطه في يوم ثلاثاء هادئ ثم تتركه دون مساس؛ فإذا كانت لا تُختصَر إلا حين يحدث شيء ما، فهي لم تكن سياسة من الأساس.
الفرق نفسه يمتدّ عبر بقية هذا الموقع: هناك خطّ حقيقي بين هندسة الخصوصية وبين نهج "bulletproof" الذي يُسوِّق نفسه على أنه حصانة. وتصميم خدمة لا تُراكم أبدًا تاريخًا لزوّارها يقع براحة في الجانب الصحيح من ذلك الخطّ، في الخانة نفسها التي تضمّ تشفير أقراصك وعدم طلب عنوان بريد إلكتروني من المستخدمين لا تحتاجه.
نتيجتان عمليّتان. إذا نشرتَ سياسة خصوصية، اجعل أرقام الاحتفاظ فيها مطابقة لِما هو موجود فعلاً على القرص — فأربعة عشر يومًا مُعلَنة وستة أشهر حقيقية في مستودع النسخ الاحتياطية هي نوع الفجوة التي تحوّل مُشغِّلاً حسن النية إلى مُشغِّل سيّئ النية على الورق. ودوِّن القرار لنفسك، في تعليق أعلى ملفّ logrotate إن لم يكن في أي مكان آخر، لأن الشخص الذي سيضطرّ إلى تبرير هذه الأرقام بعد ثمانية عشر شهرًا هو أنت، ولن يتذكّر لماذا كان الرقم سبعة. لا شيء مما سبق استشارة قانونية؛ فإن كنتَ تُشغّل نشاطًا في مكان له واجبات احتفاظ خاصة بقطاعه، تحقّق منها في ضوء وضعك الخاص قبل أن تُقصِّر أي شيء.
SP·09خطوة بخطوة
-
01
اكتشف ما يحتفظ به الجهاز بالفعل
لا تضبط أي شيء قبل أن تقيس. غاية هذه الجولة هي إيجاد الملفّ الذي نسيتَه، وهو في معظم الأجهزة إما سجلّ حاوية أو سجلّ تطبيق لم ينظر إليه أحد منذ النشر.
# biggest log files anywhere on the box, largest last sudo du -ah /var/log /var/lib/docker/containers 2>/dev/null \ | sort -h | tail -20 # how much disk the journal holds, and how far back it goes journalctl --disk-usage journalctl --output=short-iso | head -1 # how old is the oldest nginx line still on disk? zcat -f /var/log/nginx/access.log* 2>/dev/null | head -1
ثم انظر إلى سطر واحد من كل ملفّ واسأل ماذا يُعرِّف. الأمر أدناه يعدّ كم عنوانًا كاملاً مختلفًا يمكن استرجاعه حاليًا من سجلّات الويب لديك — وهو عادةً الرقم الذي يُقيم الحجّة لبقية هذا الدليل.
zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | wc -lدوِّن ما تجده. ستُشغّل هذه الأوامر نفسها في النهاية لتُثبت أن التغيير أخذ مفعوله.
-
02
قلِّم عنوان العميل قبل أن يكتب nginx السطر
أنشئ الـmap والصيغتين في ملفّ يُحمَّل ضمن سياق
http. في Debian وUbuntu، يُضمَّن/etc/nginx/conf.d/منnginx.confقبل إعدادات المواقع، وهو بالضبط المكان الذي ينتمي إليه هذا.sudo tee /etc/nginx/conf.d/00-privacy-log.conf > /dev/null <<'EOF' # The client address is reduced here, at log time, and never written in full # to the long-retention file. $realip_remote_addr still holds the connecting # address if you need it while debugging a proxy problem. map $remote_addr $ip_trunc { ~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0"; ~(?<v6>[^:]+:[^:]+): "${v6}::"; # anything the two patterns cannot parse -- including compressed forms # like ::1 -- falls through here, i.e. fails closed rather than open default "0.0.0.0"; } # Long retention: no full address, no forwarded-for header, request id instead. log_format privacy '$ip_trunc - - [$time_local] "$request" $status ' '$body_bytes_sent "$http_referer" "$http_user_agent" ' 'rid=$request_id rt=$request_time'; # Short retention: the one file allowed to hold a complete address. log_format secip '$remote_addr [$time_local] "$request" $status'; EOF sudo nginx -t && sudo systemctl reload nginxالآن وجِّه الموقع إليهما. في كتلة server الخاصة بك، استبدل سطر
access_logالحالي بهذا الزوج، وأوقف التسجيل عن الأصول الثابتة (static assets) طالما أنت هناك.access_log /var/log/nginx/access.log privacy; access_log /var/log/nginx/security.log secip;
أعد التحميل، وافتح صفحة، واقرأ النتيجة. يجب أن ينتهي الحقل الأول بـ
.0، ويجب أن يحمل السطر قيمةrid=.sudo nginx -t && sudo systemctl reload nginx curl -s -o /dev/null https://your-domain.example/ sudo tail -1 /var/log/nginx/access.log
إذا كان العنوان لا يزال كاملاً، فإن كتلة server تتجاوز الصيغة في مكان ما أبعد —
grep -rn access_log /etc/nginx/يجد السطر الذي يفوز. -
03
احتفظ بالعناوين الكاملة فقط حيث يتصرّف شيء بناءً عليها
إذا كان fail2ban يعمل، فهو يقرأ حاليًا الملفّ الذي قلَّمته للتوّ. وجِّهه بدلاً من ذلك إلى سجلّ الأمان، وامنح ذلك السجلّ عمر يوم واحد حتى تنتهي صلاحية العناوين الكاملة من تلقاء نفسها.
sudo tee /etc/fail2ban/jail.d/nginx-privacy.local > /dev/null <<'EOF' [nginx-http-auth] enabled = true logpath = /var/log/nginx/error.log [nginx-botsearch] enabled = true logpath = /var/log/nginx/security.log maxretry = 6 findtime = 10m bantime = 1h EOF sudo fail2ban-client reload
ثم تعامل مع ذاكرة fail2ban الخاصة، التي لا يلمسها معظم الناس أبدًا: فهو يحتفظ بسجلّه الخاص بعمليات الحجب تحت الإعداد الافتراضي
rotate 4 weekly، وبقاعدة بيانات SQLite لكل حجب أصدره.dbpurgeageهو ما يُنهي صلاحية الصفوف في تلك القاعدة — اضبطه على قيمة قريبة من أطول مدة حجب لديك بدلاً من تركه على الافتراضي.sudo tee /etc/fail2ban/fail2ban.d/retention.local > /dev/null <<'EOF' [Definition] dbpurgeage = 2d loglevel = NOTICE EOF sudo systemctl restart fail2ban
والأفضل من ذلك، بالنسبة للفيضانات البسيطة، أنجز العمل داخل nginx حيث لا يُكتَب شيء على الإطلاق. يحتفظ
limit_reqبعدّاداته في الذاكرة المشتركة، ويستجيب خلال ميكروثوانٍ بدلاً من فاصل فحص دوري، ولا يترك أي سجلّ بمن جرى تقييد سرعته — راجع دليل الساعة الأولى لهجمات DDoS لتحديد أحجام المناطق تحت حِمل حقيقي.# http context: keyed on the truncated address, so nothing complete is # held in memory either. 10m of shared state is plenty for a small site. limit_req_zone $ip_trunc zone=perip:10m rate=20r/s; # in the location you want protected limit_req zone=perip burst=40 nodelay;
ربط المنطقة بمفتاح
$ip_truncبدلاً من$binary_remote_addrهو مقايضة متعمَّدة: فالحدّ الآن ينطبق على/24كاملة دفعة واحدة، بحيث يتشارك مكتب مزدحم خلف كتلة واحدة ميزانيةً واحدة. لموقع صغير، هذا مناسب عادةً، بل يُعَدّ تحسينًا أحيانًا. أما إذا لم يكن كذلك، فاربط المنطقة بالعنوان الكامل — فحالة أداة تحديد المعدّل تعيش في الذاكرة المشتركة ولا تُكتَب على القرص أبدًا، لذا فهي ليست جزءًا مما تُقلِّله هنا. -
04
احصر سجلّ journald وأوقف النسخة الثانية
يقبل journald ملفًّا تكميليًّا (drop-in)، وهو ينجو من ترقيات الحزم بطريقة لا ينجو بها تعديل
journald.conf. القيم أدناه تحتفظ بأسبوع تقريبًا — يكفي للتحقيق في شيء تلاحظه يوم الاثنين وبدأ يوم الجمعة — ضمن سقف صارم قدره 200 MB.sudo mkdir -p /etc/systemd/journald.conf.d sudo tee /etc/systemd/journald.conf.d/retention.conf > /dev/null <<'EOF' [Journal] Storage=persistent SystemMaxUse=200M SystemMaxFileSize=20M MaxRetentionSec=7day MaxFileSec=1day ForwardToSyslog=no EOF sudo systemctl restart systemd-journald journalctl --disk-usage
تُطبِّق إعادة التشغيل حدود الحجم فورًا؛ أما ساعة الاحتفاظ فتُفرَض مع تدوير الملفّات الجديدة، لذا فإن سجلّ journald الضخم الموجود مسبقًا يتقلّص عند التدوير التالي لا فورًا. يفرضه
journalctl --vacuum-time=7dالآن إذا أردتَ استعادة مساحة القرص اليوم.ForwardToSyslog=noمهمّ على أي صورة تُشحَن مع rsyslog: فبدونه، يُضاف كل مُدخَل من journald أيضًا إلى/var/log/syslog، تحت جدول logrotate لا جدول journald، فيكون لديك نسختان بتاريخَي انتهاء صلاحية مختلفَين. تحقّق في أي حالة أنت قبل أن تفترض:systemctl is-active rsyslog 2>/dev/null || echo "rsyslog not running" ls -la /var/log/auth.log /var/log/syslog 2>/dev/null
إذا كانت تلك الملفّات موجودة، فإن rsyslog يملكها والخطوة السادسة هي حيث يُضبَط احتفاظها. أما إذا لم تكن موجودة، فإن سجلّ journald هو النسخة الوحيدة وأنت لتوّك قد حصرتَ حجمه.
-
05
أوقف التطبيق عن إعادة تسجيل ما أزلته
لا شيء مما سبق يلمس تطبيقك، وتطبيق في الوضع الخطأ سيكتب بكل سرور العنوان الكامل، وURL الكامل، ورمز الجلسة في ملفّ خاصّ به. ثلاثة أشياء تستحق التحقّق.
PHP-FPM يأتي بتوجيه
access.logفي إعدادات مجمعه (pool)، مُعطَّلًا بتعليق افتراضيًا لكن عددًا كبيرًا جدًا من لوحات التحكّم يُفعِّله. إذا كان مُفعَّلاً، فهو نسخة ثانية من كل سطر طلب، من ملفّ لم يلمسه عملك في nginx إطلاقًا.grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"
مستوى تسجيل إطار عملك يحدّد ما إذا كانت سلاسل الاستعلام وأجسام الطلبات تصل إلى القرص. يسجّل وضع debug في معظم أطر العمل عنوان URL كاملاً، ورابط إعادة تعيين كلمة المرور هو عنوان URL كامل. اضبط مستوى الإنتاج (production) وتأكّد أنه فعلاً المستوى المُحمَّل، لا المستوى الموجود في الملفّ الذي تظنّ أنه يُقرَأ.
مُشغِّل Docker الافتراضي لا يُدوِّر أبدًا. أصلح ذلك على مستوى الخدمة (daemon) حتى ترث كل حاوية مستقبلية الحدّ. لاحظ أن هذا ينطبق على الحاويات التي تُنشأ بعد إعادة التشغيل — أما الحاويات الموجودة فتحتفظ بملفّها الحالي غير المحدود حتى تُعاد إنشاؤها.
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF' { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } EOF sudo systemctl restart docker docker inspect --format '{{.HostConfig.LogConfig}}' $(docker ps -q) 2>/dev/nullإذا كانت حاوية ما تحمل بالفعل ملفًّا ضخمًا، فإعادة إنشائها بـ
docker compose up -d --force-recreateهو ما يُقلِّم التاريخ فعلاً — إعادة التشغيل وحدها تُبقي على ملفّ السجلّ نفسه. -
06
اضبط مدة الاحتفاظ عمدًا، في مكان واحد لكل ملفّ
logrotate هو المكان الذي تعيش فيه الساعة لكل ما يكتبه rsyslog وnginx. عدِّل الفقرة (stanza) المشحونة بدلاً من إضافة فقرة ثانية: فقرتان تُسمّيان المسار نفسه تجعلان logrotate يفشل بخطأ إدخال مكرّر ويتوقّف عن تدوير ذلك الملفّ كليًا، وهذه هي الطريقة الأشيع التي يتحوّل بها تغيير في مدة الاحتفاظ بصمت إلى احتفاظ لا نهائي.
# check for duplicates BEFORE editing, then again after sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -i 'duplicate\|error'
بالنسبة لـnginx، يحتاج الملفّان ساعتين مختلفتين: يمكن للملفّ المُقلَّم أن يعيش أسابيع، أما الملفّ الذي يحمل العناوين الكاملة فلا ينبغي أن يبقى بعد نهاية اليوم. أضِف فقرة منفصلة لسجلّ الأمان — مسار مختلف، فلا تكرار — وقصِّر الفقرة المشحونة.
sudo tee /etc/logrotate.d/nginx-security > /dev/null <<'EOF' # The only file on this box that holds complete client addresses. # One day, uncompressed so fail2ban can read it. Do not lengthen without # a reason you would be happy to write down here. /var/log/nginx/security.log { daily rotate 1 maxage 1 missingok notifempty nocompress create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript } EOF sudo sed -i 's/^\trotate 14$/\trotate 7/' /etc/logrotate.d/nginx sudo logrotate --debug /etc/logrotate.d/nginx-securityأجرِ الحساب نفسه على
/etc/logrotate.d/rsyslogإذا كان rsyslog مثبَّتًا — فإعداداته الافتراضية تحتفظ بأربعة أسابيع منauth.log، وهي أربعة أسابيع من جلسات SSH الخاصة بك أنت. وشغِّل جولة debug مرة أخيرة: فهي تطبع بالضبط الملفّات التي كانت ستُدوِّرها وتحذفها، وهذه هي الطريقة الوحيدة للتأكّد من أن الأرقام التي كتبتَها للتوّ هي الأرقام السارية فعلاً. -
07
أثبِت ذلك، بما في ذلك عبر النسخة الاحتياطية
أعد تشغيل القياسات من الخطوة الأولى. يجب أن يتوقّف الآن عدد العناوين الكاملة المختلفة في سجلّ الاحتفاظ الطويل عن النموّ، وبعد دورة تدوير واحدة يجب أن يكون صفرًا.
# should print 0 once the pre-change files have rotated out zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' \ | grep -v '\.0$' | sort -u | wc -l # and the journal should now be bounded journalctl --disk-usageثم الفحص الذي لا يُجريه أحد تقريبًا: انظر داخل النسخة الاحتياطية. مستودع لا يزال يحمل
/var/logيحتفظ بالدقّة ومدة الاحتفاظ اللتين أمضيتَ للتوّ بعد ظهر كامل تُزيلهما، وسيستمرّ في الاحتفاظ بهما طالما بقي أقدم أرشيف لديك.borg list | head -3 # borg lists oldest first borg list ::"$(borg list --short | head -1)" 2>/dev/null \ | grep -c '^var/log/' || echo "no logs in oldest archive"
إذا كان العدد غير صفري، فإما أن تضيف الاستبعاد المذكور سابقًا وتترك الأرشيفات القديمة تنتهي صلاحيتها من تلقاء نفسها، أو تُقلِّمها عمدًا. وإلى أن يحدث أحد هذين، فإن احتفاظك الحقيقي هو احتفاظ المستودع — وهذه هي الجملة الأكثر فائدة في هذا الدليل كلّه، والأسهل نسيانًا.
أخيرًا، دوِّن الأرقام في مكان سيجده الشخص التالي: مدة الاحتفاظ التي اخترتها، والسبب، والتاريخ. يكفي تعليق أعلى ملفّ logrotate. الإعداد أعلاه قرار، والقرار الذي لا يستطيع أحد إعادة بنائه بعد عام يعود ليصبح افتراضيًا مجدَّدًا.


