Loading...

Paketleme

Kendi imzalı APT ve YUM deponuzu CDN üzerinde nasıl barındırırsınız

Bir Linux aracını tar.gz olarak dağıtmak çalışır, ama insanların gerçekten istediği `apt-get install aracınız` komutudur — çünkü güncellemeyi zaten kullandıkları döngüye taşır. Bu yazı, imzalı APT ve YUM depolarını kurmanın, bunları bir CDN üzerinden sunmanın ve yolda sizi bekleyen imzalama ile önbellek tuzaklarının tam ve test edilmiş bir anlatımı. Burada anlatılan her hatayı kendi CLI'ımızı yayınlarken bizzat yaşadık.

12 Orta seviye Güncellendi

Kendi imzalı APT ve YUM deponuzu CDN üzerinde nasıl barındırırsınız

tar.gz çalışıyorken neden uğraşalım?

`curl | tar` ile kurulum ilk seferde gayet iyi çalışır. Sorun ikinci seferde başlar. Kullanıcıya yeni sürüm çıktığını hiçbir şey söylemez, diğer paketleriyle birlikte hiçbir şey güncellemez, temiz bir kaldırma yolu yoktur. Herkes kendi küçük güncelleme ritüelini icat eder.

Paket deposu bunların hepsini işletim sisteminde zaten var olan mekanizmaya devreder. `apt-get upgrade` yeni sürümünüzü diğer her şeyle birlikte alır. Kaldırmak `apt-get remove`'dur. Kaynağın doğruluğu ise paket yöneticisinin kimse düşünmeden otomatik kontrol ettiği bir GPG imzasıdır.

Bedeli bir öğleden sonralık kurulum ve script'e bağlayabileceğiniz küçük bir sürüm başı iş. İşte tamamı.

Depo aslında nedir?

Her iki format da HTTPS üzerinden sunulan düz statik dosyalardan ibarettir. Ne bir servis ne de veritabanı vardır — CDN'in bu işi bu kadar iyi yapmasının sebebi tam olarak budur.

Bir APT deposunda `.deb` dosyalarını tutan bir `pool/` ve metadata'yı tutan bir `dists/<suite>/` ağacı bulunur. `Packages` her paketi boyutu, hash'leri ve yolu ile listeler; `Release` bu `Packages` dosyalarının hash'lerini listeler; imza da `Release`'i kapsar. Güven zincirinin tamamı budur — ve bu ayrıntı ilerideki bölümde önemli olacak.

YUM deposu daha düzdür: `.rpm` dosyaları, `repomd.xml` ve onun işaret ettiği sıkıştırılmış indeksleri barındıran `repodata/` dizininin yanında durur.

Depo yapısı

deb/
  dists/stable/
    Release  InRelease  Release.gpg
    main/binary-amd64/Packages{,.gz}
    main/binary-arm64/Packages{,.gz}
  pool/main/a/aracim/aracim_1.0.0_amd64.deb

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

İmzalama anahtarını üretin

Anahtar, gelişigüzel yeniden yapamayacağınız tek parçadır: kaybederseniz her kullanıcının yeni anahtar içe aktarması gerekir. Bir kez üretin, hem gizli anahtarı hem iptal sertifikasını yedekleyin ve özel yarısını ihtiyacı olmayan hiçbir makinede tutmayın.

Sürüm script'leri imzayı etkileşimsiz atar, bu yüzden depo imzalama anahtarları genelde parolasız olur — tıpkı bir CI imzalama anahtarı gibi, dosya izinleriyle ve kısıtlı bir yerde durarak korunur. Gerçek bir son kullanma tarihi verin (beş yıl yaygındır) ve yenileme tarihini gerçekten göreceğiniz bir yere yazın.

Üret ve dışa aktar
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

# public yarısı — kullanıcıların içe aktardığı budur
gpg --armor --export packages@example.com > example-com.gpg

# BUNLARI YEDEKLEYİN, çevrimdışı:
#   gpg --export-secret-keys --armor packages@example.com
#   ~/.gnupg/openpgp-revocs.d/<fingerprint>.rev

APT deposunu kurun ve imzalayın

`apt-ftparchive` (apt-utils paketinden) her iki metadata dosyasını da üretir. İki ayrıntı önemli.

Komutu deponun kökünden çalıştırın; çünkü `Packages` içine yazdığı `Filename:` yolları komutu nerede çalıştırdığınıza görecelidir. Bunu yanlış yaparsanız apt metadata'yı sorunsuz okur, ama her indirmede 404 alırsınız.

Sonra imzayı iki biçimde yayınlayın: `InRelease` modern satır içi imzalı biçim, `Release.gpg` ise eski istemciler için ayrık imza. İkisini birden üretmenin maliyeti yok ve bir sürü destek sorusunu baştan engelliyor.

APT metadata'sını üret
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 tek değil, iki imza ister

Neredeyse herkesin takıldığı adım budur; biz de takıldık. APT ile YUM paketleri temelden farklı biçimde doğrular.

