Tüm yazılar
Altyapı 6 dk okuma

SFTP dosya alışverişi otomasyonu: anahtar doğrulama, atomik yükleme ve bütünlük manifesti

SFTP otomasyonu rehberi: paramiko ile anahtar doğrulama, known_hosts sabitleme, dosyaları bir kez indirme, atomik yükleme ve SHA-256 manifestiyle bütünlük kontrolü.

#sftp #python #paramiko #entegrasyon #guvenlik

SFTP otomasyonu, bankayla, lojistik firmasıyla ya da mali müşavirle dosya alışverişini elle yapılan bir işten güvenilir bir boru hattına çevirmenin yoludur. Bu kurumların çoğu hâlâ bir API yerine bir SFTP hesabı verir: her sabah ödeme listesi bir klasöre düşer, siz de sevk irsaliyelerinizi başka bir klasöre bırakırsınız. İş basit görünür, ama elle yapıldığında unutulan dosyalar, yarım yüklemeler ve kimin neyi ne zaman aldığı sorusu birikir.

Bu yazıda Python ve paramiko kütüphanesiyle güvenli bir SFTP hattı kuruyorum: anahtarla kimlik doğrulama, karşı sunucunun kimliğini sabitleme, yeni dosyaları yalnız bir kez indirme, yüklemeyi karşı tarafın yarım dosya görmeyeceği biçimde yapma ve bütünlüğü bir manifest dosyasıyla kanıtlama. Her adımın kodu var; kodların çoğu standart kütüphaneyle çalışır, yalnız bağlantı kısmı paramiko gerektirir.

Amaç, elle yapılan işi birebir kopyalamak değil. Elle yapılan aktarımda insan gözü dosyanın tam indiğini, doğru klasöre gittiğini kontrol eder. Otomasyonda bu kontrollerin her birinin koda dönüşmesi gerekir; yoksa hat sessizce yarım dosya işler ve hata haftalar sonra muhasebede ortaya çıkar.

Neden hâlâ SFTP ve sınırları

SFTP, SSH bağlantısı üzerinden dosya aktarımı yapan bir protokoldür; şifreleme ve kimlik doğrulama SSH katmanından gelir, dosya işlemleri ise listeleme, indirme, yükleme, yeniden adlandırma gibi küçük bir komut kümesinden oluşur. Kurumların tercih etmesinin nedeni basitliğidir: tek bir port, bilinen bir güvenlik modeli ve her işletim sisteminde hazır istemci.

Eski FTP ile karıştırmamak gerekir. FTP şifresiz çalışır, parolayı ve dosyayı açık gönderir ve iki ayrı bağlantı kullandığı için güvenlik duvarıyla sürekli çatışır. FTPS, FTP'nin üstüne TLS ekler ama bağlantı modeli aynı kalır. Karşı taraf hâlâ FTP sunuyorsa, otomasyona başlamadan önce SFTP'ye geçişi istemek, yazacağınız kodun en değerli satırıdır.

SFTP'nin sınırı da açıktır: protokol size dosyanın tamamlandığını ya da içeriğinin doğru olduğunu söylemez. Karşı taraf dosyayı yazarken siz listeleyip indirirseniz yarım dosya alırsınız; ağ kesilirse yükleme yarıda kalır ve karşı tarafta eksik bir dosya durur. Bu yazıdaki desenlerin tamamı bu iki boşluğu kapatmak için vardır.

Anahtarla kimlik doğrulama ve sunucu kimliğini sabitleme

Anahtarla kimlik doğrulama, parola yerine bir açık ve gizli anahtar çiftiyle oturum açmaktır: gizli anahtar sunucunuzda kalır, açık anahtarı karşı tarafa verirsiniz. Otomasyon için parolasız anahtar kullanılır, çünkü parolayı yazacak insan yoktur; bunun bedeli, anahtar dosyasının izinlerini sıkı tutmak ve onu yalnız bu iş için ayırmaktır.

