رفتن به محتوا
زیرساخت فناوری اطلاعات

Docker 29.9.2 گواهی‌های لورفتهٔ eMudhra را رد می‌کند: رفع خطای x509 در registry

Docker Engine 29.9.2، Buildx 0.38.1 و Compose 5.6.1 زنجیره‌های گواهی‌ای را که از هشت CA میانیِ لورفتهٔ eMudhra عبور کنند رد می‌کنند. چطور بفهمید registry شما درگیر است، چرا insecure-registries راه‌حل اشتباهی است و چه چیزی را باید دوباره build کنید.

H
Hamze Zare Nasiri
۱۱ آبان ۱۴۰۵
Docker 29.9.2 گواهی‌های لورفتهٔ eMudhra را رد می‌کند: رفع خطای x509 در registry

Docker در ۲۰ اکتبر ۲۰۲۶ نسخهٔ Docker Engine 29.9.2 را منتشر کرد؛ یک وصلهٔ ظاهراً معمولی که فقط runtime زبان Go را به‌روز می‌کند. اما معمولی نیست. runtime جدید باعث می‌شود daemon هر زنجیرهٔ گواهی TLS را که از یکی از هشت CA میانیِ eMudhra عبور کند رد کند، چون کلیدهای خصوصی این هشت گواهی لو رفته‌اند. اگر registry خصوصی، mirror یا سرور فایلِ شما گواهی‌اش را از یکی از این CAهای میانی گرفته باشد، از لحظهٔ ارتقا docker pull و docker push روی آن کار نمی‌کنند.

پیام خطا هم اوضاع را بدتر می‌کند، چون علت را اشتباه نشان می‌دهد. در ادامه می‌گوییم چه چیزی تغییر کرد، چطور در چند دقیقه هاست‌ها و registryهایتان را بررسی کنید و در تنظیمات daemon، Dockerfileها و CI چه چیزی را باید عوض کنید.

چه اتفاقی افتاد، به ترتیب

طبق افشای عمومی این ماجرا (همان صفحه‌ای که یادداشت‌های انتشار خودِ Docker به آن لینک داده‌اند)، یک پژوهشگر با امضای یک پیام با کلید خصوصی هر کدام از هشت گواهی میانی eMudhra ثابت کرد که این کلیدها در اختیارش است و در ۱۷ اکتبر موضوع را به برنامه‌های root مرورگرها و سیستم‌عامل‌ها گزارش داد. همان صفحه می‌گوید این کلیدها «توسط CA به‌اشتباه تولید شده بودند»، یعنی هر کسی می‌توانست آن‌ها را از روی خودِ گواهی‌ها بازیابی کند. Chrome و Firefox از ۱۷ اکتبر، Microsoft از ۲۰ اکتبر و Apple از ۲۲ اکتبر این گواهی‌های میانی را مسدود کردند.

مرورگرها از طریق فیدهای ابطال (revocation) اصلاح شدند. اما بسیاری از کلاینت‌های دیگر اصلاً داده‌های ابطال را نمی‌خوانند و صفحهٔ افشا صریحاً می‌گوید این کلاینت‌ها «برای اینکه دیگر به گواهی‌های میانیِ لورفته اعتماد نکنند به به‌روزرسانی نرم‌افزار نیاز دارند». CLI، daemon، Buildx و Compose در Docker همگی برنامه‌های Go هستند، پس اصلاحشان از Go آمد.

هشت گواهی میانیِ مسدودشده، طبق یادداشت‌های انتشار Engine، این‌ها هستند: EM DV TLS CA - G3B، EM DV TLS CA - G3C، EM OV TLS CA - G3B، EM OV TLS CA - G3C، EM EV TLS CA - G3B، EM EV TLS CA - G3C و دو CA امضای کد، یعنی EM OV CS CA - G3B و EM OV CS CA - G3C.

چه چیزی خراب می‌شود و خطا چه شکلی است

یادداشت‌های انتشار 29.9.2 موضوع را در یک جمله گفته‌اند: «اگر زنجیرهٔ گواهی یک registry به یکی از این گواهی‌های میانی TLS متکی باشد، pull و push ایمیج‌ها در تأیید گواهی TLS شکست می‌خورند.» هیچ پرچمی برای خاموش کردن این فیلتر وجود ندارد.

سایت افشا هاست‌های آزمایشی دارد که گواهی‌هایی از هر کدام از این CAهای مسدود ارائه می‌دهند. ما در ۲ نوامبر هاست Docker 29.9.2 خودمان را به یکی از آن‌ها وصل کردیم:

