Skip to content

Bölüm 5 / 13

Ağ araçları

Port, curl, ss, ssh; nginx ile ters vekil.

21 dakikalık okuma

"Uygulamaya erişemiyorum" cümlesi, bir geliştiricinin duyacağı en belirsiz hata bildirimidir: sorun uygulamada, portta, güvenlik duvarında, DNS'te, ters vekilde ya da sertifikada olabilir. Bu ders o belirsizliği sıralı bir teşhis merdivenine çevirir ve merdivenin her basamağının aracını verir: ss, ip, dig, curl, nc, ufw/firewalld. İkinci yarıda üretim düzeni kurulur: SSH ile güvenli erişim (16-04'ün SSH senedi burada tamamlanır), rsync ile dosya taşıma, nginx ters vekil ve ASP.NET Core'un ForwardedHeaders ayarı (13-02 ve 14-06 senetleri ödenir), TLS sonlandırma ve Let's Encrypt. Dersin sonunda bir Linux sunucusunda ayakta duran, HTTPS konuşan bir uygulama düzenini uçtan uca anlatabilir olursun.

01Ne öğreneceksin

  • IP, port, soket; ss -tulpn ile dinleyen servisleri okumak; ip a / ip r
  • Ad çözümleme sırası: /etc/hosts ↔ DNS; dig, getent ve "uygulama farklı çözüyor" tuzağı
  • curl derinlemesine: -i -v -H -d -X --resolve, çıkış kodları; nc, ping/traceroute'un sınırları
  • Güvenlik duvarı (ufw/firewalld) ve "port açık ≠ servis dinliyor" ayrımı; altı basamaklı teşhis merdiveni
  • SSH: anahtar üretimi, ~/.ssh/config, ssh-agent, parola girişini kapatmak, -L/-J ile tünel
  • scp/rsync ile dosya taşıma ve yayın
  • nginx ters vekil: proxy_pass, X-Forwarded-* ve ASP.NET Core ForwardedHeaders; TLS sonlandırma, certbot, HTTP→HTTPS

02IP, port, soket: kim nerede dinliyor

2. hattaki ağ dersinden hatırla: veri IP adresiyle makineye, port numarasıyla o makinedeki uygulamaya ulaşır. Bir bağlantıyı benzersiz kılan dörtlüdür (kaynak IP, kaynak port, hedef IP, hedef port) ve işletim sistemi bu bağlantıyı bir soket olarak tutar — 1. dersteki "her şey dosyadır" ilkesi gereği soket de bir dosya tanıtıcısıdır.

