Mert Özen Her Satırda Daha İleri
DevOps Linux ve Shell (geliştirici için) — #01

Geliştirici İçin Linux Lab'ı ve SSH: VM Kurmak, Key ile Bağlanmak ve Sunucuyu İlk Turda Tanımak

Mert Özen 14 Kas 2026 15 dk 2 görüntülenme
Geliştirici İçin Linux Lab'ı ve SSH: VM Kurmak, Key ile Bağlanmak ve Sunucuyu İlk Turda Tanımak

Bir Linux sunucusuna şifre yerine ed25519 SSH key'iyle bağlanılır: key'i ssh-keygen ile üretir, ssh-copy-id ile sunucuya koyar, ~/.ssh/config'te bir alias tanımlarsın. Windows'ta karşılıklarıyla birlikte ssh-agent'ı, host key uyarısının neden ciddiye alınması gerektiğini, uname/df/free/ss ile ilk turu, rsync ile Bookstore.Api'yi taşımayı ve 127.0.0.1'e bind edilmiş bir uygulamaya SSH tüneliyle ulaşmayı gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki komutlar Ubuntu Server 24.04.5 LTS (kernel 6.8, OpenSSH 9.6p1, bash 5.2) çalıştıran bir test VM'inde, istemci tarafında OpenSSH 9.6 client'lı bir Ubuntu 24.04 ortamında ve ASP.NET Core 10.0.15 runtime'ıyla denenmiştir. Windows PowerShell komutları Microsoft'un OpenSSH dokümanından aktarılmıştır; Windows'ta çalıştırılmamıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'sini yeni açtığın bir Ubuntu sunucusuna kopyaladın ve çalıştırdın. Sunucunun içinden sorunca cevap geliyor:

ada@bookstore-dev:~$ curl -s http://127.0.0.1:5000/health
{"status":"Healthy"}

Aynı isteği kendi bilgisayarından sunucunun IP adresine atıyorsun:

$ curl -sS -m 5 http://10.20.30.10:5000/health
curl: (7) Failed to connect to 10.20.30.10 port 5000 after 0 ms: Couldn't connect to server

Sence sorun ne: firewall mı, uygulama mı, yoksa ikisi de değil mi? Ve 5000 port'unu dışarı açmadan, kendi makinendeki tarayıcıdan bu API'ye ulaşmanın bir yolu var mı? İkisinin de cevabı yazının sonuna doğru, SSH (uzak makineye şifreli bağlantı kuran protokol) tünelinde.

Bu seri, sistem yöneticisi olmayan ama uygulamasını bir Linux sunucusuna koyan geliştiriciler için. İlk yazıda bir lab kuruyoruz, sunucuya şifre yerine key ile bağlanıyoruz, sunucuyu ilk turda tanıyoruz ve dosya taşıyoruz. Kendi makinendeki terminal ve shell kurulumu Terminal ve Command Line serisinde anlatıldı; burada sunucu tarafındayız.

Lab Seçenekleri: VM, WSL 2 ya da Bulut

Seri boyunca Ubuntu Server 24.04 LTS (uzun süreli destek sürümü) kullanıyoruz; Ubuntu 26.04 ile gelen farkları serinin son yazısında topluyoruz. Lab için Windows'ta WSL (Windows Subsystem for Linux; Windows içinde Linux çalıştıran katman) de bir seçenek. Komutları bozmaktan korkmadan deneyebileceğin bir makineye ihtiyacın var. Üç gerçekçi seçenek:

SeçenekNe zamanDikkat
Lokal VM (Multipass, Hyper-V, VirtualBox)Gerçek bir sunucuya en yakın deneyim: kendi kernel'i, diski, ağ kartı ve SSH'ı varMultipass Windows Pro'da Hyper-V'yi, Windows Home'da VirtualBox'ı kullanır
WSL 2Komutları ve text processing'i denemek için hızlı başlangıçBir sunucu değil; ağ, SSH ve reboot davranışı farklı
Ucuz bir bulut VM'iİnternete açık bir sunucunun gerçek hâlini görmek (SSH saldırı denemeleri, firewall)Saatlik ücret; açık bıraktığın her port gerçekten dünyaya açık