$ docker pull dv-g3c-emudhra.we-have-a-problem.com/test:latest
Error response from daemon: Get "https://dv-g3c-emudhra.we-have-a-problem.com/v2/": tls: failed to verify certificate: x509: certificate signed by unknown authority

این خطا را با دقت بخوانید. «unknown authority» همان پیامی است که برای registry با گواهی self-signed هم می‌بینید، برای همین اولین واکنش بیشتر کسانی که آنکال هستند این است که registry را insecure علامت بزنند یا یک فایل CA اضافه کنند. هیچ‌کدام راه‌حل درست این مشکل نیست و اولی اوضاع را بدتر می‌کند (پایین‌تر توضیح می‌دهیم).

registryهای عمومی بزرگ درگیر نیستند. در ۲ نوامبر زنجیرهٔ گواهی Docker Hub (registry-1.docker.io)، quay.io و public.ecr.aws (همه زیر rootهای Amazon)، ghcr.io و registry.gitlab.com (Sectigo) و gcr.io (Google Trust Services) را بررسی کردیم. هیچ‌کدام از eMudhra عبور نمی‌کنند. ریسک متوجه Harbor، Nexus، Artifactory یا registry:2 خودمیزبان، mirrorهای pull-through و سرورهای داخلی است که CI از آن‌ها فایل دانلود می‌کند.

گام ۱: ببینید چه نسخه‌هایی اجرا می‌کنید

این سه دستور فقط‌خواندنی هستند و روی هاست production هم بی‌خطرند:

docker version --format '{{.Client.Version}} / {{.Server.Version}}'
docker buildx version
docker compose version --short

روی هاست ما خروجی 29.9.2 / 29.9.2، v0.38.1 و 5.6.1 است. هر نسخه‌ای قدیمی‌تر از Engine 29.9.2، Buildx 0.38.1 یا Compose 5.6.1 از بسته‌های خودِ Docker با runtime قدیمی‌تر Go ساخته شده و هنوز زنجیره‌های لورفته را می‌پذیرد. این همان مشکل امنیتی است که به‌روزرسانی برای رفعش آمده، پس راه‌حل ارتقا و بعد رسیدگی به چیزهایی است که خراب می‌شوند، نه عقب ماندن.

گام ۲: همهٔ registryهایی را که از آن‌ها pull می‌کنید بررسی کنید

هاست‌های registry را که stackهایتان به آن‌ها ارجاع می‌دهند فهرست کنید. این دستور فایل‌های compose روی دیسک را هم پیدا می‌کند، از جمله stackهایی که الان متوقف‌اند:

grep -rhoE 'image: *"?[a-z0-9.-]+\.[a-z]{2,}(:[0-9]+)?/' --include='*compose*.y*ml' /srv /opt 2>/dev/null \
  | sed -E 's/image: *"?//; s#/$##' | sort -u

بعد از هر کدام بپرسید چه زنجیره‌ای ارائه می‌دهد و دنبال نام گواهی‌های میانی eMudhra بگردید:

for reg in registry.example.com mirror.example.com:5000; do
  host=${reg%%:*}; port=${reg##*:}; [ "$port" = "$reg" ] && port=443
  if echo | openssl s_client -connect "$host:$port" -servername "$host" -showcerts 2>/dev/null \
     | grep -qE 'CN ?= ?EM (DV|OV|EV) TLS CA - G3[BC]'; then
    echo "AFFECTED: $reg"
  else
    echo "ok: $reg"
  fi
done

ما این حلقه را روی ghcr.io و روی هاست آزمایشی G3C اجرا کردیم: اولی ok و دومی AFFECTED برگرداند. هاست‌هایی را که pipelineهای CI از آن‌ها دانلود می‌کنند هم اضافه کنید، چون buildی که یک فایل باینری را از سرور داخلی می‌گیرد هم یک کلاینت TLS دیگر است.

گام ۳: registry را درست کنید، نه کلاینت را

یادداشت‌های Docker صریح است: «registryهای آسیب‌دیده به یک زنجیرهٔ گواهی جایگزین نیاز دارند که از این گواهی‌های میانی استفاده نکند.» در عمل یعنی یک گواهی جدید از یک CA دیگر (یا زنجیرهٔ صدور دیگری از فروشنده‌تان)، نصب‌شده روی هر چیزی که جلوی registry اتصال TLS را خاتمه می‌دهد؛ خودِ registry باشد یا nginx یا Traefik یا load balancer.

میان‌بُرِ وسوسه‌انگیز این است، در /etc/docker/daemon.json:

{
  "insecure-registries": ["registry.example.com"]
}

این را به production نبرید. این تنظیم به daemon می‌گوید بررسی گواهی را برای آن هاست کنار بگذارد، یعنی دقیقاً همان حفاظتی که به‌روزرسانی Go اضافه کرد. هر کسی که بتواند در مسیر شبکه بنشیند و گواهی‌ای از یکی از این کلیدهای لورفته داشته باشد، می‌تواند هر ایمیجی که بخواهد را به اسم registry شما تحویلتان بدهد.

اگر registry داخلی است و نمی‌خواهید گواهی عمومی دیگری بخرید، از CA خودتان یک گواهی صادر کنید و فایل CA را همان‌طور که مستندات Docker دربارهٔ گواهی registry توضیح می‌دهد در اختیار Docker بگذارید:

/etc/docker/certs.d/
  registry.example.com/
    ca.crt

زنجیره‌ای که به CA خودتان ختم می‌شود هیچ‌وقت از eMudhra رد نمی‌شود، پس فیلتر چیزی برای رد کردن ندارد. وقتی گواهی جدید فعال شد، حلقهٔ گام ۲ را دوباره اجرا کنید و برای اطمینان یک ایمیج pull کنید.

گام ۴: ایمیج‌هایتان هم همین اصلاح را لازم دارند

ارتقای Docker از خودِ Docker محافظت می‌کند و برای نرم‌افزارِ داخل کانتینرها کاری نمی‌کند. صفحهٔ افشا کلاینت‌هایی را که هنوز در خطرند نام می‌برد: «اپلیکیشن‌ها و کتابخانه‌هایی که بستهٔ CA خودشان را همراه دارند» به‌علاوهٔ اپ‌های Electron، و یادآوری می‌کند که بسیاری از کلاینت‌های غیرمرورگر ابطال را بررسی نمی‌کنند.

ایمیجی که با بستهٔ تازهٔ ca-certificates دوباره ساخته شود به‌تنهایی این را حل نمی‌کند. بستهٔ CA فقط rootهای مورد اعتماد را فهرست می‌کند و راهی ندارد بگوید «به این root اعتماد کن، به‌جز این گواهی‌های میانیِ زیرش». رد کردن باید از کتابخانهٔ TLS بیاید، همان کاری که Go کرد. پس برای سرویس‌های Go، راه‌حل build دوباره با toolchain وصله‌شده است. در یک Dockerfile چندمرحله‌ای:

# before
FROM golang:1.26.9 AS build

# after
FROM golang:1.26.10 AS build

هر دو ایمیج golang:1.26.10 و golang:1.27.3 روی Docker Hub موجودند. اگر go.mod شما toolchain را pin کرده، آن را هم بالا ببرید، وگرنه نسخهٔ قدیمی به‌هرحال دانلود می‌شود:

# before
toolchain go1.26.9

# after
toolchain go1.26.10

build دوباره فقط وقتی فایده دارد که کانتینرها از ایمیج جدید دوباره ساخته شوند. دربارهٔ این تله در راهنمای rebuild برای PHP2Shell نوشته‌ایم: base image وصله‌شده برای کانتینری که از ایمیج قدیمی بالا آمده هیچ کاری نمی‌کند.

runnerهای CI هم ارزش یک نگاه را دارند. اگر یک workflow نسخهٔ Buildx را از طریق docker/setup-buildx-action pin کرده باشد، همین pin تعیین می‌کند push‌ها با کدام runtime Go انجام شوند:

# before
- uses: docker/setup-buildx-action@v4
  with:
    version: v0.38.0

# after
- uses: docker/setup-buildx-action@v4
  with:
    version: v0.38.1

لپ‌تاپ‌های دارای Docker Desktop

Docker در یادداشت‌های 4.96.1 هشدار داد که «این اصلاح به‌صورت دسته‌ای و همزمان با به‌روزرسانی تک‌تک اجزای Docker Desktop عرضه می‌شود.» تیمی که روی 4.96.0 است هیچ بخشی از آن را ندارد و تیمی که روی 4.96.1 است بخش Engine را دارد ولی بقیه را نه. همه را دست‌کم به 4.96.3 برسانید.

کاربران Mac یک شکاف دیگر هم دارند. به‌روزرسانی ۲۸ اکتبر در صفحهٔ افشا می‌گوید ابزارهای توسعه‌ای که Apple همراه macOS عرضه می‌کند، از جمله git، curl و python3، هنوز به گواهی‌های لورفته اعتماد دارند و macOS 27.2 اصلاحاتی را که کاربران دستی اعمال کرده بودند برگرداند. Docker Desktop وصله‌شده روی آن لپ‌تاپ چیزی را که git clone به آن اعتماد می‌کند تغییر نمی‌دهد.

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

سرویس‌های production ما در Docker اجرا می‌شوند، پس این کار یک چک‌لیست کوتاه بود، نه یک پروژه: نسخه‌های Engine، Buildx و Compose را مثل بالا تأیید کردیم، بررسی زنجیره را روی همهٔ registryهایی که فایل‌های compose ما به آن‌ها ارجاع می‌دهند اجرا کردیم و مطمئن شدیم هیچ‌کدام از گواهی‌های خودمان از eMudhra نیست. چیزی درگیر نبود، و این نتیجه‌ای است که می‌خواهید قبل از ارتقا بدانید، نه بعد از یک deploy ناموفق در ساعت ۲ بامداد. اگر عادت دارید نسخه‌ها را روی همهٔ هاست‌ها بررسی کنید، همان روش راهنمای ارتقای PostgreSQL 14 اینجا هم کار می‌کند، و نوشتهٔ ما دربارهٔ اینکه CMS خودمان را چطور روی Docker اجرا می‌کنیم نشان می‌دهد در یک stack خودمیزبانِ معمولی، TLS کجا خاتمه پیدا می‌کند.

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

آیا می‌توانم Docker Engine را downgrade کنم تا pull دوباره کار کند؟

می‌توانید، اما engine قدیمی گواهی جعلی‌ای را هم که با یکی از کلیدهای لورفته برای هر registry، از جمله Docker Hub، امضا شده باشد می‌پذیرد. به‌جای آن گواهی registry را عوض کنید.

آیا Docker Hub درگیر است؟

نه. در ۲ نوامبر ۲۰۲۶ زنجیرهٔ گواهی registry-1.docker.io از Amazon RSA 2048 M01 و Amazon Root CA 1 عبور می‌کرد، نه eMudhra. همین برای ghcr.io، quay.io، public.ecr.aws، registry.gitlab.com و gcr.io هم صادق بود.

registry ما از گواهی self-signed یا CA داخلی استفاده می‌کند. باید کاری بکنیم؟

به‌خاطر این تغییر نه. فیلتر فقط زنجیره‌هایی را حذف می‌کند که شامل هشت گواهی میانی eMudhra باشند. زنجیره‌ای که به CA خودتان ختم می‌شود و در /etc/docker/certs.d تنظیم شده همچنان کار می‌کند.

آیا به‌روز کردن ca-certificates در ایمیج‌ها گواهی‌های میانیِ لورفته را مسدود می‌کند؟

نه. بستهٔ CA فقط rootها را فهرست می‌کند، پس نمی‌تواند گواهی‌های میانیِ مشخصی را زیر rootی که مورد اعتماد می‌ماند کنار بگذارد. مسدودسازی باید از کتابخانهٔ TLS یا بررسی ابطال بیاید. برای برنامه‌های Go، با Go 1.26.10 یا 1.27.3 دوباره build کنید.

کدام نسخه‌های Docker این اصلاح را دارند؟

Docker Engine 29.9.2، Buildx 0.38.1 و Compose 5.6.1 که در ۲۰ و ۲۱ اکتبر ۲۰۲۶ منتشر شدند. در Docker Desktop این اصلاح در نسخه‌های 4.96.1، 4.96.2 و 4.96.3 رسید.

منابع: یادداشت‌های انتشار Docker Engine 29، انتشار docker-v29.9.2 در moby/moby، Buildx v0.38.1، Compose v5.6.1، یادداشت‌های انتشار Docker Desktop، issue شمارهٔ 82345 در Go، تاریخچهٔ انتشار Go و افشای لو رفتن CAهای میانی eMudhra. دستورها در ۲ نوامبر ۲۰۲۶ روی Docker Engine 29.9.2، Buildx 0.38.1 و Compose 5.6.1 آزمایش شدند.

registry یا هاست‌های Docker خودتان را اجرا می‌کنید؟

ما stackهای Docker خودمیزبان را بررسی و اداره می‌کنیم: زنجیرهٔ گواهی‌ها، راه‌اندازی registry و mirror، pipelineهای rebuild ایمیج و ارتقاهایی که پیش از خراب کردن یک deploy برنامه‌ریزی شده‌اند.

خدمات ابر و DevOps

اشتراک‌گذاری این مقاله