Sunucuda ilk sorulacak soru şudur: hangi uygulama hangi adres ve portta dinliyor? Cevabı ss verir (eski netstat'ın yerini almıştır):

$ sudo ss -tulpn
Netid State  Local Address:Port  Process
tcp   LISTEN 127.0.0.1:5080      users:(("dotnet",pid=4821,fd=12))
tcp   LISTEN 0.0.0.0:80          users:(("nginx",pid=1180,fd=6))
tcp   LISTEN [::]:22             users:(("sshd",pid=940,fd=3))
udp   UNCONN 127.0.0.53:53       users:(("systemd-resolve",pid=610,fd=12))

Seçeneklerin anlamı: -t TCP, -u UDP, -l yalnız dinleyenler, -p süreç adı (sudo gerekir), -n numaraları çözmeden göster (hızlı). Bu çıktıdaki en önemli ayrım şudur:

Dinlenen adresAnlamıSonuç
127.0.0.1:5080Yalnız bu makinenin içinden erişilebilirDışarıdan bağlanılamaz — kasıtlı: önüne ters vekil konur (08. bölüm)
0.0.0.0:80Bütün arayüzlerde, dışarıya açıkTers vekil, güvenlik duvarıyla korunur
[::]:22IPv6 (çoğu sistemde IPv4'ü de kapsar)SSH
10.0.0.5:5432Yalnız o iç ağ arayüzündeVeritabanı için doğru desen

Geliştirme makinesinde çalışan bir ASP.NET Core uygulamasının "sunucuda erişilemez" olmasının en sık nedeni, uygulamanın localhost'a bağlanmasıdır. Konteyner dünyasında aynı tuzak daha da keskindir: konteynerin içinde 127.0.0.1'e bağlanan bir uygulamaya -p ile yayımlanan porttan erişilemez, çünkü konteynerin kendi localhost'u vardır (7. ders). Kural: ters vekil arkasındaysa 127.0.0.1, konteynerdeyse 0.0.0.0 dinlenir (ASPNETCORE_URLS=http://0.0.0.0:8080).

Arayüz ve yönlendirme bilgisi de iki komuttadır:

$ ip -brief a            # arayüzler ve IP'ler (kısa)
lo    UNKNOWN  127.0.0.1/8
eth0  UP       10.0.0.12/24
$ ip r                   # yönlendirme tablosu; "default via" = ağ geçidi
default via 10.0.0.1 dev eth0
$ ss -s                  # özet: kaç bağlantı, hangi durumda
$ ss -tan state time-wait | wc -l    # TIME-WAIT yığılması (bağlantı sızıntısı belirtisi)

Bir kıdem detayı: 1024'ün altındaki portlara yalnız root (ya da CAP_NET_BIND_SERVICE yetkisi verilmiş süreç) bağlanabilir. Bu yüzden uygulamayı root olarak koşturmak yerine yüksek bir portta dinletip önüne 80/443'te duran ters vekil koymak standart çözümdür — 3. dersin en az ayrıcalık ilkesiyle 08. bölümün mimarisini birleştiren nokta budur.

03Ad çözümleme: hosts, DNS ve tuzakları

Bir ad IP'ye çevrilmeden hiçbir bağlantı kurulmaz ve bu çevirinin sırası /etc/nsswitch.conf'ta tanımlıdır: önce /etc/hosts, sonra DNS. /etc/hosts'a elle satır eklemek, DNS kaydı yayılmadan önce test etmenin ya da bir ortamı yönlendirmenin en hızlı yoludur:

$ cat /etc/hosts
127.0.0.1   localhost
10.0.0.30   basvuru-db.kurum.local basvuru-db
$ cat /etc/resolv.conf          # hangi DNS sunucusu kullanılıyor
nameserver 127.0.0.53           # systemd-resolved yerel önbelleği
$ resolvectl status              # gerçek yukarı akış DNS sunucuları

Sorgu araçları arasındaki fark önemlidir ve çoğu yanlış teşhisin kaynağıdır:

AraçNeyi sorarNe zaman
dig adDoğrudan DNS'e sorar, /etc/hosts'u atlarDNS kaydı doğru mu; dig +short, dig @8.8.8.8 ad, dig ad TXT
getent hosts adSistemin gerçek çözümleme zincirini kullanır (hosts + DNS)"Uygulama neyi görüyor" sorusunun doğru cevabı
host / nslookupDNS sorgusu (basit çıktı)Hızlı bakış

Tipik olay: dig doğru IP'yi döndürür ama uygulama başka yere bağlanır — çünkü /etc/hosts'ta eski bir satır vardır ve dig onu görmez. Teşhis getent hosts ad ile yapılır. İkinci tipik olay: DNS kaydı değişti ama uygulama eski IP'yi kullanıyor — DNS önbelleği ve TTL. .NET tarafında bu, uzun ömürlü HttpClient ile birlikte bilinen bir konudur: SocketsHttpHandler.PooledConnectionLifetime ayarı DNS değişikliklerinin fark edilmesi için vardır (5. hattaki HttpClient dersinin ağ tarafı).

04curl, nc ve "ping çalışıyor" yanılgısı

curl bir geliştiricinin en çok kullanacağı ağ aracıdır; 6. hattaki API dersinde gördüğün istek/yanıt yapısını komut satırından kurar. Bilinmesi gereken seçenekler:

$ curl -i https://api.kurum.gov.tr/basvuru/42          # -i: yanıt başlıklarını da yaz
$ curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://site.local/saglik
200 0.043s
$ curl -v https://site.local/api 2>&1 | head -30      # -v: TLS el sıkışması ve başlıklar
$ curl -X POST https://api.local/basvuru \
    -H 'Content-Type: application/json' \
    -H 'Authorization: Bearer '"$TOKEN" \
    -d '{"tcKimlik":"11111111111","sinavId":7}'
$ curl -H 'Host: basvuru.kurum.gov.tr' http://10.0.0.12/       # IP'ye git, sanal konağı test et
$ curl --resolve basvuru.kurum.gov.tr:443:10.0.0.12 https://basvuru.kurum.gov.tr/
$ curl -L http://site.local        # yönlendirmeleri izle (HTTP→HTTPS testinde gerekli)
$ curl --cacert /usr/local/share/ca-certificates/kurum.crt https://ic-servis.local/

Üç ayrıntı kıdem farkı yaratır:

  • --resolve ve -H 'Host: …' DNS'i atlayarak belirli bir sunucuyu test etmenin yoludur. Yayın öncesi "yeni sunucu doğru yanıt veriyor mu" sorusu, DNS'i değiştirmeden böyle cevaplanır. -H Host yalnız HTTP'de yeter; HTTPS'te sertifika/SNI eşleşmesi için --resolve gerekir.
  • Çıkış kodları betikler için kritiktir: 0 başarı, 6 ad çözülemedi (DNS), 7 bağlanılamadı (port/güvenlik duvarı), 28 zaman aşımı, 35/60 TLS sorunu (60 = sertifika doğrulanamadı), 22 HTTP hata kodu (-f ile). Sağlık kontrol betiklerinde curl -fsS --max-time 5 kalıbı kullanılır.
  • -k yasaktır. -k/--insecure sertifika doğrulamasını kapatır; teşhis sırasında "sorun sertifikada mı" ayrımı için bir kez kullanılabilir ama çözüm asla değildir. Doğru çözüm kök sertifikayı sisteme tanıtmaktır (09. bölüm).

HTTP dışı portları sınamak için nc (netcat) ya da ss'nin hedef tarafı kullanılır:

$ nc -zv 10.0.0.30 5432        # PostgreSQL portu açık mı
Connection to 10.0.0.30 5432 port [tcp/postgresql] succeeded!
$ nc -zv -w 3 db.local 5432    # 3 saniye zaman aşımıyla
$ timeout 3 bash -c '</dev/tcp/10.0.0.30/5432' && echo açık    # nc yoksa: bash'in kendi yolu

"Ping çalışıyor ama uygulama açılmıyor" yanılgısı. ping ICMP kullanır, TCP portlarıyla ilgisi yoktur: ping'in çalışması yalnız makinenin ağda olduğunu söyler, servisin dinlediğini söylemez. Tersi de doğrudur — birçok kurumsal ağ ve bulut sağlayıcısı ICMP'yi kapatır, bu yüzden ping'in çalışmaması makinenin erişilemez olduğu anlamına gelmez. Aynı sınır traceroute için de geçerlidir: yol üstündeki cihazlar yanıt vermeyebilir, çıktıdaki yıldızlar mutlaka arıza demek değildir. Doğru soru her zaman "şu port üzerinden TCP bağlantısı kurulabiliyor mu"dur — nc -zv veya curl.

Daha derin inceleme gerektiğinde paket yakalama devreye girer: sudo tcpdump -i any -n port 5432 -c 20. Bu, "istek sunucuya hiç geliyor mu" sorusunun kesin cevabıdır ve ters vekil hata ayıklamasında (08. bölüm) son çare olarak kullanılır.

05Güvenlik duvarı ve teşhis merdiveni

Linux'ta paket süzme çekirdekteki netfilter altyapısıyla yapılır; gündelik arayüzü Ubuntu'da ufw, RHEL ailesinde firewalld'dir (ikisi de alttaki nftables/iptables kurallarını yazar).

# Ubuntu (ufw)
$ sudo ufw status verbose
$ sudo ufw default deny incoming && sudo ufw default allow outgoing
$ sudo ufw allow 22/tcp            # SSH'ı AÇMADAN enable etmek seni dışarıda bırakır
$ sudo ufw allow 80,443/tcp
$ sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp    # yalnız iç ağa veritabanı
$ sudo ufw enable

# RHEL (firewalld)
$ sudo firewall-cmd --list-all
$ sudo firewall-cmd --permanent --add-service=https
$ sudo firewall-cmd --permanent --add-port=5080/tcp
$ sudo firewall-cmd --reload        # --permanent olmadan verilen kural yeniden başlatmada kaybolur

İki uyarı: (1) Uzak bir sunucuda güvenlik duvarını etkinleştirmeden önce SSH portuna izin ver — aksi hâlde bağlantın kopar ve konsola erişimin yoksa makineye giremezsin. (2) firewalld'de --permanent yazmayı unutmak, kuralın yeniden başlatmada kaybolmasına yol açar; tersi de olur: --permanent verip --reload yapmamak kuralı hemen etkinleştirmez.

"Port açık ≠ servis dinliyor". Bu iki kavram ayrı katmandır ve karıştırılması saatlerce yanlış yerde arama demektir. Güvenlik duvarında bir porta izin vermek, o portta bir şey dinlediğini garanti etmez; tersine, servis dinliyor olsa da güvenlik duvarı paketleri düşürüyorsa dışarıdan erişilemez. Ayırt etme yöntemi davranış farkıdır: bağlantı hemen reddediliyorsa (Connection refused) genellikle o portta dinleyen yoktur; bağlantı asılıp zaman aşımına düşüyorsa (timed out) paket bir yerde düşürülüyor demektir — güvenlik duvarı, bulut güvenlik grubu ya da yönlendirme.

Bu yüzden "uygulamaya erişemiyorum" sorunu her zaman şu merdivenle çözülür:

#SoruKomut
1Süreç ayakta mı?systemctl status uygulama · journalctl -u uygulama -p err -n 50 (4. ders)
2Doğru adres/portta dinliyor mu?sudo ss -tulpn | grep 5080
3Makinenin kendisinden yanıt veriyor mu?curl -i http://127.0.0.1:5080/saglik
4Ters vekil doğru yere mi gönderiyor?curl -i http://127.0.0.1/ · nginx -t · /var/log/nginx/error.log
5Ağdan TCP kurulabiliyor mu?Başka makineden nc -zv sunucu 443 — refused mı, timeout mu
6Ad doğru çözülüyor, sertifika geçerli mi?getent hosts ad · curl -v https://ad/ · openssl s_client

Merdivenin değeri sırasındadır: içten dışa gidersin ve ilk başarısız basamak sorunun katmanını söyler. Kıdemli bir geliştiriciyi ayıran şey bu sıranın disiplinidir — 3. basamak çalışıyorsa uygulamada sorun yoktur, oradan sonrası altyapıdır.

06SSH: sunucuya güvenli erişim

SSH hem uzak kabuk hem dosya taşıma hem tünel aracıdır. 16-04'te Git için anahtar kullandık; 3. derste anahtar dosyalarının izinlerini gördük. Burada düzeni tamamlıyoruz.

$ ssh-keygen -t ed25519 -C "ayse@kurum"           # ~/.ssh/id_ed25519 (+ .pub)
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub ayse@10.0.0.12
# ssh-copy-id yoksa: açık anahtarı sunucudaki ~/.ssh/authorized_keys dosyasına ekle (600 izinle)
$ ssh ayse@10.0.0.12
$ ssh -i ~/.ssh/ozel_anahtar -p 2222 ayse@sunucu
$ ssh sunucu 'systemctl is-active basvuru'        # tek komut çalıştır (kabuk profili okunmaz — 2. ders)

~/.ssh/config gündelik kullanımı kısaltır ve unutulan parametreleri ortadan kaldırır (izni 644, dizin 700):

Host basvuru-prod
    HostName 10.0.0.12
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    ServerAliveInterval 60

Host db-prod
    HostName 10.0.1.30
    User ayse
    ProxyJump basvuru-prod        # atlama sunucusu üzerinden (bastion)

# Artık:  ssh basvuru-prod   ·   scp dosya basvuru-prod:/tmp/

ssh-agent, anahtar parolasını her bağlantıda yazmamak için anahtarı bellekte tutar: eval "$(ssh-agent -s)" ve ssh-add ~/.ssh/id_ed25519. Kıdem uyarısı: ForwardAgent yes (ajan yönlendirme) kullanışlıdır ama bağlandığın sunucuda root yetkisi olan biri ajan soketin üzerinden senin kimliğinle başka sistemlere bağlanabilir; güvenmediğin sunucuda açılmaz, yerine ProxyJump kullanılır.

Sunucu tarafı sertleştirme (/etc/ssh/sshd_config, tercihen /etc/ssh/sshd_config.d/ altında ek dosya) — 14. hattaki sunucu sertleştirme maddelerinin en somut hâli:

PermitRootLogin no
PasswordAuthentication no        # anahtar dışında girişi kapat (kaba kuvvet saldırısını bitirir)
KbdInteractiveAuthentication no
AllowGroups ssh-erisim           # yalnız bu gruptaki kullanıcılar
X11Forwarding no
ClientAliveInterval 300
$ sudo sshd -t && sudo systemctl reload ssh      # ÖNCE sözdizimi testi

Pazarlıksız sıra: parola girişini kapatmadan önce anahtarla girişin çalıştığını ikinci bir oturumda doğrula. Açık oturumunu kapatmadan test etmek, kendini sunucudan kilitlememenin tek güvencesidir. Ek katmanlar: fail2ban (tekrarlayan başarısız denemede IP engelleme), SSH portunu değiştirmek (gürültüyü azaltır, güvenlik sağlamaz), anahtarları donanım anahtarına bağlamak.

Tünel (port yönlendirme), dışarıya açılmayan bir servise güvenli erişimin standart yoludur:

$ ssh -L 15432:10.0.1.30:5432 basvuru-prod
# yerel 15432 → sunucudan db'nin 5432'sine. Artık kendi makinenden:
$ psql -h 127.0.0.1 -p 15432 -U basvuru
$ ssh -L 8080:127.0.0.1:5080 basvuru-prod        # yalnız localhost'ta dinleyen uygulamayı görmek
$ ssh -J bastion ayse@10.0.1.30                  # atlama sunucusu (ProxyJump'ın komut hâli)

Bu desen, veritabanını internete açmadan yönetmenin doğru yoludur: veritabanı yalnız iç ağda dinler, erişim SSH kimliğiyle denetlenir ve kayda geçer.

07Dosya taşıma: scp ve rsync

$ scp yayim.zip basvuru-prod:/tmp/                    # tek dosya, hızlı ve basit
$ scp -r wwwroot basvuru-prod:/opt/basvuru/            # dizin
$ rsync -avz --delete ./yayim/ basvuru-prod:/opt/basvuru/    # DİKKAT: sonundaki eğik çizgi
$ rsync -avz --exclude '*.pdb' --exclude 'appsettings.Development.json' \
        --chown=basvuru:basvuru ./yayim/ basvuru-prod:/opt/basvuru/
$ rsync -avzn --delete ./yayim/ basvuru-prod:/opt/basvuru/   # -n: DENEME, hiçbir şey yazmaz

rsync'in scp'ye üstünlüğü artımlı çalışmasıdır: yalnız değişen parçaları taşır, izinleri ve zaman damgalarını korur (-a), sıkıştırır (-z), ve --delete ile hedefi kaynakla birebir eşitler. İki kıdem uyarısı: (1) Eğik çizgi tuzağı — kaynak/ "içeriğini kopyala", kaynak "dizinin kendisini kopyala" demektir; karıştırınca /opt/basvuru/yayim/yayim/ gibi bir yapı oluşur. (2) --delete yıkıcıdır; her zaman önce -n (dry run) ile çalıştır ve çıktıda ne silineceğini gör. Yayın betiklerinde bu sıra değişmez kuraldır.

Not: yayın (deployment) için doğru desen, dosyaları çalışan dizinin üzerine yazmak değil, yeni bir sürüm dizinine açıp simgesel bağlantıyı çevirmektir (1. ders): /opt/basvuru/releases/2026-09-17 + current bağlantısı. Geri alma, bağlantıyı eski dizine döndürüp servisi yeniden başlatmaktan ibaret olur — 18. hattaki "geri alma planı" maddesinin en yalın hâli. Uçtan uca yayın akışı 11. derste (dck-deploy) konteynerle birlikte kurulur.

08nginx ters vekil: 13-02 ve 14-06 senedi

Uygulama neden doğrudan 80/443'te dinlemez? Beş gerekçe: (1) 1024 altı portlar root ister, uygulama root koşmamalıdır (3. ders); (2) TLS sertifikasını tek yerde yönetmek istersin; (3) aynı makinede birden çok uygulamayı ad/yola göre ayırman gerekir; (4) statik dosya sunumu, sıkıştırma, önbellek başlıkları ve hız sınırlama vekilin işidir; (5) yayın sırasında uygulamayı yeniden başlatırken vekil sabit kalır. Ters vekil (reverse proxy) bu katmandır: istemciden isteği alır, arkadaki uygulamaya iletir, yanıtı geri verir.

# /etc/nginx/sites-available/basvuru  (RHEL'de /etc/nginx/conf.d/basvuru.conf)
server {
    listen 80;
    server_name basvuru.kurum.gov.tr;
    return 301 https://$host$request_uri;          # HTTP → HTTPS
}

server {
    listen 443 ssl http2;
    server_name basvuru.kurum.gov.tr;

    ssl_certificate     /etc/letsencrypt/live/basvuru.kurum.gov.tr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/basvuru.kurum.gov.tr/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    client_max_body_size 20m;                      # dosya yükleme sınırı (413 hatasının kaynağı)

    location /saglik {
        proxy_pass http://127.0.0.1:5080;
        access_log off;
    }

    location / {
        proxy_pass http://127.0.0.1:5080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;     # WebSocket/SignalR
        proxy_set_header Connection        $connection_upgrade;
        proxy_read_timeout 120s;
    }
}

$ sudo ln -s /etc/nginx/sites-available/basvuru /etc/nginx/sites-enabled/
$ sudo nginx -t && sudo systemctl reload nginx     # ÖNCE test, sonra reload (kesintisiz)

Senet ödemesi: X-Forwarded-* ve ASP.NET Core. 13-02'de (HTTPS ve yönlendirme) ve 14-06'da (ağ güvenliği) şu söz verilmişti: uygulama ters vekilin arkasında çalışırken istemcinin gerçek IP'sini ve şemanın HTTPS olduğunu bilmesi gerekir. Vekil bunu üç başlıkla söyler; uygulamanın bu başlıkları okuması için açıkça yapılandırılması gerekir:

// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
    o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    o.KnownProxies.Add(IPAddress.Parse("127.0.0.1"));   // yalnız güvenilen vekile inan
});