Önerim lokal VM. Windows'ta Multipass ile tek komut yeterli: multipass launch 24.04 --name bookstore-dev --cpus 2 --memory 2G --disk 20G. Multipass'in varsayılanları 1 CPU, 1 GB bellek ve 5 GB disk; .NET runtime ve Nginx'i rahat çalıştırmak için biraz büyütüyoruz. Multipass VM'i cloud-init ile kurar ve kendi key'ini içine koyar; multipass shell bookstore-dev ile hemen girersin. Ama gerçek hayatta sunucuya kendi SSH key'inle bağlanacağın için aşağıdaki adımları yine de yap.

WSL 2 gerçek bir Linux kernel'i çalıştırır ve bu serideki komutların çoğu için yeterli. wsl --install ile kurulan güncel Ubuntu'da systemd varsayılan olarak açık, yani systemctl çalışır. Ama WSL bir sunucu gibi davranmaz: Windows'la ağı paylaşır, kimse ona SSH ile bağlanmaz ve systemd service'leri WSL instance'ını ayakta tutmaz. WSL kurulumu Terminal ve Command Line serisinin ilk yazısında; bu seride WSL'i daha çok "istemci" tarafı olarak kullanacağız, çünkü ssh-copy-id gibi araçlar orada hazır geliyor. Docker container'ı da bir lab gibi görünebilir, ama container kendi kernel'ini çalıştırmaz ve genellikle içinde systemd yoktur; farkı Docker serisinin ilk yazısında anlattık.

Key ile Bağlanmak: ssh-keygen ve ssh-copy-id

Şifreyle bağlanmak çalışır ama iki sorunu var: şifre tahmin edilebilir ve her bağlantıda yazman gerekir. SSH key'i bir çifttir. Private key senin makinende kalır ve asla paylaşılmaz; public key sunucuda ~/.ssh/authorized_keys dosyasına eklenir. Sunucu bağlantıda senden private key'inle bir imza ister ve bunu public key ile doğrular; private key ağdan hiç geçmez.

$ ssh-keygen -t ed25519 -C "ada@my-pc"
Generating public/private ed25519 key pair.
Your identification has been saved in /home/ada/.ssh/id_ed25519
Your public key has been saved in /home/ada/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:tYd+1iuceLFVUiYELrByeWsrkhqberfvyWYFK6epk0A ada@my-pc
$ ls -l ~/.ssh
-rw------- 1 ada ada 444 Nov 14 14:01 id_ed25519
-rw-r--r-- 1 ada ada  91 Nov 14 14:01 id_ed25519.pub

ed25519 kısa, hızlı ve güvenli bir key tipi; OpenSSH 9.5'ten beri ssh-keygen parametresiz çalıştırıldığında da bunu üretiyor, Microsoft'un Windows dokümanı da Windows'taki ssh-keygen.exe için aynı varsayılanı yazıyor. -C yalnızca bir açıklama; key'in nereden geldiğini sunucuda authorized_keys'e bakınca anlamak için işe yarar. Passphrase sorulunca boş geçme: private key dosyası çalınırsa passphrase onu tek başına kullanılamaz hâle getirir. Dosya izinlerine de bak: private key yalnızca sahibinin okuyabileceği -rw------- (600) ile oluşturuldu.

Windows'ta aynı komut PowerShell'de çalışır; Windows 10 (1809) ve sonrasında OpenSSH Client bir Windows özelliği olarak gelir, ssh -V ile kontrol et. Key'ler C:\Users\user\.ssh\ altına yazılır.

Public key'i sunucuya koymanın en kısa yolu ssh-copy-id. Bunun için sunucuya bir kez şifreyle girebilmen gerekir:

$ ssh-copy-id ada@10.20.30.10
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/ada/.ssh/id_ed25519.pub"
The authenticity of host '10.20.30.10 (10.20.30.10)' can't be established.
ED25519 key fingerprint is SHA256:Yn16rbVCEQjPr1Vmyyfc/SVWZTYwiT5ye2Bb7mFEUXs.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
ada@10.20.30.10's password:

