Arşiv İşlemlerinde Namespace İzolasyonu: unshare ve chroot ile Güvenli Açma Ortamı Oluşturma

Yıllar içinde kaç tane “zararsız görünen” arşiv dosyasının açılması sırasında sistem bütünlüğünü tehlikeye attığını gördüm, saymakla bitemem. Özellikle üçüncü taraf kaynaklardan gelen tar.gz, zip ya da cpio paketlerini doğrudan üretim sisteminde veya hatta test ortamında açmak, farkında olmadan kapıyı aralamak gibidir. Bu yazıda, unshare ve chroot araçlarını kullanarak arşiv işlemleri için nasıl izole, güvenli bir ortam oluşturabileceğinizi anlatacağım. Teorik değil, sahada işe yarayan yöntemler bunlar.

Sorun Nedir, Neden Önemseyelim?

Klasik bir senaryo düşünelim: Bir yazılım tedarikçisi size büyük bir uygulama paketi gönderiyor. Tar arşivi, içinde yüzlerce dosya var. tar xzf vendor_package.tar.gz komutunu çalıştırıyorsunuz ve işlem tamamlanıyor. Peki ya arşiv içinde ../../etc/cron.d/malicious gibi bir yol varsa? Ya da sembolik bağlantılar aracılığıyla sistem dizinlerine erişmeye çalışan bir yapı söz konusuysa?

Path traversal saldırıları, arşiv dosyaları üzerinden gerçekleştirilen en yaygın saldırı vektörlerinden biridir. GNU tar’ın --no-overwrite-dir ve --no-same-owner gibi bayrakları bir nebze koruma sağlasa da bunlar yetersiz kalabilir. Asıl çözüm, işlemi baştan izole edilmiş bir ortamda yapmaktır.

İkinci bir senaryo: Otomatik bir pipeline üzerinden müşterilerden gelen kullanıcı yüklü arşivleri işliyorsunuz. Bu arşivleri açan sürecin sisteminizin geri kalanına erişimi olmamalı. İşte unshare ve chroot burada devreye giriyor.

Temel Kavramlar: Namespace ve chroot

Linux namespace’leri, işlemlerin sistem kaynaklarının izole edilmiş görünümlerine sahip olmasını sağlar. Mount namespace’i, PID namespace’i, network namespace’i, UTS namespace’i gibi çeşitler var. unshare komutu, mevcut bir işlemin yeni namespace’ler oluşturmasına ve bu namespace’lere “katılmasına” izin verir.

chroot ise çok daha eskidir: Bir işlem için kök dizini değiştirir. Bu sayede chroot ortamı içindeki bir süreç, / sanıyor ama aslında sizin belirlediğiniz bir alt dizinde hapsolmuş oluyor.

İkisini birlikte kullandığınızda oldukça sağlam bir izolasyon katmanı elde edersiniz. chroot tek başına yeterli değil çünkü aynı mount namespace’ini paylaşan bir süreç bazı atlatma tekniklerine karşı savunmasız kalabilir. unshare ile mount namespace’ini de izole ettiğinizde tablo değişiyor.

Ortamı Hazırlamak

Önce çalışacağımız temel dizin yapısını oluşturalım. Bu yapı, arşivlerin açılacağı “sandbox” olarak işlev görecek.

#!/bin/bash
# sandbox_setup.sh - Arşiv açma sandbox'ı hazırlama

SANDBOX_DIR="/opt/archive-sandbox"
SANDBOX_ROOT="${SANDBOX_DIR}/rootfs"

# Temel dizin yapısını oluştur
mkdir -p "${SANDBOX_ROOT}"/{tmp,proc,sys,dev,etc,bin,lib,lib64,usr}

# Minimal /etc/passwd ve /etc/group dosyaları (bazı araçlar bunları bekler)
echo "nobody:x:65534:65534:nobody:/nonexistent:/bin/false" > "${SANDBOX_ROOT}/etc/passwd"
echo "nogroup:x:65534:" > "${SANDBOX_ROOT}/etc/group"

# Busybox varsa kopyala, yoksa temel araçları bağla
if command -v busybox &>/dev/null; then
    cp "$(which busybox)" "${SANDBOX_ROOT}/bin/busybox"
    ln -s busybox "${SANDBOX_ROOT}/bin/sh"
    ln -s busybox "${SANDBOX_ROOT}/bin/tar"
    ln -s busybox "${SANDBOX_ROOT}/bin/ls"
fi

echo "Sandbox hazır: ${SANDBOX_ROOT}"

Bu script’i çalıştırdıktan sonra elimizde minimal bir root dosya sistemi olacak. Gerçek üretim ortamlarında bunu her arşiv açma işlemi için sıfırdan oluşturmak yerine bir “template” dizini tutup rsync veya cp --archive ile kopyalayabilirsiniz.

unshare ile Namespace İzolasyonu

Şimdi asıl izolasyon katmanını ekleyelim. unshare komutunun önemli parametreleri şunlar:

–mount veya -m: Yeni bir mount namespace oluşturur. Ana sistemdeki mount değişiklikleri izole edilir. –pid veya -p: Yeni bir PID namespace. Süreçler kendi PID uzaylarında çalışır. –fork veya -f: PID namespace ile birlikte kullanılır, yeni bir child süreç fork’lar. –user veya -U: User namespace. Kullanıcı/grup ID eşlemesi yapılır. –net veya -n: Network namespace. Ağ arayüzlerini izole eder. –uts: UTS namespace. Hostname ve domainname izolasyonu. –mount-proc: /proc’u yeni namespace için mount eder. –map-root-user: Kullanıcı namespace’inde mevcut kullanıcıyı root olarak eşler.

Temel kullanım örneği:

# Root olmadan namespace izolasyonu (user namespace kullanarak)
unshare --user --mount --pid --fork --map-root-user 
    chroot /opt/archive-sandbox/rootfs /bin/sh

Bu komutu root olmayan bir kullanıcı olarak bile çalıştırabilirsiniz çünkü --user ve --map-root-user kombinasyonu, o kullanıcıyı yeni namespace içinde root gibi gösterir ama dış sistemde gerçek root değildir.

Arşiv Açma Script’i

Gerçek işi yapan script’i yazalım. Bu script bir arşiv dosyasını alıp tamamen izole bir ortamda açacak, çıktıyı da kontrollü bir şekilde dışarı alacak:

#!/bin/bash
# secure_extract.sh - Güvenli arşiv açma scripti

set -euo pipefail

ARCHIVE="$1"
OUTPUT_DIR="${2:-/tmp/extracted_$(date +%s)}"
SANDBOX_TEMPLATE="/opt/archive-sandbox/rootfs"
WORK_DIR=$(mktemp -d /tmp/sandbox_XXXXXX)

cleanup() {
    # Bind mount'ları temizle
    if mountpoint -q "${WORK_DIR}/rootfs/proc" 2>/dev/null; then
        umount "${WORK_DIR}/rootfs/proc" 2>/dev/null || true
    fi
    if mountpoint -q "${WORK_DIR}/rootfs/dev" 2>/dev/null; then
        umount "${WORK_DIR}/rootfs/dev" 2>/dev/null || true
    fi
    rm -rf "${WORK_DIR}"
}
trap cleanup EXIT

# Sandbox'ı kopyala (template'den)
cp -a "${SANDBOX_TEMPLATE}" "${WORK_DIR}/rootfs"

# Arşiv dosyasını sandbox içine kopyala
ARCHIVE_BASENAME=$(basename "${ARCHIVE}")
cp "${ARCHIVE}" "${WORK_DIR}/rootfs/tmp/${ARCHIVE_BASENAME}"

# Arşivi izole ortamda aç
unshare --user --mount --pid --fork --map-root-user 
    bash -c "
        chroot '${WORK_DIR}/rootfs' /bin/sh -c '
            cd /tmp && 
            mkdir -p /tmp/output && 
            tar xzf /tmp/${ARCHIVE_BASENAME} -C /tmp/output 
                --no-overwrite-dir 
                --no-same-owner 
                --no-same-permissions 
                2>/tmp/extract.log
        '
    "

# Güvenlik kontrolü: path traversal dene
if grep -r ".." "${WORK_DIR}/rootfs/tmp/extract.log" 2>/dev/null; then
    echo "UYARI: Şüpheli yol tespit edildi!" >&2
fi

# Çıktıyı güvenli şekilde dışarı al
mkdir -p "${OUTPUT_DIR}"
cp -r "${WORK_DIR}/rootfs/tmp/output/." "${OUTPUT_DIR}/"

echo "Arşiv başarıyla açıldı: ${OUTPUT_DIR}"

Bu script’te trap cleanup EXIT kullanımına dikkat edin. Herhangi bir hata oluşsa bile geçici dosyalar temizlenir. Üretim ortamında bu tür kaynak temizliği kritiktir.

İleri Seviye: Ağ İzolasyonu ile Birleştirme

Bazı arşiv araçları veya post-extraction hook’ları ağa bağlanmaya çalışabilir. Bunu engellemek için network namespace’i de ekleyelim:

#!/bin/bash
# secure_extract_netns.sh - Ağ izolasyonlu versiyon

ARCHIVE="$1"
WORK_DIR=$(mktemp -d /tmp/sandbox_XXXXXX)
SANDBOX_TEMPLATE="/opt/archive-sandbox/rootfs"

cleanup() {
    umount "${WORK_DIR}/rootfs/proc" 2>/dev/null || true
    rm -rf "${WORK_DIR}"
}
trap cleanup EXIT

cp -a "${SANDBOX_TEMPLATE}" "${WORK_DIR}/rootfs"

ARCHIVE_BASENAME=$(basename "${ARCHIVE}")
cp "${ARCHIVE}" "${WORK_DIR}/rootfs/tmp/${ARCHIVE_BASENAME}"

# --net ile network namespace'i de izole ediyoruz
# Bu sayede sandbox içinden hiçbir ağ bağlantısı kurulamaz
unshare --user --mount --pid --net --fork --map-root-user 
    bash -c "
        # Mount namespace içinde proc'u mount et
        mount -t proc proc '${WORK_DIR}/rootfs/proc' 2>/dev/null || true
        
        chroot '${WORK_DIR}/rootfs' /bin/sh << 'CHROOT_EOF'
            cd /tmp
            mkdir -p /tmp/output
            
            # Arşiv türünü otomatik tespit et
            case '${ARCHIVE_BASENAME}' in
                *.tar.gz|*.tgz)  tar xzf /tmp/${ARCHIVE_BASENAME} -C /tmp/output ;;
                *.tar.bz2|*.tbz) tar xjf /tmp/${ARCHIVE_BASENAME} -C /tmp/output ;;
                *.tar.xz|*.txz)  tar xJf /tmp/${ARCHIVE_BASENAME} -C /tmp/output ;;
                *.zip)           unzip /tmp/${ARCHIVE_BASENAME} -d /tmp/output ;;
                *)               echo 'Bilinmeyen arşiv formatı'; exit 1 ;;
            esac
CHROOT_EOF
    "

echo "Tamamlandı. Çıktı: ${WORK_DIR}/rootfs/tmp/output"

Arşiv İçeriğini Açmadan Doğrulama

Açmadan önce arşiv içeriğini kontrol etmek iyi bir pratik. Şüpheli yolları, setuid bit’li dosyaları ve sembolik bağlantıları önceden tespit edebilirsiniz:

#!/bin/bash
# archive_audit.sh - Arşiv içerik denetimi

audit_archive() {
    local archive="$1"
    local issues=0
    
    echo "=== Arşiv Denetim Raporu: ${archive} ==="
    echo ""
    
    # Path traversal kontrolü
    echo "-- Path Traversal Kontrolü --"
    local traversal_hits
    traversal_hits=$(tar tzf "${archive}" 2>/dev/null | grep -E '^../|/../|^/' || true)
    if [[ -n "${traversal_hits}" ]]; then
        echo "TEHLIKE: Şüpheli yollar tespit edildi:"
        echo "${traversal_hits}"
        ((issues++))
    else
        echo "OK: Path traversal tespit edilmedi"
    fi
    
    # Sembolik bağlantı kontrolü
    echo ""
    echo "-- Sembolik Bağlantı Kontrolü --"
    local symlinks
    symlinks=$(tar tvzf "${archive}" 2>/dev/null | grep '^l' || true)
    if [[ -n "${symlinks}" ]]; then
        echo "BİLGİ: Arşivde sembolik bağlantılar mevcut:"
        echo "${symlinks}"
        # Dış hedeflere işaret eden symlink'leri işaretle
        echo "${symlinks}" | grep -E '..' && echo "UYARI: Dışa işaret eden symlink!" && ((issues++)) || true
    else
        echo "OK: Sembolik bağlantı yok"
    fi
    
    # Toplam boyut kontrolü
    echo ""
    echo "-- Sıkıştırılmamış Boyut Kontrolü --"
    local total_size
    total_size=$(tar tzf "${archive}" --totals 2>&1 | grep "Total bytes" | awk '{print $NF}' || echo "0")
    echo "Toplam boyut: ${total_size} byte"
    
    # 10GB üzerindeyse uyar (zip bomb koruması)
    if [[ "${total_size}" -gt 10737418240 ]]; then
        echo "UYARI: Olası zip bomb! Boyut 10GB'ı aşıyor."
        ((issues++))
    fi
    
    echo ""
    echo "=== Denetim Tamamlandı. Toplam sorun: ${issues} ==="
    return "${issues}"
}

# Kullanım
audit_archive "$1"

Bu script’i secure_extract.sh‘dan önce çağırarak bir ön eleme yapabilirsiniz. İssue sayısı sıfır değilse arşivi açmayı reddedebilir ya da manuel incelemeye alabilirsiniz.

Systemd-nspawn ile Entegrasyon

Biraz daha kurumsal bir yaklaşım isteyenler için systemd-nspawn, chroot’un gelişmiş halidir ve namespace izolasyonunu otomatik olarak yönetir. Hâlihazırda systemd kullanan ortamlarda bu araç çok daha yönetilebilir bir çözüm sunuyor:

#!/bin/bash
# nspawn_extract.sh - systemd-nspawn ile arşiv açma

ARCHIVE="$1"
CONTAINER_DIR=$(mktemp -d /tmp/nspawn_XXXXXX)
ARCHIVE_BASENAME=$(basename "${ARCHIVE}")

cleanup() {
    # nspawn container'ı durdur (eğer hâlâ çalışıyorsa)
    machinectl terminate "archive-sandbox" 2>/dev/null || true
    rm -rf "${CONTAINER_DIR}"
}
trap cleanup EXIT

# Minimal Debian rootfs oluştur (debootstrap gerektirir)
# Alternatif: önceden hazırlanmış bir template kullan
if ! command -v debootstrap &>/dev/null; then
    echo "debootstrap bulunamadı, busybox tabanlı sandbox kullanılıyor..."
    cp -a /opt/archive-sandbox/rootfs/. "${CONTAINER_DIR}/"
else
    debootstrap --variant=minbase stable "${CONTAINER_DIR}" 
        http://deb.debian.org/debian 2>/dev/null
fi

# Arşivi container dizinine kopyala
cp "${ARCHIVE}" "${CONTAINER_DIR}/tmp/${ARCHIVE_BASENAME}"

# systemd-nspawn ile izole çalıştır
# --read-only: Root filesystem salt okunur
# --tmpfs=/tmp: /tmp'yi geçici olarak mount et
# --private-network: Ağ erişimini kapat
systemd-nspawn 
    --directory="${CONTAINER_DIR}" 
    --private-network 
    --read-only 
    --tmpfs=/tmp 
    --bind-ro="/tmp/${ARCHIVE_BASENAME}:/tmp/${ARCHIVE_BASENAME}" 
    --user=nobody 
    /bin/sh -c "
        mkdir -p /tmp/output
        tar xzf /tmp/${ARCHIVE_BASENAME} -C /tmp/output 
            --no-overwrite-dir 
            --no-same-owner 
            2>/tmp/extract.log
        echo 'Extraction complete'
    "

echo "İzole arşiv açma tamamlandı."

Gerçek Dünya Senaryosu: CI/CD Pipeline Entegrasyonu

Bunu gerçekten işe yarar hale getirmek için bir GitLab CI veya Jenkins pipeline’ına nasıl entegre edebileceğimize bakalım. Çoğu ekipte bu ihtiyaç, üçüncü taraf bağımlılıkların güvenli işlenmesinden doğuyor:

#!/bin/bash
# ci_secure_extract.sh - CI/CD için optimize edilmiş versiyon
# Kullanım: ci_secure_extract.sh <arşiv_yolu> <çıktı_dizini> [maksimum_boyut_mb]

set -euo pipefail

ARCHIVE="${1:?Arşiv dosyası belirtilmedi}"
OUTPUT_DIR="${2:?Çıktı dizini belirtilmedi}"
MAX_SIZE_MB="${3:-500}"
MAX_SIZE_BYTES=$((MAX_SIZE_MB * 1024 * 1024))

SANDBOX_BASE="/opt/ci-sandbox"
WORK_DIR=$(mktemp -d "${SANDBOX_BASE}/work_XXXXXX")
LOG_FILE="${WORK_DIR}/extract.log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
error() { log "HATA: $*" >&2; exit 1; }

cleanup() {
    log "Temizlik yapılıyor..."
    # Olası mount noktalarını temizle
    for mount_point in proc sys dev; do
        mountpoint -q "${WORK_DIR}/rootfs/${mount_point}" 2>/dev/null && 
            umount "${WORK_DIR}/rootfs/${mount_point}" 2>/dev/null || true
    done
    rm -rf "${WORK_DIR}"
    log "Temizlik tamamlandı."
}
trap cleanup EXIT

# Ön kontroller
[[ -f "${ARCHIVE}" ]] || error "Arşiv bulunamadı: ${ARCHIVE}"

ACTUAL_SIZE=$(stat -c%s "${ARCHIVE}")
if [[ "${ACTUAL_SIZE}" -gt "${MAX_SIZE_BYTES}" ]]; then
    error "Arşiv boyutu (${ACTUAL_SIZE} byte) izin verilen maksimumu aşıyor (${MAX_SIZE_BYTES} byte)"
fi

log "Arşiv denetimi başlıyor: ${ARCHIVE}"

# Path traversal hızlı kontrolü
DANGEROUS_PATHS=$(tar -tzf "${ARCHIVE}" 2>/dev/null | grep -cE '^../|/../|^/' || echo "0")
if [[ "${DANGEROUS_PATHS}" -gt 0 ]]; then
    error "Güvenlik ihlali: ${DANGEROUS_PATHS} şüpheli yol tespit edildi. İşlem reddedildi."
fi

log "Güvenlik kontrolü geçildi. Sandbox hazırlanıyor..."

# Sandbox oluştur
cp -a "${SANDBOX_BASE}/template/." "${WORK_DIR}/rootfs/"
mkdir -p "${WORK_DIR}/rootfs/tmp/input" "${WORK_DIR}/rootfs/tmp/output"