var app = builder.Build();
app.UseForwardedHeaders();      // pipeline'ın EN BAŞINDA olmalı
app.UseHttpsRedirection();

Bu yapılandırma yoksa dört somut arıza görülür: (1) log'larda ve hız sınırlamada bütün isteklerin IP'si 127.0.0.1 görünür (14. hattaki iz kaydı gereksinimi çalışmaz); (2) Request.Scheme http döner, bu yüzden uygulamanın ürettiği yönlendirme ve mutlak bağlantılar http:// ile çıkar; (3) OAuth/OIDC akışında redirect_uri yanlış şemayla üretilir ve sağlayıcı isteği reddeder (13. hat); (4) UseHttpsRedirection sonsuz yönlendirme döngüsüne girer. Güvenlik notu: bu başlıklar istemci tarafından uydurulabilir; bu yüzden KnownProxies/KnownNetworks ile yalnız kendi vekiline güvenilir — aksi hâlde IP tabanlı denetimler kandırılabilir.

Ters vekilin log'u ayrı dosyadadır (/var/log/nginx/access.log, error.log) ve logrotate ile döner (4. ders). Teşhiste error.log doğrudan cevap verir: connect() failed (111: Connection refused) arkadaki uygulamanın ayakta olmadığını, upstream timed out ise uygulamanın yavaş olduğunu söyler — merdivenin 3. ve 4. basamağını bir bakışta ayırır.