APT bir zincirdir: imza `Release`'i kapsar, o `Packages`'ın hash'lerini tutar, o da her `.deb`'in hash'ini tutar. Metadata'yı imzaladığınızda paketler kendiliğinden kapsanır — tekil `.deb` dosyaları hiç imzalanmaz.

RPM'de ise birbirinden bağımsız iki kontrol vardır. `repo_gpgcheck=1`, ayrık `repomd.xml.asc` üzerinden `repomd.xml`'i doğrular. Ama `gpgcheck=1`, her paketin **içine gömülü** bir imzayı doğrular ve bunu başka hiçbir şey sağlamaz. Yalnızca metadata'yı imzalarsanız dnf şu hatayla durur: `Package … is not signed / Error: GPG check FAILED`.

Bu yüzden her `.rpm` için `rpmsign` çalıştırın — ve bunu `createrepo_c`'den **önce** yapın, çünkü imzalama dosyaları yeniden yazar ve daha önce kaydedilmiş her checksum anında geçersiz olur.

Önce paketleri imzala, sonra metadata üret
# rpmsign, Debian/Ubuntu'da 'rpm' paketiyle gelir.
# rpm, %__gpg değerini /usr/bin/gpg2 olarak sabitler; Debian/Ubuntu bunu sunmaz:
rpmsign --define "_gpg_name packages@example.com" \
        --define "__gpg $(command -v gpg)" \
        --addsign rpm/*.rpm

# imzasız bir şey yayınlamaktansa gürültülü hata ver
for r in rpm/*.rpm; do
  rpm -qpi "$r" | grep -qi '^Signature *: *(none)' \
    && { echo "IMZASIZ: $r"; exit 1; }
done

# indeksleri ancak şimdi üret, sonra repomd.xml'i imzala
createrepo_c --quiet rpm/
gpg --batch --yes --detach-sign --armor \
    -o rpm/repodata/repomd.xml.asc rpm/repodata/repomd.xml

CDN üzerinden sunmak — ve tuzağı

Paket deposu, CDN için neredeyse ideal bir yüktür: statik dosyalar, değiştiğinden çok daha sık indirilir, hem de dünyanın her yerinden. Edge'i origin'inize yöneltin, bant genişliği sorunu ortadan kalksın.

Tuzak şu: depo metadata'sı **kendi içinde tutarlıdır**. Her dosya başka dosyaların hash'lerine atıfta bulunur. Size taze bir `Release` ile bayat bir `Packages` veren bir önbellek, biraz eski bir depo vermiş olmaz — **bozuk** bir depo vermiş olur. Üstelik hata mesajı önbellekten hiç söz etmez.

Tek bir yayında yaşadığımız üç hata şunlar:

• Bayat `InRelease`, taze `Packages` — apt hash uyuşmazlığı bildirir.

• Bayat `.rpm` — dnf `Package … is not signed` der; çünkü imzalama dosyayı yeniden yazmıştır ve önbellekteki kopya eski, imzasız olandır.

• Taze `repomd.xml`, bayat `repomd.xml.asc` — dnf `Bad PGP signature` der. İkisi de değişmiştir, ama yalnızca biri purge edilmiştir.

Buradan çıkan kural: **eksik purge, hiç purge etmemekten kötüdür.** Yayının yeniden yazdığı **her** dosyayı purge edin — giriş noktalarını, `repodata/` altındaki hash isimli indeks dosyalarını ve paketlerin kendisini. Alternatif olarak metadata'ya çok kısa bir TTL verip paketleri uzun süre önbellekte tutun; nasılsa paket adları her sürümde değişiyor.

Dokunduğunuz her şeyi purge edin
# giriş noktaları
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

# hash isimli indeksler ve paketler de değişti
for f in rpm/repodata/* rpm/*.rpm; do
  cdnctl purge --account "$ACC" \
    --path "/rpm/$(basename "$f")" --type exact
done

Yayınlama: dosyalar CDN'e nasıl gidiyor?

Depo yalnızca statik dosyalardan ibaret olduğu için "yayınlamak", o dizin ağacını CDN'in sunabileceği bir yere koymak demek. İki farklı biçimi var ve hangisini isteyeceğiniz zaten bir web sunucusu işletip işletmediğinize bağlı.

**Zaten bir origin'iniz varsa** — HTTPS sunan herhangi bir makine — CDN'i onun önüne alın ve yayınlamayı dosyaları oraya kopyalayarak yapın. SSH üzerinden `rsync` yeter; edge ilk istekte origin'den çeker, sonrasında önbellekten sunar. cdn.com.tr için biz bunu yapıyoruz, çünkü indirme sunucusu siteyi sunan makinenin ta kendisi.

Deneyimden bir uyarı: üst dizini değil, alt dizinleri senkronlayın. Diğer indirmelerinizi de barındıran bir dizine yöneltilmiş `rsync --delete`, onları seve seve siler.

**Hiç origin işletmek istemiyorsanız**, dosyaları doğrudan CDN depolamasına gönderin. cdn.com.tr ile ağacı yüklersiniz ve kendi CDN alan adınızdan sunulur — kurulacak, yamalanacak, ayakta tutulacak bir web sunucusu yok. `cdnctl cp -r` bütün bir dizini yükler; bir deponun yapısı da tam olarak budur.

Her iki durumda da kullanıcılarınızın `sources.list` ya da `.repo` dosyasına yazacağı temel adres, CDN alan adınız artı yayınladığınız yoldur. Ve her iki durumda da bir önceki bölümdeki purge adımıyla bitirin — asıl unutulan kısım orası.

Aynı ağacı yayınlamanın iki yolu
# A) origin'iniz var: alt dizinleri senkronlayın, üst dizini asla
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) origin yok: doğrudan CDN depolamasına gönderin
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

# temel adres = CDN alan adınız + bu yol:
#   deb  -> https://cdn.example.com/downloads/deb
#   rpm  -> https://cdn.example.com/downloads/rpm

Temiz bir konteynerde doğrulayın

Doğrulamayı kendi makinenizde yapmayın. O makine anahtarınıza zaten güveniyor, elinde önbelleğe alınmış bir indeks olabilir ve kullanıcılarınızın birazdan çarpacağı sorunları seve seve gizler. Tek dürüst test, tek kullanımlık bir konteynerdir — hem eksik paket imzasını hem de bayat önbellek hatalarını böyle bulduk.

Test ederken imza kontrolünü **açık** tutun. "Gerisi çalışıyor mu bir bakayım" diye `gpgcheck`'i kapatmak, deponun imzasız yayına çıkmasının en bilinen yoludur.

Fedora tarafı için bir ipucu: `--disablerepo="*" --enablerepo="depomuz"` verin. Aksi halde dnf zamanının çoğunu Fedora'nın kendi metadata'sını indirmekle geçirir ve yavaş ya da başarısız bir çalıştırma size kendi deponuz hakkında hiçbir şey söylemez.

İki konteyner, gerçek kurulum
# 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 aracim && aracim --version'

# Fedora / RHEL — sadece kendi depomuz, ki test deponuzu ölçsün
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 aracim
  rpm -qi aracim | grep Signature'

Son bir şey: pakette self-update'i kapatın

Aracınız kendini güncelleyebiliyorsa, bu özellik artık paket yöneticisiyle çakışır. Self-update, `dpkg` ya da `rpm`'in sahip olduğu bir dosyanın üzerine yazar; onların veritabanı ise artık diskte olmayan bir sürümü göstermeye devam eder — bir sonraki güncellemede sürpriz bir şey olur.

Çözüm küçük: derlemeye hangi yolla dağıtıldığını damgalayın ve paket yöneticisi kurmuşsa self-update kenara çekilsin. Doğrudan indirmeler eskisi gibi kendini güncellemeye devam eder.

Derleme zamanı damga (Go örneği)
// installChannel, paketlenmiş derlemelerde derleme anında değiştirilir.
var installChannel = "direct"

func update() error {
    if installChannel != "direct" {
        return fmt.Errorf(
            "%s ile kurulmuş — güncellemeyi onunla yapın",
            installChannel)
    }
    // ... normal self-update
}

// paketlenmiş derlemeler:
//   go build -ldflags "-X main.installChannel=deb"

Sık sorulanlar

Depoyu imzalamak zorunda mıyım?

Teknik olarak hayır — apt ve dnf'e doğrulamayı atlaması söylenebilir. Pratikte evet. Aksi halde kullanıcıların yazılımınızı kurmak için bir güvenlik kontrolünü kapatması gerekir; bu hem istenmesi kötü bir şey hem de öğretilmesi daha kötü bir alışkanlık. İmzalamanın maliyeti bir anahtar ve iki komut.

Metadata'yı imzaladım, dnf neden paket imzasız diyor?

Çünkü RPM birbirinden ayrı iki şeyi kontrol eder. `repo_gpgcheck` `repomd.xml`'i kapsar; `gpgcheck` ise her `.rpm`'in içine gömülü imzayı. Paketlere de `rpmsign --addsign` uygulamanız gerekir — üstelik bunu `createrepo_c`'den önce yapmalısınız, çünkü imzalama dosyaları değiştirir.

Yayından hemen sonra apt neden hash uyuşmazlığı veriyor?

Neredeyse her zaman, eski ve yeni metadata'yı karışık sunan bir önbellek katmanı yüzünden. Depo metadata'sı kendi içinde tutarlıdır, dolayısıyla kısmi bir güncelleme bozuk bir depo demektir. Yayının yeniden yazdığı her dosyayı purge edin ya da metadata'ya kısa bir TTL verin.

Tek depo hem amd64 hem arm64 sunabilir mi?

Evet. APT için aynı suite altında mimari başına bir `Packages` dosyası üretin ve ikisini de `Architectures` içinde listeleyin. YUM için iki paketi aynı dizine koyun — `createrepo_c` her paketin mimarisini kendisi kaydeder.

Depo dosyaları git'te dursun mu?

Genellikle hayır. Paket havuzları her sürümde büyür ve git her sürümü sonsuza dek saklar. Ağacı sürüm çıktılarınızdan üretip origin'e senkronlayın; git'te yalnızca script'ler ve public anahtar kalsın. Derlemeyi tekrarlanabilir yapın ki depo istendiği an sıfırdan yeniden üretilebilsin.