Tüm sistemler çalışıyor 6 offshore bölge No-KYC ödeme
Hands-on Saha kılavuzu

VPS'te Docker: sessizce atladığı güvenlik duvarınız

İlk saati doğru yaptınız: isimli bir kullanıcı, yalnızca anahtarla SSH, her iki IP ailesinde de varsayılan reddetme, otomatik güvenlik güncellemeleri. Sonra bir konteyner portu yayımladınız ve bir veritabanını herkese açık internete çıkardınız — çünkü Docker o portu, güvenlik duvarınıza hiç danışılmadan çok önce çalışan bir kuralla teslim eder ve ufw her şeyin reddedildiğini bildirmeye devam eder. Bu rehber tam olarak o boşlukla ilgilidir: neden var olduğu, konteynerlerinizin şu anda gerçekte neyi açığa çıkardığını nasıl okuyacağınız ve yazdığınız güvenlik duvarının, düşündüğünüz şeyi gerçekten ifade etmesini sağlayan birkaç değişiklik.

2026-09-14 tarihinde güncellendi · 15 dk okuma · Filo operasyonları
Bu sayfada
  1. Sizin yazdığınız kural, bir de Docker'ın yazdığı kural
  2. Açığa çıkan bir port dışarıdan nasıl görünür
  3. Gerçekte neyi yayımladığınızı okuyun
  4. Loopback'e bağlama ve ports ile expose arasındaki fark
  5. DOCKER-USER: tam olarak bunun için var olan zincir
  6. Diğer kapı: soket ve root olarak çalışan şey
  7. Sizin yazmadığınız imajlar, kaybetmeyi göze alamayacağınız bir kutuda
  8. Günlükler, volume'ler ve konteynerden uzun ömürlü durum
  9. Adım adım
SP·01

Sizin yazdığınız kural, bir de Docker'ın yazdığı kural

İki program aynı güvenlik duvarını, birbirinden farklı varsayımlarla düzenliyor ve bunlardan yalnızca biri size haber veriyor. ufw kurallarını filter tablosunun INPUT zincirine yazar — bu, bir paketin, hedefi sunucunun kendisi olduğunda izlediği yoldur. Docker ise nat tablosuna ve FORWARD'a yazar — bu da bir paketin, hedefi başka bir yer olduğunda izlediği yoldur.

Tek bir paketi izleyin, boşluk hemen belirginleşsin. Başka bir ülkeden biri, adresinizin 5432 portuna bir bağlantı açar. Paket gelir ve karşılaştığı ilk şey nat PREROUTING'dur; bu da onu Docker'ın DOCKER zincirine gönderir. Orada bir DNAT kuralı hedefi 172.17.0.2:5432'ye yeniden yazar — yani konteynere. Paket artık sunucunuza adreslenmiş değildir, dolayısıyla çekirdek onu yerel olarak teslim etmek yerine yönlendirir: FORWARD'dan geçer ve orada Docker, yayımlanmış bir porta giden trafik için zaten bir ACCEPT kuralı kurmuştur. Bu yolculuk boyunca paket hiçbir noktada, ufw'nin süzdüğü tek zincir olan INPUT'tan geçmez. Kuralınız hiçbir zaman yanlış değildi. Sadece hiç danışılmadı.

Belirtinin bu kadar şaşırtıcı olmasının nedeni de budur. ufw status verbose hâlâ Default: deny (incoming) bildirir, 80, 443 ve SSH portunuz dışında hiçbir şeye izin verilmediğini söyler — bu arada başka bir kıtadaki bir tarayıcı, veritabanınıza açık bir oturum tutmaktadır. İki ifade de doğrudur. Güvenlik duvarı, kendisine verilen trafik üzerinde, tam olarak yapılandırdığınız şeyi yapıyordur; sorun, önemli olan trafiğin ona hiç verilmiyor olmasıdır.

Bunların hiçbiri bir hata değil, Docker'ın dikkatsizliği de değil. Bir konteyner motoru, konteynerler ağa hiç ulaşamayacağı için NAT ve yönlendirme kurallarını programlamak zorundadır ve host güvenlik duvarınızın hangi kurallarının onlara uygulanması gerektiğini güvenle tahmin edemez. O yüzden dürüst olanı yapar: kendi zincirlerini yönetir ve size özel bir zincir bırakır — DOCKER-USER, FORWARD'daki her şeyden önce değerlendirilir — ve bunun üzerine asla yazmayacağına söz verir. Boşluk, bu mekanizmanın var olmasında değildir. Boşluk, -p 5432:5432'nin "bunu erişilebilir yap" gibi okunup aslında "bunu, bir saatinizi harcayarak yazdığınız güvenlik duvarının ötesinde, bu makinenin yanıt verdiği her adrese yayımla" anlamına gelmesidir.

SP·02

Açığa çıkan bir port dışarıdan nasıl görünür

İnternet, insanların beklediğinden daha hızlı fark eder. Barındırma önekleri sürekli ve eksiksizce taranır — size özel bir ilgi duyan biri tarafından değil, yönlendirilebilir her adresi, ilgi çekici her portta tarayıp sonuçları yayımlayan ya da satan ticari tarayıcılar, araştırma projeleri ve fırsatçı botnet'ler tarafından. docker compose up -d ile yeni yayımlanmış bir veritabanı portuna gelen ilk davetsiz bağlantı arasındaki süre saatlerle ölçülür. Kimsenin hostname'inizi tahmin etmesi gerekmedi. Kimsenin adınızı bilmesi gerekmedi. Adres zaten taranan aralıktaydı.

Ne bulacakları tamamen ne yayımladığınıza bağlıdır ve sık görülen durumlar iç karartıcıdır. Bir imajın hızlı başlangıç örneğinden kaldırılmış, o örnekteki önemsiz parola hâlâ yerinde duran bir PostgreSQL ya da MySQL konteyneri. Kimlik doğrulama isteyecek şekilde hiç yapılandırılmamış bir Elasticsearch ya da MongoDB örneği — çünkü yalnızca uygulama konteynerinden erişilecekti. Herkese yanıt veren bir memcached — bu yalnızca bir veri sızıntısı değil, aynı zamanda başka birine yöneltilebilecek bir yükselticidir ve sunucunuzu, DDoS ilk saat çalışma kılavuzumuzda anlatılan türden bir selin katılımcısına dönüştürür. Özel bir ağ varsayan bir yönetici paneli ya da mesaj kuyruğu paneli. Çalıştırdığınız her iç hizmeti, hostname'i ve sürümü sessizce listeleyen bir metrik uç noktası.

İkincil hasarı da adlandırmakta fayda var, çünkü birincil hasar faturanın tamamı değildir. İnternetten erişilebilen bir veritabanı yalnızca okunabilir değildir — çoğu motorda aynı zamanda yazılabilirdir de, bu da saldırganın kalıcılık kurmak için ek bir açığa ihtiyaç duymadığı anlamına gelir; üstelik birkaç motor, ayrıcalıklı bir oturumdan dosya yazmaya ya da host üzerinde komut çalıştırmaya ikna edilebilir. Tam olarak hiçbir kimlik taşımadığı için seçtiğiniz bir kutuda, kimlik doğrulaması olmayan bir giriş noktası, o kutunun dokunduğu her şeye geri giden bir bağlantıdır da: erişebildiği yedekleme hedefi, ortam değişkenlerindeki anahtarlar, aynı bridge'teki diğer konteynerler.

Rahatsız edici olan kısım, hiçbirinin kendini haber vermemesidir. "Güvenlik duvarınız atlatıldı" diyen bir günlük satırı yoktur. Hizmet çalışır, uygulama bağlanır, site ayaktadır ve tek dışsal belirti, kimsenin izlemediği bir bağlantı sayısıdır. Açık, ya siz onu kasıtlı olarak önümüzdeki on dakika içinde keşfedersiniz — ya da başka biri, kendi zamanlamasında.

SP·03

Gerçekte neyi yayımladığınızı okuyun

Motorun ne yaptığını sandığıyla başlayın. docker ps bir PORTS sütunu yazdırır ve bu rehberin konusunun tamamı o sütundaki ayrımdır: 0.0.0.0:5432->5432/tcp makinedeki her adres anlamına gelir, 127.0.0.1:5432->5432/tcp yalnızca loopback anlamına gelir; ok işareti olmayan çıplak bir 5432/tcp ise portun yalnızca diğer konteynerlere açık olduğunu ve hiçbir yere yayımlanmadığını gösterir. Bir şeyi değiştirmeden önce o sütunun her satırını, her konteynerde okuyun.

Sonra soketlere ss -tulpen ile bakın. Varsayılan bir kurulumda, yayımlanmış portları elinde tutan docker-proxy'yi görürsünüz, çünkü Docker hâlâ yayımlanan her port için küçük bir userland süreci başlatır. İnsanlara bir öğleden sonrasına mal olan tuzak burada: daemon'ınızda userland-proxy devre dışıysa — bu yaygın bir ince ayar değişikliğidir ve bazı dağıtımlar onu böyle gönderir — host üzerinde dinleyen hiçbir soket yoktur. ss hiçbir şey göstermez, lsof hiçbir şey göstermez ve port yine de sonuna kadar açıktır, çünkü çekirdeğin DNAT kuralı, adresi tutması gereken hiçbir sürece ihtiyaç duymadan işi görür. Sessiz bir ss çıktısı, kapalı bir portun kanıtı değildir.

O yüzden kuralların kendisini okuyun. iptables -t nat -S DOCKER, yayımlanan her port için bir DNAT satırı listeler ve her satır aradığınız cevabı taşır: -d 127.0.0.1/32 içeren bir kural loopback yayınıdır, hedef kısıtlaması olmayan bir kural ise makinenin sahip olduğu her adrese uygulanır. Aynısını ip6tables ile de yapın, çünkü iki aile birbirinden bağımsız yapılandırılır ve bir kutu birinde sıkı, diğerinde açık olabilir.

