← blog · 5 Ekim 2026

Paket Yayınında Uzun Ömürlü Jetondan Kurtulmak: OIDC ile Güvenilir Yayıncı (npm, PyPI, crates.io)

CI gizli değişkeninde duran yayın jetonu sızar, süresi dolar ve gerekenden geniştir. Güvenilir yayıncı bunun yerine CI'ın OIDC kimliğini birkaç dakikalık bir jetonla takas eder. npm, PyPI ve crates.io'nun farkları, örnek workflow, geçiş sırası ve dosya adı, ortam, fork PR'ları, yeniden yayın ile iki kanallı geçiş tuzakları.

Bir paketi npm, PyPI ya da crates.io'ya yayınlamanın klasik yolu, registry'de bir API jetonu üretip CI sistemine gizli değişken olarak koymaktır. Beş dakikalık bir kurulumdur ve sonra yıllarca kimse dokunmaz. Oysa o jeton, paketinizi kuran her makineye kod gönderebilen bir anahtardır. Üç registry de artık aynı alternatifi sunuyor: güvenilir yayıncı (trusted publishing). CI her koşuda kendi kimliğini kanıtlar, registry de karşılığında birkaç dakikalık bir jeton verir. Saklanacak bir sır kalmaz.

Uzun ömürlü jetonun üç sorunu

Sızıntı. Yayın jetonu taşıyıcı (bearer) bir sırdır, kimin elindeyse oradan çalışır. CI günlüğüne basılan bir değişken ya da bir dizüstündeki .npmrc yeter; jeton iptal edilene kadar geçerli kalır.

Süre. Süresiz jeton bir risktir, süreli jeton ise yayını en olmadık günde kırar. npm bu tartışmayı kapattı: 2025 sonunda klasik jetonlar kalıcı olarak iptal edildi, yazma yetkili granüler jetonların ömrü en fazla 90 güne indi.

Kapsam. PyPI'de hesap kapsamlı bir jeton hesaptaki bütün projelere yükleme yapabilir. Daha önemlisi, hiçbir jeton onu kullanan kodu tanımaz: depoya yazma yetkisi olan biri, gizli değişkeni okuyan bir workflow'u herhangi bir dalda çalıştırabilir.

Güvenilir yayıncı nasıl çalışır

Mekanizma OpenID Connect (OIDC) üzerine kurulu:

  1. Registry'de paketin ayarlarına yayıncıyı tanımlarsınız: depo sahibi, depo adı, workflow dosyasının adı ve isteğe bağlı bir ortam (environment) adı. Sır üretilmez.
  2. Yayın işi, CI'ın OIDC sağlayıcısından imzalı bir kimlik jetonu (JWT) ister. GitHub Actions'ta bunun için id-token: write izni gerekir. Jetonda depo, sahip, workflow, dal ya da etiket ve ortam bilgileri bulunur.
  3. Registry imzayı sağlayıcının açık anahtarlarıyla doğrular, bilgileri tanımla karşılaştırır ve eşleşirse kısa ömürlü bir yayın jetonu üretir. Bu jeton PyPI'de 15, crates.io'da 30 dakika geçerlidir.
  4. Paket bu jetonla yüklenir. crates.io'nun resmi action'ı jetonu iş sonunda ayrıca iptal eder.

Kimlik "bu metni bilen herkes" olmaktan çıkıp "bu depodaki bu workflow, bu ortamda" olur. Ama güven ortadan kalkmaz, yer değiştirir: workflow dosyasını ya da içinde çalışan kodu değiştirebilen herkes artık yayın yapabilir.

npm, PyPI ve crates.io yan yana

