Loading...

بسته‌بندی

چگونه مخزن APT و YUM امضاشده خودتان را روی CDN میزبانی کنید

توزیع یک ابزار لینوکسی به‌صورت آرشیو کار می‌کند، اما چیزی که مردم واقعاً می‌خواهند `apt-get install ابزارشما` است — این کار به‌روزرسانی را به همان چرخه‌ای می‌برد که از پیش اجرا می‌کنند. این یک راهنمای کامل و آزموده است برای ساخت مخازن APT و YUM امضاشده، سرویس‌دادن آن‌ها از روی CDN، و تله‌های امضا و کشی که گازتان خواهند گرفت. هر خطایی که اینجا توضیح داده شده، خودمان هنگام انتشار CLI خودمان با آن روبه‌رو شدیم.

12 متوسط Updated

چگونه مخزن APT و YUM امضاشده خودتان را روی CDN میزبانی کنید

چرا زحمت بکشیم وقتی آرشیو کار می‌کند؟

نصب با `curl | tar` بار اول خوب کار می‌کند. مشکل بار دوم است. هیچ چیز به کاربران نمی‌گوید نسخه تازه‌ای آمده، هیچ چیز آن را همراه بقیه بسته‌هایشان به‌روز نمی‌کند و هیچ چیز آن را تمیز حذف نمی‌کند. در نهایت هر کاربر آیین به‌روزرسانی کوچک خودش را از خودش درمی‌آورد.

مخزن بسته همه این‌ها را به سازوکاری می‌سپارد که سیستم‌عامل از قبل دارد. `apt-get upgrade` نسخه تازه شما را همراه بقیه چیزها برمی‌دارد. حذف می‌شود `apt-get remove`. اصالت هم یک امضای GPG است که مدیر بسته خودکار بررسی می‌کند، بی‌آنکه کسی به آن فکر کند.

هزینه‌اش یک بعدازظهر راه‌اندازی است و کمی کار در هر انتشار که می‌شود اسکریپتش کرد. تمامش اینجاست.

مخزن واقعاً چیست

هر دو قالب فقط فایل‌های ایستا روی HTTPS هستند. نه سرویسی در پس‌زمینه و نه پایگاه داده‌ای — و دقیقاً به همین دلیل CDN آن‌ها را این‌قدر خوب سرویس می‌دهد.

مخزن APT یک `pool/` دارد که فایل‌های `.deb` در آن است و یک درخت متادیتا `dists/<suite>/`. فایل `Packages` هر بسته را با اندازه، هش‌ها و مسیرش فهرست می‌کند؛ `Release` هش فایل‌های `Packages` را فهرست می‌کند؛ و امضا روی `Release` است. کل زنجیره اعتماد همین است، و جلوتر اهمیت پیدا می‌کند.

مخزن YUM ساده‌تر است: فایل‌های `.rpm` در پوشه‌ای کنار `repodata/` قرار می‌گیرند که `repomd.xml` و ایندکس‌های فشرده‌ای که به آن‌ها اشاره می‌کند در آن است.

ساختار مخزن

deb/
  dists/stable/
    Release  InRelease  Release.gpg
    main/binary-amd64/Packages{,.gz}
    main/binary-arm64/Packages{,.gz}
  pool/main/y/yourtool/yourtool_1.0.0_amd64.deb

rpm/
  yourtool-1.0.0-1.x86_64.rpm
  repodata/
    repomd.xml  repomd.xml.asc
    *-primary.xml.gz  *-filelists.xml.gz  *-other.xml.gz

کلید امضا بسازید

کلید تنها بخشی است که نمی‌شود سرسری دوباره ساخت: اگر گمش کنید، هر کاربر باید کلید تازه‌ای وارد کند. یک بار بسازیدش، هم از کلید محرمانه و هم از گواهی ابطال نسخه پشتیبان بگیرید، و نیمه خصوصی را از هر ماشینی که به آن نیاز ندارد دور نگه دارید.