Son olarak — ve gerçekten bir şey kanıtlayan tek adım budur — makineye başka bir yerden bakın. Yukarıdaki her komut host üzerinde çalışır ve host'un kendi ağ görüşünü miras alır. Loopback trafiği önemli olan zincirleri atlar, dolayısıyla curl 127.0.0.1:5432'nin başarılı olması, bir yabancının aynısını yapıp yapamayacağı konusunda size hiçbir şey söylemez; başarısız olması ise daha da az şey söyler. Yetkili test, başka bir ağdaki başka bir makineden, her iki IP ailesinde yapılan bir taramadır. Ondan önceki her şey yalnızca bir varsayımdır.

SP·04

Loopback'e bağlama ve ports ile expose arasındaki fark

En küçük işe yarar düzeltme on bir karakterdir. -p 127.0.0.1:5432:5432, Docker'a DNAT kuralını bir hedef kısıtlamasıyla yazmasını söyler, böylece yeniden yazma yalnızca zaten yerel olan trafiğe uygulanır. Genel adresinizi hedefleyen uzak bir paket artık eşleşmez, konteynere yönlendirilmez ve nihayet her zaman varsaydığınız yere ulaşır: INPUT, yani ufw'nin onu reddettiği yer. Bir Compose dosyasında aynı şey ports: ["127.0.0.1:5432:5432"] olur ve tırnaklar önemlidir — iki nokta üst üste içeren tırnaksız bir değer, olmayı bekleyen bir ayrıştırma kazasıdır.

Ama daha iyi soru şudur: port neden yayımlanıyor ki? Aynı kullanıcı tanımlı ağa bağlı konteynerler, hiçbir yayımlama olmadan, hizmet adıyla ve konteynerin kendi portu üzerinden birbirine doğrudan ulaşır. Uygulamanız 127.0.0.1:5432'ye bağlanmaz; Docker'ın gömülü DNS'i tarafından özel bridge üzerindeki bir adrese çözülen postgres:5432'ye bağlanır. Bu düzenlemedeki bir veritabanının hiçbir ports: satırına ihtiyacı yoktur — ne loopback olanına, ne başka birine. En güvenli yayımlanmış port, sildiğiniz porttur. ports:'u yalnızca gerçekten herkese açık olan bir iki hizmet için saklayın, gerisinin özel ağda konuşmasına izin verin.

expose:'un yanlış anlaşıldığı yer de burasıdır. Hiçbir şeyi yayımlamaz, hiçbir şeyi açmaz; yalnızca bir hizmetin hangi portu dinlediğini kaydeden belgelemedir ve güvenlik duvarı üzerinde hiçbir yönde etkisi yoktur. İnsanlar onu, ports:'un güvenli sürümü olduğunu umarak eklerler — ki öyledir de, tıpkı bir yorum satırının kodun güvenli sürümü olması anlamında. Bir hizmetin yalnızca komşuları tarafından erişilebilir olmasını istiyorsanız expose:'a ihtiyacınız yoktur; ports:'un yokluğuna ihtiyacınız vardır.

Loopback'e bağlamanın iki dürüst sınırı var. Birincisi, komşuluğu değil host sınırını korur: aynı bridge ağındaki konteynerler birbirine yine de serbestçe ulaşabilir, dolayısıyla ele geçirilmiş bir ön uç konteynerinin, hiçbir şey yayımlamayan bir veritabanına açık bir yolu vardır. Etki alanı önemliyse hizmetleri ayrı ağlara bölün ve arka uç olanı internal: true olarak işaretleyin. İkincisi, 127.0.0.1 bir IPv4 adresidir ve yalnızca IPv4'ü kısıtlar; host'un yönlendirilebilir bir /64'ü varsa — buradaki her plan bir tane sağlar — v6'yı ayrı düşünün ve ayrı test edin.

SP·05

DOCKER-USER: tam olarak bunun için var olan zincir

Loopback'e bağlama, hatırladığınız konteynerleri düzeltir. DOCKER-USER ise sizi, bir sonraki olaydan tek bir -p uzakta olmaktan çıkarır. Docker onu FORWARD'daki ilk atlama olarak, kendi kabul kurallarından önce kurar ve — zincirlerindeki her şeyin aksine — yeniden başlatmalar, yükseltmeler ve yeni konteynerler boyunca içeriğine dokunmaz. Motorun çıkarım yapamayacağı politika için desteklenen yer burasıdır: hangi kaynakların bu host üzerindeki konteynerlere ulaşmasına izin verildiği.

Desen, genel arayüz üzerinde üç kuraldan oluşur ve her şey sıralamadadır. Birincisi, established ve related trafiği geri döndürün, böylece konteynerlerinizin açtığı bağlantılara gelen yanıtlar akmaya devam etsin. İkincisi, gerçekten içeri almak istediğiniz kaynakları geri döndürün — bir ofis adresi, bir izleme sunucusu, bir eş sunucu. Üçüncüsü, internetten gelen geri kalan her şeyi düşürün. Burada klasik hata, conntrack kuralını yanlış yapmaktır: DROP'u en başa koyarsanız, her konteynerden giden her bağlantı dönüş paketinde ölür — bu da "Docker DNS'i ve paket kurulumlarını bozdu" gibi görünür ve insanları tamamen yanlış yere baktırır.

Bunun gerçekle karşılaştıktan sonra ayakta kalıp kalmayacağını belirleyen iki işlevsel ayrıntı vardır. Genel arayüze isimle sınırlanmalıdır — -i eth0, ya da kutunuzda ip route get 1.1.1.1'in bildirdiği her neyse — yoksa kendi bridge'leriniz arasındaki trafiği de düşürürsünüz. Ayrıca her yeniden başlatmadan sonra, Docker daemon'ı zinciri oluşturduktan sonra, yeniden uygulanmalıdır. oneshot tipinde, After=docker.service ile sıralanmış bir systemd birimi güvenilir biçimdir; netfilter-persistent ile kaydetmek de işe yarar — yeter ki Docker'ın aynı anda yeniden kurduğu bütün bir kural kümesini geri yüklemenin, varsayılacak değil doğrulanacak bir yarış olduğunu kabul edin.

Sonra IPv6 yarısı. ip6tables -S DOCKER-USER bir zincir yazdırıyorsa, her kuralı ona da yansıtın. Hata veriyorsa, daemon'ınız v6 kurallarını hiç yönetmiyordur — bu da v6 trafiğinin konteynerlerinize ulaşıp ulaşmadığı hakkında hiçbir şey söylemez, yalnızca Docker'ın onu süzmediğini söyler. Orada mantık yürüterek bir cevaba varmaya çalışmayın; yollar sürüme, daemon ayarlarına ve dağıtıma göre değişir ve kendinden emin yanlış bir sonuç, hiç sonuç olmamasından beterdir. nmap -6 ile kendinizi tarayın ve sonuca inanın.

Yapılmaması gereken bir şey: "iptables": false'u /etc/docker/daemon.json içine yazmaya başvurmak. Bu, Docker'ın güvenlik duvarına dokunmasını durdurur, ama aynı zamanda konteyner NAT'ının, giden masquerading'in ve ağlar arası izolasyonun hiçbir şey tarafından yapılandırılmamasına da yol açar. Sorunu ortadan kaldırmış olmazsınız, işi devralmış olursunuz — elle, her konteyner için, sonsuza dek. Tek bir VPS'te dürüst tercih, Docker'ın kendi zincirlerini yönetmesine izin vermek ve DOCKER-USER'ın sahibi olmaktır.

SP·06

Diğer kapı: soket ve root olarak çalışan şey

Yukarıdaki her şey gelen paketlerle ilgiliydi. Bu bölüm onların ne bulduğuyla ilgili ve geri kalanından o kadar kötü tek bir madde var ki, tek başına söylenmeyi hak ediyor: /var/run/docker.sock'u bir konteynere bağlamak, o konteynere host üzerinde root vermekle eşdeğerdir. "Yakın" değil. Eşdeğer. O sokete konuşabilen her şey, host'un dosya sistemi içine bağlanmış yeni, ayrıcalıklı bir konteyner başlatabilir ve oradan her anahtarı okuyabilir, her dosyayı yazabilir ve istediği her şeyi kurabilir. Pek çok kullanışlı imaj bunu ister — panolar, otomatik güncelleyiciler, hizmet keşifli reverse proxy'ler. Bu isteği, imaja kendi root kabuğunuza duyduğunuz kadar güvenme kararı olarak ele alın ve TCP üzerinden Docker API'siyle de aynı özeni gösterin: 2375 portundaki kimlik doğrulamasız bir daemon, tüm internete açılmış aynı kapıdır.

Bunun ötesinde, konteyner çalışma zamanı size hiçbir şeyi yeniden tasarlamayı gerektirmeyen dört ucuz azaltma sunar. user: "1000:1000" ile root olmayan bir kullanıcı olarak çalıştırın, çünkü varsayılan, ad alanı içinde root'tur ve bu her kaçışın başlangıç noktasıdır. security_opt: ["no-new-privileges:true"] ayarlayın; bu, imaj içindeki bir setuid ikili dosyasının, sürecin başladığından daha fazla yetki kazanmasını engeller. cap_drop: [ALL] ile bütün capability'leri düşürün ve yalnızca hizmetin kanıtlanabilir şekilde ihtiyaç duyduklarını geri ekleyin — çoğu web uygulamasının hiçbirine ihtiyacı yoktur. Kök dosya sistemini read_only: true ile bağlayın ve taslak çalışmalar için küçük bir tmpfs verin; bu da "uygulama dizinine bir web shell bırakmayı" bir adımdan çıkmaz bir sokağa dönüştürür.

Aynı mantık, konteynere teslim edilen şeyler için de geçerlidir. Ortam değişkeni olarak geçirilen sırlar, süreç ortamını okuyabilen her şeye görünürdür ve docker inspect çıktısına ve yapılandırmayı döken her günlük ya da çökme raporuna sadakatle kopyalanır; bilinen bir yolda salt okunur bağlanmış bir dosya daha az kullanışlı ama çok daha az sızdırandır. Ve bir konteynerin işinden fazla ağa ihtiyacı yoktur: yalnızca veritabanıyla konuşan bir worker'ın internete bağlantı açabilmesinin hiçbir anlamı yoktur.