ARCHIVE_BASENAME=$(basename "${ARCHIVE}")
cp "${ARCHIVE}" "${WORK_DIR}/rootfs/tmp/input/${ARCHIVE_BASENAME}"

log "İzole arşiv açma başlıyor..."

# Namespace izolasyonu ile aç
unshare --user --mount --pid --net --fork --map-root-user 
    bash -c "
        chroot '${WORK_DIR}/rootfs' /bin/sh -c '
            tar xzf /tmp/input/${ARCHIVE_BASENAME} 
                -C /tmp/output 
                --no-overwrite-dir 
                --no-same-owner 
                --no-same-permissions 
                --no-wildcards-match-slash 
                2>&1
        '
    " >> "${LOG_FILE}" 2>&1

# Çıktıyı hedef dizine taşı
mkdir -p "${OUTPUT_DIR}"
cp -r "${WORK_DIR}/rootfs/tmp/output/." "${OUTPUT_DIR}/"

FILE_COUNT=$(find "${OUTPUT_DIR}" -type f | wc -l)
log "Başarıyla tamamlandı. ${FILE_COUNT} dosya çıkarıldı: ${OUTPUT_DIR}"

# CI/CD için makine tarafından okunabilir özet
cat > "${WORK_DIR}/summary.json" << EOF
{
  "status": "success",
  "archive": "${ARCHIVE}",
  "output_dir": "${OUTPUT_DIR}",
  "file_count": ${FILE_COUNT},
  "archive_size_bytes": ${ACTUAL_SIZE},
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF

cat "${WORK_DIR}/summary.json"

Sık Yapılan Hatalar ve Kaçınma Yolları

Sahada karşılaştığım birkaç kritik hata şunlar:

Sandbox template’i kirletmek: Her çalışmada aynı template dizinini doğrudan kullanmak, önceki çalışmadan kalan dosyaların bir sonraki çalışmayı etkilemesine neden olur. Her seferinde cp -a ile taze bir kopya alın.

Cleanup trap’i unutmak: set -e aktifken bir hata çıkabilir ve mount noktaları ya da geçici dizinler temizlenmeden kalabilir. trap cleanup EXIT her zaman olmalı.

Sadece chroot’a güvenmek: chroot tek başına yetmez. Özellikle root olarak çalışan bir süreç /proc veya /sys üzerinden chroot’tan kaçabilir. Mount namespace izolasyonu şart.

Arşiv boyutunu kontrol etmemek: Zip bomb saldırıları gerçek. Küçük bir arşiv milyarlarca bayta açılabilir. --limit veya boyut kontrolü ekleyin.

Hata çıktısını yutmak: Extraction sırasında oluşan hatalar önemli bilgiler içerebilir. Stderr’i mutlaka log’a yönlendirin.

Sonuç

unshare ve chroot birlikteliği, arşiv işlemleri için pratik ve kurulumu kolay bir izolasyon katmanı sağlıyor. Bunu karmaşık bir container altyapısı kurmadan, neredeyse her modern Linux sisteminde çalıştırabilirsiniz. Özellikle üçüncü taraf arşivleri işleyen otomasyon sistemlerinde, CI/CD pipeline’larında ve kurumsal ortamlarda bu yaklaşım gerçek bir güvenlik katmanı ekliyor.

Bu çözüm mükemmel bir güvenlik garantisi vermez, hiçbir şey vermez. Ama “açıp bakalım” refleksiyle doğrudan sisteme zarar verme riskini ciddi ölçüde azaltır. Kernel exploit’leri ile namespace atlatmaları teorik olarak mümkün, bu yüzden gerçekten yüksek güvenlik gereksinimleri olan ortamlarda bunu VM izolasyonu veya Kata Containers gibi daha ağır çözümlerle desteklemenizi öneririm.

Ama günlük sysadmin işi için, izole bir arşiv açma ortamı kurmak artık iki script meselesi. Bunu yapmamak için geçerli bir neden kalmıyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir