~/yazilar/ssh-sikilastirma

SSH'ı biraz daha güvenli hale getirmek

9 dk okuma

Dışarıdan erişilebilen bir sunucunun SSH loglarına bir gün sonra baktığında tablo hep aynı oluyor: tanımadığın adreslerden root, admin, oracle, test gibi kullanıcı adlarıyla sırayla deneme yapılıyor.

sudo journalctl -u ssh --since "24 hours ago" | grep -E "Invalid user|Failed password" | tail -n 4
... sshd[2211]: Invalid user admin from 203.0.113.45 port 51122
... sshd[2211]: Failed password for invalid user admin from 203.0.113.45 port 51122 ssh2
... sshd[2240]: Failed password for root from 198.51.100.7 port 40312 ssh2
... sshd[2263]: Invalid user oracle from 203.0.113.45 port 52990

Bunlar hedefli saldırı değil, internetin arka plan gürültüsü. Parolan güçlüyse büyük ihtimalle bir şey olmaz. Ben "büyük ihtimalle" ile yetinmek istemiyorum, parola ile girişi tamamen kapatınca bu denemelerin hiçbirinin şansı kalmıyor. Sadece kampüs ağından erişilen sunucular için de durum çok farklı değil. Ağın içinde ele geçirilmiş tek bir makine aynı taramayı içeriden yapabilir.

Sıra şöyle: önce anahtarla girişi kurup çalıştığını görüyorum, sonra parola ve root girişini kapatıyorum, en son fail2ban ve UFW geliyor. Sıra önemli. Tersini yaparsan kapıyı içeriden kilitleyip anahtarı dışarıda bırakırsın.

Kendini kilitlememek için tek kural

Bu yazıdaki her değişiklikten sonra açık olan SSH oturumunu kapatmıyorum. Yeni bir terminal açıp ikinci bir oturumla bağlanmayı deniyorum. İkinci oturum açılıyorsa devam ediyorum, açılmıyorsa birinci oturumdan düzeltiyorum. sshd yeniden başlasa da kurulmuş bağlantılar kopmuyor, yani o ilk oturum senin can simidin.

Bunun yanında bir yedek yolun olsun. Sanal makinedeysen hipervizör konsoluna, fiziksel sunucudaysan iDRAC, iLO gibi uzaktan yönetim kartına girebildiğini başlamadan önce kontrol et. Gece yarısı veri merkezine gitmek kimsenin istediği şey değil.

Anahtar çifti ve sunucuya kopyalamak

Anahtarı kendi bilgisayarında üretiyorsun, sunucuda değil:

ssh-keygen -t ed25519 -C "ad.soyad@dizustu"

Ed25519 anahtarları kısa ve hızlı, güncel her OpenSSH sürümü destekliyor. Çok eski bir cihaza da bağlanman gerekiyorsa ikinci seçenek -t rsa -b 4096. Passphrase sorusunu boş geçmiyorum. Dizüstü çalınırsa ya da ev dizini bir yedekte açıkta kalırsa anahtar tek başına işe yaramasın. Her bağlantıda passphrase yazmak can sıkıyorsa ssh-agent bunu oturum başına bir kereye indiriyor.

Açık anahtarı sunucuya gönder:

ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@192.0.2.10

ssh-copy-id anahtarı sunucudaki ~/.ssh/authorized_keys dosyasına ekliyor ve izinleri ayarlıyor. Windows'ta bu komut yok, açık anahtarın içeriğini sunucuda o dosyaya elle ekleyebilirsin. Elle eklediysen izinlere bak:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Bulut image'larında parolayla SSH girişi çoğu zaman baştan kapalı geliyor ve ssh-copy-id bağlanamıyor. O durumda sağlayıcının kurulumda root'a (ya da ubuntu gibi varsayılan kullanıcıya) koyduğu anahtarla bağlanıp dosyayı yeni kullanıcıya kopyalamak en kısa yol:

sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys

sshd varsayılan olarak StrictModes açık çalışıyor ve başkalarının yazabildiği bir .ssh dizinini ya da authorized_keys dosyasını sessizce yok sayıyor. İstemci tarafında hiçbir hata görmüyorsun, sadece anahtar yerine parola soruluyor. Sunucu logunda ise şuna benzer bir satır çıkıyor:

Authentication refused: bad ownership or modes for directory /home/deploy/.ssh

Şimdi anahtarın gerçekten çalıştığını, istemciye sadece anahtar kullanmasını söyleyerek test et:

ssh -o PreferredAuthentications=publickey deploy@192.0.2.10

İçeri girdiysen tamam. Permission denied (publickey) alıyorsan bir sonraki bölüme geçme, önce bunu çöz. ssh -v çıktısında hangi anahtarın denendiğini görebilirsin.

Ayarları sshd_config.d altında tut

Ubuntu'daki /etc/ssh/sshd_config dosyasının en başında şu satır var:

Include /etc/ssh/sshd_config.d/*.conf

Ana dosyaya dokunmak yerine kendi ayarlarımı bu dizine ayrı bir dosya olarak koyuyorum. Paket güncellemesi sshd_config dosyasının yeni bir sürümünü getirdiğinde "bu dosyayı değiştirmişsin, ne yapayım?" sorusuyla uğraşmıyorum. Bütün sunuculara aynı dosyayı Ansible gibi bir araçla dağıtmak da kolaylaşıyor.

Burada bilmen gereken bir tuhaflık var: sshd çoğu ayar için ilk okuduğu değeri kullanıyor, sonra gelenleri yok sayıyor. Include satırı dosyanın en başında olduğu için sshd_config.d altındaki dosyalar ana dosyayı eziyor, kendi aralarında da alfabetik sırada önce gelen kazanıyor. Bulut image'larında bu dizinde PasswordAuthentication yes içeren hazır bir dosya gelebiliyor. Kendi dosyamın adını 10- ile başlatmamın sebebi bu, ondan önce okunsun.

SSH'a kimlerin girebileceğini bir grupla sınırlamak istiyorum. Grubu ayarlardan önce oluşturup kendimi içine alıyorum:

sudo groupadd ssh-erisim
sudo usermod -aG ssh-erisim deploy
id deploy

Ayar dosyası:

# /etc/ssh/sshd_config.d/10-sertlestirme.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitEmptyPasswords no

MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no

AllowGroups ssh-erisim

KbdInteractiveAuthentication satırını atlama. Parolayı kapatıp bunu açık bırakırsan PAM üzerinden klavye etkileşimli giriş hâlâ parola sorabiliyor. Eski yazılarda bu ayarın adı ChallengeResponseAuthentication olarak geçiyor, yeni sürümler eski adı hâlâ takma ad olarak kabul ediyor.

AllowGroups bu dosyadaki en etkili satır ama en tehlikelisi de o. Kendi kullanıcın grupta değilse dışarıda kalırsın. id deploy çıktısında ssh-erisim görmeden devam etme.

MaxAuthTries 3 ise bir yan etki yapabiliyor. ssh-agent içinde çok sayıda anahtar varsa istemci hepsini sırayla deniyor ve doğru anahtara gelmeden hakkı bitiyor:

Received disconnect from 192.0.2.10 port 22:2: Too many authentication failures

Çözümü istemci tarafında. ~/.ssh/config içinde o sunucu için hangi anahtarın kullanılacağını söylüyorsun:

Host web01
    HostName 192.0.2.10
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Uygulamadan önce sözdizimini ve birleşmiş son hali kontrol et:

sudo sshd -t
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowgroups)'
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
allowgroups ssh-erisim

sshd -t hiçbir şey yazmıyorsa sözdizimi doğru. sshd -T ise bütün dosyalar birleştikten sonra geçerli olan değerleri gösteriyor. Hangi dosyanın kazandığından emin değilsen bakacağın yer burası. Burada passwordauthentication yes görüyorsan dizinde senin dosyandan önce okunan başka bir dosya var.

Her şey yerindeyse servisi yeniden başlat:

sudo systemctl restart ssh

Ve ilk oturumu kapatmadan ikinci terminalden üç test:

ssh deploy@192.0.2.10                                 # açılmalı
ssh -o PubkeyAuthentication=no deploy@192.0.2.10     # reddedilmeli
ssh root@192.0.2.10                                   # reddedilmeli

Son ikisinde beklediğim çıktı bu:

deploy@192.0.2.10: Permission denied (publickey).

Parola sorulmuyor bile. Sunucu sadece anahtar kabul ettiğini en başta söylüyor.

Socket activation ile gelen sürümler

Ubuntu'nun yeni sürümlerinde SSH socket activation ile geliyor. 22. portu sshd değil systemd dinliyor, bağlantı gelince sshd başlatılıyor. ss -tlpn çıktısında port 22'nin karşısında sshd yerine systemd görmenin sebebi bu. Hangi modda olduğunu şöyle görüyorsun:

systemctl is-enabled ssh.socket ssh.service
systemctl status ssh.socket --no-pager

Günlük kullanımda fark etmiyor ama iki yerde karşına çıkıyor. Birincisi port ya da dinlenen adres. Bu modda sshd_config içindeki Port ve ListenAddress satırlarını bir generator okuyup ssh.socket ayarına çeviriyor. Sadece restart ssh yaparsan sshd yeni portu bilir ama systemd eski portu dinlemeye devam eder. Port değiştirdiysen şu ikisi gerekiyor:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

İkincisi, bazı yönetim araçları ve eski dokümanlar sshd'nin hep çalıştığını varsayıyor. systemctl status ssh çıktısında bağlantı yokken inactive görmek bu modda normal.

Port değiştirmeyi ben güvenlik önlemi saymıyorum. Log gürültüsünü azaltıyor, o kadar. Parola kapalıyken o gürültünün zararı da yok. Yine de değiştireceksen aşağıdaki UFW kuralını yeni porta göre yaz, yoksa güvenlik duvarını açtığın anda kendini dışarıda bırakırsın.

fail2ban

Parola kapalıyken kaba kuvvet denemelerinin başarı şansı yok. Bu yüzden fail2ban'i burada ek bir güvenlik katmanından çok temizlik aracı olarak görüyorum: aynı adresten dakikada onlarca bağlantı açan bir şeyi bir süreliğine susturuyor, loglar okunur kalıyor.

sudo apt install fail2ban

Ubuntu paketi /etc/fail2ban/jail.d/defaults-debian.conf ile sshd jail'ini açık getiriyor. Ben yine de değerleri kendim belirlemek için .local uzantılı bir dosya ekliyorum. .conf dosyaları paketle birlikte değişebilir, .local dosyalarına paket dokunmuyor ve aynı ayarlar için onlar kazanıyor.

# /etc/fail2ban/jail.d/sshd.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
# Yönetim ağını hiçbir zaman yasaklama
ignoreip = 127.0.0.1/8 ::1 10.10.0.0/16

[sshd]
enabled = true
backend = systemd

ignoreip satırına kendi yönetim ağını ya da VPN adres havuzunu yazmak bence şart. Örnekteki 10.10.0.0/16 sadece bir örnek. Hangi adresten bağlandığını echo $SSH_CLIENT gösteriyor, ilk alan istemcinin adresi. NAT arkasındaki bir ofisten yanlış anahtarla birkaç kez denendiğinde o çıkış adresinin arkasındaki herkes bir saatliğine dışarıda kalır.

backend = systemd logları dosyadan değil journal'dan okutuyor. /var/log/auth.log dosyasının olmadığı minimal kurulumlarda varsayılan backend ile fail2ban hiç açılmayabiliyor, journal ise her zaman orada.

sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:	2
|  |- Total failed:	37
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned:	1
   |- Total banned:	1
   `- Banned IP list:	203.0.113.45

Jail'in açık görünmesi, filtrenin logları gerçekten yakaladığını göstermiyor. Bunu ayrıca deniyorum:

sudo fail2ban-regex systemd-journal sshd

Çıktının sonundaki Lines: satırında matched sayısı sıfırsa filtre hiçbir satırı tanımıyor demek, jail boşuna çalışıyor.

Yanlışlıkla yasaklanan bir adresi elle açmak için:

sudo fail2ban-client set sshd unbanip 198.51.100.7

UFW

Kendini kilitleme ihtimali en yüksek adım bu. ufw enable dediğin anda varsayılan politika gelen her şeyi reddetmeye başlıyor. SSH'a izin veren kural o anda yoksa açık oturum bağlantı takibi sayesinde çoğu zaman ayakta kalıyor ama yeni bağlantı açamıyorsun, o oturum bir kez koptuğunda da geri dönemiyorsun.

O yüzden önce kuralları ekliyorum, sonra açıyorum. ufw kapalıyken eklenen kurallar kaydediliyor ama uygulanmıyor:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.10.0.0/16 to any port 22 proto tcp comment 'SSH yonetim agi'
sudo ufw show added
Added user rules (see 'ufw status' for running firewall):
ufw allow from 10.10.0.0/16 to any port 22 proto tcp comment 'SSH yonetim agi'

ufw enable demeden önce echo $SSH_CLIENT ile bağlandığın adresin bu aralıkta olduğunu kontrol et ve aralığı kendi ağına göre değiştir. Adresin aralığın dışındaysa yeni oturum açamazsın. Aşağıdaki timer seni kurtarır ama beş dakikan gider.

SSH'ı sadece yönetim ağına açmak en temizi. Sunucuya dışarıdan erişilmesi gerekiyorsa ve araya VPN koyamıyorsan allow yerine limit kullanıyorum. sudo ufw limit 22/tcp aynı adresten son 30 saniyede altı ya da daha fazla bağlantı denemesi geldiğinde yenilerini reddediyor.

Açmadan önce bir emniyet kemeri takıyorum: beş dakika sonra güvenlik duvarını kendiliğinden kapatacak geçici bir timer. Her şey yolunda giderse iptal ediyorum, gitmezse beş dakika beklemek yetiyor.

sudo systemd-run --unit=ufw-geri-al --on-active=5m /usr/sbin/ufw disable
sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

İkinci terminalden yeni bir SSH oturumu aç. Açılıyorsa timer'ı iptal et ve son duruma bak:

sudo systemctl stop ufw-geri-al.timer
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    10.10.0.0/16               # SSH yonetim agi

Bu sunucuya ileride bir web servisi koyduğunda 80 ve 443 için ayrı kural ekleyeceksin. Docker kuracaksan şunu şimdiden bil: Docker'ın yayınladığı portlar UFW kurallarının etrafından dolaşıyor. Nedenini Docker'a giriş yazısında ayrıntısıyla anlatıyorum.

Artık sunucuya erişim yönetimi iki dosyaya iniyor. Yeni biri gelince açık anahtarını authorized_keys dosyasına ekleyip kullanıcıyı ssh-erisim grubuna alıyorsun, ayrılan biri için tersini yapıyorsun.

Sıradaki yazı: Ubuntu'da güvenlik güncellemelerini otomatiğe bağlamak