İkinci ve çoğu zaman atlanan adım, karşı sunucunun kimliğini sabitlemektir. SSH istemcisi ilk bağlantıda sunucunun ana makine anahtarını sorar; otomasyonda bu soruya cevap veren kimse olmadığı için geliştiriciler çoğu zaman bilinmeyen anahtarı otomatik kabul eden ayarı açar. Bu ayar, ağ üzerinde araya giren birinin kendini karşı sunucu gibi göstermesine kapı aralar. Doğrusu, anahtarı önceden almak, parmak izini karşı tarafla ayrı bir kanaldan teyit etmek ve bilinmeyen anahtarı reddetmektir.

# 1) Otomasyon için ayrı, parolasız bir anahtar çifti üret (ed25519 kısa ve hızlıdır)
ssh-keygen -t ed25519 -N "" -C "fatura-alim@otomasyon" -f /opt/otomasyon/anahtar/sftp_ed25519

# 2) Karşı sunucunun ana makine anahtarını al ve known_hosts dosyasına yaz
ssh-keyscan -p 22 -t ed25519 sftp.ornek-partner.com > /opt/otomasyon/anahtar/known_hosts

# 3) Parmak izini karşı tarafın size ayrıca ilettiği değerle karşılaştır
ssh-keygen -lf /opt/otomasyon/anahtar/known_hosts

Gizli anahtar bir sırdır ve diğer sırlarla aynı yerde yaşamalıdır. Dosya izinleri yalnız otomasyon kullanıcısına okuma vermeli, anahtar depoya asla girmemeli ve bir sır kasası kullanıyorsanız anahtar oradan çalışma zamanında dosyaya yazılmalıdır; bu geçişi sır kasasına geçiş yazısında anlattım. Karşı tarafa verdiğiniz açık anahtar ise sır değildir, e-postayla gönderilebilir.

Yeni dosyaları yalnız bir kez indirme

Yeni dosyaları bir kez indirme deseni, uzak klasördeki her dosyayı ad, boyut ve değişiklik zamanından oluşan bir anahtarla tanımlar ve daha önce işlenen anahtarları yerel bir kayıtta tutar. Betik her çalıştığında klasörü listeler, kayıtta olmayanları indirir ve indirdiğini kayda yazar. Böylece aynı dosya iki kez işlenmez, karşı taraf aynı adla yeni içerik bırakırsa da fark edilir.

İndirme yerel tarafta da atomik olmalıdır. Dosya önce geçici bir ada iner, boyutu uzak boyutla karşılaştırılır ve ancak tutuyorsa asıl adına taşınır. Aynı klasörü izleyen başka bir süreç böylece hiçbir zaman yarım dosya görmez. Aşağıdaki betik bu üç parçayı bir araya getirir; karşı taraf dosyayı hâlâ yazıyorsa boyut tutmaz ve dosya bir sonraki çalıştırmaya kalır.

import json
import os
import stat
import tempfile
from pathlib import Path

import paramiko

ANAHTAR_DIZINI = Path("/opt/otomasyon/anahtar")
YEREL_GELEN = Path("/opt/otomasyon/gelen")
ISLENEN_KAYDI = Path("/opt/otomasyon/durum/islenen.json")


def baglan(sunucu: str, kullanici: str, port: int = 22) -> paramiko.SSHClient:
    istemci = paramiko.SSHClient()
    istemci.load_host_keys(str(ANAHTAR_DIZINI / "known_hosts"))
    istemci.set_missing_host_key_policy(paramiko.RejectPolicy())
    istemci.connect(
        hostname=sunucu,
        port=port,
        username=kullanici,
        key_filename=str(ANAHTAR_DIZINI / "sftp_ed25519"),
        allow_agent=False,
        look_for_keys=False,
        timeout=30,
        banner_timeout=30,
    )
    return istemci


def islenenleri_yukle() -> dict:
    if ISLENEN_KAYDI.exists():
        return json.loads(ISLENEN_KAYDI.read_text(encoding="utf-8"))
    return {}


def islenenleri_kaydet(kayit: dict) -> None:
    gecici = ISLENEN_KAYDI.with_suffix(".tmp")
    gecici.write_text(json.dumps(kayit, ensure_ascii=False, indent=1), encoding="utf-8")
    os.replace(gecici, ISLENEN_KAYDI)