Number of key(s) added: 1

Buradaki "authenticity of host" sorusu ayrı bir konu ve aşağıda ona döneceğiz. Windows PowerShell'de ssh-copy-id yok. WSL 2 kullanıyorsan oradaki Ubuntu'da hazır gelir; PowerShell'de kalacaksan public key'i pipe ile sunucudaki dosyanın sonuna ekleyebilirsin:

type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh ada@10.20.30.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Tırnak içindeki kısım sunucuda çalışır. Windows satır sonları (CRLF) sorun olur mu diye merak ettim: sonu \r\n ile biten bir key satırını aynı komutla ekleyip o key ile giriş yaptım, OpenSSH 9.6 sorunsuz kabul etti. Bulut sağlayıcılarda ve Multipass'te ise genellikle bu adıma gerek kalmaz: public key'i VM'i oluştururken verirsin, cloud-init onu yerine koyar ve şifreyle giriş zaten kapalıdır.

~/.ssh/config ve ssh-agent

Her seferinde ssh ada@10.20.30.10 yazmak hem uzun hem hataya açık. ~/.ssh/config dosyasına bir host alias'ı ekle (Windows'ta C:\Users\user\.ssh\config):

Host bookstore-dev
    HostName 10.20.30.10
    User ada
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Artık ssh bookstore-dev yeterli; aynı alias scp, rsync ve Visual Studio Code'un Remote-SSH eklentisi gibi SSH kullanan her araçta çalışır. IdentitiesOnly yes, SSH'ın elindeki bütün key'leri sırayla denemesini engeller; çok key'in varsa sunucu "Too many authentication failures" diyerek seni bağlantıdan atabiliyor. SSH'ın bir host için sonunda hangi ayarları kullanacağını görmek istersen ssh -G işe yarar:

$ ssh -G bookstore-dev | grep -E "^(hostname|user|port|identityfile|identitiesonly) "
user ada
hostname 10.20.30.10
port 22
identitiesonly yes
identityfile ~/.ssh/id_ed25519

Key'e passphrase verdiysen her bağlantıda sorulur. ssh-agent private key'i passphrase çözülmüş hâliyle bellekte tutar ve SSH client'ına imza hizmeti verir:

$ ssh bookstore-dev hostname
Enter passphrase for key '/home/ada/.ssh/id_ed25519':
bookstore-dev
$ eval "$(ssh-agent -s)"
Agent pid 503
$ ssh-add
Identity added: /home/ada/.ssh/id_ed25519 (ada@my-pc)
$ ssh-add -l
256 SHA256:tYd+1iuceLFVUiYELrByeWsrkhqberfvyWYFK6epk0A ada@my-pc (ED25519)
$ ssh bookstore-dev hostname
bookstore-dev

Windows'ta ssh-agent bir Windows service'i ve varsayılan olarak kapalı. Microsoft'un dokümanındaki sıra şöyle: yönetici PowerShell'de Get-Service ssh-agent | Set-Service -StartupType Automatic, ardından Start-Service ssh-agent, sonra normal kullanıcıyla ssh-add $env:USERPROFILE\.ssh\id_ed25519. Bu service key'i Windows oturumunla birlikte saklar, reboot'tan sonra tekrar ssh-add gerekmez.

Host Key Uyarısı Neden Ciddiye Alınmalı

İlk bağlantıdaki "authenticity of host can't be established" sorusu, sunucunun kendisini kanıtlamasıyla ilgili. Senin key'in sunucuya kim olduğunu kanıtlıyor; sunucunun host key'i de sana karşındakinin gerçekten o sunucu olduğunu kanıtlıyor. yes dediğinde fingerprint ~/.ssh/known_hosts'a yazılır ve sonraki her bağlantıda karşılaştırılır. Doğru yol, fingerprint'i başka bir kanaldan doğrulamak: VM'in konsolundan, bulut sağlayıcının seri konsolundan ya da Multipass'te multipass exec ile sunucudaki host key'e bakmak.

ada@bookstore-dev:~$ ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:OPGgW2jQqmd36iAMuiWEl3UFEwdxO47/mRo1GI/gFSI root@bookstore-dev (ED25519)

Bu çıktı, sunucunun host key'lerini yeniden ürettikten sonraki hâli; VM'i silip aynı IP ile yeniden kurduğunda olan şeyin aynısı. Bir sonraki bağlantıda SSH şunu söyledi ve bağlanmayı reddetti:

$ ssh bookstore-dev hostname
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:OPGgW2jQqmd36iAMuiWEl3UFEwdxO47/mRo1GI/gFSI.
...
Offending ECDSA key in /home/ada/.ssh/known_hosts:3
  remove with:
  ssh-keygen -f '/home/ada/.ssh/known_hosts' -R '10.20.30.10'
Host key verification failed.

Bu mesajın iki açıklaması var: ya sunucuyu sen yeniden kurdun, ya da biri araya girdi (man-in-the-middle, yani trafiği arada dinleyip değiştiren bir saldırgan). Hangisi olduğunu bilmeden known_hosts'u silmek, alarm çaldığında alarmı kapatmak demek. Sunucuyu sen yeniden kurduysan ve yeni fingerprint konsolda gördüğünle aynıysa eski kaydı sil ve tekrar bağlan:

$ ssh-keygen -R 10.20.30.10
# Host 10.20.30.10 found: line 1
# Host 10.20.30.10 found: line 2
# Host 10.20.30.10 found: line 3
/home/ada/.ssh/known_hosts updated.
Original contents retained as /home/ada/.ssh/known_hosts.old
$ ssh -o StrictHostKeyChecking=accept-new bookstore-dev hostname
Warning: Permanently added '10.20.30.10' (ED25519) to the list of known hosts.
bookstore-dev

accept-new yeni host'ları sormadan kaydeder ama değişmiş bir key'i yine reddeder; CI ortamlarında kullanılan StrictHostKeyChecking=no'dan bu yüzden çok daha güvenli. GitHub'ın host key'i ve known_hosts konusunu Git & GitHub serisinin "GitHub Hesabına Bağlanmak" yazısında da görmüştük; aynı mekanizma.

Sunucuyu İlk Turda Tanımak

Yeni bir sunucuya girdiğinde ilk beş dakikada "bu makine ne, üstünde ne çalışıyor, ne kadar yeri var" sorularına cevap ararsın. Sırayla:

$ uname -a
Linux bookstore-dev 6.8.0-147-generic #147-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 24 11:33:16 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
$ grep -E "^(PRETTY_NAME|VERSION_CODENAME)=" /etc/os-release
PRETTY_NAME="Ubuntu 24.04.5 LTS"
VERSION_CODENAME=noble
$ hostnamectl
 Static hostname: bookstore-dev
       Icon name: computer-vm
         Chassis: vm
  Virtualization: qemu
Operating System: Ubuntu 24.04.5 LTS
          Kernel: Linux 6.8.0-147-generic
    Architecture: x86-64
 Hardware Vendor: QEMU
$ uptime
 14:05:54 up 1 min,  1 user,  load average: 2.53, 1.16, 0.44
$ whoami; id
ada
uid=1000(ada) gid=1000(ada) groups=1000(ada),27(sudo)

uname kernel sürümünü ve mimariyi verir; .NET publish ederken linux-x64 mi linux-arm64 mi seçeceğini buradan anlarsın. /etc/os-release dağıtımı ve sürümünü söyler; paket kurarken "noble" adını göreceğin yer burası. hostnamectl bunları tek ekranda toplar ve makinenin VM mi, container mı olduğunu da gösterir (çıktıdaki Machine ID ve firmware satırlarını kısalttım). uptime'daki load average'ı dördüncü yazıda ayrıntılı okuyacağız; burada yalnızca makinenin yeni açıldığını görüyoruz. id çıktısındaki 27(sudo), bu kullanıcının sudo ile root yetkisi alabileceği anlamına geliyor; user, group ve sudo üçüncü yazının konusu.

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           197M 1008K  196M   1% /run
/dev/vda1        19G  1.7G   17G   9% /
tmpfs           984M     0  984M   0% /dev/shm
/dev/vda16      881M   64M  756M   8% /boot
/dev/vda15      105M  6.2M   99M   6% /boot/efi
$ free -h
               total        used        free      shared  buff/cache   available