09TLS: sertifika almak, yenilemek ve güvenmek

13. hatta TLS'in nasıl çalıştığını, 14. hatta PKI ve güven zincirini gördük. Sunucu tarafında iş üç parçadır: sertifikayı almak, yenilemeyi otomatikleştirmek ve giden bağlantılarda karşı tarafa güvenmek.

Sertifika almak (Let's Encrypt). ACME protokolü, alan adının sahibi olduğunu doğrulayıp ücretsiz ve kısa ömürlü (90 gün) sertifika verir:

$ sudo apt install certbot python3-certbot-nginx
$ sudo certbot --nginx -d basvuru.kurum.gov.tr        # nginx yapılandırmasını da düzenler
$ sudo certbot certonly --nginx -d basvuru.kurum.gov.tr   # yalnız sertifika al, düzenleme yapma
$ sudo certbot renew --dry-run                        # yenileme mekanizmasını sına
$ systemctl list-timers | grep certbot                 # yenileme bir systemd timer'dır (4. ders)

Kıdem notları: (1) Yenileme otomatik olmalı — 90 günlük sertifikanın elle yenilenmesi unutulur ve sonuç tam bir kesintidir; certbot bunu timer ile yapar, ama yenileme sonrası nginx'in reload edilmesi gerekir (--deploy-hook "systemctl reload nginx"). (2) HTTP-01 doğrulaması 80 portunun dışarıdan erişilebilir olmasını ister; kapalı ağlarda DNS-01 doğrulaması kullanılır. (3) Let's Encrypt'in hız sınırı vardır, deneme yaparken --dry-run ya da staging ortamı kullanılır. (4) Kamu ve kurum içi sistemlerde sertifika genelde kurumun kendi CA'sından ya da nitelikli bir sağlayıcıdan gelir; o durumda fullchain dosyasının ara sertifikaları içerdiğinden emin olunur — eksik ara sertifika, tarayıcıda çalışıp mobil istemcide ya da .NET'te "güven zinciri kurulamadı" hatası veren klasik bir arızadır. Doğrulama:

$ openssl s_client -connect basvuru.kurum.gov.tr:443 -servername basvuru.kurum.gov.tr < /dev/null \
    | openssl x509 -noout -subject -issuer -dates
$ curl -vI https://basvuru.kurum.gov.tr 2>&1 | grep -E 'subject|issuer|expire'

Giden bağlantılarda güven. Sunucundaki uygulama başka bir HTTPS servisine bağlanırken karşı tarafın sertifikasını sistem güven deposuna göre doğrular. Kurum içi bir servis ya da TLS'i sonlandıran bir kurumsal vekil söz konusuysa kök sertifikanın sisteme tanıtılması gerekir (4. dersteki paket yöneticisi örneğinin aynısı, bu kez uygulaman için):

$ sudo cp kurum-koku.crt /usr/local/share/ca-certificates/kurum-koku.crt
$ sudo update-ca-certificates                 # Debian/Ubuntu
# RHEL: /etc/pki/ca-trust/source/anchors/ + sudo update-ca-trust
$ curl https://ic-servis.kurum.local/saglik   # -k OLMADAN çalışmalı

Bu adım 14-04'ün PKI senedini ve 16-04'ün "vekil arkasında sertifika hatası" notunu kapatır. .NET, Linux'ta bu sistem deposunu kullanır (Windows'un mağazasını değil), bu yüzden HttpClient'ta sertifika doğrulamasını devre dışı bırakan geri çağırmalar yazmak yerine kök sertifikayı tanıtmak doğru çözümdür. Konteynerdeki hâli 8. derste karşına çıkacak: imaj kendi güven deposunu taşır, kök sertifika Dockerfile'da eklenmeli — aksi hâlde uygulama hostta çalışıp konteynerde TLS hatası verir.

