ESC را فشار دهید تا بسته شود

رفع خطای Permission denied در Docker Bind Mount؛ عیب‌یابی UID/GID، SELinux و Volume

فهرست

اگر پس از اتصال پوشه میزبان به کانتینر Docker با خطای Permission denied یا EACCES مواجه می‌شوید، نخست UID/GID فرآیند داخل کانتینر را با مالک و مجوز مسیر میزبان مقایسه کنید. سپس وضعیت Read-only بودن Mount و سیاست‌های SELinux یا AppArmor را بررسی کنید. اجرای chmod -R 777 یا غیرفعال‌کردن SELinux راهکار امنی نیست. این راهنما برای عیب‌یابی عملی روی VPS لینوکس و Docker Compose نوشته شده است.

سناریوی واقعی: کانتینر اجرا می‌شود اما نمی‌تواند فایل بسازد

فرض کنید پوشه /srv/app/data روی میزبان به /data در کانتینر app متصل است. برنامه با UID برابر 10001 اجرا می‌شود ولی مالک پوشه root و مجوز آن 755 است. در این حالت برنامه معمولاً می‌تواند فایل‌ها را بخواند ولی اجازه نوشتن ندارد. خطا ممکن است در لاگ برنامه با عبارت EACCES: permission denied, open '/data/file' دیده شود.

docker ps --format 'table {{.Names}} {{.Image}} {{.Status}}'
docker logs --tail 60 app

نام کانتینر و مسیرهای نمونه را با مقادیر واقعی جایگزین کنید. اگر خطا هنگام Build رخ می‌دهد، باید مجوز فایل‌های COPY و دستور USER در Dockerfile را جداگانه بررسی کنید؛ Bind Mount زمان اجرا با Build یکسان نیست.

پیش‌نیاز و ثبت وضعیت پیش از تغییر

در سرور Production ابتدا از داده مهم بکاپ بگیرید، دسترسی کنسول داشته باشید و مالک و مجوز فعلی پوشه را ثبت کنید. روی مسیر داده دیتابیس زنده، دستور آزمایشی نوشتن اجرا نکنید. دستورات زیر به مجوز مدیریت Docker نیاز دارند:

docker inspect app --format '{{json .Mounts}}'
docker inspect app --format 'Config.User={{.Config.User}}'
sudo stat -c '%n UID=%u GID=%g MODE=%a' /srv/app/data
sudo namei -l /srv/app/data
sudo getfacl -p /srv/app/data

در خروجی docker inspect، فیلدهای Source، Destination، Type و RW را بررسی کنید. مقدار RW=false یعنی Mount برای نوشتن تنظیم نشده است. مقدار خالی Config.User الزاماً UID فرآیند برنامه را نشان نمی‌دهد؛ ممکن است Entrypoint کاربر را تغییر دهد.

مرحله اول: UID و GID واقعی برنامه را مشخص کنید

مجوز فایل در لینوکس با شناسه عددی سنجیده می‌شود. یکسان‌بودن نام کاربر داخل و خارج کانتینر کافی نیست. این دستورات UID شل و فرآیند PID 1 را نشان می‌دهند؛ اگر برنامه با کاربر دیگری اجرا می‌شود، شناسه همان فرآیند را بررسی کنید.

docker exec app sh -c 'id; ps -o pid,user,group,args -p 1 2>/dev/null || true'
docker exec app sh -c 'ls -ldn /data; stat -c "%u:%g %a %n" /data'

اگر تصویر Distroless است یا Shell ندارد، docker exec ممکن است با خطای executable file not found مواجه شود. در این حالت از تنظیمات Image و یک ابزار عیب‌یابی مجاز استفاده کنید؛ برای تست، برنامه اصلی را بی‌دلیل با root اجرا نکنید.

مرحله دوم: مجوز پوشه و والدها را بررسی کنید

برای عبور از هر دایرکتوری مسیر مجوز x لازم است. برای ساخت فایل جدید، معمولاً روی دایرکتوری مقصد w و x نیاز دارید. namei -l مجوز والدها، stat مالک عددی و getfacl ACLهای اضافی را نشان می‌دهد.

sudo namei -l /srv/app/data
sudo stat -c 'owner=%u group=%g mode=%a path=%n' /srv /srv/app /srv/app/data
sudo getfacl -p /srv/app/data

اگر UID برنامه 10001 و مالک پوشه 0 و Mode برابر 755 است، تنها مالک حق نوشتن دارد. تغییر مجوز کل /srv یا /var/lib/docker اشتباه است؛ فقط مسیر اختصاصی همان سرویس را بررسی و اصلاح کنید.

مرحله سوم: تست ایزوله روی مسیر غیرحساس