Mem:           1.9Gi       297Mi       1.5Gi       1.0Mi       313Mi       1.6Gi
Swap:             0B          0B          0B

df -h'te asıl bakacağın satır /: 19 GB'ın 1.7 GB'ı dolu. Production'da "disk doldu" vakalarının çoğu buradan başlar ve onuncu yazıda bir tanesini baştan sona çözeceğiz. free -h'te "free" değil "available" sütununa bak; Linux boştaki belleği dosya cache'i için kullanır ve gerektiğinde geri verir. Bu VM'de swap da yok, cloud image'larda bu yaygın.

$ ss -tlnp
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*
LISTEN 0      4096   127.0.0.53%lo:53        0.0.0.0:*
LISTEN 0      4096      127.0.0.54:53        0.0.0.0:*
LISTEN 0      4096            [::]:22           [::]:*
$ sudo ss -tlnp
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=1053,fd=3),("systemd",pid=1,fd=58))
LISTEN 0      4096   127.0.0.53%lo:53        0.0.0.0:*    users:(("systemd-resolve",pid=520,fd=15))
LISTEN 0      4096      127.0.0.54:53        0.0.0.0:*    users:(("systemd-resolve",pid=520,fd=17))
LISTEN 0      4096            [::]:22           [::]:*    users:(("sshd",pid=1053,fd=4),("systemd",pid=1,fd=59))

ss -tlnp "bu makinede kim hangi TCP port'unu dinliyor" sorusunun cevabı: -t TCP, -l yalnızca dinleyenler, -n port'ları isim yerine sayı olarak, -p process'i göster. İki satır dikkat çekici. Birincisi, sudo olmadan Process sütunu boş kalıyor; başka kullanıcıların process'lerini görmek için yetki gerekiyor. İkincisi, 22 numaralı port'u hem sshd hem PID (process kimliği) 1 olan systemd tutuyor. Ubuntu 22.10'dan beri SSH socket activation ile geliyor: port'u systemd açıyor, bağlantı gelince sshd'yi başlatıyor. 127.0.0.53'teki ise Ubuntu'nun lokal DNS resolver'ı. Port ve bind adresi ayrıntıları sekizinci yazıda.

Bir komutu hatırlamadığında üç kaynak var. ls --help kısa bir özet verir. man ss tam dokümandır; / ile içinde arama yapar, q ile çıkarsın. man -k ^ssh ise adı ssh ile başlayan bütün man page'leri listeler ve aradığın aracın adını hatırlamadığında kurtarır. Örnek odaklı kısa açıklamalar için tldr projesi var; ama dikkat: Ubuntu 24.04 deposundaki iki client'ı da (tldr 0.9.2 ve tealdeer 1.6.1) denedim, ikisi de sayfa arşivini indiremedi. Sebebi, eski client'ların indirdiği tldr.sh/assets/tldr.zip adresinin artık zip yerine bir HTML yönlendirme sayfası döndürmesi. tldr.sh sitesinden okumak ya da daha yeni bir client kullanmak gerekiyor.

Dosya Taşımak ve SSH Tüneli

Bookstore.Api'nin bu seri için sade hâli yalnızca /health ve /api/books endpoint'lerini sunuyor. Kendi makinende dotnet publish src/Bookstore.Api -c Release -r linux-x64 --self-contained false -o out/publish ile publish edip çıktıyı sunucuya taşıyoruz. Sunucuda ASP.NET Core runtime'ının kurulu olması gerekiyor (sudo apt install aspnetcore-runtime-10.0; paket yönetimi dokuzuncu yazıda).

$ rsync -avz out/publish/ bookstore-dev:~/bookstore-api/
sending incremental file list
created directory /home/ada/bookstore-api
./
Bookstore.Api
Bookstore.Api.deps.json
Bookstore.Api.dll
Bookstore.Api.pdb
Bookstore.Api.runtimeconfig.json
Bookstore.Api.staticwebassets.endpoints.json