10Mini alıştırma — erişimi uçtan uca kur ve teşhis et

Yaklaşık 20 dakika · bir Linux kabuğu (WSL2 ya da sanal makine); nginx kurulu olsun

  1. Dinleyenleri oku. sudo ss -tulpn çalıştır; her satır için "hangi süreç, hangi adres, dışarıya açık mı" sorularını cevapla. Sonra ip -brief a ve ip r ile makinenin IP'sini ve ağ geçidini yaz.
  2. Basit sunucu. python3 -m http.server 8080 --bind 127.0.0.1 başlat. Başka bir terminalde curl -i http://127.0.0.1:8080/ çalışıyor mu? Makinenin kendi IP'siyle (curl -i http://10.x.x.x:8080/) dene — neden başarısız? Sunucuyu --bind 0.0.0.0 ile yeniden başlatıp yinele.
  3. refused ↔ timeout. nc -zv 127.0.0.1 9999 (dinleyen yok) ve nc -zv -w 3 10.255.255.1 80 (erişilemez adres) çalıştır. İki hata mesajını ve hangi katmanı işaret ettiklerini yaz.
  4. Ad çözümleme. /etc/hosts'a 127.0.0.1 basvuru.test satırını ekle. getent hosts basvuru.test ve dig +short basvuru.test çıktılarını karşılaştır; farkı bir cümleyle açıkla.
  5. Ters vekil. nginx'te basvuru.test için bir server bloğu yaz; location / içinde proxy_pass http://127.0.0.1:8080; ve dört X-Forwarded-*/ Host başlığını kur. sudo nginx -t ile doğrula, reload et, curl -i http://basvuru.test/ ile test et.
  6. Başlıkları gör. Python sunucusunu kapat; yerine gelen başlıkları yazdıran bir şey koy (ör. nc -l 8080 ile tek istek dinleyip curl at) ve X-Forwarded-For ile X-Forwarded-Proto başlıklarının geldiğini gözle doğrula.
  7. Merdiven provası. nginx'te proxy_pass portunu kasıtlı yanlış yaz (8081), reload et, curl -i http://basvuru.test/ çalıştır ve sudo tail -5 /var/log/nginx/error.log çıktısını oku. Hangi mesajın hangi basamağı gösterdiğini yaz, sonra düzelt.
  8. SSH. ssh-keygen -t ed25519 -f ~/.ssh/deneme -N "" ile anahtar üret; ~/.ssh/config'e bir Host bloğu yaz (HostName localhost, IdentityFile, IdentitiesOnly yes). Mümkünse açık anahtarı ~/.ssh/authorized_keys'e ekleyip ssh deneme-host ile bağlan; izinleri 700/600 yapmayı unutma (3. ders).

11Kendini sına

ss -tulpn çıktısı nasıl okunur; 127.0.0.1 ile 0.0.0.0 dinlemek arasındaki fark nedir ve hangi durumda hangisi seçilir?

-t TCP, -u UDP, -l yalnız dinleyenler, -p süreç (sudo gerekir), -n çözümleme yapmaz. Çıktıda hangi sürecin hangi adres:portta dinlediği görünür. 127.0.0.1:5080 yalnız makinenin içinden erişilebilir demektir — ters vekil arkasındaki uygulama için doğru ve kasıtlı seçim, çünkü uygulama internete doğrudan açılmaz. 0.0.0.0:80 bütün arayüzlerde dinler; ters vekil ve güvenlik duvarıyla korunur. Belirli bir iç ağ arayüzüne bağlamak (10.0.0.5:5432) veritabanı için doğru desendir. Kural: ters vekil arkasındaysa 127.0.0.1, konteyner içindeyse 0.0.0.0 dinlenir — konteynerin kendi localhost'u olduğu için 127.0.0.1'e bağlanan uygulamaya yayımlanan porttan erişilemez. Ek olarak 1024 altındaki portlara yalnız root ya da CAP_NET_BIND_SERVICE yetkili süreç bağlanabilir; bu yüzden uygulama yüksek portta koşar, 80/443'te ters vekil durur. Arayüz/rota bilgisi ip -brief a ve ip r ile, bağlantı özeti ss -s ile alınır.

dig ile getent hosts arasındaki fark nedir? curl'ün hangi seçenekleri ve çıkış kodları teşhiste kullanılır; ping neden yeterli bir kanıt değildir?

dig doğrudan DNS sunucusuna sorar ve /etc/hosts'u atlar; getent hosts ad ise sistemin gerçek çözümleme zincirini (nsswitch.conf: hosts → DNS) kullanır, yani uygulamanın göreceği cevabı verir. "dig doğru IP diyor ama uygulama başka yere bağlanıyor" olayının nedeni genelde /etc/hosts'taki eski satırdır; DNS değişti ama uygulama eski IP'yi kullanıyorsa neden önbellek/TTL'dir (.NET'te PooledConnectionLifetime bu yüzden ayarlanır). curl: -i yanıt başlıkları, -v TLS el sıkışması ve istek detayı, -H başlık, -d gövde, -X yöntem, -L yönlendirme izleme, -w '%{http_code} %{time_total}' ölçüm, --resolve ad:443:IP DNS'i atlayıp belirli sunucuyu test etme (HTTPS'te -H Host yetmez, SNI/sertifika eşleşmesi için --resolve gerekir). Çıkış kodları: 0 başarı, 6 DNS, 7 bağlanamadı, 28 zaman aşımı, 60 sertifika doğrulanamadı, -f ile 22 HTTP hatası; sağlık kontrolünde curl -fsS --max-time 5 kalıbı kullanılır. -k yalnız "sorun sertifikada mı" ayrımı için geçici teşhis aracıdır, çözüm değildir. ping ICMP kullanır: çalışması yalnız makinenin ağda olduğunu gösterir, portta bir servis dinlediğini göstermez; ayrıca birçok kurumsal ağ ve bulut ICMP'yi kapattığı için ping'in çalışmaması erişilemezlik kanıtı değildir. Doğru soru "şu portta TCP kurulabiliyor mu"dur: nc -zv host port ya da curl; daha derinde tcpdump.

"Port açık ≠ servis dinliyor" ne demektir; Connection refused ile timed out farkı neyi gösterir? "Uygulamaya erişemiyorum" sorununu hangi sırayla teşhis edersin?

Güvenlik duvarında bir porta izin vermek, o portta bir şeyin dinlediğini garanti etmez; tersine servis dinlerken güvenlik duvarı paketleri düşürüyorsa dışarıdan erişilemez — bunlar ayrı katmanlardır. Davranış farkı ayırt ettirir: Connection refused paket hedefe ulaştı ama o portta dinleyen yok (ya da açıkça reddedildi); timed out paket bir yerde sessizce düşürülüyor — güvenlik duvarı, bulut güvenlik grubu ya da yönlendirme sorunu. Teşhis merdiveni içten dışa doğrudur: (1) süreç ayakta mı (systemctl status, journalctl -u … -p err); (2) doğru adres/portta dinliyor mu (ss -tulpn); (3) makinenin kendisinden yanıt veriyor mu (curl -i http://127.0.0.1:port/saglik) — buraya kadar çalışıyorsa uygulama sağlamdır, sorun altyapıdadır; (4) ters vekil doğru yere mi gönderiyor (nginx -t, /var/log/nginx/error.log: "connect() failed (111: Connection refused)" arkadaki uygulamanın kapalı olduğunu, "upstream timed out" yavaşlığı gösterir); (5) ağdan TCP kurulabiliyor mu, refused mı timeout mu (nc -zv); (6) ad doğru çözülüyor ve sertifika geçerli mi (getent hosts, curl -v, openssl s_client). Güvenlik duvarı yönetiminde iki tuzak: uzak sunucuda ufw enable öncesi SSH portuna izin vermemek bağlantıyı koparır; firewalld'de --permanent ve --reload ikilisini atlamak kuralı ya geçici ya etkisiz bırakır.

Ters vekil neden kullanılır; X-Forwarded-For/-Proto ile ASP.NET Core ForwardedHeaders ilişkisi nedir? TLS sertifikası nasıl alınır/yenilenir ve kurum kök sertifikası neden sisteme tanıtılır?

Ters vekil beş nedenle kullanılır: 1024 altı portlar root ister (uygulama root koşmamalı), TLS tek yerde yönetilir, aynı makinede birden çok uygulama ad/yola göre ayrılır, statik dosya/sıkıştırma/önbellek/hız sınırı vekilin işidir, ve yayın sırasında uygulama yeniden başlarken vekil sabit kalır. nginx'te proxy_pass http://127.0.0.1:5080 ile birlikte Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto başlıkları kurulur; WebSocket/SignalR için Upgrade/Connection eklenir; değişiklik nginx -t ile doğrulanıp reload edilir. Uygulamanın bu başlıkları okuması için yapılandırılması gerekir: ForwardedHeadersOptions + app.UseForwardedHeaders() (pipeline'ın en başında) ve KnownProxies ile yalnız kendi vekiline güven — başlıklar istemci tarafından uydurulabildiği için bu sınırlama güvenlik gereğidir. Eksikse: bütün istekler 127.0.0.1'den görünür (iz kaydı ve hız sınırı bozulur), Request.Scheme http döner, üretilen mutlak bağlantılar ve OIDC redirect_uri yanlış şemayla çıkar, UseHttpsRedirection sonsuz döngüye girer. TLS: Let's Encrypt/certbot ile ACME doğrulamasıyla 90 günlük sertifika alınır, yenileme systemd timer'ıyla otomatiktir ve --deploy-hook "systemctl reload nginx" ile vekil tazelenir; HTTP-01 için 80 portu erişilebilir olmalı, kapalı ağda DNS-01 kullanılır. Kurum/kamu sertifikalarında fullchain'in ara sertifikaları içermesi şarttır — eksikse tarayıcıda çalışıp .NET/mobil istemcide "güven zinciri kurulamadı" hatası verir (openssl s_client ile doğrulanır). Giden bağlantılarda karşı tarafa güvenmek için kurum kök sertifikası /usr/local/share/ca-certificates/ + update-ca-certificates (RHEL: anchors + update-ca-trust) ile sisteme tanıtılır; .NET Linux'ta bu sistem deposunu kullanır, doğrulamayı kapatan geri çağırma yazmak yanlış çözümdür. Konteynerde imaj kendi güven deposunu taşıdığı için aynı kurulum Dockerfile'da yapılmalıdır.

12Sınav köşesi

Yazılıda nasıl sorulur

(1) IP adresi, port ve soket kavramlarını açıklayınız. (2) Bir sunucuda dinlemede olan portlar hangi komutla listelenir? (3) /etc/hosts dosyası ne işe yarar; ad çözümleme sırası nasıldır? (4) curl komutu ile bir API'ye POST isteği nasıl gönderilir? (5) ping komutunun çalışması servisin erişilebilir olduğunu kanıtlar mı; gerekçelendiriniz. (6) Güvenlik duvarında bir portu açma komutunu yazınız (ufw veya firewalld). (7) SSH ile anahtar tabanlı kimlik doğrulama nasıl kurulur; parola girişi neden kapatılır? (8) SSH port yönlendirme (tünel) hangi amaçla kullanılır? (9) Ters vekil (reverse proxy) nedir; hangi avantajları sağlar? (10) TLS sonlandırma nedir; X-Forwarded-Proto başlığı ne işe yarar? (11) scp ile rsync arasındaki fark nedir?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Teşhis disiplini — "'Erişemiyorum' dendiğinde içten dışa giderim: servis ayakta mı, doğru portta dinliyor mu (ss -tulpn), makinenin kendisinden curl 127.0.0.1 yanıt veriyor mu, vekil doğru yere mi gönderiyor, ağdan TCP kurulabiliyor mu, ad ve sertifika doğru mu. İlk başarısız basamak katmanı söyler." (2) Ayrım bilgisi — "Ping ICMP'dir, port kanıtı değil; refused dinleyen yok demek, timeout paketin düşürüldüğünü gösterir. Güvenlik duvarında port açık olması servisin dinlediği anlamına gelmez." (3) Üretim düzeni — "Uygulamayı 127.0.0.1'de yüksek portta koştururum, önünde nginx: TLS sonlandırma, HTTP→HTTPS yönlendirme, statik dosya ve hız sınırı orada. X-Forwarded-For/-Proto başlıklarını geçirir, uygulamada UseForwardedHeaders ile KnownProxies vererek okurum — yoksa log'da gerçek IP kaybolur ve OIDC yönlendirmesi yanlış şemayla üretilir." (4) Erişim ve taşıma — "Sunucuya yalnız anahtarla SSH; PermitRootLogin no, PasswordAuthentication no — ama parolayı kapatmadan önce ikinci bir oturumda anahtarla girişi doğrularım. Veritabanına ssh -L tüneliyle erişirim, internete açmam. Dosya taşımada rsync -avz, --delete öncesi mutlaka -n denemesi." (5) Kıdem vuruşu — "Sertifikada -k ile ilerlemem: kurum kök sertifikasını update-ca-certificates ile sisteme tanıtırım; fullchain'de ara sertifika eksikse tarayıcı çalışır, .NET istemcisi hata verir — bunu openssl s_client ile doğrularım. Let's Encrypt yenilemesini timer'a bağlar, --deploy-hook ile nginx'i reload ettiririm."