Bütün bunlar varsayılanla boğuşmak gibi hissettiriyorsa, bu adil bir okumadır ve rootless Docker ya da Podman için asıl gerekçe de budur — burada daemon ve konteynerler ayrıcalıksız bir kullanıcı olarak çalışır, bir konteyner kaçışı sizi root yerine o kullanıcı olarak bırakır ve yayımlanmış portlar host üzerinde sıradan bir userland süreci tarafından tutulur; bu da ufw kurallarınızın onlara her zamanki gibi uygulandığı anlamına gelir. Bedeli gerçektir: bazı capability'ler, bazı depolama sürücüleri ve bazı ağ numaraları farklı davranır ya da hiç çalışmaz. Bir olaydan sonra varsayılanın da bir tercih olduğunu keşfetmek yerine, bu seçeneğin var olduğunu ve neye mal olduğunu önceden bilmekte fayda vardır.

SP·07

Sizin yazmadığınız imajlar, kaybetmeyi göze alamayacağınız bir kutuda

Bir konteyner imajı, sizin okumadığınız katmanlardan bir araya getirilmiş, makinenizde çalışan başkasının dosya sistemidir. Bu, onları kullanmaya karşı bir gerekçe değildir — hangilerini çalıştırdığınızı ve ne kadar eski olduklarını bilmeniz gerektiğinin gerekçesidir. Küçük altyapılarda gerçekten yaşanan iki başarısızlık da sıradandır: imaj hiçbir zaman güvenilir değildi, ya da martta güvenilirdi ve o zamandan beri kimse onu yeniden derlemedi.

Birincisi, çoğunlukla imajların nereden geldiği konusundaki disiplinle çözülür. Üç yıldızlı kullanışlı bir fork yerine resmi ya da satıcı tarafından yayımlanan depoları tercih edin ve çekiciliği altı hizmeti tek bir YAML satırında paketlemek olan imajlardan özellikle şüphelenin. Önemli olduğu yerde, etikete değil digest'e sabitleyin: postgres:17, iki docker compose pull çalıştırması arasında ayağınızın altında değişebilen hareketli bir hedeftir; postgres@sha256:… ise test ettiğiniz tam dosya sistemidir. Sabitlemek, yeniden üretilebilirlik karşılığında otomatik düzeltmelerden vazgeçmektir — bu, yeniden derleme alışkanlığınız varsa doğru takas, yoksa yanlış takastır.

İkincisi ise sessizce ısıran türdendir. Unattended upgrades konteynerlerinize dokunmaz. İlk saatte yapılandırdığınız otomatik güvenlik güncellemeleri host'un paketlerini yamalar ve bir imajın içindeki userland'e hiçbir görünürlüğü yoktur — dolayısıyla kendini tamamen yamalı olarak bildiren bir kutu, içinde bir yıllık düzeltilmemiş açığı olan bir temel imajdan web sunucusu çalıştırıyor olabilir. Konteynerler güncellenmez, yerine yenisi konur: çekin, yeniden oluşturun ve artık referans verilmeyeni kaldırın — gerçekten sürdürdüğünüz bir sıklıkta. Ayda bir, yazılı hâlde, kusursuz bir niyetten daha iyidir.

Bu da Compose dosyasını sunucudaki en değerli nesne yapar. O makinenin ne olduğunun tek eksiksiz tarifidir ve ondan yeniden kurmak bir arkeoloji projesi değil, rutin bir işlem olmalıdır — sürüm kontrolünde tutun, ortam dosyalarını yanında tutun ve ikisini de verilerinizi barındıran şifrelenmiş, sunucu dışı yedekte saklayın. Bir konteyner sunucusunun sınavı, çalışıyor olması değildir. Onu, yeni bir VPS üzerinde, aynı şekilde ve yaklaşık 15 min içinde yeniden ayağa kaldırabilip kaldıramayacağınızdır.

SP·08

Günlükler, volume'ler ve konteynerden uzun ömürlü durum

Konteynerler tek kullanımlık diye pazarlanır; bu, süreç için doğrudur ama arkasında bıraktığı her şey için yanlıştır. Bir Docker host'unda iki tür durum birikir ve ikisi de insanları en kötü anda şaşırtır.

Birincisi günlüklerdir. Varsayılan json-file sürücüsü, konteynerlerinizin stdout ve stderr'e yazdığı her satırı yakalar ve — siz aksini söylemedikçe — onları hiçbir zaman döndürmez. Konuşkan bir uygulama, bir diski bu şekilde haftalar içinde doldurabilir ve bir veritabanı host'unda dolu bir disk, başlı başına bir olaydır. Bir tavanı, /etc/docker/daemon.json içinde bir kez ayarlayın, böylece bundan sonra başlattığınız her şeye uygulanır.

Aynı ayarın bir de gizlilik okuması var ve gizlilik için seçtiğiniz bir sunucuda muhtemelen daha önemli olan da odur. Döndürülmeyen bir erişim günlüğü, her ziyaretçinin IP adresinin, user agent'ının ve istek yolunun, fiziksel olarak kontrol etmediğiniz bir diskte duran, kalıcı ve indekslenmemiş bir kaydıdır. Bunu tutmaya siz karar vermediniz. Varsayılan sizin adınıza karar verdi. Kasıtlı karar vermek, saklama süresini sınırlamak — daha da iyisi, en baştan neyin yazıldığını kırpmak demektir: proxy'de istemci adreslerini atlayan ya da kısaltan bir günlük biçimi, operasyonel değeri korurken yükümlülüğü düşürür. Hiç yazmadığınız veri el konulamaz, celp edilemez ya da sızdırılamaz — insanları en baştan adlarını sunucudan uzak tutmaya yönelten mantık da tam olarak budur.

