خطای 403 Forbidden با بدنهٔ SignatureDoesNotMatch گرفتهاید، ولی کلید و secret را ده بار چک کردهاید و درست است. یا بدتر: با aws s3 ls روی endpoint خودتان همهچیز کار میکند، اما همان درخواست از داخل کد پایتون یا از یک سرور دیگر جواب نمیدهد. مشکل تقریباً همیشه در یکی از سه چیز است: نحوهٔ امضای درخواست، سیاست باکت، یا آدرسی که به کلاینت دادهاید. این متن دقیقاً همان سهتا را باز میکند.
سازگاری با پروتکل S3 یعنی چه و کجا میشکند
وقتی میگوییم یک سرویس «با S3 سازگار است»، یعنی endpoint آن دستورهای HTTP با امضای AWS Signature Version 4 را میفهمد. این تعریف رسمی است، نه یک ادعای بازاریابی. در عمل یعنی میتوانید از boto3، aws-cli، rclone یا s3cmd استفاده کنید و فقط endpoint را عوض کنید.
اما سازگاری، دودویی نیست. جایی که در عمل میشکند:
- Virtual-hosted style در برابر path-style. آدرس
https://bucket.example.com/keyباhttps://example.com/bucket/keyیکی نیستند. اگر DNS wildcard برای زیردامنه تنظیم نشده باشد، حالت اول با خطای TLS یاNoSuchBucketبرمیگردد. - عملیات چندبخشی (Multipart Upload). بعضی پیادهسازیها
UploadPartCopyرا پشتیبانی نمیکنند و فقطUploadPartدارند. - Versioning و Object Lock. اینها اختیاریاند و همهجا نیستند.
- هدرهای امضا. اگر یک پروکسی معکوس هدر
Hostیاx-amz-dateرا دست بزند، امضا میشکند و همانSignatureDoesNotMatchظاهر میشود.
نکتهٔ عملی: قبل از هر چیز با aws s3api get-bucket-location --bucket mybucket --endpoint-url https://s3.example.com تست کنید. اگر این جواب داد ولی کد شما نه، مشکل پروتکل نیست، مشکل SDK شماست.
کلید دسترسی: چرا secret را نمیبینید و چطور باید ذخیرهاش کنید
کلید دسترسی دو بخش دارد: یک Access Key ID که مثل نام کاربری است و یک Secret Access Key که فقط یک بار نمایش داده میشود. اگر آن را گم کردید، راهی برای بازیابی نیست؛ باید کلید جدید بسازید. این رفتار عمدی است، نه نقص محصول.
سه اشتباه رایج در مدیریت کلید:
- گذاشتن secret در متغیر محیطی داخل Dockerfile. با
docker historyقابل خواندن است. - استفاده از یک کلید برای همهٔ سرویسها. اگر یک سرویس لو برود، همهچیز لو میرود.
- نچرخاندن کلید. کلیدی که دو سال است کار میکند، یک ریسک پنهان است.
الگوی درست: برای هر سرویس یک کاربر جدا با حداقل دسترسی بسازید، secret را در یک secret manager یا فایل با مجوز chmod 600 نگه دارید، و چرخش کلید را در تقویم بگذارید. اگر روی زیرساخت ابری کار میکنید، نقشهای IAM مبتنی بر نمونه (Instance Role) بهتر از کلید ثابت هستند، چون کلید روی دیسک نمینشیند.
یک نکتهٔ ظریف: خیلیها فکر میکنند چون کلید را در کد هاردکد نکردهاند و در .env گذاشتهاند، امن هستند. اما اگر .env در گیت commit شده باشد، همان فاجعه است. با git log --all --full-history -- .env چک کنید.
سیاست باکت: تفاوت policy و ACL و اینکه کدام را انتخاب کنید
دو مکانیزم کنترل دسترسی وجود دارد: Bucket Policy (یک سند JSON که به خود باکت میچسبد) و ACL (لیست دسترسی سطح آبجکت). توصیهٔ من صریح است: ACL را خاموش کنید و فقط از Bucket Policy استفاده کنید. ACL در AWS عملاً منسوخ شده و مدیریتش در مقیاس، کابوس است. فقط در یک حالت سراغ ACL بروید: وقتی کلاینت قدیمیای دارید که policy را نمیفهمد و فقط ACL میفرستد.
یک policy حداقلی برای خواندن عمومی یک پوشهٔ مشخص:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadAssets",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mybucket/assets/*"
}
]
}
دو چیز را در همین سند ببینید. اول، Principal: "*" یعنی هر کسی در اینترنت. دوم، assets/* یعنی فقط همان پوشه، نه کل باکت. اگر ستاره را به mybucket/* تغییر دهید، همهٔ فایلهای خصوصی هم عمومی میشوند. این دقیقاً همان چیزی است که در نشتهای داده میبینیم.
و یک هشدار: s3:ListBucket با s3:GetObject فرق دارد. اگر فقط GetObject بدهید، کسی نمیتواند لیست فایلها را ببیند ولی اگر نام فایل را بداند، دانلود میکند. برای فایلهای حساس، این کافی نیست.
آدرس امضاشده: چطور بدون عمومیکردن باکت، دسترسی موقت بدهید
Presigned URL یک آدرس است که با secret key شما امضا شده و برای مدت محدود معتبر است. با این کار باکت خصوصی میماند ولی کاربر میتواند یک فایل مشخص را دانلود کند.
با AWS CLI:
aws s3 presign s3://mybucket/report.pdf \
--expires-in 3600 \
--endpoint-url https://s3.example.com
عدد 3600 ثانیه است، یعنی یک ساعت. حداکثر مقدار در AWS Signature V4 هفت روز (۶۰۴۸۰۰ ثانیه) است. اگر بیشتر بخواهید، باید از مکانیزم دیگری مثل CloudFront Signed URL استفاده کنید.
اینجا اشتباه میکنند: آدرس امضاشده را در دیتابیس ذخیره میکنند و بعد از انقضا، کاربر خطای Request has expired میگیرد و تیم فکر میکند سرویس خراب است. آدرس امضاشده یک توکن زودگذر است، نه یک لینک دائمی. آن را در لحظه بسازید و بفرستید.
نکتهٔ دوم: امضا به هدرها و پارامترهای درخواست حساس است. اگر آدرس را کپی کنید و یک پارامتر اضافه کنید، امضا میشکند. همچنین اگر ساعت سروری که آدرس را میسازد با ساعت سرویس استوریج بیش از ۱۵ دقیقه اختلاف داشته باشد، امضا رد میشود. با timedatectl status یا chronyc tracking ساعت را چک کنید.
انتخاب بین آدرس عمومی، امضاشده و پروکسی
| روش | مناسب برای | هزینهای که میپردازید |
|---|---|---|
| Bucket عمومی | فایلهای استاتیک سایت، تصاویر | هر کسی با حدس نام فایل دانلود میکند |
| Presigned URL | دانلود موقت، آپلود از مرورگر | باید در لحظه ساخته شود؛ قابل کش نیست |
| پروکسی از سمت سرور | کنترل کامل، لاگ دقیق | پهنای باند سرور شما مصرف میشود |
برای فایلهای استاتیک عمومی، باکت عمومی بههمراه یک CDN بهترین انتخاب است، چون هزینهٔ پهنای باند سرور را حذف میکند و TTFB را پایین میآورد. برای هر چیز دیگری، presigned URL. پروکسی را فقط وقتی انتخاب کنید که نیاز به لاگ دقیق یا watermark دارید.
اگر بار ترافیک بالا میرود و میخواهید بدون تغییر کد، لایهٔ کش جلوی استوریج بگذارید، شبکهٔ توزیع محتوا گزینهٔ منطقی است. و اگر میخواهید کل این جریان را روی زیرساخت خودتان بالا بیاورید، فضای ذخیرهسازی آبجکت نقطهٔ شروع است.
چند عدد و تنظیم که در عمل به آنها میخورید
- حداکثر اندازهٔ یک آبجکت در یک درخواست PUT معمولاً ۵ گیگابایت است. برای بزرگتر، Multipart لازم است.
- هر بخش Multipart (بهجز بخش آخر) حداقل ۵ مگابایت است. اگر کوچکتر بفرستید، خطای
EntityTooSmallمیگیرید. - حداکثر تعداد بخشها ۱۰۰۰۰ است. با بخشهای ۵ مگابایتی، سقف عملی حدود ۵۰ گیگابایت میشود؛ برای فایل بزرگتر، اندازهٔ بخش را بالا ببرید.
- اختلاف ساعت مجاز برای امضا ۱۵ دقیقه است.
- پیشفرض
aws s3 cpبرای فایلهای بالای ۸ مگابایت خودش Multipart میکند.
یک تلهٔ ظریف: اگر Multipart را نیمهکاره رها کنید، بخشهای ناتمام در باکت میمانند و هزینهٔ ذخیرهسازی میخورند بدون اینکه در لیست آبجکتها دیده شوند. با aws s3api list-multipart-uploads --bucket mybucket چک کنید و یک lifecycle rule برای پاککردن خودکارشان بگذارید.
اگر روی Kubernetes کار میکنید و میخواهید استوریج را بهعنوان volume سوار کنید، کوبرنتیز مدیریتشده این کار را سادهتر میکند. برای پایش نرخ خطا و latency درخواستها، راهنمای مانیتورینگ ابری و هشدار را ببینید؛ هشدار روی نرخ 403 خیلی زودتر از شکایت کاربر، مشکل policy را لو میدهد.
پرسشهای پرتکرار
چرا با وجود کلید درست، خطای SignatureDoesNotMatch میگیرم؟
در نود درصد موارد، ساعت سیستم شما با سرویس استوریج اختلاف دارد. امضای V4 به زمان حساس است و اختلاف بیش از ۱۵ دقیقه باعث رد شدن میشود. با timedatectl status ساعت را چک کنید و NTP را فعال کنید.
اگر ساعت درست بود، احتمال بعدی این است که یک پروکسی معکوس هدر Host یا x-amz-* را تغییر میدهد. امضا روی همین هدرها محاسبه میشود.
تفاوت presigned URL و لینک عمومی چیست؟
لینک عمومی برای هر کسی و همیشه کار میکند. آدرس امضاشده فقط برای مدت مشخصی که تعیین میکنید معتبر است و بعد از آن Request has expired برمیگرداند. برای فایلهای حساس، همیشه presigned استفاده کنید.
حداکثر مدت اعتبار در Signature V4 هفت روز است. برای بیشتر از این، باید از مکانیزم امضای CDN استفاده کنید.
آیا باید ACL را روی باکت فعال نگه دارم؟
نه، مگر اینکه کلاینت قدیمی داشته باشید که فقط ACL میفرستد. Bucket Policy خواناتر، قابل نسخهبندی و قابل ممیزی است. ACL در سطح هر آبجکت جداگانه اعمال میشود و در مقیاس، مدیریتش تقریباً غیرممکن میشود.
چرا فایلهای ناتمام در باکت من فضا اشغال میکنند؟
چون Multipart Upload نیمهکاره رها شده است. این بخشها در لیست آبجکتها دیده نمیشوند ولی ذخیره میشوند. با list-multipart-uploads پیدایشان کنید و یک lifecycle rule برای حذف خودکار بعد از چند روز بگذارید.
قدم بعدی: یک کلید جدا برای هر سرویس بسازید، ACL را خاموش کنید، و یک هشدار روی نرخ خطای ۴۰۳ در پایش خود تنظیم کنید. همین سه کار، بیشتر مشکلاتی که اینجا دربارهشان نوشتم را از بین میبرد.