sent 51,191 bytes  received 179 bytes  9,340.00 bytes/sec
total size is 109,009  speedup is 2.12
$ rsync -avz out/publish/ bookstore-dev:~/bookstore-api/
sending incremental file list

sent 181 bytes  received 12 bytes  42.89 bytes/sec
total size is 109,009  speedup is 564.81

İkinci çalıştırmada hiçbir dosya gönderilmedi: rsync iki taraftaki dosyaları karşılaştırıp yalnızca farkı taşır. -a izinleri ve zamanları korur, -z sıkıştırır. Kaynak yolun sonundaki / önemli: out/publish/ klasörün içeriğini, out/publish ise klasörün kendisini kopyalar. Tek dosya için scp out/publish/Bookstore.Api.dll bookstore-dev:/tmp/ yeterli; OpenSSH 9.0'dan beri scp arka planda SFTP (SSH üzerinden dosya aktarım protokolü) kullanıyor, eski bir sunucuyla uyumsuzluk çıkarsa -O eski protokole döner. Windows'ta scp hazır gelir ama rsync gelmez; rsync için WSL 2 en kolay yol.

Şimdi tahminin cevabı. Sunucuda uygulamayı yalnızca loopback adresine bind ederek başlattım:

ada@bookstore-dev:~$ cd ~/bookstore-api && nohup dotnet Bookstore.Api.dll --urls http://127.0.0.1:5000 > ~/api.log 2>&1 &

127.0.0.1'e bind edilen bir process yalnızca aynı makinenin içinden gelen bağlantıları kabul eder. Dışarıdan gelen istek firewall'a bile varmadan "Couldn't connect" ile döner, çünkü o adreste dinleyen kimse yok. Bu bir hata değil, istenen davranış: Nginx gibi bir reverse proxy önünde duracak bir uygulama dışarı açılmamalı. Peki sen geliştirici olarak buna nasıl ulaşırsın? SSH local port forwarding ile:

$ ssh -f -N -L 5000:127.0.0.1:5000 bookstore-dev
$ curl -s http://localhost:5000/api/books
[{"isbn":"9780441172719","title":"Dune","price":14.99},{"isbn":"9780141439587","title":"Emma","price":9.50},{"isbn":"9780547928227","title":"The Hobbit","price":12.00}]