İkincisi volume'lerdir ve keskin kenar bir bayraktır. docker compose down konteynerleri durdurup kaldırır ve isimli volume'lere dokunmaz; docker compose down -v ise onları da aynı güvenle, kalıcı olarak siler. Veritabanları o volume'lerde yaşar. Özleyeceğiniz başka her şey de öyle. Verilerinizi anonim volume'ler yerine isimli volume'lerde tutun, sabahın ikisinde hangi komutu yazdığınızı bilin ve docker system prune'un yer açmak için var olduğunu ve sizinkini de açmaktan mutluluk duyacağını unutmayın.

Geriye, konteyner soyutlamasının son bir kez daha yanılttığı yer olan yedeğin kendisi kalıyor. Canlı bir veritabanının volume dizinini kopyalamak yedek değildir; siz onları okurken yazılmakta olan dosyaların bir kopyasıdır ve tam da en çok ihtiyacınız olduğu anda bozuk bir veritabanı olarak geri yüklenir. Motor üzerinden yedek alın — pg_dump, mysqldump, motorun kendi anlık görüntü API'si — sonra sonucu şifreleyin ve sunucunuzun ulaşamayacağı bir yere gönderin. Son kısım, saldırgan zaten konteynerin içindeyken önem kazanan kısımdır: ele geçirilmiş host'unuzun silebileceği bir yedek de yedek sayılmaz.

SP·09