اسکریپت‌های انتشار بدون دخالت انسان امضا می‌کنند، پس کلید امضای مخزن معمولاً رمز عبور ندارد: با مجوزهای فایل و قرارگرفتن در جایی محدود محافظت می‌شود، درست مثل کلید امضا در CI. یک تاریخ انقضای واقعی برایش بگذارید (پنج سال رایج است) و تاریخ تمدید را جایی بنویسید که واقعاً ببینیدش.

ساخت و خروجی‌گرفتن
cat > key.batch <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Name-Real: example.com Package Signing
Name-Email: packages@example.com
Expire-Date: 5y
%commit
EOF
gpg --batch --gen-key key.batch && rm key.batch

# the public half — this is what users import
gpg --armor --export packages@example.com > example-com.gpg

# BACK THESE UP, offline:
#   gpg --export-secret-keys --armor packages@example.com
#   ~/.gnupg/openpgp-revocs.d/<fingerprint>.rev

مخزن APT را بسازید و امضا کنید

`apt-ftparchive` (از بسته `apt-utils`) هر دو فایل متادیتا را می‌سازد. دو نکته مهم است.

آن را از ریشه مخزن اجرا کنید، چون مسیرهای `Filename:` که در `Packages` می‌نویسد نسبت به همان جایی است که فراخوانش کرده‌اید. اگر این را اشتباه کنید، apt متادیتای شما را بی‌دردسر می‌خواند و بعد سر هر دانلود ۴۰۴ می‌گیرد.

سپس امضا را دو بار منتشر کنید: `InRelease` شکل مدرن و درون‌خطی امضا است و `Release.gpg` امضای جداشده برای کلاینت‌های قدیمی‌تر. ساختن هر دو هزینه‌ای ندارد و یک دسته کامل از پرسش‌های پشتیبانی را از بین می‌برد.

ساخت متادیتای APT
cd deb
for arch in amd64 arm64; do
  mkdir -p "dists/stable/main/binary-$arch"
  apt-ftparchive --arch "$arch" packages pool \
    > "dists/stable/main/binary-$arch/Packages"
  gzip -9cn "dists/stable/main/binary-$arch/Packages" \
    > "dists/stable/main/binary-$arch/Packages.gz"
done

apt-ftparchive \
  -o APT::FTPArchive::Release::Origin=example.com \
  -o APT::FTPArchive::Release::Suite=stable \
  -o APT::FTPArchive::Release::Components=main \
  -o APT::FTPArchive::Release::Architectures="amd64 arm64" \
  release dists/stable > dists/stable/Release

gpg --batch --yes -abs -o dists/stable/Release.gpg dists/stable/Release
gpg --batch --yes --clearsign -o dists/stable/InRelease dists/stable/Release

RPM دو امضا می‌خواهد، نه یکی

این همان گامی است که تقریباً همه رویش می‌لغزند، و ما هم لغزیدیم. APT و YUM بسته‌ها را به شکل بنیادی متفاوتی راستی‌آزمایی می‌کنند.

APT یک زنجیره است: امضا روی `Release` است که هش‌های `Packages` را دارد و آن هم هش هر `.deb` را. متادیتا را امضا می‌کنید و بسته‌ها رایگان پوشش داده می‌شوند — فایل‌های `.deb` جداگانه اصلاً امضا نمی‌شوند.

اما RPM دو بررسی مستقل دارد. `repo_gpgcheck=1` فایل `repomd.xml` را با یک `repomd.xml.asc` جداشده بررسی می‌کند. ولی `gpgcheck=1` امضایی را بررسی می‌کند که داخل خودِ هر بسته جاسازی شده و هیچ چیز دیگری آن را فراهم نمی‌کند. اگر فقط متادیتا را امضا کنید، dnf با پیام `Package … is not signed / Error: GPG check FAILED` متوقف می‌شود.

پس `rpmsign` را روی هر `.rpm` اجرا کنید — و پیش از `createrepo_c`، چون امضا فایل‌ها را بازنویسی می‌کند و هر چکسامی که قبل‌تر ثبت شده باشد بی‌درنگ نادرست می‌شود.