npm. GitHub Actions, GitLab CI/CD ve CircleCI destekleniyor, ama yalnız bulutta barındırılan çalıştırıcılarda (runner). npm CLI 11.5.1 ve Node 22.14.0 ya da sonrası gerekir, takası npm publish kendisi yapar. Açık depodan açık paket yayınlanınca, sürümü çıktığı workflow koşusuna bağlayan provenance kaydı kendiliğinden üretilir. Sık atlanan üç ayrıntı: package.json içindeki repository.url depoyla birebir eşleşmeli; tanımda doğrudan npm publish iznini ayrıca açmak gerekir (elle onay bekleyen npm stage publish hep serbest); yeni bir tanım iki gün içinde ilk yayınını yapmazsa geçersiz olur.

PyPI. En geniş desteği veren registry: GitHub Actions, GitLab CI/CD, Google Cloud ve ActiveState. GitHub'da pypa/gh-action-pypi-publish kullanılır ve dağıtım dosyaları için imzalı onay kayıtları (attestation) varsayılan olarak üretilir. Henüz var olmayan bir proje için bekleyen yayıncı (pending publisher) tanımlanabilir. Bu tanım adı rezerve etmez; siz yayınlamadan başkası aynı adı alırsa geçersiz olur.

crates.io. GitHub Actions'ı ve kamuya açık beta olarak GitLab.com üzerindeki GitLab CI/CD'yi destekliyor. Bir crate'in ilk sürümü API jetonuyla çıkmak zorunda, güvenilir yayıncı yalnız var olan crate'lere tanımlanabiliyor. Takası rust-lang/crates-io-auth-action yapar, ürettiği jeton CARGO_REGISTRY_TOKEN olarak cargo publish'e verilir. Crate ayarlarındaki bir seçenekle jetonla yayın tamamen kapatılabilir.

Örnek workflow

Aşağıdaki yapı Python paketleme rehberinin resmi örneğini izliyor: derleme ve yayın ayrı işlerde, kimlik jetonunu yalnız yayın işi isteyebiliyor.

name: release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/setup-python@v6
        with:
          python-version: "3.x"
      - run: python3 -m pip install build --user
      - run: python3 -m build
      - uses: actions/upload-artifact@v5
        with:
          name: dist
          path: dist/

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v6
        with:
          name: dist
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1

Ayrım bir zevk meselesi değil: rehber derlemeyi yayın işinin içinde yapmayı desteklemiyor, böylece derlemede çalışan betikler ve bağımlılıklar kimlik jetonuna uzanamıyor. npm'de iş actions/setup-node ve npm publish ile biter; hiçbir yere NODE_AUTH_TOKEN verilmez.

Geçiş sırası

Önce tanımla, sonra yayınla, eski jetonu ancak ilk yeşil yayından sonra sil.

  1. Tanımla. npm'deki iki günlük süre yüzünden tanımı haftalar önce değil, yayından hemen önce yapın. Yeni bir crate'in ilk sürümünde kullanılan jetonu tek kullanımlık sayın.
  2. Yayınla. Workflow'a id-token: write iznini ve ortamı ekleyin, jetonu workflow'dan çıkarın ama registry'de henüz iptal etmeyin. Yeni bir sürüm çıkarın.
  3. Doğrula, sonra sil. Yayının OIDC ile yapıldığını görün, jetonu registry'de iptal edin ve kanalı kapatın: npm'de paket ayarlarındaki "Require two-factor authentication and disallow tokens", crates.io'da güvenilir yayıncıyı zorunlu kılan seçenek. PyPI'de jetonu hesap ayarlarından silin.

Tercihim, yeni paketleri ilk günden güvenilir yayıncıyla açmak ve mevcut paketlerde geçişi bir sonraki rutin sürüme bağlamak. Gerekçesi şu: geçişin tek gerçek riski yayının kırılması. Rutin bir sürümde kırılan yayın kimseyi bekletmez, acil bir güvenlik yamasının ortasında kimlik doğrulama hatası ayıklamak ise en kötü andır.

Tuzaklar