Adım adım

  1. 01

    Kutunun şu anda neyi yayımladığının envanterini çıkarın

    Bir şeyi değiştirmeden önce mevcut durumu not edin — sonradan onunla karşılaştırmak isteyeceksiniz. Her konteyner için PORTS sütununu okuyun, sonra Docker'ın gerçekte kurduğu NAT kurallarını, her iki IP ailesinde de okuyun. 127.0.0.1 hedefi olmadan yazdırılan her şey, ufw status ne derse desin, internetten erişilebilirdir.

    docker ps --format 'table {{.Names}}\t{{.Ports}}'
    ss -tulpen | grep -E 'docker|LISTEN'
    sudo iptables  -t nat -S DOCKER      # -d 127.0.0.1/32 = loopback only
    sudo ip6tables -t nat -S DOCKER      # may not exist; that is an answer too
    sudo ufw status verbose              # what you believed was true

    Buradayken genel arayüz adını da not edin — güvenlik duvarı kuralları ilerde buna ihtiyaç duyacak ve bu, her imajda eth0 değildir.

    ip route get 1.1.1.1 | awk '{print $5; exit}'
  2. 02

    Kendinizi başka bir yerden tarayın

    Host kendi kendini denetleyemez: loopback trafiği, test etmeye çalıştığınız zincirlerden asla geçmez. Bunu başka bir ağdaki başka bir makineden çalıştırın — başka bir bölgede $8.00/aydan başlayan ikinci bir VPS, dürüst bir test tezgâhıdır ve sonrasında da işe yaramaya devam eder. Her iki aileyi de tarayın, çünkü ayrı ayrı yapılandırılırlar.

    # from ANOTHER machine, against your server
    nmap -Pn -sS -p- --min-rate 1000 203.0.113.10
    nmap -Pn -6 -p 22,80,443,3306,5432,6379,9200,27017 2001:db8::1
    # no nmap? one port at a time is enough to settle an argument
    nc -zv 203.0.113.10 5432

    80, 443 ya da SSH portunuz dışında açık olan her port bir bulgudur. Çıktıyı saklayın; bu, sonunda isteyeceğiniz kanıtın "önce" yarısıdır.

  3. 03

    Herkese açık olması gerekmeyeni yayımlamayı durdurun

    Şimdi bunu kaynağında, Compose dosyasında düzeltin. Arka uç hizmetleri özel bir ağa taşınır ve ports: satırlarını tamamen kaybeder — uygulama onlara hizmet adıyla ulaşır. Host'un kendisinden erişilebilir olması gereken her şey, çıplak bir port yerine açık bir loopback bağı alır. Arka uç ağını internal olarak işaretleyin, böylece üzerindeki hiçbir şey davetsiz biçimde internetle konuşamaz.

    services:
      db:
        image: postgres:17
        # ports: ["5432:5432"]   # deleted: the app reaches it as db:5432
        networks: [back]
        volumes: [dbdata:/var/lib/postgresql/data]
    
      app:
        image: myapp:1.4
        environment:
          DATABASE_URL: postgres://app@db:5432/app
        ports: ["127.0.0.1:8080:8080"]   # loopback only; proxy sits in front
        networks: [back, front]
    
    networks:
      front: {}
      back:
        internal: true
    
    volumes:
      dbdata: {}

    Yığını yeniden oluşturun ve NAT kurallarının şeklinin değiştiğini doğrulayın — uygulamanın DNAT satırı artık bir loopback hedefi taşımalı, veritabanının ise hiç satırı olmamalıdır.

    docker compose up -d
    docker ps --format 'table {{.Names}}\t{{.Ports}}'
    sudo iptables -t nat -S DOCKER
  4. 04

    Herkesin önüne tam olarak tek bir şey koyun

    Her şey loopback'teyken, tek bir reverse proxy tek genel yüzey hâline gelir — TLS'in sonlandırıldığı tek yer, bir istemci adresinin görüldüğü tek yer ve günlük biçiminin karara bağlandığı tek yer. Onu host üzerinde ya da yalnızca 80 ve 443'ü yayımlayan bir konteynerde çalıştırın; bir konteynerde çalışıyorsa, front ağına katılır ve loopback'e değil hizmet adlarına proxy yapar.

    # in the http{} block of /etc/nginx/nginx.conf — no client address recorded
    log_format privacy '- - [$time_local] "$request" $status $body_bytes_sent';
    
    # /etc/nginx/conf.d/app.conf  (proxy on the host)
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name example.com;
    
        access_log /var/log/nginx/app.log privacy;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

    Bu çalışmanın amacı kimsenin makinenin nerede olduğunu öğrenmemesiyse, proxy tamamen başka bir kutuda olmalıdır — bu, kaynak IP'nizi bir reverse proxy arkasında gizleyin rehberindeki kurulumdur ve buradaki her şeyle birlikte çalışır.

  5. 05

    Boşluğu DOCKER-USER ile, her iki ailede de kapatın

    Loopback bağları bugünkü konteynerleri düzeltir; bu ise henüz yazmadıklarınızı düzeltir. Genel arayüzde üç kural: konteyner çıkışının çalışmaya devam etmesi için önce conntrack, sonra izin verdiğiniz kaynaklar, sonra bir drop. Uyguladıktan hemen sonra bir konteynerin giden bağlantısını test edin — insanların tersine çevirdiği kural tam olarak budur.

    PUB=$(ip route get 1.1.1.1 | awk '{print $5; exit}')
    
    sudo iptables -I DOCKER-USER 1 -i "$PUB" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
    sudo iptables -I DOCKER-USER 2 -i "$PUB" -s 203.0.113.7/32 -j RETURN   # your admin address
    sudo iptables -I DOCKER-USER 3 -i "$PUB" -j DROP
    
    sudo iptables -S DOCKER-USER
    docker run --rm alpine sh -c 'apk update' >/dev/null && echo 'egress OK'

    Daemon v6 kurallarını yönetiyorsa bunu IPv6 için de yansıtın, sonra Docker zincirlerini yeniden kurduktan sonra çalışan bir birimle bunun yeniden başlatmadan sağ çıkmasını sağlayın.

    sudo ip6tables -S DOCKER-USER || echo 'no v6 chain: verify v6 reachability by scanning'
    
    # /etc/systemd/system/docker-user-rules.service
    [Unit]
    After=docker.service
    Requires=docker.service
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/sbin/docker-user-rules.sh
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
  6. 06

    Konteynerlerin içindeki ayrıcalığı düşürün ve günlükleri sınırlayın

    Ele geçirilmiş bir imajın yapabileceklerini azaltın ve host'un, kimsenin tutmayı seçmediği bir kayıtla dolmasını durdurun. Dört konteyner ayarının normal bir web hizmetine hiçbir maliyeti yoktur; günlük tavanı, daemon yeniden yüklendikten sonra başlatılan her konteynere uygulanır.

    services:
      app:
        image: myapp:1.4
        user: "1000:1000"
        read_only: true
        tmpfs: [/tmp]
        cap_drop: [ALL]
        security_opt: ["no-new-privileges:true"]
        # never: volumes: ["/var/run/docker.sock:/var/run/docker.sock"]
    # /etc/docker/daemon.json
    {
      "log-driver": "json-file",
      "log-opts": { "max-size": "10m", "max-file": "3" }
    }
    sudo systemctl reload docker
    docker inspect --format '{{.HostConfig.LogConfig}}' app
  7. 07

    Dışarıdan doğrulayın, sonra çalışma kılavuzunu yazın

    Hiçbir şey sonuca bağlı değilken, kasıtlı olarak yeniden başlatın ve kurduğunuz durumun geri gelen durum olduğunu kanıtlayın. Sonra ikinci adımdaki dış taramayı tekrarlayın ve sakladığınız çıktıyla karşılaştırın — teslim edilecek olan bu fark, komutlar değil.

    sudo reboot
    
    # after it returns:
    sudo iptables -S DOCKER-USER          # rules reapplied?
    docker ps --format 'table {{.Names}}\t{{.Ports}}'
    
    # from ANOTHER machine again:
    nmap -Pn -p- 203.0.113.10
    nmap -Pn -6 -p- 2001:db8::1

    Bulacağınız bir yere beş satır yazın: kurallar hangi arayüze bağlı, birim nerede duruyor, hangi hizmetler kasıtlı olarak herkese açık, imajları en son ne zaman yeniden derlediniz ve volume'ler nasıl geri yüklenir. Bir konteyner host'u, en kötü günden sonra onu yeniden kurmanızı sağlayan tarif kadar iyidir.

SP·10 — SSS