ابتدا روی یک پوشه آزمایشی مستقل تست کنید تا بدون دست‌زدن به داده اصلی، رفتار Mount و UID مشخص شود. در صورت نبود تصویر BusyBox روی سرور، ممکن است Docker آن را از Registry دریافت کند.

mkdir -p /tmp/docker-bind-check
docker run --rm --user "$(id -u):$(id -g)" --mount type=bind,src=/tmp/docker-bind-check,dst=/data busybox:1.36 sh -c 'id; touch /data/.test && ls -ln /data/.test && rm /data/.test'

موفقیت تست موقت به معنی سالم‌بودن مجوز مسیر Production نیست. اگر این تست موفق است اما برنامه اصلی خطا دارد، UID فرآیند، مسیر واقعی Source، ACL و SELinux پوشه اصلی را مقایسه کنید.

مرحله چهارم: اصلاح مالکیت یا ACL با کمترین دسترسی

اگر پوشه فقط به سرویس موردنظر تعلق دارد و UID/GID برنامه 10001:10001 است، پس از ثبت وضعیت قبلی می‌توانید مالک همان پوشه را اصلاح کنید. این نمونه بازگشتی نیست؛ فایل‌های زیرپوشه را باید جداگانه بررسی کنید.

sudo stat -c '%u:%g %a %n' /srv/app/data
sudo chown 10001:10001 /srv/app/data
sudo chmod 0750 /srv/app/data

برای مسیر مشترک، ACL می‌تواند بهتر از تغییر مالکیت باشد. ابتدا ابزار setfacl و مجوز عبور پوشه‌های والد را بررسی کنید:

sudo setfacl -m u:10001:rwx /srv/app/data
sudo getfacl -p /srv/app/data

از تغییر بازگشتی مالکیت مسیرهای عمومی مانند /var/lib/docker یا chmod 777 پرهیز کنید. در Docker Rootless و userns-remap، UID داخل کانتینر ممکن است روی میزبان به UID متفاوتی نگاشت شود؛ پس مالکیت را حدسی تغییر ندهید.

مرحله پنجم: بررسی SELinux و AppArmor

اگر مجوزهای Unix درست است اما دسترسی هنوز رد می‌شود، SELinux یا AppArmor را بررسی کنید. SELinux مستقل از chmod عمل می‌کند. ابزار ausearch ممکن است نیازمند auditd باشد.

getenforce
ls -Zd /srv/app/data
sudo ausearch -m avc -ts recent 2>/dev/null | tail -n 35

در Docker، گزینه :z برای برچسب اشتراکی و :Z برای برچسب خصوصی یک کانتینر در syntax کوتاه -v پشتیبانی می‌شود. استفاده نادرست از :Z روی پوشه مشترک می‌تواند دسترسی کانتینرهای دیگر را قطع کند. مسیرهای سیستمی مانند /home یا /usr را بی‌دلیل relabel نکنید و SELinux را برای رفع دائمی خطا غیرفعال نکنید. برای آشنایی بیشتر، مقایسه AppArmor و SELinux را بخوانید.

مرحله ششم: آیا Mount فقط‌خواندنی است؟

اگر در docker inspect مقدار RW=false است، تغییر مجوز پوشه کافی نیست. گزینه readonly یا :ro را در Docker و volumes در Compose بررسی کنید. read_only: true روی فایل‌سیستم اصلی کانتینر اثر دارد و باید جدا از Mountهای مشخص بررسی شود.

docker inspect app --format '{{range .Mounts}}{{println .Destination .RW .Type}}{{end}}'
docker compose config

اگر پوشه عمداً فقط‌خواندنی است، مسیر نوشتن برنامه را اصلاح کنید؛ تغییر Mount ممکن است به Recreate کانتینر نیاز داشته باشد. برای شناخت فایل پیکربندی، آموزش Docker Compose روی سرور را ببینید.

مرحله هفتم: تفاوت Docker Volume و Bind Mount

در Bind Mount مسیر واقعی میزبان و مجوزهای آن مهم است. در Named Volume محل داده توسط Docker مدیریت می‌شود، اما مالکیت عددی فایل‌های داخل Volume همچنان می‌تواند خطای دسترسی ایجاد کند. تغییر نوع ذخیره‌سازی همیشه راه‌حل نیست. برای تصمیم بهتر، تفاوت Docker Volume و Bind Mount را مطالعه کنید.

اگر Source وجود ندارد، در syntax قدیمی -v ممکن است دایرکتوری جدید ساخته شود، اما –mount type=bind به‌طور پیش‌فرض برای Source ناموجود خطا می‌دهد. خطاهای not a directory یا is a directory را با Permission denied اشتباه نگیرید.

مرحله هشتم: بررسی Rootless و User Namespace

در Docker Rootless هم daemon و هم کانتینر در فضای نام کاربر اجرا می‌شوند. در userns-remap، شناسه‌های داخل کانتینر به شناسه‌های دیگری در میزبان نگاشت می‌شوند. قبل از chown باید Mode را تشخیص دهید:

docker context show
docker info --format '{{json .SecurityOptions}}'
docker inspect app --format 'User={{.Config.User}}'

روی Docker Desktop در Windows/macOS، اشتراک فایل میان میزبان و VM Docker نیز دخیل است؛ دستورهای chown لینوکس همیشه راه‌حل یکسانی ندارند.

نمونه Docker Compose با UID مشخص

این الگو فقط برای Imageهایی مناسب است که اجرای غیرریشه با UID 10001 را پشتیبانی می‌کنند. نام Image باید با نسخه واقعی جایگزین شود.

services:
  app:
    image: your-app-image:stable
    user: '10001:10001'
    volumes:
      - type: bind
        source: /srv/app/data
        target: /data
        read_only: false

پس از تغییر Compose، ابتدا docker compose config را اجرا کنید. تغییر user به‌تنهایی مالکیت پوشه میزبان را اصلاح نمی‌کند و تغییر پیکربندی معمولاً به بازآفرینی کانتینر در پنجره نگهداری نیاز دارد.

تست نهایی و خروجی مورد انتظار

اگر مسیر آزمایشی قابل نوشتن دارید، یک فایل موقت با همان کاربر برنامه بسازید و حذف کنید. روی پوشه دیتابیس یا داده حساس این تست را اجرا نکنید.

docker exec app sh -c 'id; touch /data/.permission-test && ls -ln /data/.permission-test && rm /data/.permission-test'
docker logs --since 5m app
docker inspect app --format '{{range .Mounts}}{{println .Destination .RW}}{{end}}'

خروجی مورد انتظار: touch بدون خطا انجام شود، EACCES جدید در لاگ نباشد و Mount مقصد برای مسیر نوشتنی RW=true باشد. اگر کانتینر Shell ندارد، از Endpoint تست برنامه یا ابزار عیب‌یابی سازگار استفاده کنید.

Rollback و بازیابی امن

قبل از تغییر، مالکیت و Mode قبلی را با stat و ACL را با getfacl ثبت کنید. اگر فقط ACL آزمایشی افزوده‌اید، می‌توانید همان ورودی را حذف کنید. برای بازگرداندن مالکیت، شناسه‌های واقعی ذخیره‌شده را جایگزین کنید؛ متن ORIGINAL_UID و ORIGINAL_GID جای‌نگهدار است.

sudo setfacl -x u:10001 /srv/app/data
sudo chown ORIGINAL_UID:ORIGINAL_GID /srv/app/data

اگر Compose تغییر کرده، نسخه قبلی را برگردانید و با docker compose config تأیید کنید. برگرداندن برچسب SELinux باید بر اساس Policy همان مسیر انجام شود. از حذف Volume یا تغییر بازگشتی مجوزها به‌عنوان Rollback استفاده نکنید.

خطاهای رایج در تشخیص

  • Read-only file system: Mount یا فایل‌سیستم اصلی فقط‌خواندنی است.
  • not a directory: Source و Destination از نظر فایل یا پوشه سازگار نیستند.
  • operation not permitted: ممکن است Capability، User Namespace یا SELinux مانع باشد.
  • Permission denied هنگام Build: مجوزهای COPY و USER در Dockerfile را بررسی کنید.
  • دسترسی به docker.sock: موضوع مجوز Docker daemon است و عضویت در گروه docker سطح دسترسی بسیار بالایی ایجاد می‌کند.

پرسش‌های متداول

چرا با وجود chmod 777 هنوز Permission denied می‌گیرم؟

چون SELinux، AppArmor، Mount فقط‌خواندنی، User Namespace یا مجوز عبور والدها می‌توانند مستقل از Mode فایل دسترسی را مسدود کنند.

برای حل خطا بهتر است Docker Volume استفاده کنم یا Bind Mount؟

اگر به مسیر ثابت روی میزبان نیاز ندارید، Named Volume مدیریت داده را ساده‌تر می‌کند؛ اما UID/GID همچنان مهم است. Bind Mount برای دسترسی مستقیم به فایل میزبان مناسب است.

آیا اجرای برنامه با root مشکل را رفع می‌کند؟

ممکن است بعضی خطاها را موقتاً پنهان کند اما ریسک امنیتی دارد و در SELinux یا Mount فقط‌خواندنی تضمینی نیست. اصل بر حداقل دسترسی است.

در سرور مجازی لینوکس وان‌سرور پیش از تغییر مجوزهای داده از اطلاعات بکاپ بگیرید و ابتدا روی محیط غیرProduction تست کنید.

مراجع رسمی: Docker Bind mounts، Docker Rootless و Docker Compose in production.

Rate this post
اشتراک گذاری نوشته در:

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

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *