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.