اول بسته‌ها را امضا کنید، بعد متادیتا را بسازید
# rpmsign ships in the 'rpm' package on Debian/Ubuntu.
# rpm hardcodes %__gpg to /usr/bin/gpg2, which Debian/Ubuntu do not ship:
rpmsign --define "_gpg_name packages@example.com" \
        --define "__gpg $(command -v gpg)" \
        --addsign rpm/*.rpm

# fail loudly rather than publish something unsigned
for r in rpm/*.rpm; do
  rpm -qpi "$r" | grep -qi '^Signature *: *(none)' \
    && { echo "UNSIGNED: $r"; exit 1; }
done

# only now build the indexes, then sign repomd.xml
createrepo_c --quiet rpm/
gpg --batch --yes --detach-sign --armor \
    -o rpm/repodata/repomd.xml.asc rpm/repodata/repomd.xml

سرویس‌دادن از روی CDN — و آن تله

مخزن بسته تقریباً بار ایده‌آل یک CDN است: فایل‌های ایستا که خیلی بیشتر از آنکه تغییر کنند دانلود می‌شوند، آن هم توسط کاربرانی از سراسر جهان. لبه را به مبدأتان بدهید و مشکل پهنای باند از بین می‌رود.

تله اینجاست که متادیتای مخزن درونی سازگار است. هر فایل به هش فایل‌های دیگر ارجاع می‌دهد. کشی که `Release` تازه و `Packages` کهنه به شما بدهد، مخزنی کمی قدیمی تحویلتان نداده — مخزنی خراب داده، و پیام خطا هیچ اشاره‌ای به کش نخواهد کرد.

این سه خطا را در یک انتشار واحد تجربه کردیم:

• `InRelease` کهنه با `Packages` تازه — apt از ناهمخوانی هش خبر می‌دهد.

• `.rpm` کهنه — dnf می‌گوید `Package … is not signed`، چون امضا فایل را بازنویسی کرده و نسخه داخل کش همان قدیمیِ بدون امضاست.

• `repomd.xml` تازه با `repomd.xml.asc` کهنه — dnf از `Bad PGP signature` خبر می‌دهد. هر دو عوض شده بودند و فقط یکی پاک شده بود.

قاعده‌ای که از این بیرون می‌آید: پاکسازی ناقص از پاک‌نکردن بدتر است. هر فایلی را که انتشار بازنویسی کرده پاک کنید — نقاط ورود، فایل‌های ایندکس هش‌دار زیر `repodata/`، و خودِ بسته‌ها. یا به متادیتا TTL بسیار کوتاهی بدهید و بگذارید بسته‌ها مدت زیادی کش شوند، چون نامشان با هر نسخه عوض می‌شود.

هر چیزی را که دست زدید پاک کنید
# entry points
for p in /deb/dists/stable/InRelease \
         /deb/dists/stable/Release \
         /deb/dists/stable/Release.gpg \
         /deb/dists/stable/main/binary-amd64/Packages \
         /deb/dists/stable/main/binary-amd64/Packages.gz \
         /rpm/repodata/repomd.xml \
         /rpm/repodata/repomd.xml.asc; do
  cdnctl purge --account "$ACC" --path "$p" --type exact
done

# the hashed indexes and the packages changed too
for f in rpm/repodata/* rpm/*.rpm; do
  cdnctl purge --account "$ACC" \
    --path "/rpm/$(basename "$f")" --type exact
done

انتشار: فایل‌ها چطور به CDN می‌رسند

مخزن فقط فایل‌های ایستاست، پس «انتشار» یعنی گذاشتن آن درخت پوشه جایی که CDN بتواند سرویسش دهد. دو شکل دارد و اینکه کدام را بخواهید بستگی دارد به اینکه از قبل وب‌سروری اجرا می‌کنید یا نه.

**اگر از قبل مبدأیی دارید** — هر ماشینی که HTTPS سرویس دهد — CDN را جلویش بگذارید و با کپی‌کردن فایل‌ها به آنجا منتشر کنید. `rsync` روی SSH کافی است؛ لبه در نخستین درخواست از مبدأ می‌گیرد و از آن پس کش می‌کند. برای cdn.com.tr همین کار را می‌کنیم، چون ماشین دانلودها همان ماشینی است که سایت را سرویس می‌دهد.

یک هشدار از تجربه: زیرپوشه‌ها را همگام کنید، نه پوشه والد را. یک `rsync --delete` که به پوشه‌ای نشانه رفته باشد که دانلودهای دیگرتان هم در آن است، آن‌ها را با کمال میل پاک می‌کند.

**اگر اصلاً نمی‌خواهید مبدأیی نگه دارید**، فایل‌ها را مستقیم به فضای ذخیره‌سازی CDN بفرستید. با cdn.com.tr درخت را آپلود می‌کنید و از دامنه CDN خودتان سرویس داده می‌شود — هیچ وب‌سروری برای نصب، وصله‌کردن یا زنده نگه‌داشتن نیست. `cdnctl cp -r` یک پوشه کامل را آپلود می‌کند، و این دقیقاً شکل یک مخزن است.

در هر دو حالت، نشانی پایه‌ای که کاربرانتان در `sources.list` یا فایل `.repo` می‌گذارند فقط دامنه CDN شما به‌علاوه مسیری است که در آن منتشر کرده‌اید. و در هر دو حالت، با گام پاکسازی بخش پیشین تمام کنید — همان بخشی که فراموش می‌شود.

دو راه برای انتشار یک درخت
# A) you have an origin: sync the subdirectories, never the parent
rsync -a --delete dist/repo/deb/ user@origin:/var/www/downloads/deb/
rsync -a --delete dist/repo/rpm/ user@origin:/var/www/downloads/rpm/
rsync -a          dist/repo/example-com.gpg user@origin:/var/www/downloads/

# B) no origin: push straight into CDN storage
cdnctl cp -r dist/repo/deb <account_uuid>:/downloads/deb
cdnctl cp -r dist/repo/rpm <account_uuid>:/downloads/rpm
cdnctl files put --file dist/repo/example-com.gpg \
                 --target-path /downloads/example-com.gpg

# the base URL is simply your CDN hostname + that path:
#   deb  -> https://cdn.example.com/downloads/deb
#   rpm  -> https://cdn.example.com/downloads/rpm

در یک کانتینر تمیز بیازمایید

روی ماشین خودتان آزمایش نکنید. آن ماشین از قبل به کلید شما اعتماد دارد، ممکن است ایندکسی کش‌شده داشته باشد، و با کمال میل دقیقاً همان مشکلاتی را پنهان می‌کند که کاربرانتان قرار است با آن‌ها روبه‌رو شوند. کانتینر یک‌بارمصرف تنها آزمون صادقانه است: هم امضای گمشده بسته و هم خطاهای کش کهنه را همین‌طور پیدا کردیم.

هنگام آزمایش بررسی امضا را روشن نگه دارید. خاموش‌کردن `gpgcheck` برای اینکه «ببینیم بقیه‌اش کار می‌کند» همان راه شناخته‌شده انتشار یک مخزن بدون امضاست.

یک نکته برای سمت فدورا: `--disablerepo="*" --enablerepo="مخزن‌شما"` را بدهید. وگرنه dnf وقتش را صرف دانلود متادیتای خود فدورا می‌کند و اجرای کند یا شکست‌خورده هیچ چیزی درباره مخزن شما نمی‌گوید.

دو کانتینر، نصب واقعی
# Debian / Ubuntu
docker run --rm debian:12 sh -c '
  apt-get update -qq && apt-get install -y -qq curl gnupg ca-certificates
  curl -fsSL https://example.com/example-com.gpg |
    gpg --dearmor -o /usr/share/keyrings/example-com.gpg
  echo "deb [signed-by=/usr/share/keyrings/example-com.gpg] \
        https://example.com/deb stable main" \
    > /etc/apt/sources.list.d/example.list
  apt-get update && apt-get install -y yourtool && yourtool --version'

# Fedora / RHEL — only your repo, so the test is about your repo
docker run --rm fedora:40 sh -c '
  printf "[example]\nbaseurl=https://example.com/rpm\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=https://example.com/example-com.gpg\n" \
    > /etc/yum.repos.d/example.repo
  dnf --disablerepo="*" --enablerepo="example" -y install yourtool
  rpm -qi yourtool | grep Signature'

یک نکته آخر: در بسته‌ها به‌روزرسانی خودکار را غیرفعال کنید

اگر ابزار شما می‌تواند خودش را به‌روز کند، این قابلیت حالا با مدیر بسته تداخل دارد. به‌روزرسانی خودکار روی فایلی می‌نویسد که `dpkg` یا `rpm` مالک آن است، و پایگاه داده آن‌ها همچنان به نسخه‌ای اشاره می‌کند که دیگر روی دیسک نیست — پس به‌روزرسانی بعدی کار غافلگیرکننده‌ای می‌کند.

راه‌حل کوچک است: روی بیلد مهر بزنید که چطور توزیع شده و کاری کنید به‌روزرسان خودکار وقتی مدیر بسته نصبش کرده کنار برود. دانلودهای مستقیم مثل قبل خودشان را به‌روز می‌کنند.

مهر زمان بیلد (نمونه Go)
// installChannel is overridden at build time for packaged builds.
var installChannel = "direct"

func update() error {
    if installChannel != "direct" {
        return fmt.Errorf(
            "installed via %s — upgrade it with that instead",
            installChannel)
    }
    // ... normal self-update
}

// packaged builds:
//   go build -ldflags "-X main.installChannel=deb"

پرسش‌های پرتکرار

اصلاً باید مخزن را امضا کنم؟

از نظر فنی نه — می‌شود به apt و dnf گفت راستی‌آزمایی را رد کنند. عملاً بله. کاربران باید برای نصب نرم‌افزار شما یک بررسی امنیتی را خاموش کنند؛ خواستن این کار بد است و آموزش‌دادنش بدتر. امضا هزینه‌اش یک کلید و دو دستور است.

متادیتا را امضا کردم، پس چرا dnf می‌گوید بسته امضا ندارد؟

چون RPM دو چیز جدا را بررسی می‌کند. `repo_gpgcheck` روی `repomd.xml` است؛ `gpgcheck` روی امضایی که داخل هر `.rpm` جاسازی شده. به `rpmsign --addsign` روی بسته‌ها هم نیاز دارید — و باید پیش از `createrepo_c` اجرا شود، چون امضا فایل‌ها را تغییر می‌دهد.

چرا apt درست بعد از انتشار از ناهمخوانی هش خبر می‌دهد؟

تقریباً همیشه به‌خاطر لایه‌ای از کش که ترکیبی از متادیتای قدیمی و جدید را سرویس می‌دهد. متادیتای مخزن درونی سازگار است، پس به‌روزرسانی ناقص یعنی مخزن خراب. هر فایلی را که انتشار بازنویسی کرده پاک کنید، یا به متادیتا TTL کوتاهی بدهید.

آیا یک مخزن می‌تواند هم amd64 و هم arm64 را سرویس دهد؟

بله. برای APT، برای هر معماری یک فایل `Packages` زیر همان suite بسازید و هر دو را در `Architectures` فهرست کنید. برای YUM، هر دو بسته را در یک پوشه بگذارید — `createrepo_c` معماری هرکدام را خودش ثبت می‌کند.

فایل‌های مخزن باید در git باشند؟

معمولاً نه. مجموعه بسته‌ها با هر نسخه بزرگ‌تر می‌شود و git همه نسخه‌ها را برای همیشه نگه می‌دارد. درخت را از خروجی‌های انتشارتان بسازید و به مبدأ همگام کنید و در git فقط اسکریپت‌ها و کلید عمومی را نگه دارید. بیلد را تکرارپذیر کنید تا مخزن هر لحظه از صفر قابل بازسازی باشد.