-L 5000:127.0.0.1:5000 şunu söylüyor: kendi makinemde 5000'i dinle, gelen her bağlantıyı SSH içinden sunucuya taşı ve sunucunun gözünden 127.0.0.1:5000'e bağla. -N uzakta komut çalıştırma, -f arka plana geç demek. Tüneli kapatınca (ssh process'ini durdurunca) aynı istek yine "Couldn't connect" döndü. Sunucuda yalnızca localhost'tan erişilebilen bir SQL Server'a da aynı yöntemle bağlanırsın: ssh -N -L 14333:127.0.0.1:1433 bookstore-dev ve SSMS (SQL Server'ın masaüstü yönetim aracı) ya da connection string'de Server=127.0.0.1,14333. Veritabanı port'u internete hiç açılmaz, erişim SSH key'in kadar güvenli olur. Sunucunun 1433'e eriştiği başka bir makineye de tünel kurabilirsin: -L 14333:db-host:1433'te hedef adres sunucunun gözünden çözülür.

Üretimde Dikkat

Production sunucusunda kişisel key kullan ve key'i paylaşma. Ekipte herkesin kendi key'i authorized_keys'te ayrı bir satır olmalı; biri ayrıldığında yalnızca onun satırını silersin. Ortak bir "deploy key"i Slack'te dolaştırmak, kimin ne yaptığını ayırt edememek ve ayrılan birinin erişimini kapatamamak demek. CI'dan deploy için ayrı, yalnızca o işe yetkili bir key kullan; GitHub Actions serisinin SSH ile deploy yazısında bu kurulum var.

Bu VM'de cloud-init ssh_pwauth ayarıyla şifreyle girişe izin verdi; sshd -T etkin değeri gösteriyor:

ada@bookstore-dev:~$ sudo sshd -T | grep -E "^(passwordauthentication|permitrootlogin|pubkeyauthentication) "
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication yes

Key ile girişin çalıştığını gördükten sonra şifreyle girişi kapatmak, internete açık bir sunucuda yapılacak ilk işlerden biri. /etc/ssh/sshd_config.d/ altındaki dosyaların sırası ve kendini sunucudan kilitlememek için "ikinci oturum" kuralı on birinci yazıda. Bir de tünelleri unutma: arka planda açık kalan ssh -f -L process'leri port'u tutmaya devam eder; aynı port'a ikinci bir tünel açmaya çalışınca bind [127.0.0.1]:5000: Address already in use hatasını alırsın.

Sık Düşülen Tuzaklar

Private key'in izinlerini gevşek bırakmak. Key'i bir yerden kopyaladığında ya da zip'ten çıkardığında izinler 644 olabilir ve OpenSSH key'i kullanmayı reddeder:

$ chmod 644 ~/.ssh/id_ed25519 && ssh bookstore-dev hostname
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/ada/.ssh/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/home/ada/.ssh/id_ed25519": bad permissions
ada@10.20.30.10: Permission denied (publickey,password).

Çözüm chmod 600 ~/.ssh/id_ed25519. Son satıra dikkat: hata "bad permissions" ama bağlantı "Permission denied" diye bitiyor; sorunu sunucuda aramaya başlamadan önce yukarıdaki satırları oku.

Root ile şifreyle bağlanmak. Ubuntu'nun varsayılanı root'a şifreyle girişi zaten engelliyor (permitrootlogin without-password) ve bunu açmak, internete açık her sunucuya gelen tahmin denemelerine tam yetkili bir hedef vermek demek. Kendi kullanıcınla gir, gerektiğinde sudo kullan.

Private key'i paylaşmak ya da public key yerine private key'i sunucuya koymak. authorized_keys'e giden dosya her zaman .pub uzantılı olan. Private key bir kez başka bir yere gittiyse onu sızmış say: yeni bir çift üret, eski public key'i her sunucudan sil.

Host key uyarısını alışkanlıkla geçmek. StrictHostKeyChecking=no ya da her uyarıda known_hosts'u silmek, SSH'ın sana sunduğu tek man-in-the-middle korumasını kapatır.

Küçük Bir Deneme

★☆☆ Multipass ya da Hyper-V ile bookstore-dev adında bir Ubuntu Server 24.04 VM'i aç. ed25519 key'ini üret, sunucuya koy, ~/.ssh/config'e alias ekle ve ssh bookstore-dev ile şifresiz bağlan. İlk turdaki komutları çalıştırıp kendi VM'inin kernel sürümünü, disk boyutunu ve dinleyen port'larını not et.

★★☆ Bookstore.Api'yi publish et, rsync ile sunucuya taşı ve 127.0.0.1:5000'de çalıştır. Önce dışarıdan ulaşamadığını, sonra ssh -L tüneliyle tarayıcından http://localhost:5000/api/books'u açabildiğini gör. Aynı tüneli ~/.ssh/config'e LocalForward 5000 127.0.0.1:5000 satırıyla kalıcı hâle getir.

★★★ VM'i silip aynı IP ile yeniden oluştur (ya da sunucuda host key'leri yeniden üret) ve REMOTE HOST IDENTIFICATION HAS CHANGED uyarısını kendin gör. Yeni fingerprint'i VM'in konsolundan doğrulamadan known_hosts'a dokunma; doğruladıktan sonra ssh-keygen -R ile eski kaydı temizle.

Artık sunucuya key ile giriyor, ne olduğunu biliyor ve dosya taşıyabiliyorsun. Bir sonraki yazıda sunucunun içinde dolaşacağız: /etc, /var, /opt ve /proc'un ne işe yaradığını, Bookstore'un dosyalarının nereye konacağını ve current symlink'iyle release klasörlerinin nasıl çalıştığını göreceğiz.