Hızlı yanıtlar

ufw, bir Docker host'unda hiçbir şey yapıyor mu?

Evet, ve onu tutmalısınız. Hâlâ sunucunun kendisine adreslenmiş her şeyi yönetir — SSH, Docker dışında çalışan bir proxy, paket yöneticisiyle kurduğunuz herhangi bir daemon — ve bu, küçük bir sunucuya saldıran şeylerin büyük bir kısmıdır. Yönetmediği şey, Docker'ın bir konteynere DNAT yaptığı trafiktir, çünkü o trafik yerel olarak teslim edilmek yerine yönlendirilir ve ufw'nin süzdüğü zincire hiç ulaşmaz. İki araç, iki yol: host için ufw, konteynerler için DOCKER-USER.

127.0.0.1'e bağlamak tek başına yeterli mi?

O port, o aile, o konteyner için yeterlidir — ki bir strateji olarak zayıflığı tam olarak budur. Gelecekteki her -p'nin, her kişi ve her kopyala-yapıştır parçası tarafından doğru yazılmasına bağlıdır ve yalnızca IPv4'ü kısıtlar. Konteynerler arasında da hiçbir işe yaramaz: aynı bridge ağındaki her şey, loopback'e bağlanmış bir komşuya yine de ulaşır. Bunu varsayılan alışkanlık olarak kullanın, arkasına kimsenin hatırlamasına bağlı olmayan politika olarak DOCKER-USER'ı koyun ve etki alanı önemli olduğunda ağları bölün.

Sadece "iptables": false ayarlayıp güvenlik duvarını kendim mi yönetsem?

Yapabilirsiniz ve tek bir VPS'te neredeyse kesinlikle yapmamalısınız. O anahtar size temiz bir sayfa vermez — Docker'ın, konteyner ağını çalıştıran NAT, masquerading ve izolasyon kurallarını yapılandırmasını durdurur, dolayısıyla oluşturduğunuz her konteyner ve her ağ için bu işi elle devralırsınız. Sonuç genellikle ya bozuk bir çıkış ya da çalışanla sessizce uyumunu yitiren bir kural kümesidir. Zincirlerinin sahibi olmayı daemon'a bırakın, siz de kasıtlı olarak size bıraktığı ek yeri, DOCKER-USER'ı sahiplenin.

Rootless Docker ya da Podman bu sorunu ortadan kaldırır mı?

Büyük ölçüde, ve iki ayrı nedenden ötürü. Yayımlanmış portlar, ayrıcalıksız bir kullanıcıya ait sıradan bir userland süreci tarafından tutulur, dolayısıyla host'a giden trafik olarak gelirler ve ufw kurallarınız her zaman beklediğiniz gibi uygulanır. Bir konteyner kaçışı da saldırganı root yerine o ayrıcalıksız kullanıcı olarak bırakır. Bedeli; bazı capability'lerin, depolama sürücülerinin ve ağ düzenlemelerinin farklı davranması ya da hiç kullanılamamasıdır — üstelik 1024 altındaki portlar ek yapılandırma ister. Yeni bir kutuya başlıyorsanız ve iş yükleriniz sıradan web hizmetleriyse, bu ciddi bir seçenektir. Çalışan bir sistemi denetliyorsanız, önce zinciri düzeltin, geçişi ayrı olarak değerlendirin.

VPS'im sizin ağ filtrelemenizin arkasında duruyor. Bu yeterli değil mi?

Hayır, ve bu ayrımın üzerinde titizlikle durmaya değer. Filodaki her makine, hacimsel saldırıları soğurmak için var olan, 1.5 Tbps'a kadar kesintisiz bir azaltmanın arkasında durur; bu katman trafiğin şeklini okur, niyetini değil — yayımladığınız veritabanı portunun herkese açık olması gerekmediğini bilemez. Açığa çıkmış bir PostgreSQL'e tek, kurallara uygun bir bağlantı, o katmanda meşru trafikten ayırt edilemez, çünkü ağ açısından bakıldığında o zaten meşru trafiktir: portu siz yayımladınız. Üst akıştaki temizleme sunucunuzu erişilebilir tutar. Onun üzerinde neyin erişilebilir olduğu sizin yapılandırmanıza bağlı kalır.

Artık gerçekten kapalı olduğunu nasıl bilirim?

Tek bir test sayılır: sizin olmayan bir makineden, sizin olmayan bir ağdan, her iki IP ailesinde, bir yeniden başlatmanın ardından bağlanmak. Yerel kontroller sistematik olarak iyimserdir — loopback trafiği söz konusu zincirleri atlar ve userland proxy devre dışıyken ve çekirdek dinleyen bir soket olmadan yönlendirmeyi yaparken sessiz bir ss çıktısı hiçbir anlama gelmez. Beklediğiniz portlar yerine tüm port aralığını tarayın, çıktıyı çalışma kılavuzunun yanında saklayın ve bir konteyner ekleyen her değişiklikten sonra tekrarlayın.

Pratiğe dökün

VPS 15 min içinde çevrimiçi, özel sunucu 2–12 h içinde teslim edilir. $30.00'den kripto ile yükleyin — kimlik bağlı değil.

VPS dağıt