MongoDB Güvenliği ve İşletimi: Authentication, Role'ler, TLS, Queryable Encryption, Backup ve İzleme
Uygulamanın MongoDB kullanıcısı readWrite ile bile collection silebilir; en az yetki için yalnız gereken action'ları veren bir custom role tanımlanmalı. Gerçek çıktılarla: readWrite'ın 25 action'ı, drop ve createIndex'i reddeden custom role, CA zorunlu TLS, Community'de explicit Queryable Encryption ile şifreli alanda eşitlik sorgusu, mongodump ve restore testi, filtreli profiler, %1 örneklemeli $queryStats ve driver'ın OpenTelemetry span'leri.
Sürüm notu: Bu yazıdaki örnekler MongoDB 9.0.8 Community Server, mongosh 2.6, MongoDB Database Tools 100.20, .NET 10 ve MongoDB.Driver 3.14 ile çalıştırılmıştır. Script'ler MongoDB 8.0.38'de de çalıştırıldı; farklar ilgili bölümlerde.
Önce Tahmin Et
2. yazıda Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) uygulama kullanıcısını bookstore database'inde yalnız readWrite role'üyle oluşturmuştuk ve başka database'lere erişemediğini görmüştük. Aynı kullanıcıyla kitapların bir kopyasını oluşturup kopyayı silmeyi deneyelim:
// 14 - Önce Tahmin Et: uygulama kullanıcısı document okuyup yazmanın dışında ne yapabilir?
db.books.aggregate([{ $match: {} }, { $out: "books_copy" }]);
print("books_copy documents:", db.books_copy.countDocuments());
print("drop() as bookstore_app:", db.books_copy.drop());
"Okuma ve yazma" yetkisi bir collection'ı silmeye yeter mi?
books_copy documents: 77
drop() as bookstore_app: true
Yetiyor. readWrite document okuyup yazmanın ötesinde collection silmeyi, index kurup kaldırmayı ve collection'ı yeniden adlandırmayı da içeriyor. Uygulamada bir SQL injection benzeri NoSQL injection açığı ya da çalınan bir connection string, db.orders.drop() ile bütün siparişleri götürebilir. Bu yazıda MongoDB'yi korumanın ve işletmenin temel araçlarını Bookstore üzerinde deniyoruz: role'ler ve en az yetki, ağ ve TLS (bağlantıyı şifreleyen protokol), Queryable Encryption, backup ve restore testi, izleme. Script'ler 2. yazının lab'ında; 02-load.sh ve 06-bookstore-indexes.js ile başlıyor ve yaptıklarını sonda geri alıyor.
Authentication ve Role'ler: En Az Yetki
MongoDB authentication'da (kimlik doğrulama) varsayılan olarak SCRAM (Salted Challenge Response Authentication Mechanism; parolanın kendisini göndermeden kimlik kanıtlama) kullanıyor; authorization (yetkilendirme) ise role tabanlı. Built-in role'ler kaba taneli: read, readWrite, dbAdmin, userAdmin, clusterMonitor, backup, restore, root. 14-roles.js önce readWrite'ın gerçekte neyi içerdiğini sunucuya soruyor:
readWrite: 25 actions on every collection, among them: convertToCapped, createCollection, createIndex, createSearchIndexes, dropCollection, dropIndex, dropSearchIndex, renameCollectionSameDB
Çalışan bir API'nin bunların hiçbirine ihtiyacı yok. Custom role, action'ları collection bazında veriyor:
// Çalışan API'nin ihtiyacı document'ler, şema değişikliği değil: drop yok, index ya da collection yönetimi yok
bookstore.createRole({
role: "bookstoreApi",
privileges: [
{ resource: { db: "bookstore", collection: "books" }, actions: ["find", "update"] },
{ resource: { db: "bookstore", collection: "orders" }, actions: ["find", "insert", "update"] },
{ resource: { db: "bookstore", collection: "reviews" }, actions: ["find", "insert", "update", "remove"] },
{ resource: { db: "bookstore", collection: "carts" }, actions: ["find", "insert", "update", "remove"] },
{ resource: { db: "bookstore", collection: "customers" }, actions: ["find", "insert", "update"] },
{ resource: { db: "bookstore", collection: "categories" }, actions: ["find"] },
{ resource: { db: "bookstore", collection: "authors" }, actions: ["find"] }
],
roles: []
});
bookstore.createUser({ user: "bookstore_api", pwd: process.env.BOOKSTORE_API_PASSWORD || passwordPrompt(), roles: ["bookstoreApi"] });
find a book: {"title":"Dune"}
decrement the stock: 1
delete a book: Unauthorized: not authorized
drop orders: Unauthorized: not authorized
create an index: Unauthorized: not authorized
read another database: Unauthorized: not authorized
API kitap okuyup stok düşürebiliyor; kitap silemiyor, collection düşüremiyor, index kuramıyor. Siparişlerde remove yok: 5. yazıdaki archive pattern'i siparişi silmek yerine taşıyor, bu iş ayrı bir zamanlanmış işin kullanıcısına ait. Böylece iki kullanıcı oluyor: çalışan uygulama için bookstore_api, deploy ve migration için readWrite'lı bookstore_app (02-load.sh'nin mongoimport --drop'u ve 6. yazının index'leri bunu istiyor). Migration kullanıcısının parolası uygulamanın ortamında durmaz; yalnız deploy pipeline'ında kullanılır. Raporlar için built-in read ile ayrı bir kullanıcı ve 13. yazıdaki hidden üye yeterli. Bir uyarı: action listesi büyüdükçe role'ün bakımı zorlaşır; yeni bir collection eklediğinde role'ü güncellemeyi deploy adımının parçası yap.
Ağ: bindIp ve TLS
MongoDB binary'leri varsayılan olarak yalnız localhost'u dinliyor. Docker image'ı ise container içinde bütün arayüzleri dinliyor; dışarıya neyin açılacağına port eşlemesi karar veriyor. 2. yazının compose.yaml'ı portu yalnız 127.0.0.1'e yayınlıyordu ve auth açıktı. İkisinden biri eksik olduğunda ne olduğu biliniyor: geçmişte auth'suz ve internete açık MongoDB sunucularındaki verinin toplu olarak silinip fidye notu bırakıldığı saldırı dalgaları yaşandı. Ders basit: auth her zaman açık, port yalnız gereken ağa.
Ağ üzerindeki trafik için TLS. 14-tls.sh lab container'ının içinde bir lab CA'sı (sertifika yetkilisi) ve localhost için bir sunucu sertifikası üretiyor, 27040 portunda TLS zorunlu ikinci, geçici bir mongod başlatıyor ve üç bağlantı deniyor:
mongod --port 27040 --dbpath "$dir/db" --tlsMode requireTLS --tlsCertificateKeyFile server.pem --tlsCAFile ca.pem \
--tlsAllowConnectionsWithoutCertificates --logpath "$dir/mongod.log" --fork > /dev/null
--- without TLS
MongoServerSelectionError: connection <monitor> to 127.0.0.1:27040 closed
--- with TLS, without trusting the lab CA
MongoNetworkError: self-signed certificate in certificate chain
--- with TLS and the lab CA
hello ok: 1
{
'1.0': Long('0'),
'1.1': Long('0'),
'1.2': Long('0'),
'1.3': Long('4'),
unknown: Long('0')
}
TLS'siz bağlantı sunucu tarafından kapatıldı; TLS'li ama CA'ya güvenmeyen istemci sertifikayı reddetti; doğru CA ile bağlantı TLS 1.3 üzerinden kuruldu. Script'in ilk denemesinde iki ders çıktı. --tlsCAFile vermeden başlatılan mongod açılmadı ve güven zinciri belirtmeden TLS kullanımının artık desteklenmediğini söyleyen bir hatayla durdu. --tlsCAFile verilince de sunucu istemcilerden sertifika istemeye başladı; istemci sertifikası olmadan bağlanabilmek için --tlsAllowConnectionsWithoutCertificates gerekti. İstemci sertifikasını zorunlu tutmak x.509 authentication'ın temeli; replica set üyelerinin birbirini doğrulaması için de lab'daki keyfile yerine production'da x.509 sertifikaları öneriliyor. Log'daki bir satır da TLS 1.0 ve 1.1'in otomatik olarak kapatıldığını söylüyor. .NET tarafında connection string'e tls=true eklemek yetiyor; Windows'ta CA'nın sertifika deposunda güvenilir olması gerekiyor.
Queryable Encryption: Şifreli Alanda Sorgu
TLS trafiği, disk şifreleme de diski korur; ama veritabanı yöneticisi, bir backup dosyası ya da sunucuya sızan biri veriyi açık görür. In-use encryption (verinin kullanılırken de şifreli kalması) bu boşluk için: alan istemcide şifrelenir, sunucu yalnız şifreli byte'ları görür. İki yol var: CSFLE (Client-Side Field Level Encryption; istemci tarafında alan düzeyinde şifreleme) ve onun devamı olan Queryable Encryption. Queryable Encryption şifreli alan üzerinde eşitlik ve range sorgularına izin veriyor; 9.0 release notes'una göre prefix, suffix ve substring sorguları da 9.0 ile GA (genel kullanıma açık, production'a hazır) oldu. Uyumluluk tablosu önemli: otomatik şifreleme Enterprise ve Atlas'ta, Community'de yalnız explicit (alanı uygulamanın kendisinin şifrelediği) şifreleme var. 14-qe.js Community'de explicit yolu deniyor:
const localKey = crypto.randomBytes(96); // yalnız lab: gerçek kurulumda master key bir KMS'te durur
const encrypted = Mongo(db.getMongo()._uri, {
keyVaultNamespace: "encryption.__keyVault",
kmsProviders: { local: { key: localKey } },
bypassQueryAnalysis: true
});
const clientEncryption = encrypted.getClientEncryption();
clientEncryption.createEncryptedCollection("bookstore", "customers_secure", {
provider: "local",
createCollectionOptions: { encryptedFields: { fields: [{ path: "phone", bsonType: "string", queries: { queryType: "equality", contention: 0 } }] } }
});
// Şifreli alanda eşitlik sorgusu: sorgudaki değer de şifrelenir
const query = clientEncryption.encrypt(keyId, target, { algorithm: "Indexed", contentionFactor: 0, queryType: "equality" });
const found = secure.findOne({ phone: query });
found through the encrypted client: Linus Torvalds +90 555 017 95 53
raw phone field: Binary subtype 6 | 226 bytes
other fields in the raw document: _id, name, phone, __safeContent__
plain-text search for the phone: 0 documents
Şifreleyen istemci telefon numarasıyla müşteriyi buldu ve sonucu otomatik olarak çözdü. Aynı document'e normal bir bağlantıyla bakan biri 226 byte'lık bir Binary (subtype 6, şifreli veri) ve sorgu için sunucunun tuttuğu __safeContent__ alanını görüyor; açık metinle arama sıfır sonuç veriyor. contention değerini 0 yaptım: ilk denemede varsayılan 8 ile kurulan collection'a 0 ile sorgu gönderince sunucu "max contention 0 ... does not match collection's configured contention 8" hatasını verdi; iki tarafın değeri aynı olmalı. Lab'daki 96 byte'lık yerel anahtar yalnız deneme için; production'da master key bir KMS'te (Key Management System; anahtarları saklayan ve yöneten servis) durur. Anahtarı silmek veriyi geri dönüşsüz okunamaz yapar; KVKK'daki (Kişisel Verilerin Korunması Kanunu) silme taleplerinde müşteri başına anahtar kullanmak bu yüzden bir seçenek. Bir sürüm notu: 9.0 ile mongocryptd deprecated oldu; otomatik şifrelemede yerine ayrı process istemeyen Automatic Encryption Shared Library kullanılıyor. Entity Framework Core provider'ının da Queryable Encryption yapılandırması var; 11. yazının sınırlarına bunu da ekle.
Backup ve Restore Testi
Dokümanın karşılaştırmasında üç self-managed yol var: mongodump/mongorestore (küçük kurulumlar için, mantıksal kopya), filesystem snapshot (journal'ın aynı volume'da olması şartıyla, hızlı ama sharded cluster'da zahmetli) ve Ops Manager/Cloud Manager (point-in-time restore dahil). Atlas'ın kendi backup'ı da point-in-time destekliyor; bunu Atlas hesabı gerektirdiği için çalıştırmadım. Hangisini seçersen seç, restore edilmemiş bir backup bir umut. 14-backup.sh bir dump alıp onu başka bir database adıyla geri yüklüyor ve sayıları karşılaştırıyor:
mongodump "${auth[@]}" --oplog --gzip --archive="$archive" 2>&1 | grep oplog
mongorestore "${auth[@]}" --gzip --archive="$archive" \
--nsInclude 'bookstore.*' --nsFrom 'bookstore.*' --nsTo 'bookstore_restore.*' 2>&1 | tail -1
2026-11-14T14:55:18.906+0000 writing captured oplog to ``
2026-11-14T14:55:18.907+0000 dumped 1 oplog entry
archive size: 40K
2026-11-14T14:55:19.186+0000 759 document(s) restored successfully. 0 document(s) failed to restore.
books documents 77 -> 77 | indexes 3 -> 3
orders documents 379 -> 379 | indexes 4 -> 4
reviews documents 181 -> 181 | indexes 2 -> 2
customers documents 40 -> 40 | indexes 1 -> 1
--oplog dump sürerken gelen yazmaları da yakalıyor (burada 1 kayıt); aynı adlarla geri yüklerken --oplogReplay ile uygulanınca dump tek bir ana göre tutarlı oluyor. --oplog yalnız bütün instance'ın dump'ında çalışıyor, tek database'te çalışmıyor. Restore testi index'leri de saydı; 6. yazının index'leri geri gelmeseydi restore sonrası ilk sorgular COLLSCAN'e düşerdi. Bu testi bir zamanlanmış işe bağla ve süresini ölç: büyüyen bir veritabanında mongorestore saatler sürebilir ve bu süre gerçek RTO'n (Recovery Time Objective; bir arızadan sonra hizmetin geri gelmesi için hedeflenen süre) olur.
İzleme: Profiler, Slow Query Log, $queryStats ve Driver Tracing
// Yalnız bütün collection'ı tarayan işlemleri profile'la
bookstore.setProfilingLevel(1, { filter: { planSummary: "COLLSCAN" } });
profiled: query bookstore.books COLLSCAN docsExamined 77 nreturned 22
slow query log: COLLSCAN docsExamined 77 durationMillis 0
WiredTiger cache: 5 MB used of 3406 MB
$queryStats: count bookstore.customers execCount 1
$queryStats: aggregate local.oplog.rs execCount 1
sample rate: reads 0.01 writes 0.01
Profiler'a süre yerine bir filtre verdim: yalnız bütün collection'ı tarayan işlemler. Script iki sorgu çalıştırdı; 3. yazının $regex araması (77 document okuyup 22 döndüren COLLSCAN) kaydedildi, index'li müşteri sorgusu kaydedilmedi. Aynı filtre slow query log'unu da belirliyor: sorgu 0 ms sürdüğü hâlde log'a düştü. Profiler'ı production'da kısa süreli ve filtreli aç; her işlemi kaydetmek kendi başına yük. WiredTiger cache satırı 1. yazıdaki cache formülünün canlı karşılığı. 9.0'da $queryStats varsayılan olarak açık, ama okuma ve yazmaların yalnız %1'ini örnekliyor; bu yüzden listede bizim iki sorgumuz yok; backup script'inin bir count'u ve sunucunun kendi iç işlerinden biri var, hangilerinin görüneceği her koşuda değişiyor. Sorgu şekillerini bir süre boyunca istatistiksel olarak görmek için iyi, tek bir yavaş sorguyu yakalamak için değil. 8.0'daki farkı aşağıda.
Çalışan bir işlemi görmek için db.currentOp({ appName: "Bookstore.MongoLab", secs_running: { $gte: 5 } }) ve gerekirse db.killOp(opid); 9. yazıda ApplicationName ayarlamanın faydası burada. Uygulama tarafında driver 3.7'den beri OpenTelemetry uyumlu span üretiyor: MongoDB.Driver adlı ActivitySource'a bir listener bağlamak yetiyor, OpenTelemetry SDK'sı şart değil:
// Driver (3.7+) span'leri "MongoDB.Driver" ActivitySource'unda üretir; herhangi bir listener toplayabilir
using ActivityListener listener = new()
{
ShouldListenTo = source => source.Name == MongoTelemetry.ActivitySourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
find 2.2 ms db.system.name=mongodb, db.command.name=find, db.namespace=bookstore, db.query.summary=find bookstore.books, db.collection.name=books, server.address=localhost, network.transport=tcp
find bookstore.books 55.9 ms db.system.name=mongodb, db.operation.name=find, db.operation.summary=find bookstore.books, db.namespace=bookstore, db.collection.name=books
found Dune
İki span var: dıştaki operation (bağlantı kurma ve server selection dahil 56 ms, ilk istek olduğu için) ve içteki command (sunucuya giden find, 2 ms). Sorgu metni varsayılan olarak span'e yazılmıyor; TracingOptions.QueryTextMaxLength ile açılabiliyor, ama müşteri verisinin trace'lere sızmaması için açmadan önce düşün. ASP.NET Core'da OpenTelemetry kuruluyorsa AddSource(MongoTelemetry.ActivitySourceName) satırı MongoDB çağrılarını HTTP isteklerinin trace'ine bağlıyor.
8.0'da Farkı
Script'lerin hepsi 8.0.38'de de çalıştı ve aynı sonucu verdi; üç fark var. readWrite 8.0'da 24 action içeriyor, 9.0'daki 25. action performRawDataOperations. $queryStats 8.0'da varsayılan olarak hiçbir şey toplamıyor: internalQueryStatsRateLimit 0, sorgu sıfır sorgu şekli döndürdü ve yazma komutları için örnekleme parametresi yok; 9.0'da hem okuma hem yazma için örnekleme varsayılan olarak açık. mongocryptd'nin deprecated olması da 9.0'a ait. Aynı kalanlar da not edilmeli: güven zinciri olmadan TLS başlatma hatası 8.0.38'de de aynı metinle geldi, explicit Queryable Encryption ve backup/restore testi 8.0'da da birebir aynı çıktıyı verdi.
Üretimde Dikkat
Kullanıcı ve role değişikliklerini kodla yönet. 14-roles.js gibi script'ler repoda dursun ve deploy'da çalışsın; elle verilmiş bir root, yıllarca kimse fark etmeden kalır. Parolaları bir secret store'da tut ve düzenli değiştir; değişimi uygulamanın yeni parolayı alıp yeniden başlamasıyla aynı deploy'da planla.
İzlemede dört sayı: replication gecikmesi (12. yazı), oplog penceresi (13. yazı), WiredTiger cache doluluğu ve yavaş sorgu sayısı. Bunlar için bir alarm eşiği belirle; profiler ve $queryStats sorunu bulduktan sonra ayrıntıya inmek için.
Sık Düşülen Tuzaklar
Uygulamayı root ile bağlamak. "Önce Tahmin Et" readWrite'ın bile fazla olduğunu gösterdi; root ise bütün database'leri, kullanıcıları ve cluster ayarlarını açar. Uygulama kodundaki tek bir injection açığı bütün sunucuyu kaybettirir.
Backup'ı oplog'suz alıp tutarlı sanmak. Birkaç dakika süren bir mongodump sırasında sipariş ve stok farklı anlarda okunur; --oplog olmadan geri yüklenen veride sipariş var, stok düşmemiş olabilir. Restore'u da hiç denememek aynı derecede tehlikeli.
Küçük Bir Deneme
★☆☆ Raporlama için built-in read role'üyle bookstore_reports adlı bir kullanıcı oluştur. 14-roles.js'teki denemeleri onunla tekrarla; stok düşürme satırı nasıl bir hata veriyor?
★★☆ 14-qe.js'e müşterinin e-posta adresini ikinci şifreli alan olarak ekle ve hem telefonla hem e-postayla sorgula. contention değerini 8'e çıkar; şifreli collection'ın yanında oluşan enxcol_ collection'larının boyutu nasıl değişiyor?
★★★ 14-backup.sh'ı dump sırasında failover benzeri bir yazma yükü altında çalıştır ve aynı adlarla --oplogReplay ile yeni bir container'a geri yükle. Geri yüklenen veride stok ve siparişler tutarlı mı?
Bir sonraki ve son yazıda sürümlere bakıyoruz: MongoDB'nin yeni sürüm modeli, 6.0'dan 9.0'a gelen değişiklikler, C# driver 2.x'ten 3.x'e geçiş ve bir yükseltme kontrol listesi.