درخواستی که پاسخ دادید، حالا رکوردی است که نگه داشتهاید
بقیهٔ این مجموعه به بیرون اشاره میکند. مبدأ را پنهان کنید، دیسک را رمزنگاری کنید، حوزهٔ قضایی را انتخاب کنید، جلوی نگهداشتنِ لاگِ خودتان توسطِ یک نفرِ دیگر را بگیرید. این یکی به خودِ ماشین اشاره میکند، چون سروری که به درخواستها پاسخ میدهد ثبت میکند چه کسی آنها را ساخته — پیشفرض، در چند جا همزمان، با نگهداریای که هیچکس انتخابش نکرده.
به آنچه یک ایمیجِ استاندارد دو هفته بعد از دیپلوی نگه داشته نگاه کنید. /var/log/nginx/access.log بهازای هر درخواست یک خط دارد: آدرس، تایماستمپ، مسیر، referrer، user agent، که روزانه rotate میشود و چهارده روز نگه داشته میشود. /var/log/auth.log هر نشستِ SSH را دارد، از جمله آدرسِ مبدأِ تکِتکِ نشستهای خودتان. journal همان رویدادها را دوباره دارد، بهعلاوهٔ هر چیزی که سرویسهای شما به stderr چاپ کردهاند. اگر Docker در حالِ اجراست، هر کانتینر یک فایلِ لاگِ JSON دارد که از کارخانه اصلاً هیچ محدودیتِ اندازهای ندارد. اگر fail2ban در حالِ اجراست، یک لاگ از هر آدرسی که تا بهحال بن کرده و یک دیتابیسِ SQLite که همان چیز را میگوید دارد، و آدرسها در هر دو کاملاند.
هیچکدامِ اینها مخرب نیستند و بیشترشان واقعاً مفیدند — برای حدودِ یک ساعت بعد از آنکه چیزی خراب شود. مشکل شکلِ ماجراست: با وفاداریِ کامل، برای مدتِ طولانی نگه داشته میشوند، از سرِ تصادف نه تصمیم. سؤالی که ارزش دارد دربارهٔ هرکدام از این فایلها پرسیده شود این نیست که "آیا این حساس است؟" بلکه این است که "واقعاً با خطی که سه هفته پیش نوشتم چهکار میکردم؟" برای اکثریتِ قریببهاتفاقِ خطها روی اکثریتِ قریببهاتفاقِ سرورها جواب هیچی است، و رکوردی که هرگز نمیخوانیدش چیزی جز ریسکِ خالص نیست — در برابرِ یک نفوذ، در برابرِ بکآپی که از خودِ ماشین بیشتر عمر میکند، در برابرِ هر کسی که بالاخره درخواستش کند.
کمینهسازیِ داده نامِ رسمیِ این راهحل است و کمجنجالترین ایده در حوزهٔ حفاظتِ داده است: همان را جمع کنید که کار به آن نیاز دارد، تا وقتی کار به آن نیاز دارد نگهش دارید، بعد متوقف شوید. آنچه در ادامه میآید این ایده را روی باکسی پیاده میکند که واقعاً اجرا میکنید، با این فرض که اولین ساعت پس از دیپلوی را از قبل انجام دادهاید و چیزی روی ماشین هست که ارزشِ محافظتکردن دارد.
SP·02شش لاگ، و دوتایی که نامِ آدمها را ثبت میکنند
پیش از آنکه چیزی را تغییر دهید، فهرستِ موجودی را بشناسید. یک VPSِ کوچکِ Debian یا Ubuntu که یک سرویسِ وب اجرا میکند معمولاً شش جریان مینویسد، و اینها بیشتر از آنچه فکر میکنید همپوشانی دارند — یک رویدادِ واحد اغلب در سه فایل با سه نگهداریِ متفاوت فرود میآید.
لاگِ accessِ nginx همان چیزی است که بازدیدکنندههای شما را نام میبرد. بهازای هر درخواست یک خط، با آدرسِ کلاینت، مسیرِ دقیق، referrer و یک رشتهٔ user-agent که بهتنهایی هم بهقدرِ کافی دقیق است که یک fingerprintِ ضعیف باشد. لاگِ errorِ nginx همان چیزی است که آدمها فراموشش میکنند: روی هر upstream timeout، هر 403، هر درخواستِ بدشکل، client: 203.0.113.9 را ثبت میکند — و برخلافِ لاگِ access، فرمتش ثابت است و نمیتوان templateاش کرد.
لاگِ auth — /var/log/auth.log روی خانوادهٔ Debian، /var/log/secure روی خانوادهٔ RHEL — شما را نام میبرد. هر خطِ publickeyِ پذیرفتهشده آدرسِ مبدأِ شما و fingerprintِ همان کلیدی که نشست را باز کرده حمل میکند. هر کسی که یک ماه از آن را بخواند یاد میگیرد از کدام شبکهها ادمین میکنید، در چه ساعتهایی، و چند کلیدِ متمایز دارید. روی باکسی که مالکیتش ناشناس است، آن فایل اغلب افشاگرتر از هر چیزی است که بازدیدکنندههای شما تولید کردهاند.
journal یک کپی از بیشترِ موارد بالا را بهعلاوهٔ stdout و stderrِ هر یونیت نگه میدارد، و روی یک نصبِ پیشفرض اجازه دارد تا ۱۰٪ از فایلسیستم رشد کند، با سقفِ ۴ گیگابایت، پیش از آنکه شروع کند به انداختنِ قدیمیترین رکوردها. روی هر دیسکِ ۴۰ گیگابایتی یا بزرگتر، آن سقف همان ۴ گیگابایتِ کامل است، که با حجمی که یک سرورِ کوچک تولید میکند، چندین ماه میشود.
لاگهای اپلیکیشن مجهولِ این معادلهاند. یک فریمورک در حالتِ debug تمامِ URLها را همراه با query string ثبت میکند، و query stringها بهطورِ روتین توکنهای نشست، لینکهای ریستکردنِ پسورد و عبارتهای جستوجو حمل میکنند. PHP-FPM میتواند طوری تنظیم شود که لاگِ accessِ خودش را بنویسد، که همان خطِ درخواست را که nginx از قبل نوشته تکرار میکند، از فایلی که تنظیماتِ nginxِ شما هرگز لمسش نمیکند.
لاگهای کانتینر همان موردِ ساکتاند. درایورِ پیشفرضِ json-file در Docker تا وقتی خودتان تنظیمش نکنید هیچ rotationای ندارد، پس /var/lib/docker/containers/*/*-json.log هر چیزی را که کانتینر از لحظهٔ ساختهشدنش گفته نگه میدارد. این یک راهِ رایج برای کشفکردنِ این است که یک سیاستِ نگهداریِ "۱۴ روزه" در واقع دارد یازده ماه را نگه میدارد، و همان دستهی غافلگیریای است که قوانینِ فایروالی که Docker پشتِ ufw مینویسد هم هست.
از این ششتا، دوتایشان شناسههایی دربارهٔ انسانها حمل میکنند که ارزش دارد عمداً کمینه شوند: لاگِ accessِ nginx (بازدیدکنندههای شما) و لاگِ auth (خودتان). بقیه بیشتر به یک سقفِ اندازه و یک ساعتِ کوتاهتر نیاز دارند.
SP·03کوتاهکردن در لحظهٔ نوشتن، نه در لحظهٔ rotation
غریزه این است که لاگگیری را عادی نگه دارید و بعداً پاکسازی کنید — یک cronِ شبانه که فایلِ دیروز را با آدرسهای حذفشده بازنویسی میکند. آن را نسازید. یک jobِ پاکسازی یعنی آدرسهای خام واقعاً تا یک روز روی دیسک وجود داشتهاند، و در طولِ آن روز، رانِ بکآپِ شما از آنها اسنپشات گرفته، احتمالاً اسنپشاتِ سطحِ بلاکِ هاستِ شما هم آنها را کپی کرده، و هر کاری که فایلسیستم با بلاکهای قدیمی کرده در آنها باقی مانده. بدتر آنکه، این job یک قطعهٔ متحرک است: همان هفتهای که دیسک پر میشود بیسروصدا شکست میخورد، و هیچچیز به شما نمیگوید که فایلِ دیروز هنوز کامل است.
تنها کاهشی که واقعاً میتوانید رویش حساب کنید همان است که پیش از نوشتهشدنِ خط اتفاق میافتد. در nginx آن یک بلاکِ map است که در لحظهٔ لاگ ارزیابی میشود و آدرس را در یک متغیرِ تازه بازمینویسد، و یک log_format که بهجای $remote_addr از همان متغیرِ تازه استفاده میکند. آدرسِ کامل هرگز serialize نمیشود. بعدش چیزی برای پاککردن نیست، چیزی برای زمانبندیکردن نیست، و چیزی نیست که روزی که شما تماشا نمیکنید خراب شود.
# /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 یا هر edgeِ دیگری نشستهاید، چک کنید $remote_addr چه چیزی دارد. ماژولِ realip آن را در همان اوایلِ پردازشِ درخواست با آدرسِ واقعیِ کلاینت جایگزین میکند، و آدرسِ متصلشونده را در $realip_remote_addr نگه میدارد. آن ترتیب به نفعِ شماست: map در لحظهٔ لاگ اجرا میشود، پس بازدیدکنندهٔ واقعی را کوتاه میکند نه edge را. اما این هم به این معناست که اگر realip را تنظیم نکردهاید، دارید آدرسِ Cloudflare را کوتاه میکنید و هیچی یاد نمیگیرید، درحالیکه آدرسِ واقعی داخلِ یک هدر نشسته است.
سوم، بقیهٔ رشتهٔ فرمت را audit کنید. کوتاهکردنِ $remote_addr دقیقاً هیچ کاری نمیکند اگر خط همچنان با "$http_x_forwarded_for" تمام شود یا $http_cf_connecting_ip را حمل کند — و تعدادِ زیادی از فرمتهای استاندارد و کنترلپنلی یکی از آنها را دارند. آدرسِ کامل داخلِ هدرهای درخواست است؛ فقط وقتی از فایل بیرون میماند که هر هدری که آن را حمل میکند از فرمت بیرون بگذارید.
به آنچه جایگزینش شده توجه کنید: $request_id، یک رشتهٔ hexِ ۳۲کاراکتریِ تصادفی که nginx بهازای هر درخواست تولید میکند. همین همان محورِ بخشِ بعدی است.
کوتاهسازی چه چیزی را خراب میکند، و سه کاری که یک لاگ واقعاً انجام میدهد
همیشه یک نفر اعتراض میکند که لاگهای ناشناسشده بیفایدهاند، و نیمی حق دارد — چون "لاگها" سه کارِ نامرتبط است که یک نامِ فایل پوشیده، و فقط یکی از آنها به آدرس نیاز دارد.
کارِ اول: بلاککردنِ هر کسی که همین الان دارد به شما فشار میآورد. این به آدرسِ کامل نیاز دارد و در عرضِ چند ثانیه به آن نیاز دارد. فردا به آن نیاز ندارد. fail2ban همان ابزارِ معمول است و واقعاً نمیتواند از روی یک فایلِ کوتاهشده کار کند — بنکردنِ 203.0.113.0 یک هاستِ بیگناه را بن میکند و مهاجم را متصل رها میکند. اما این کار با فایلی که فقط یک روز وجود دارد هم برآورده میشود، یا با اصلاً استفادهنکردن از فایل: limit_req و limit_connِ خودِ nginx وضعیتشان را در حافظهٔ مشترک نگه میدارند، در میکروثانیه عمل میکنند نه با فاصلهٔ پولینگِ fail2ban، و هیچی روی دیسک نمینویسند.
کارِ دوم: فهمیدنِ اینکه چرا آن درخواست یک 500 برگرداند. این به correlation نیاز دارد، نه هویت. یک request ID که از nginx تا اپلیکیشن رشته شده، خطِ access، خطای upstream و stack traceِ اپلیکیشن را به یک رویداد گره میزند — که همان کاری است که وقتی سراغِ آدرس میرفتید واقعاً میخواستید انجام دهید. در عمل، ID بهتر است: از یک کلاینتِ روی شبکهٔ موبایل که آدرسش وسطِ نشست عوض میشود جان سالم به در میبرد، و وقتی چهار بازدیدکننده یک آدرسِ CGNAT مشترک را دارند بیات نمیشود.
کارِ سوم: فهمیدنِ ترافیک در طولِ زمان. حجم، ترکیبِ status code، اینکه کدام مسیرها داغاند، اینکه آیا crawler از کنترل خارج شده. اینجا یک آدرسِ کوتاهشده کاملاً کافی است، و همان /24ای که از کوتاهسازی جان سالم به در میبرد کافی است تا ببینید یک شبکه مسئولِ ۴۰٪ از درخواستهای شماست.
پس طراحی "کمتر لاگ بگیر" نیست بلکه تقسیم بر اساسِ ساعت است: یک جریانِ با وفاداریِ کامل که یک روز زنده میماند و به ابزارهای بلاککردن خوراک میدهد، و یک جریانِ کوتاهشده که تا هر وقت آماری میخواهید زنده میماند. هر دو در همان لحظه توسطِ 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، تایماستمپ، درخواست و status، هیچ چیزِ دیگری. این تنها فایلِ روی باکس است که اجازه دارد یک آدرسِ کاملِ بازدیدکننده در آن بنشیند، که همین نگهداریِ آن را به یک تصمیمِ واحد در یک جای واحد تبدیل میکند نه یک ویژگی که باید رویش در شش فایل استدلال کنید.
journald، auth.log، و ردی که به خودتان برمیگردد
حریمِ خصوصیِ بازدیدکنندهها همان بخشی است که دربارهٔ آن نوشته میشود. ردِ ادمین همان بخشی است که روی باکسی که کلِ هدفش این است که نامِ شما به آن وصل نیست اهمیت دارد، و تقریباً تماموکمال در دو جا نشسته.
/var/log/auth.log بهازای هر نشستی که باز میکنید یک خطِ Accepted publickey ثبت میکند، با آدرسِ مبدأ و fingerprintِ کلیدِ شما. در طولِ یک ماه، این یک جدولِ زمانی از عادتهای کاریِ شما و فهرستی از شبکههایی است که استفاده میکنید. اگر همیشه از همان تونل وصل میشوید، آن یک آدرسِ تکرارشونده و نسبتاً کسلکننده است. اگر از هر جا که هستید وصل میشوید، این یک تاریخچهٔ سفر است.
journal همان رویدادها را بهعلاوهٔ هر چیزی که یونیتهای شما چاپ کردهاند نگه میدارد، و پیشفرضهایش سخاوتمندند: SystemMaxUse= برابرِ ۱۰٪ از فایلسیستم است، و MaxRetentionSec= تنظیمنشده باقی مانده، که یعنی اصلاً هیچ محدودیتِ زمانیای نیست — فقط یک محدودیتِ اندازه. روی یک سرورِ آرام، آن ترکیب ماهها را نگه میدارد.
اینجا یک معاملهٔ واقعی هست و ارزش دارد صریح گفته شود نه با دست تکاندادن رد شود. همان لاگهایی که شما را توصیف میکنند، همان لاگهاییاند که به شما میگویند یک نفر چطور وارد شده. Storage=volatile را تنظیم کنید و journal فقط در RAM زندگی میکند، و با ریاستارت ناپدید میشود — واقعاً خصوصی، و واقعاً بیفایده همان صبحی که یک پروسه پیدا میکنید که خودتان استارتش نکردهاید، چون اولین ریاستارتِ مهاجمِ شما مدرک را پاک کرده. برای بیشترِ آدمها، حدِ وسطِ عاقلانه یک storageِ پایدار با یک سقفِ سخت و یک ساعتِ کوتاه است: بهاندازهٔ کافی طولانی برای بررسیِ حادثهای که در عرضِ یک هفته متوجهش میشوید، بهاندازهٔ کافی کوتاه که آن فایل یک دفترچهخاطرات نباشد.
دو نکتهٔ پیادهسازی که آدمها را گیر میاندازد. ایمیجهای Debian و Ubuntu در اینکه آیا rsyslog نصب است یا نه فرق دارند؛ اگر /var/log/auth.log روی باکسِ شما وجود دارد پس rsyslog دارد آن را مینویسد، و سقفهای journald اصلاً آن فایل را اداره نمیکنند — آن کارِ logrotate است. و ForwardToSyslog= همان چیزی است که rsyslog را از journal تغذیه میکند، پس خاموشکردنش روی باکسی که هر دو را دارد جلوی این را میگیرد که دو کپی از همهچیز را زیرِ دو سیاستِ نگهداریِ متفاوت نگه دارید.
سیاستِ نگهداریِ شما همان چیزی است که بکآپهایتان میگویند
این همان چیزی است که کلِ کارِ دقیقِ بالا را خنثی میکند، و تا وقتی دنبالش نگردید نامرئی است.
فرض کنید 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"
حالا که در همین حالوهوا هستید، دو همسایهٔ همین مشکل. اسنپشاتهای هاستِ شما مالِ شما نیستند. یک اسنپشاتِ سطحِ هایپروایزر دیسک را همانطور که بوده ضبط میکند، از جمله لاگهایی که از آنموقع rotate کردهاید، و روی storageِ provider، زیرِ نگهداریِ provider زندگی میکند — که یک دلیلِ دیگر است برای اینکه آدرسها اصلاً نباید کامل نوشته میشدند، نه یک دلیل برای انجامِ کارِ باهوشانهای بعدش.
و روی یک VPS سراغِ shred نروید. بازنویسیِ یک فایل روی یک دیسکِ مجازیِ thin-provisioned، روی یک فایلسیستمِ copy-on-write، بالای یک SSD که بلاکها را برای wear-levelling جابهجا میکند، سلولهای فیزیکیای که فایل را نگه داشته بودند بهطورِ قابلاعتماد بازنویسی نمیکند. حذفِ امن روی storageِ اجارهای و مجازیسازیشده تئاتر است. تنها کاهشی که کار میکند همان است که در لحظهٔ نوشتن انجام دادید؛ هر چیزِ بعدِ آن یک تلاشِ حداکثری است که نمیتوانید تأییدش کنید. رمزنگاریِ ولوم این معادله را عوض میکند — اما آن را پیش از نوشتهشدنِ داده عوض میکند، که همان درسِ قبلی است، دوباره.
کپیهایی که کنترلشان دست شما نیست
کمینهسازی روی باکسِ خودتان فقط یک لایه از چند لایه است، و روشنبودن دربارهٔ بقیه همان چیزی است که جلوی تبدیلشدنِ این کار به یک حسِ کاذبِ کاملبودن را میگیرد.
شبکهٔ هاستینگِ شما رکوردِ flow را میبیند. مبدأ، مقصد، پورتها، بایتها، زمانبندی — برای هر اتصالی که به داخل و خارجِ ماشین میرود، فرق نمیکند شما چیزی لاگ بگیرید یا نه. هیچ کانفیگی روی سرور آن را عوض نمیکند. این بخشِ بزرگی از دلیلِ این است که چرا حوزهٔ قضاییای که باکس در آن نشسته یک متغیرِ واقعی است نه یک متغیرِ بازاریابی؛ آنچه قانوناً از شبکه خواسته میشود کشور به کشور بهشدت فرق میکند.
CDN یا edgeِ شما در همان edge لاگ میگیرد. اگر Cloudflare برایتان TLS را terminate میکند، پیش از آنکه سرورِ شما اصلاً درگیر شود، خطِ درخواست و آدرسِ کلاینت را دارد، روی برنامهٔ نگهداریِ خودش، تابعِ فرایندِ قانونیِ خودش. کوتاهکردنِ لاگِ originِ شما از میانِ آن به عقب نمیرسد. اجرای یک edgeِ خودتان همان نسخهای از این کار است که واقعاً میتوانید کانفیگش کنید — و قوانینِ لاگگیریِ این راهنما اول روی همان باکسِ edge اعمال میشوند، چون آنجا همانجایی است که آدرسهای کوتاهنشده میرسند.
ردیابهای خطا و analytics آن را از باکس برایتان خارج میکنند. Sentry و بیشترِ همتاهایش بهطورِ پیشفرض IPِ کلاینت را به هر رویداد میچسبانند؛ تنظیمش معمولاً یک چیزی شبیهِ send_default_pii نام دارد و ارزش دارد چکش کنید نه اینکه فرضش بگیرید. هر analyticsِ هاستشده، از اساس، یک کپیِ شخصِثالث از همان access logی است که یک بعدازظهر وقت گذاشتید کوتاهش کنید.
ایمیل نشتیترینِ همه است. اگر هر چیزی روی باکس ایمیل میفرستد، هدرها هاست و آدرسِ فرستنده را حمل میکنند، و هر relay در مسیر یک کپی از envelope را با تایماستمپ نگه میدارد. یک فرمِ تماس که برایتان ایمیل میفرستد یک لاگ است که خودتان ادارهاش نمیکنید.
هیچکدام از اینها کارِ محلی را بیفایده نمیکند — کپیِ محلی همان چیزی است که همراهِ ماشین توقیف میشود، در یک نفوذ exfiltrate میشود، یا توسطِ خودتان تحویل داده میشود. این فقط همان تکلایهای است که کاملاً کنترلش میکنید، و اشتباه این است که آن را کلِ تصویر فرض کنید.
SP·08کمینهسازی از طریقِ طراحی، نه حذفِ پس از اخطار
اینجا ارزش دارد دقیق باشیم، چون این دو با هم قاطی میشوند، و فرقشان دقیقاً همان فرقِ بینِ یک رویهٔ مهندسیِ استاندارد و کاری است که نباید انجامش دهید.
از قبل تصمیمگرفتن دربارهٔ اینکه سرویسِ شما چه چیزی را جمع میکند و چهمدت نگهش میدارد یک رویهٔ عادی، مستندشده و تشویقشده است. زیرِ GDPR این یکی از دو اصلِ اصلی است — کمینهسازیِ داده و محدودیتِ نگهداری — و یک نگهداریِ لاگِ کوتاهتر همان کنترلی است که auditorها میخواهند، نه چیزی که به آن اعتراض کنند. هیچ تعهدِ عمومیای روی یک اپراتورِ وبسایت یا یک مشتریِ هاستینگ در اتحادیهٔ اروپا برای نگهداریِ لاگهای ترافیک نیست؛ دستورالعملِ نگهداریِ سراسری که زمانی خلافِ این را نشان میداد در سالِ ۲۰۱۴ توسطِ دیوانِ دادگستریِ اتحادیهٔ اروپا لغو شد، و قوانینِ ملیای که از آن جان سالم به در بردهاند بیشتر ارائهدهندگانِ مخابراتی را مقید میکنند، نه آدمهایی که یک وبسرور اجرا میکنند. ایالاتِ متحده هم هیچ الزامِ عمومیِ نگهداریای برای اپراتورهای سایت ندارد.
نابودکردنِ رکوردهای مشخص بعد از آنکه دربارهٔ آنها به شما اخطار داده شده یک عملِ کاملاً متفاوت است. یک درخواستِ نگهداری، یک litigation hold، یک حکمِ دادگاه، یا یک استعلامِ پلیس، آنچه را که میتوانید با دادهای که در همان لحظه وجود دارد انجام دهید عوض میکند، و "سیاستِ نگهداریِ من حذفش کرد" دفاعی نیست اگر بهخاطرِ آن اخطار حذف را تسریع کرده باشید. هیچچیز در این راهنما دربارهٔ آن نیست. یک سیاستِ نگهداری چیزی است که یک سهشنبهٔ آرام تنظیمش میکنید و بعد رهایش میکنید؛ اگر فقط وقتی چیزی اتفاق میافتد کوتاهش میکنید، آن اصلاً یک سیاست نبوده.
همین تمایز در بقیهٔ این سایت هم جاری است: یک خطِ واقعی بینِ مهندسیِ حریمِ خصوصی و همان موضعِ "بولتپروف" وجود دارد که خودش را مصونیت جا میزند. طراحیِ سرویسی که هرگز یک تاریخچهٔ بازدیدکننده انباشته نمیکند بهراحتی سمتِ درستِ آن خط مینشیند، در همان دستهای که رمزنگاریِ دیسکهایتان و نخواستنِ آدرسِ ایمیلی از کاربران که نیازش ندارید قرار دارند.
دو پیامدِ عملی. اگر یک سیاستِ حریمِ خصوصی منتشر میکنید، عددهای نگهداریِ آن را با آنچه واقعاً روی دیسک است منطبق کنید — چهارده روزِ اعلامشده و شش ماهِ واقعی در یک ریپازیتوریِ بکآپ همان نوع شکافی است که یک اپراتورِ باحسننیت را روی کاغذ به یک اپراتورِ بدحسننیت تبدیل میکند. و تصمیم را برای خودتان یادداشت کنید، اگر جای دیگری نه، در یک کامنت بالای فایلِ 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موجود را با آن جفت جایگزین کنید، و همینجا لاگگیری را برای استاتیکاسِتها خاموش کنید.access_log /var/log/nginx/access.log privacy; access_log /var/log/nginx/security.log secip;
Reload کنید، یک صفحه را لود کنید، و نتیجه را بخوانید. فیلدِ اول باید به
.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 دارد فرمت را یکجای دیگر پایینتر override میکند —
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شمارندههایش را در حافظهٔ مشترک نگه میدارد، در میکروثانیه پاسخ میدهد بهجای یک فاصلهٔ پولینگ، و هیچ رکوردی از اینکه چه کسی throttle شده باقی نمیگذارد — برای اندازهگیریِ zoneها زیرِ بارِ واقعی به رانبوکِ اولین ساعتِ 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;
کلیدزدنِ zone روی
$ip_truncبهجای$binary_remote_addrیک معاملهٔ عمدی است: حالا محدودیت بهطورِ یکجا روی یک/24کامل اعمال میشود، پس یک دفترِ شلوغ که پشتِ یک بلاک است یک بودجه را مشترک میشود. برای یک سایتِ کوچک این معمولاً خوب است و گاهی یک پیشرفت؛ اگر نبود، zone را روی آدرسِ کامل کلید بزنید — وضعیتِ یک rate-limiter در حافظهٔ مشترک زندگی میکند و هرگز روی دیسک نوشته نمیشود، پس اینجا بخشی از آنچه دارید کمینه میکنید نیست. -
04
journal را سقف بگذارید و جلوی کپیِ دوم را بگیرید
journald یک drop-in قبول میکند، که به شکلی که ویرایشِ
journald.confندارد از آپگریدهای پکیج جان سالم به در میبرد. مقادیرِ زیر حدودِ یک هفته را نگه میدارند — بهاندازهٔ کافی برای بررسیِ چیزی که دوشنبه متوجهش میشوید و جمعه شروع شده بود — داخلِ یک سقفِ سختِ ۲۰۰ مگابایت.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
ریاستارت سقفهای اندازه را فوراً اعمال میکند؛ ساعتِ نگهداری همانطور که فایلهای تازه rotate میشوند اجرا میشود، پس یک journalِ قدیمیِ بیشازحدبزرگ در rotationِ بعدی کوچک میشود نه فوراً.
journalctl --vacuum-time=7dاگر امروز دیسک را پس میخواهید همین الان مجبورش میکند.ForwardToSyslog=noروی هر ایمیجی که rsyslog را همراه دارد اهمیت دارد: بدونِ آن، هر ورودیِ journal به/var/log/syslogهم append میشود، زیرِ برنامهٔ 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 مالکشان است و مرحلهٔ شش همانجایی است که نگهداریِ آنها تنظیم میشود. اگر وجود ندارند، journal تنها کپی است و همین الان سقفش زدید.
-
05
جلوی این را بگیرید که اپلیکیشن دوباره چیزی را که حذف کردید لاگ کند
هیچکدام از موارد بالا به اپلیکیشنِ شما دست نمیزند، و یک اپلیکیشن در حالتِ غلط با کمالِ میل آدرسِ کامل، URLِ کامل و توکنِ نشست را در فایلِ خودش مینویسد. سه چیز برای چککردن.
PHP-FPM یک دایرکتیوِ
access.logرا در کانفیگِ poolِ خودش همراه میآورد، که پیشفرض کامنت شده اما توسطِ تعدادِ زیادی کنترلپنل فعال میشود. اگر روشن است، یک کپیِ دومِ هر خطِ درخواست است، از فایلی که کارِ nginxِ شما هرگز لمسش نکرده.grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"
سطحِ لاگِ فریمورکِ شما تصمیم میگیرد که آیا query stringها و بدنهٔ درخواستها روی دیسک مینشینند یا نه. حالتِ debug در بیشترِ فریمورکها URLِ کامل را لاگ میکند، و یک لینکِ ریستکردنِ پسورد یک URLِ کامل است. سطحِ production را تنظیم کنید و مطمئن شوید واقعاً همانی است که لود میشود، نه همانی که فکر میکنید در حالِ خواندهشدن است.
درایورِ پیشفرضِ Docker هرگز rotate نمیکند. آن را در سطحِ دیمن درست کنید تا هر کانتینرِ آینده آن سقف را به ارث ببرد. توجه کنید این فقط روی کانتینرهایی اعمال میشود که بعد از ریاستارت ساخته میشوند — کانتینرهای موجود همان فایلِ فعلی و بیسقفشان را تا وقتی دوباره ساخته شوند نگه میدارند.
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 مینویسند زندگی میکند. بندِ از قبل موجود را ویرایش کنید نه اینکه یکی دوم اضافه کنید: دو بند که نامِ یک مسیرِ یکسان را میبرند باعث میشوند logrotate با یک خطای duplicate-entry شکست بخورد و rotateکردنِ آن فایل را کاملاً متوقف کند، که رایجترین راهی است که یک تغییرِ نگهداری بیسروصدا به یک نگهداریِ بینهایت تبدیل میشود.
# 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ِ خودتان. و مرحلهٔ دیباگ را یکبارِ آخر اجرا کنید: دقیقاً چاپ میکند کدام فایلها را rotate و حذف میکرد، که تنها راه برای تأییدِ این است که عددهایی که تازه تایپ کردید همان عددهای در حالِ اجرا هستند. -
07
ثابتش کنید، حتی از میانِ بکآپ
اندازهگیریهای مرحلهٔ اول را دوباره اجرا کنید. شمارشِ آدرسهای کاملِ متمایز در لاگِ با نگهداریِ بلند حالا باید از رشدکردن بایستد، و بعد از یک چرخهٔ rotation باید صفر باشد.
# 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"
اگر شمارش صفر نیست، یا همان exclusionِ قبلی را اضافه کنید و بگذارید آرشیوهای قدیمی خودشان منقضی شوند، یا عمداً هرسشان کنید. تا وقتی یکی از اینها اتفاق نیفتد، نگهداریِ واقعیِ شما همان نگهداریِ ریپازیتوری است — که تنها مفیدترین جملهٔ این راهنماست، و راحتترینش برای فراموشکردن.
در آخر، عددها را همانجایی بنویسید که نفرِ بعدی پیدایشان میکند: نگهداریای که انتخاب کردید، دلیلش، و تاریخ. یک کامنت بالای فایلِ logrotate کافی است. کانفیگِ بالا یک تصمیم است، و تصمیمی که هیچکس نتواند در عرضِ یک سال بازسازیاش کند دوباره یک پیشفرض میشود.