Workflow dosya adı. Registry workflow'u içindeki name: alanından değil, dosya adından tanır. release.yml dosyasını publish.yml yapan bir temizlik yayını sessizce kırar. npm yalnız dosya adını (.yml uzantısıyla) ister, büyük küçük harfe duyarlıdır ve uyuşmazlıkta ENEEDAUTH verir; PyPI invalid-publisher döner. Yeniden kullanılabilir workflow'ları (workflow_call) PyPI desteklemez, npm ise yayını yapanın değil çağıran workflow'un adına bakar.

Ortam. Tanımda ortam adı varsa iş birebir aynı adlı ortamda koşmalı. Ortamı atlamak cazip ama yanlış: üç registry'nin tanımında da dal ya da etiket alanı yok. "Yalnız v ile başlayan etiketlerden yayın" kuralını ve zorunlu onaycıyı GitHub ortamının dağıtım kurallarında koyarsınız. Ortam yoksa yazma yetkisi olan herkes aynı adlı workflow'u herhangi bir daldan çalıştırıp yayın yapabilir. Sürüm etiketlerini de depo kurallarıyla (ruleset) koruyun.

Fork PR'ları. Fork'tan açılan PR'larda çalışan workflow'lar ana deponun OIDC jetonunu alamaz. Tehlike, ana deponun bağlamında ve onun izinleriyle çalışan pull_request_target'tadır: PR'ın kodunu id-token: write ile çalıştıran böyle bir workflow, dışarıdan gelen bir katkıya yayın yetkisi verir. crates.io bu yüzden pull_request_target ve workflow_run tetikleyicilerini güvenilir yayıncıdan tamamen engelledi. Yayın workflow'u yalnız etiketle ya da elle tetiklenmeli.

Yeniden yayın. Üç registry'de de bir sürüm numarası bir kez harcanır: npm bir paket@sürüm'ü yayından kaldırılsa bile yeniden kabul etmez, PyPI kullanılmış bir dosya adını reddeder, crates.io'da bir sürümün üzerine yazılamaz. Yarıda ölen bir işi yeniden çalıştırmak "zaten var" hatası verir. PyPI action'ının skip-existing seçeneğini kendi belgeleri bile asıl PyPI için önermiyor; ben sürümü artırıp temiz bir yayın yapmayı tercih ederim. Takas adımını da yüklemenin hemen önüne koyun, yoksa jetondan sonra koşan testler kısa ömrü yayına gelmeden tüketebilir.

İki kanallı geçiş. Geçiş sırasında jeton da OIDC de çalışır ve yeşil bir yayın hangisinin kullanıldığını söylemez. npm CLI bir OIDC ortamı bulamazsa sessizce geleneksel jetona döner; NODE_AUTH_TOKEN hâlâ tanımlıysa bozuk bir yayıncı tanımı, jetonu sildiğiniz güne kadar fark edilmez. İkinci adımda jetonu workflow'dan çıkarmanın sebebi bu. Doğrulamak için PyPI'de dosya ayrıntılarındaki "Uploaded using Trusted Publishing?" satırına, açık depolarda npm sürümünün provenance kaydına bakın. CI'daki gizli değişkeni silmek jetonu iptal etmez; başka yerlerdeki kopyalar, jeton registry'de iptal edilene kadar çalışır.

Neyi çözmez

Güvenilir yayıncı yayıncının kimliğini çözer, kodun güvenilirliğini değil: provenance paketin hangi depodan çıktığını kanıtlar, oradaki kodun zararsız olduğunu değil. Güven depoya taşındığı için zorunlu inceleme, etiket kuralları ve tam commit hash'ine sabitlenmiş action'lar artık yayın güvenliğinin parçası. Kendi barındırdığınız bir CI'dan yayın yapıyorsanız jetonla kalırsınız; o zaman jetonu tek projeyle sınırlayın ve ömrünü kısa tutun.

Döndürmesi en kolay yayın jetonu, hiç üretilmemiş olanıdır.