def yeni_dosyalari_indir(sftp: paramiko.SFTPClient, uzak_dizin: str) -> list:
    islenen = islenenleri_yukle()
    indirilen = []
    for oge in sftp.listdir_attr(uzak_dizin):
        if stat.S_ISDIR(oge.st_mode) or oge.filename.startswith("."):
            continue
        anahtar = f"{oge.filename}:{oge.st_size}:{oge.st_mtime}"
        if anahtar in islenen:
            continue
        uzak_yol = f"{uzak_dizin}/{oge.filename}"
        with tempfile.NamedTemporaryFile(dir=YEREL_GELEN, delete=False) as gecici:
            gecici_yol = Path(gecici.name)
        sftp.get(uzak_yol, str(gecici_yol))
        if gecici_yol.stat().st_size != oge.st_size:
            gecici_yol.unlink()
            raise IOError(f"boyut tutmadı: {oge.filename}")
        os.replace(gecici_yol, YEREL_GELEN / oge.filename)
        islenen[anahtar] = {"dosya": oge.filename, "boyut": oge.st_size}
        islenenleri_kaydet(islenen)
        indirilen.append(oge.filename)
    return indirilen


if __name__ == "__main__":
    istemci = baglan("sftp.ornek-partner.com", "firma_alim")
    try:
        sftp = istemci.open_sftp()
        try:
            for ad in yeni_dosyalari_indir(sftp, "/giden"):
                print("indirildi", ad)
        finally:
            sftp.close()
    finally:
        istemci.close()

Betik cron ya da systemd zamanlayıcısıyla çalıştırıldığında iki örneğin aynı anda çalışmasını bir kilit dosyasıyla engellemek gerekir; aksi halde iki süreç aynı dosyayı indirip kayda iki kez yazar. Bu kilit ve güvenli yeniden çalıştırma desenlerini zamanlanmış iş tasarımı yazısında ayrıntılı anlattım. Yeniden deneme için betiği baştan çalıştırmak yeterlidir; işlenen kaydı sayesinde kaldığı yerden devam eder.

Hata yönetiminde iki ayrı durum vardır. Bağlantı kurulamaması ya da zaman aşımı geçici bir hatadır; betik sıfır dışı bir çıkış koduyla biter, zamanlayıcı bir sonraki döngüde yeniden dener ve işlenen kaydı sayesinde hiçbir dosya iki kez işlenmez. Reddedilen anahtar ya da bilinmeyen sunucu kimliği ise kalıcı hatadır; yeniden deneme işe yaramaz, insan müdahalesi gerekir. İki durumu ayrı çıkış kodlarıyla ayırmak, uyarı sisteminin ikincisinde hemen ses çıkarmasını sağlar.

Atomik yükleme ve bütünlük manifesti

Atomik yükleme, dosyayı karşı sunucuya önce geçici bir adla göndermek, boyutunu doğrulamak ve ardından tek bir yeniden adlandırma işlemiyle asıl adına taşımak demektir. Yeniden adlandırma dosya sisteminde tek adımda gerçekleşir, bu yüzden karşı tarafın izleyicisi dosyayı ya hiç görmez ya da tamamını görür. Nokta ile başlayan geçici ad, çoğu izleyicinin gizli dosyaları atlamasından yararlanır.

import os


def atomik_yukle(sftp, yerel_yol: str, uzak_dizin: str, ad: str) -> None:
    """Önce geçici ada yükler, boyutu doğrular, sonra tek adımda asıl ada taşır."""
    gecici = f"{uzak_dizin}/.{ad}.part"
    hedef = f"{uzak_dizin}/{ad}"
    sftp.put(yerel_yol, gecici, confirm=True)
    yerel_boyut = os.path.getsize(yerel_yol)
    if sftp.stat(gecici).st_size != yerel_boyut:
        sftp.remove(gecici)
        raise IOError(f"yükleme eksik kaldı: {ad}")
    try:
        sftp.remove(hedef)          # aynı ad varsa önce kaldır; yoksa hata yutulur
    except IOError:
        pass
    sftp.rename(gecici, hedef)

Karşı sunucuların davranışı farklılık gösterir. Bazı SFTP sunucuları var olan bir dosyanın üstüne yeniden adlandırmaya izin vermez; yukarıdaki işlev bu yüzden hedefi önce kaldırır. Bazı kurumlar geçici uzantıyı kendileri tanımlar, örneğin yalnız belirli bir uzantıyla biten dosyaları işler ve diğerlerini görmezden gelir. Hattı kurarken karşı tarafın entegrasyon belgesini isteyin ve geçici ad kuralını oradaki kurala uydurun; belge yoksa kuralı kendiniz yazılı olarak bildirin.

Boyut kontrolü yarım aktarımı yakalar ama bozulmuş baytı yakalamaz. Bunun için dosyaların yanına bir manifest konur: her satırda dosyanın SHA-256 özeti ve adı bulunur. Alan taraf manifesti okur, her dosyanın özetini yeniden hesaplar ve tutmayanı reddeder. Biçimi sha256sum -c komutuyla uyumlu tutmak, karşı tarafın Python kullanmadan da doğrulama yapabilmesini sağlar. Aşağıdaki betik hem üretir hem doğrular ve dış bağımlılık taşımaz.

import hashlib
import sys
from pathlib import Path


def sha256_hex(yol: Path) -> str:
    h = hashlib.sha256()
    with yol.open("rb") as f:
        for parca in iter(lambda: f.read(1024 * 1024), b""):
            h.update(parca)
    return h.hexdigest()


def manifest_uret(dizin: Path) -> str:
    """Her satır: <sha256> iki boşluk <dosya adı>; sha256sum -c ile uyumlu."""
    satirlar = []
    for yol in sorted(dizin.iterdir()):
        if yol.is_file() and yol.name != "MANIFEST.sha256":
            satirlar.append(f"{sha256_hex(yol)}  {yol.name}")
    return "\n".join(satirlar) + "\n"


def manifest_dogrula(dizin: Path) -> list:
    """Eksik ya da bozuk dosyaların listesini döner; boş liste temiz demektir."""
    sorunlar = []
    metin = (dizin / "MANIFEST.sha256").read_text(encoding="utf-8")
    for satir in metin.splitlines():
        if not satir.strip():
            continue
        beklenen, ad = satir.split("  ", 1)
        yol = dizin / ad
        if not yol.exists():
            sorunlar.append(f"eksik: {ad}")
        elif sha256_hex(yol) != beklenen:
            sorunlar.append(f"bozuk: {ad}")
    return sorunlar


if __name__ == "__main__":
    komut, dizin = sys.argv[1], Path(sys.argv[2])
    if komut == "uret":
        (dizin / "MANIFEST.sha256").write_text(manifest_uret(dizin), encoding="utf-8")
        print("manifest yazıldı")
    elif komut == "dogrula":
        sorunlar = manifest_dogrula(dizin)
        for s in sorunlar:
            print(s)
        sys.exit(1 if sorunlar else 0)

Aktarılan dosyalar kişisel veri içeriyorsa, örneğin maaş listesi ya da müşteri ödemeleri, hat kurulurken karşı tarafla veri aktarım koşullarının yazılı olması gerekir. 6698 sayılı Kişisel Verilerin Korunması Kanunu, veri sorumlusu ile veri işleyen arasındaki ilişkinin sözleşmeyle düzenlenmesini ve veri güvenliği tedbirlerinin alınmasını ister. Şifreli kanal, sabitlenmiş sunucu kimliği ve manifest, bu tedbirlerin teknik tarafını oluşturur; işlenen kaydı ise kimin neyi ne zaman aldığını gösteren denetim izidir.

Sonuç: dört kontrol, bir hat

Güvenilir bir SFTP otomasyonu dört kontrolün üstünde durur: anahtarla kimlik doğrulama ve sabitlenmiş sunucu kimliği, yalnız bir kez indirme için işlenen kaydı, geçici ad ve yeniden adlandırmayla atomik aktarım, SHA-256 manifestiyle bütünlük kanıtı. Her biri birkaç satırdır; eksik olan her biri ise aylar sonra ortaya çıkan, bulunması zor bir hatadır.

İlk adım olarak mevcut aktarımınızda hangi kontrolün eksik olduğunu listeleyin. Çoğu kurulumda bilinmeyen sunucu anahtarını otomatik kabul eden ayar açıktır ve manifest hiç yoktur; bu ikisini kapatmak bir öğleden sonralık iştir. Karşı tarafla dosya alışverişinizi bu yapıya taşımak ya da mevcut hattı gözden geçirmek isterseniz iletişim bölümünden ulaşabilirsiniz.