24 Eylül 2025 Çarşamba

Ansible 101 - Uygulamalı Başlangıç Rehberi

    Bir master, üç tane de worker olmak üzere 4 ubuntu'dan oluşan basit bir lab kurdum. Master ubuntu makineme ansible yükledim ve ssh public keyleri worker makinelerime kopyaladım. Böylelikle ansible workerlar ile şifresiz doğrudan iletişim kurabilecek. 


SSH key oluşturduğum komut:
ssh-keygen -t rsa -b 4096

Anahtarı worker'lara kopyaladım:
cat ~/.ssh/id_rsa.pub | ssh [email protected] "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Multipass tarafı için aşağıdaki adımları uygulamadım: worker'de aşağıdaki kodlar çalıştırılır ve ubuntu user ile master makinede oluşturulan key içeriği manuel eklenir:

ssh key cat

worker ssh key ekleme


Lab için Ansible dizini oluşturdum:
mkdir -p ~/ansible-lab && cd ~/ansible-lab

İlk denemeler için bir envanter dosyası oluşturdum:
vi hosts

Bu dosyanın içeriğini worker'larıma göre doldurdum:
[web]
192.168.1.110

[db]
192.168.1.111

[prod]
192.168.1.112

[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/id_rsa

İlk ansible komutumu denedim:
ansible all -i hosts -m ping

Bu komut, hosts envanter dosyasında tanımlı tüm sunuculara SSH ile bağlanır ve Ansible’ın ping modülünü çalıştırarak bağlantıyı test eder. Başarılıysa her sunucudan "pong" cevabı döner.

Ansible Ping


Bu komutu bir .sh dosyasına yazıyorum ve sh dosyasını çalıştırarak daha hızlı bir şekilde yanıt almak istiyorum:
vi pingle.sh

Dosya içeriği:
#!/bin/bash
ansible all -i hosts -m ping

Dosyaya çalıştırma izni: 
chmod +x pingle.sh

Test:
./pingle.sh

Worker'larda sudo yetkisi gerektiren komutları çalıştırmak istediğimde ya şifre sormasını sağlamamız gerekiyor yada şifresiz sudo yetkisini her bir workerda tanımlamak gerekiyor. (test ortamı için çok uygun) Örnek üzerinden bakalım. Tüm worker'larıma htop paketini yüklemek istiyorum, şifre sormasını istiyorsam bu komuttan yararlanabilirim:
ansible all -i hosts -m apt -a "name=htop state=present" --become --ask-become-pass

Ancak şifre sormadan doğrudan komutu koşmak için bir kerelik sudo şifresiz yetkisini workerlara tek tek tanımlamam gerekiyor.
echo "ubuntu ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/ubuntu
sudo chmod 440 /etc/sudoers.d/ubuntu

Böylece şimdi koşacağım komutu şifre yazmadan veya girmeden doğrudan worker'larıma gönderebilirim:
ansible all -i hosts -m apt -a "name=htop state=present" --become

Ve aynı anda tüm makinelerime htop kurulumunu yapabildim:

htop

Bunu paket kurulumu adı altında bir .sh dosyasına da dönüştürebiliriz:


package_install.sh dosyası oluşturdum ve içeriğini bu şekilde doldurdum:
#!/bin/bash

# Kurulacak paketin adı (örnek: htop, curl, nginx, vs)
PAKET_ADI="htop"

# Ansible komutu
ansible all -i hosts -m apt -a "name=$PAKET_ADI state=present" --become

Böylece kurmak istediğim farklı paketler olduğunda içerisine girip paket adını güncellemem yeterli olabilir.

Dosyaya çalıştırma izni vermeyi de unutmuyoruz:
chmod +x paket_kur.sh

Şimdi ise ilk Playbook'umuzu deneyelim, birden fazla paket kurulumunu bir yml dosyası ile host'larımıza gönderelim.


vi package_install.yml dosyasını oluşturdum ve aşağıdaki bilgileri ekledim:

---
- name: Paket kurulumları
  hosts: all
  become: yes

  tasks:
    - name: htop kur
      apt:
        name: htop
        state: present

    - name: curl kur
      apt:
        name: curl
        state: present

    - name: git kur
      apt:
        name: git
        state: present

Ardından playbook dosyamı koşmak için bu komutu kullanıyorum:
ansible-playbook -i hosts package_install.yml

Playbook - Package Installation

    Ve böylece tek bir yml dosyasını çalıştırarak ansible sayesinde tüm hosts makinelerime aynı anda birden fazla uygulama kurulumunu gerçekleştirmiş oldum. Makinelere tek tek bağlanmadım, paketleri tek tek kurmayı denemedim ve zaman kaybı yaşamadım.


Tüm makineleri kapatmak için bir .sh dosyası
#!/bin/bash

# Ansible inventory dosyasını belirt
INVENTORY_FILE="hosts"

# Tüm makineleri anında kapat
ansible all -i "$INVENTORY_FILE" -a "/sbin/shutdown now" --become

chmod +x shutdown-all.sh

Tüm makinelerde bir klasör oluşturmak için: create_folder.sh
#!/bin/bash

# Ansible inventory dosyası
INVENTORY_FILE="hosts"

# Uzak makinelerde klasör oluştur
ansible all -i "$INVENTORY_FILE" -m file -a "path=/tmp/yeni_klasor state=directory mode=0755"
chmod +x create_folder.sh

Bir istisna, tüm hostlara değil, özel bir gruba işlem yapmak için ek bir parametre kullanılır:
ansible-playbook -i hosts package_install.yml --limit web

Command modülü ile uptime bilgisini öğrenme:
ansible all -i hosts -m command -a "uptime"

Copy komutu ile dosyaları hostlara kopyalamak:
ansible all -i hosts -m copy -a "src=dosya.txt dest=/tmp/dosya.txt"

uptime

Şimdi ise bir playbook ile hem paket kurulumu hem de bir dosya oluşturarak içerisine veri yazmayı deneyelim:


Playbook 2

- name: Paket kur ve dosya kopyala
  hosts: all
  become: yes
  tasks:
    - name: htop paketini kur
      apt:
        name: htop
        state: present

    - name: Bilgi dosyasını kopyala
      copy:
        content: "Bu dosya Ansible tarafından oluşturulmuştur\n"
        dest: /tmp/ansible_bilgi.txt
        owner: ubuntu
        group: ubuntu
        mode: '0644'

Sırada bir servis kurmak ve o servisin çalışıp çalışmadığını check etmek var:


apache_setup.yml dosyasını oluşturuyorum ve içeriğini aşağıdaki gibi yapıyorum:

- name: Apache kurulumu ve servisi yönetimi
  hosts: all
  become: yes
  tasks:
    - name: Apache2 paketini kur
      apt:
        name: apache2
        state: present
        update_cache: yes

    - name: Apache servisini başlat
      service:
        name: apache2
        state: started
        enabled: yes

Bu servisi sadece web hostlarıma kurmak istedim, o yüzden bu şekilde bir komut koşuyorum:
ansible-playbook -i hosts apache_setup.yml --limit web

Ve servislerin durumunu check etmek istiyorum:

apache2 ansible service check


When kullanarak bir yml dosyasının içerisine koşul ekleyebiliriz. Ancak bunu zaten --limit komutu ile de yapabilyoruz:


- name: Koşullu görev örneği
  hosts: all
  become: yes
  tasks:
    - name: Sadece worker1'de bu dosyayı oluştur
      copy:
        content: "Bu dosya sadece worker1 içindir\n"
        dest: /tmp/worker1_ozel.txt
      when: inventory_hostname == "worker1"

Gruplamayı yml dosyasının içerisinde de yapabiriz, böylelikle when koşulu veya --limit kullanmadan birden fazla görevi aslında yml dosyasında tek tek host gruplarına atamak mümkün:


Yeni bir yml dosyası oluşturdum: group_specific_tasks.yml 
Bu yml dosyası sadece web ve db makinelerime işlem yapacak ve bu işlem örnekteki gibi bir file oluşturma işlemi olacağı gibi, app kurulumu, güncellemesi vs. gibi herhangi bir işlem olabilir:

- name: Web grubuna özel görev
  hosts: web
  become: yes
  tasks:
    - name: Web makinesine özel bilgi dosyası
      copy:
        content: "Bu worker bir WEB sunucusudur.\n"
        dest: /tmp/web_bilgi.txt

- name: DB grubuna özel görev
  hosts: db
  become: yes
  tasks:
    - name: DB makinesine özel bilgi dosyası
      copy:
        content: "Bu worker bir VERİTABANI sunucusudur.\n"
        dest: /tmp/db_bilgi.txt

Ansible Roles Yapısının Kullanımı

    Ansible'da roles, yapılandırmaları görev, değişken, şablon, handler gibi mantıksal klasörlere ayırır.
Gerçek projelerde playbook'lar roles kullanılarak kurulur.

Örneğin webserver için bir role oluşturmak istiyorum, bu durumda aşağıdaki komutu çalıştırıyorum:
ansible-galaxy init roles/webserver

Ansible bu role için kendi özel yapısını oluşturur:


Ansible Roles

Tasks/main.yml içerisine aşağıdaki konfiği girdim:

- name: Apache kur
  apt:
    name: apache2
    state: present
    update_cache: yes

- name: Apache2 servis başlasın ve aktif kalsın
  service:
    name: apache2
    state: started
    enabled: yes

- name: Özel hoşgeldiniz sayfası
  copy:
    content: "Merhaba Hakan! Apache başarıyla çalışıyor.\n"
    dest: /var/www/html/index.html
    owner: root
    group: root
    mode: '0644'

Projeyi çalıştırabilmek için ana dizinimde (benim için ansible lab çalışmalarını yaptığım ve diğer hosts dosyamın bulunduğu yer) site.yml isimli bir dosya oluşturuyorum ve aşağıdaki gibi konfig giriyorum:

- name: Web makinelerine apache kur, servisi başlat, aktif kalsın ve özel hoşgeldin sayfası oluştur.
  hosts: web
  become: yes
  roles:
    - webserver

 Böylelikle site.yml dosyasını playbook ile koştuğumda webserver rolünde daha önce tanımladığım tüm işlemler gerçekleştiriliyor. Daha işlevsel bir yapıya kavuşmuş oluyorum. Her şey tek bir yml dosyasında olmaktansa görev ve sorumluluklara göre yani rollere göre yml dosyalarını aslında ayırmış oluyorum.

ansible-playbook -i hosts site.yml

sudo systemctl status apache2
sudo systemctl restart apache2
ss -tuln | grep :80
sudo ufw allow 80
sudo ufw allow 443
sudo ufw reload
sudo ufw status

    Ve ardından kendi ana bilgisayarımın tarayıcısını kullanarak http://192.168.1.110:80 bağlantısına gitmeyi çalıştığımda worker1 isimli makinemde çalışan apache2 üzerindeki sayfaya ulaşabildim. Ansible ile gönderilen ve sayfa girişine eklenen yazı da karşıma çıkmış oldu. Böylece çalışmayı başarıyla tamamladım.

Apache2 Main Page

Handlers Kullanımına Örnek

    Bu deneme için init komutunu tekrar koştum ve ikinci bir dizin oluşmasını sağladım, ikinci bir role gibi. ansible-galaxy init roles/webroles2-handlers ve ilgili rolü koşmak için yeni bir yml dosyası oluşturarak ilerledim.

Ansible Handlers


    Handlers, bir görev değişiklik algılandığında tetiklenen özel görevlerdir. Genellikle servisleri yeniden başlatmak, yeniden yüklemek gibi işlemler için kullanılır. Bizde Apache web sayfamızın içeriğinde bir değişiklik algılandığında örnek olması için bir tetikleyici kurgulayacağız ve sistem bu değişikliği algılayarak otomatik olarak apache servisini restart edecek. Bu sadece basit bir örnek, ihtiyaca göre buradaki handlers yeteneklerinden farklı şekillerde yararlanılabilir.


roles/webroles2-handlers/tasks/main.yml içerisine bir ekleme yapıyorum, daha önce eklediğimiz görevlere ek olarak:

---
- name: Apache web sunucusunun kurulu olduğundan emin olun
  ansible.builtin.apt:
    name: apache2
    state: present
    update_cache: yes
  become: yes
  notify: restart_apache_service

- name: Ana sayfa içeriğini değiştirin
  ansible.builtin.copy:
    content: "<html><body><h1>Ansible Handler Pratiği Başarılı!</h1></body></html>"
    dest: /var/www/html/index.html
  become: yes
  notify: restart_apache_service

Şimdi ise Handler dosyasını oluşturarak içerisine tetikleyicimizi ekliyoruz:

vi roles/webroles2-handlers/handlers/main.yml

---
- name: restart_apache_service
  ansible.builtin.service:
    name: apache2
    state: restarted
  become: yes

Şimdi ise Playbook'u oluşturalım: vi 11-handlers-web-webroles.yml

---
- name: Web server rolünü çalıştırma
  hosts: web
  roles:
    - webroles2-handlers

Şimdi ise tetikleyiciyi test edebiliriz:
ansible-playbook -i hosts 11-handlers-web-webroles.yml

Handler Ornek


    Bu çıktı bize şunu söylüyor: Ansible, web sayfasının içeriğinin değiştiğini algıladı. Bu değişiklik nedeniyle, playbook'taki diğer görevler tamamlandıktan sonra, tanımladığınız restart_apache_service handler'ını çalıştırdı. Handler başarılı bir şekilde çalışarak Apache'yi yeniden başlattı ve playbook'unuzu hatasız bir şekilde tamamladı.

Jinja2 Şablon Kullanımına Örnek


Öncelikle yeni bir role oluşturuyorum, bu 12. yml file'ım olmuş olacak.

#ansible-galaxy init info_creator

/roles/info_creator/templates/ dizini altında bilgi.txt.j2 isimli bir dosya oluşturdum ve içeriğini aşağıdaki gibi güncelledim:

Burada toplanan verilere aynı zamanda Ansible Facts ismi verilir.

Bu sunucuya ait bilgiler: Hostname: 
{{ ansible_hostname }} 
IP Adresi: {{ ansible_host }} 
İşletim Sistemi: {{ ansible_distribution }} {{ ansible_distribution_version }} 
Tarih: {{ ansible_date_time.iso8601 }}

/tasks/main.yml içerisine de ilgili ana görevimi ekliyorum:

---
- name: Dinamik bilgi dosyasını oluşturun
  ansible.builtin.template:
    src: bilgi.txt.j2
    dest: /home/ubuntu/bilgi.txt
    owner: ubuntu
    group: ubuntu
    mode: '0644'
  become: yes

ve artık playbook'u oluşturup koşabilirim:

#vi 12-create_info_jinja2_file.yml

ve içerisine:

---
- name: Dinamik bilgi dosyasını oluşturma
  hosts: web
  roles:
    - info_creator

Artık playbook'u koştuktan sonra ilgili makinenin /home/ubuntu/ dizininde yeralan bilgi.txt dosyasını kontrol edebilir ve dinamik bir şekilde bu belgenin güncellendiğini görebilirim:

jinja2


    Jinja2 şablonları, statik dosyaları olduğu gibi kopyalamak yerine, dinamik olarak oluşturmanızı sağlar. Bu şablonlar, her sunucuya özel bilgileri (IP adresi, hostname, port gibi) kullanarak yapılandırma dosyalarını otomatik olarak doldurabilir. Örneğin, bir web sunucusu için Apache sanal host yapılandırma dosyası oluştururken ServerName ve DocumentRoot gibi değerleri sunucunun hostname'ine göre dinamik olarak ayarlayabilirsiniz. Bu sayede, farklı sunucular için ayrı ayrı dosyalar oluşturmak yerine tek bir şablonu kullanabilir ve kod tekrarını önleyebilirsiniz. Aynı zamanda veritabanı yapılandırma dosyalarında bellek ayarları gibi değerleri, sunucunun özelliklerine göre belirlemek için de Jinja2 şablonlarından yararlanabilirsiniz.

    Diyelimki zabbix server'ınız var ve zabbix agentları onlarca makineye kurdunuz ve konfigüre ettiniz. Çeşitli nedenlerle zabbix server IP'ni değişti. Gidip tek tek makinelerinizdeki zabbix conf dosyasını düzenlemenize gerek yok, bunu bir yml file ile ansible kullanarak kolaylıkla halledebilirsiniz.

    Bir başka örnek, makinelerinize cronjob ekleyebilirsiniz. Hatta düzenli cronjoblarınız varsa, envanterinize yeni eklenen bir makine olduğunda crontab'ına ilgili jobların eklenmesini de otomatikleştirebilirsiniz. Cronjob yml dosyanızı bir scriptin içerisine yazarsınız ve ilgili scripti de ansible kurulu makinenizin crontab'ine istediğiniz zaman aralığına göre eklerseniz. Bundan böylelikle sadece envanter dosyanızı yani hosts file'ı güncellemeniz yeterli olacaktır. Crontab otomatik zamanı geldiğinde eklenecek.

    Veya farklı bir örnek, NTP sunucusu kurmak istediniz. Bunu elbette Crony ile yapabilirsiniz. Hem kurulumarı hem de ilgili konfigürasyonu ansible ile koşmanız mümkün. Birkaç dakikada tüm makinelerinizin baktığı NTP sunucusunu güncellenir. Ansible bu ve benzeri birden fazla esneklik ve kolaylık sağlıyor. Zaman zaman bazı örnekler paylaşmaya devam edeceğim, şimdilik bu yazıyı burada sonlandırıyorum.

Docker Buildx ile Çok Mimarili (amd64/arm64) Container İmajı Oluşturma Rehberi

Giriş

Apple Silicon (ARM64), Raspberry Pi ve buluttaki x86 sunucular gibi farklı mimarileri hedeflemek artık modern uygulamalar için kaçınılmaz. Tek bir imajın hem amd64 hem de arm64 için çalışması, dağıtım ve bakım yükünü azaltır. Bu rehberde, Docker Buildx kullanarak çok mimarili (multi-arch) container imajı oluşturmayı, kayıt depolarına (registry) göndermeyi ve doğrulamayı adım adım anlatıyorum. Ayrıca pratik ipuçları ve sık karşılaşılan sorunlara da değineceğim.

Önkoşullar

- Docker Desktop 4.x (macOS/Windows) veya Docker Engine 24+ (Linux). Çoğu sistemde Buildx ve QEMU desteği dahili gelir.
- Registry hesabı (Docker Hub veya GitHub Container Registry: ghcr.io).
- Terminal erişimi ve temel Docker komutlarına aşinalık.

Adım 1: Buildx builder oluşturun

Linux sunucularda QEMU emülasyonu gerekebilir. Gerekirse şu komutla kurun:
docker run --privileged --rm tonistiigi/binfmt --install all

Buildx builder oluşturun ve varsayılan olarak atayın:
docker buildx create --name multi --driver docker-container --use
Kurulumun aktif olduğunu doğrulayın:
docker buildx ls

Adım 2: Örnek Dockerfile (Node.js)

Aşağıdaki Dockerfile, hem amd64 hem de arm64 için derlenebilen basit bir Node.js uygulamasını paketler. Dikkat edilmesi gereken nokta, derleme aşamasında --platform=$BUILDPLATFORM kullanımıdır; bu, builder konteynerinin mimarisine uygun taban imajı çekerek tutarlı derleme sağlar.

# syntax=docker/dockerfile:1.6
ARG NODE_VERSION=20-alpine
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build || echo "derlenecek bir şey yok"

FROM node:${NODE_VERSION}
WORKDIR /app
COPY --from=builder /app ./
EXPOSE 3000
CMD ["node", "server.js"]

Not: Base imaj olarak node:20-alpine çok mimarili manifest içerir. Uygulamanız Go, Rust veya Python ise benzer yaklaşımla çok mimari derleme yapabilirsiniz. Go için CGO_ENABLED=0 gibi bayraklarla statik build tercih edilebilir.

Adım 3: Çok mimarili build ve push

Önce kayıt deposuna giriş yapın:
docker login ghcr.io

Tek komutla her iki mimari için imaj üretip manifest list olarak push edin:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/kullanici/uygulama:1.0 \
-t ghcr.io/kullanici/uygulama:latest \
--push .

Build süresini kısaltmak için --provenance=false veya uzak cache kullanabilirsiniz:
--cache-to type=registry,ref=ghcr.io/kullanici/uygulama:cache,mode=max
--cache-from type=registry,ref=ghcr.io/kullanici/uygulama:cache

Doğrulama

Manifest’ı inceleyin ve mimarileri kontrol edin:
docker buildx imagetools inspect ghcr.io/kullanici/uygulama:latest

Çıktıda Platforms: linux/amd64, linux/arm64 görmelisiniz. Farklı cihazlarda docker run çalıştırdığınızda, Docker otomatik olarak uygun mimariye karşılık gelen imajı çekecektir.

Çalıştırma

Yerelde test etmek için:
docker run -p 3000:3000 ghcr.io/kullanici/uygulama:latest
Sunucu başlatıldıktan sonra http://localhost:3000 üzerinden uygulamayı doğrulayın.

Sık karşılaşılan sorunlar ve ipuçları

Manifest bulunamadı: “no matching manifest” hatası alıyorsanız, seçtiğiniz base imajın çok mimarili desteği olmayabilir. Debian/Alpine gibi resmi imajların çoğu multi-arch’tır; özel imajlar için alternatife geçin.

Illegal instruction: Özellikle kriptografik kütüphaneler kullanan Node/Go projelerinde CPU özelliği uyumsuzluğu görülebilir. Derleme sırasında hedef mimarinin özellikleriyle uyumlu bayraklar kullanın veya musl/glibc farklarını göz önünde bulundurun.

Performans yavaşlığı: QEMU emülasyonu altında yerel olmayan mimariyi derlemek yavaş olabilir. Mümkünse arm64 runner (ör. Graviton) veya amd64 runner kullanarak native build tercih edin.

Deterministik build: npm ci, package-lock.json ve versiyon sabitleme kullanın. Go için mod dosyalarını kilitleyin, Python için requirements.txt sabitleyin.

--platform ve --build-arg: Dockerfile içinde FROM --platform=$BUILDPLATFORM kullanın, ancak çalışma katmanını mimari bağımsız tutmaya özen gösterin. Yer yer TARGETPLATFORM ile koşullu bağımlılık kurabilirsiniz.

GitHub Actions ile otomasyon (kısa örnek)

name: Build and Push Multi-Arch
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/kullanici/uygulama:latest

Sonuç

Docker Buildx ile çok mimarili imaj üretmek, farklı cihaz ve bulut altyapılarında aynı etiketle sorunsuz dağıtım yapmanızı sağlar. Doğru base imaj seçimi, QEMU/runner stratejisi ve cache kullanımıyla süreç hem hızlı hem de tekrarlanabilir hale gelir. Bu rehberdeki adımları CI/CD hattınıza taşıyarak, amd64 ve arm64 dünyalarını tek bir manifest altında birleştirebilirsiniz.

23 Eylül 2025 Salı

Next.js 14’te Server Actions ile Güvenli Form ve Dosya Yükleme: Adım Adım Rehber

Giriş

Next.js 14 ile birlikte hayatımıza giren Server Actions, form işlemleri ve veri mutasyonlarını API katmanı yazmadan, doğrudan sunucuda çalıştırılabilir fonksiyonlarla yönetmemizi sağlıyor. Bu yaklaşım hem güvenliği artırıyor hem de geliştirici deneyimini iyileştiriyor. Bu yazıda, App Router mimarisi altında Server Actions kullanarak güvenli form işleme ve dosya yükleme senaryosunu adım adım ele alacağız. Ayrıca performans, SEO ve güvenlik tarafındaki püf noktalarına da değineceğiz.

Önkoşullar ve Kurulum

Başlamadan önce Node.js 18+ ve Next.js 14 ortamınız hazır olmalı. Projeyi TypeScript ile başlatmanızı öneririm; tip güvenliği hem sunucu eylemlerinde hem de form verisi doğrulamada büyük kolaylık sağlar. App Router (app/ dizini) kullanıyorsanız, sayfalarınız React Server Components olarak varsayılan şekilde sunulur; bu sayede istemciye daha az JavaScript gönderilir ve SEO performansınız artar.

Server Actions Mantığı

Server Actions, bir fonksiyonun başına "use server" yönergesi eklenerek sunucuda çalıştırılmasını sağlar. Bu fonksiyonları doğrudan formun action props’una bağlayabilir ve formData alabilirsiniz. Ek bir API route yazmanız gerekmez. İş mantığı sunucuda kaldığı için token, bağlantı string’i ve gizli anahtarlar istemciye sızmaz. İşlem sonrası önbelleği tazelemek için revalidatePath() veya revalidateTag() çağırabilirsiniz.

Form Doğrulama Stratejisi

Doğrulama iki katmanda yapılmalı: istemci tarafında kullanıcı deneyimini iyileştiren anlık kontroller (örneğin e-posta formatı), sunucu tarafında ise kaynak güvenliği sağlayan kesin doğrulamalar. Server Actions içinde zod gibi bir şema doğrulama kütüphanesi kullanarak tip güvenli ve anlaşılır hata mesajları üretin. Hataları action’dan geriye döndürüp formda kullanıcıya göstermek, klasik API isteklerine göre daha yalın bir akış sağlar.

Dosya Yükleme: Route Handler mı, Server Action mı?

Basit senaryolarda formunuzdaki dosya alanını Server Action içinde formData.get("file") ile alabilirsiniz. Ancak büyük dosyalar, virüs taraması veya üçüncü taraf depolama (S3, R2 vb.) gerekiyorsa Route Handlers (app/route.ts) tercih edin. Bu sayede streaming ile bellek kullanımını kontrol edebilir, sınırları (limits) ve doğrulamayı daha esnek yönetebilirsiniz. Platformunuza göre runtime seçimi önemlidir: Node özelliklerine ihtiyaç varsa export const runtime = "nodejs" kullanın; global edge ağı üzerinde düşük gecikme istiyorsanız "edge" seçeneğini değerlendirin.

Adım Adım Senaryo: Profil Formu + Avatar Yükleme

1) Formu Tasarlayın: İsim, e-posta, kısa bio ve avatar dosya alanı ekleyin. Geriye dönüt için alan bazlı hata mesajları ve formun gönderim durumunu göstermek üzere bir yükleniyor göstergesi ekleyin.

2) Server Action Yazın: "use server" ile başlayan saveProfile fonksiyonu, formData içinden girişleri okur. zod ile isim ve e-posta zorunlu, bio uzunluğu sınırlı olacak şekilde doğrulayın. Doğrulama hatalarını sade bir nesne olarak geri döndürün.

3) Avatar İçin Doğrulama: MIME türü (image/jpeg, image/png), dosya boyutu (ör. 2 MB sınır), ve görüntü boyutları (ör. 1024x1024 üstü reddedilsin) kontrol edin. Şüpheli içerik için içerik taraması gerekiyorsa, asenkron bir kuyruk (örn. SQS, Pub/Sub) veya webhook tabanlı işleme düşünebilirsiniz.

4) Depolama Stratejisi: Büyük dosyaları doğrudan Server Action üzerinden aktarmak yerine, istemcide önceden imzalı URL (pre-signed URL) alıp doğrudan depolamaya yüklemek daha verimlidir. Bu yaklaşım, sunucu trafiğini azaltır ve edge bölgelerinde daha hızlıdır. Ardından Server Action sadece meta veriyi (dosya URL’si) veritabanına kaydeder.

5) Önbellek ve Yeniden Doğrulama: Profil sayfanız statikse bile güncelleme sonrası revalidatePath("/profil") ile görünümü anında tazeleyin. Eğer profil verilerini cache tag ile etiketlediyseniz, revalidateTag("profil") çağrısı da etkili olacaktır.

6) UX İyileştirmeleri: Progressive enhancement mantığıyla, JavaScript kapalıyken dahi form çalışmalı; JS açıkken ise iyileştirmeler devreye girmeli. useFormStatus ile buton durumunu yönetin, belirli alanlar için optimistic UI uygulayın.

Güvenlik İpuçları

CSRF: Server Actions form gönderiminde varsayılan olarak korumalı bir yol sağlar; yine de hassas işlemlerde ek kontrol (ör. origin doğrulama) uygulayın. Rate Limiting: Brute-force girişimleri için IP veya token bazlı sınırlama ekleyin. Dosya Güvenliği: İçerik türü ve boyut kontrolü zorunlu; antivirüs taraması ve izole geçici depolama ile zinciri güçlendirin. Gizli Anahtarlar: Tüm sırları process.env üzerinden sunucuda okuyun; istemciye sızdırmayın.

Performans ve SEO

App Router ile sayfalarınız server-rendered çalıştığı için ilk yükleme süresi ve SEO performansı güçlüdür. Server Actions, veri mutasyonunu aynı kökten ele aldığından fazladan JSON döndürme ve istemci durum yönetimi yükünü azaltır. Yalnızca değişmesi gereken segmentleri yeniden doğrulamak, gereksiz yeniden oluşturma maliyetini düşürür. Görsel ve statik varlıklar için HTTP cache başlıklarını doğru ayarlayın, kritik CSS’i minimal tutun.

Hata Ayıklama ve Test

Geliştirme sırasında action’larınızın giriş ve çıkışlarını ayrıntılı loglayın; hassas verileri loglardan uzak tutun. Birim testlerde, doğrulama şemalarını ve yan etkisiz iş mantığını izole ederek test edin. Uçtan uca testlerde (Playwright) gerçekçi form doldurma ve dosya yükleme akışlarını, hata durumlarını ve erişilebilirlik kontrollerini (klavye navigasyonu, ARIA etiketleri) kapsayın.

Sonuç

Next.js 14’te Server Actions, form ve dosya yükleme gibi günlük iş akışlarını basitleştirirken güvenlik ve performans avantajı da sağlar. Doğrulama, depolama ve önbellek stratejilerini doğru kurguladığınızda daha az kodla daha ölçeklenebilir bir çözüm elde edersiniz. Küçük bir profil formundan kurumsal bir içerik yönetim sistemine kadar aynı ilkelere dayanarak karmaşık senaryoları dahi güvenle kurgulayabilirsiniz.

22 Eylül 2025 Pazartesi

Passkey (WebAuthn) ile Şifresiz Giriş: Modern Web Uygulamalarına Adım Adım Entegrasyon

Passkey nedir ve neden şimdi?

Son yıllarda şifre sızıntıları, kimlik avı saldırıları ve kullanıcı deneyimi sorunları; geliştiricileri “şifresiz giriş” modellerine yöneltti. Passkey, FIDO2 ve WebAuthn standartlarını temel alarak kullanıcıların cihazlarında biyometrik (Face ID, Touch ID, Windows Hello) veya donanımsal güvenlik anahtarıyla güvenli, hızlı ve kimlik avına dayanıklı oturum açmalarını sağlar. Chrome, Safari, Edge ve Firefox’un güncel sürümleri ile iOS, Android ve Windows ekosistemleri passkey’i yerel olarak destekliyor.

Mimariye hızlı bakış

Passkey, sunucu (Relying Party) ve tarayıcı/cihaz (Authenticator) arasında challenge temelli bir akış kullanır. Sunucu, kayıt veya giriş için benzersiz bir challenge üretir; istemci tarafında navigator.credentials.create() (kayıt) veya navigator.credentials.get() (giriş) çağrısı yapılır ve oluşan kriptografik yanıt sunucuya geri gönderilir. Sunucu, bu yanıtı doğrular ve oturum açılır.

Önkoşullar ve en iyi uygulamalar

- Üretimde HTTPS zorunludur (localhost geliştirmede istisna).
- RP ID alan adınız olmalı (örn. example.com). Alt alan adılarında doğrulama için RP ID’yi üst alan adına ayarlayın.
- Kullanıcı kimlikleri ve cihazdan dönen credential ID’leri veritabanında güvenle saklanmalı.
- Kimlik avına dayanıklılık için cross-origin isteklerden kaçının; Origin ve Relying Party ID tutarlı olmalı.

Adım adım: Basit bir Node.js/Express ve WebAuthn entegrasyonu

Örnek, sunucuda @simplewebauthn/server kütüphanesini kullanır. İstemci tarafında ise tarayıcı API’lerini çağırıyoruz. Bu iskelet, tek başına üretime hazır değildir; amaç kavramı göstermektir.

1) Sunucuyu hazırlayın

// server.js
import express from 'express';
import cors from 'cors';
import session from 'express-session';
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';

const app = express();
app.use(cors({ origin: 'https://app.example.com', credentials: true }));
app.use(express.json());
app.use(session({ secret: 'change-me', resave: false, saveUninitialized: true }));

// Demo bellek içi depolar
const users = new Map(); // userId -> { id, username, credentials: [] }

const rpID = 'example.com';
const rpName = 'Örnek Uygulama';

// Kayıt: seçenek üret
app.post('/webauthn/register/options', (req, res) => {
  const { username, userId } = req.body;
  let user = users.get(userId);
  if (!user) {
    user = { id: userId, username, credentials: [] };
    users.set(userId, user);
  }
  const options = generateRegistrationOptions({
    rpName,
    rpID,
    userID: user.id,
    userName: user.username,
    attestationType: 'none',
    excludeCredentials: user.credentials.map(c => ({ id: c.id, type: 'public-key' })),
    authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred' },
  });
  req.session.currentChallenge = options.challenge;
  res.json(options);
});

// Kayıt: doğrula
app.post('/webauthn/register/verify', async (req, res) => {
  const { userId, attResp } = req.body;
  const user = users.get(userId);
  const verification = await verifyRegistrationResponse({
    response: attResp,
    expectedChallenge: req.session.currentChallenge,
    expectedOrigin: 'https://app.example.com',
    expectedRPID: rpID,
  });
  if (verification.verified) {
    const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;
    user.credentials.push({ id: credentialID, publicKey: credentialPublicKey, counter });
  }
  res.json({ ok: verification.verified });
});

// Giriş: seçenek üret
app.post('/webauthn/login/options', (req, res) => {
  const { userId } = req.body;
  const user = users.get(userId);
  const options = generateAuthenticationOptions({
    rpID,
    userVerification: 'preferred',
    allowCredentials: user ? user.credentials.map(c => ({ id: c.id, type: 'public-key' })) : [],
  });
  req.session.currentChallenge = options.challenge;
  res.json(options);
});

// Giriş: doğrula
app.post('/webauthn/login/verify', async (req, res) => {
  const { userId, authResp } = req.body;
  const user = users.get(userId);
  const dbCred = user.credentials.find(c => Buffer.compare(c.id, Buffer.from(authResp.rawId, 'base64url')) === 0);
  const verification = await verifyAuthenticationResponse({
    response: authResp,
    expectedChallenge: req.session.currentChallenge,
    expectedOrigin: 'https://app.example.com',
    expectedRPID: rpID,
    authenticator: dbCred,
  });
  if (verification.verified) {
    dbCred.counter = verification.authenticationInfo.newCounter;
    // Burada oturumu işaretleyin (req.session.userId = userId gibi)
  }
  res.json({ ok: verification.verified });
});

app.listen(3000, () => console.log('Server listening on http://localhost:3000'));

2) İstemci tarafı: Kayıt ve giriş akışı

// register.js (tarayıcı)
async function startRegistration(username, userId) {
  const opts = await fetch('/webauthn/register/options', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ username, userId })
  }).then(r => r.json());

  // ArrayBuffer alanlarını dönüştürün
  opts.challenge = base64urlToBuffer(opts.challenge);
  opts.user.id = new TextEncoder().encode(opts.user.id);

  const cred = await navigator.credentials.create({ publicKey: opts });
  const attResp = credentialToJSON(cred);
  const res = await fetch('/webauthn/register/verify', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ userId, attResp })
  }).then(r => r.json());
  return res.ok;
}

async function startLogin(userId) {
  const opts = await fetch('/webauthn/login/options', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ userId })
  }).then(r => r.json());

  opts.challenge = base64urlToBuffer(opts.challenge);
  opts.allowCredentials = (opts.allowCredentials || []).map(a => ({ ...a, id: base64urlToBuffer(a.id) }));

  const assertion = await navigator.credentials.get({ publicKey: opts });
  const authResp = credentialToJSON(assertion);
  const res = await fetch('/webauthn/login/verify', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ userId, authResp })
  }).then(r => r.json());
  return res.ok;
}

// Yardımcılar
function base64urlToBuffer(b64url) {
  const pad = '='.repeat((4 - (b64url.length % 4)) % 4);
  const b64 = (b64url + pad).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(b64);
  const buf = new ArrayBuffer(raw.length);
  const view = new Uint8Array(buf);
  for (let i = 0; i < raw.length; ++i) view[i] = raw.charCodeAt(i);
  return buf;
}

function credentialToJSON(cred) {
  return JSON.parse(JSON.stringify(cred, (k, v) => v instanceof ArrayBuffer
    ? bufferToBase64url(v)
    : v));
}

function bufferToBase64url(buf) {
  const bytes = new Uint8Array(buf);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,'');
}

Güvenlik, UX ve üretim tavsiyeleri

- Kullanıcı adısız (usernameless) giriş için “resident key” tercih edin ve uygun akışı tasarlayın.
- Birden fazla passkey kaydına izin vererek cihaz kaybına karşı dayanıklılık sağlayın.
- Platform passkey (cihazda) ile çapraz platform passkey (iCloud Keychain, Google Password Manager) farklarını kullanıcılara açıklayın.
- Hata yönetimini özenle yapın: tarayıcıda NotAllowedError vb. hataları kullanıcı dostu mesajlara çevirin.
- Oturum yönetiminde kısa ömürlü JWT veya güvenli, HttpOnly, SameSite=strict cookie tercih edin.

Sorun giderme

- Origin ve RP ID uyuşmazlığı en sık hatadır: “https://app.example.com” ile rpID “example.com” olmalı.
- Sunucunuz ve istemci cihazınızın saat farkları challenge doğrulamasını bozabilir; NTP senkronizasyonunu sağlayın.
- Geliştirme aşamasında sadece localhost özel izni vardır; kendi alan adınızda test ederken mutlaka HTTPS kullanın.
- Kurumsal ortamlarda WebAuthn politikaları kısıtlanmış olabilir; tarayıcı ve OS politikalarını kontrol edin.

Sonuç

Passkey/WebAuthn, hem güvenlik hem de kullanıcı deneyimi tarafında anlamlı bir sıçrama sunuyor. Doğru RP ID, sağlam challenge yönetimi ve tutarlı origin politikasıyla kurulum basit hale geliyor. Üretime geçişte çoklu cihaz desteği, kullanıcı eğitimleri ve geri dönüş (fallback) stratejileri planlandığında, şifresiz geleceğe hazır bir kimlik doğrulama katmanı elde edebilirsiniz.

21 Eylül 2025 Pazar

HTTP/3 (QUIC) Nedir? Nginx ve Cloudflare Üzerinde Etkinleştirme Rehberi

HTTP/3 ve QUIC neden önemli?

Web sitelerinin hızlı yüklenmesi, hem kullanıcı deneyimi hem de SEO açısından belirleyicidir. HTTP/3, Google’ın QUIC taşıma protokolü üzerine inşa edilen en yeni HTTP sürümüdür ve UDP üzerinden çalışır. Bağlantı kurulumu için gereksiz el sıkışmaları ortadan kalkar, paket kaybı durumunda satır sonu tıkanması azalır ve özellikle mobil ile yüksek gecikmeli ağlarda belirgin hız kazanımı sağlar. Bu sayede TTFB düşer, etkileşim metrikleri iyileşir ve Core Web Vitals performansınız olumlu etkilenir.

HTTP/3 vs. HTTP/2: Öne çıkan farklar

HTTP/2 TCP üzerinde çalışırken, HTTP/3 QUIC sayesinde UDP kullanır. Bu mimari fark, bağlantı geçişlerinde (ör. Wi‑Fi’den 4G’ye) ve paket kaybı yaşanan ortamlarda akışların birbirini beklememesini sağlar. Üstelik 0‑RTT oturum yenileme sayesinde tekrar bağlanan kullanıcılar daha az gecikme yaşar. Sonuç olarak, aynı içerik daha akıcı ve daha hızlı sunulur.

Ön gereksinimler ve uyumluluk

HTTP/3 bugün Chrome, Edge, Firefox ve Safari’nin güncel sürümlerinde desteklenir. Sunucu tarafında ise iki kritik gereksinim ortaya çıkar: UDP/443 trafiğinin güvenlik duvarında açık olması ve TLS 1.3 desteği. Ayrıca Nginx gibi web sunucularında HTTP/3 desteği için doğru sürüm ve uygun TLS kitaplığı (OpenSSL’in QUIC destekli bir sürümü, BoringSSL veya quictls) gerekir.

Cloudflare üzerinde HTTP/3’ü etkinleştirme

Cloudflare kullanıyorsanız, HTTP/3 etkinleştirmek birkaç tıklama kadar kolaydır. Cloudflare panelinde alan adınızı seçin, Network sekmesine girin ve HTTP/3 (with QUIC) seçeneğini On konumuna getirin. Cloudflare, ziyaretçilere Alt-Svc başlığı ile HTTP/3’ü ilan eder ve QUIC üzerinden içerik sunmaya başlar. Bu aşamada ek bir sunucu yapılandırması gerekmeyebilir; ancak kaynak sunucunuzun HTTP/2 veya HTTP/1.1 için sağlıklı cevap verdiğinden emin olun.

Nginx’te HTTP/3 (QUIC) kurulum adımları

HTTP/3’ü doğrudan Nginx’te sunmak istiyorsanız, önce HTTP/3 modülü destekli bir Nginx derlemesine ihtiyaç vardır. En güncel mainline Nginx sürümünü kullanın ve derleme sırasında HTTP/3 modülünü etkinleştirin. Ayrıca QUIC destekli bir TLS kütüphanesi (örneğin OpenSSL’in QUIC desteği olan bir sürümü veya BoringSSL/quictls) ile derleme yapmanız gerekir.

Yapılandırma tarafında, site (server) bloğunuzda hem klasik TLS hem de QUIC için dinleme direktifleri ekleyin. Tipik olarak 443 portu için iki dinleme satırı kullanılır: biri listen 443 ssl; diğeri listen 443 quic reuseport;. TLS 1.3’ü açın ve geçerli bir sertifika/anahtar atayın. İstemcilerin HTTP/3’ü kolay keşfetmesi için Alt-Svc başlığı tavsiye edilir; örneğin Alt-Svc: h3=":443"; ma=86400 gibi. Böylece tarayıcılar sonraki isteklerde HTTP/3 yolunu tercih eder.

Konfigürasyon güncellemesinden sonra Nginx’i hatasız yeniden başlatın. Güvenlik duvarında UDP 443’ün gerçekten açık olduğundan emin olun; aksi halde tarayıcılar HTTP/3’e geçemeyip HTTP/2’ye geri döner.

Doğrulama ve test

Hızlı bir kontrol için tarayıcınızın geliştirici araçlarındaki Network panelinde ilgili isteğin Protocol sütununa bakın; h3 görüyorsanız HTTP/3 aktif demektir. Alternatif olarak HTTP/3 destekli bir curl sürümüyle başlıkları sorgulayabilirsiniz: curl --http3 -I https://alanadiniz.com. Dönen yanıtta alt-svc başlığı ve protokolün h3 olduğuna dair işaretler görmelisiniz. Dış doğrulama için çevrimiçi HTTP/3 test araçlarını da kullanabilirsiniz.

Performans ipuçları ve en iyi uygulamalar

HTTP/3 tek başına bir sihirli değnek değildir; ancak iyi bir önbellekleme stratejisi, doğru cache-control başlıkları, Early Hints (103) ve kaynak önceliklendirmesiyle birleştiğinde etkisi katlanır. Resimleri modern formatlara (AVIF/WebP) taşıyın, kritik CSS’i optimize edin ve CDN’inizi edge’de önbelleklemek üzere yapılandırın. Mobil ağlarda daha da iyi sonuç için DNS çözümleme sürelerini kısaltın ve gereksiz üçüncü taraf istekleri budayın.

Sorun giderme: En sık görülen engeller

- UDP kapalı: Birçok güvenlik duvarı varsayılan olarak UDP 443’ü engeller. Kuralı açın ve sağlayıcınızın UDP’yi desteklediğinden emin olun.
- Ortak barındırma kısıtları: Bazı paylaşımlı hosting ortamlarında HTTP/3 henüz sunulmaz. Bu durumda bir CDN üzerinde HTTP/3’ü etkinleştirmek pratik bir ara çözümdür.
- Yanlış TLS kitaplığı: Nginx derlemenizde QUIC destekli bir TLS kitaplığı kullanmadıysanız HTTP/3 aktif olmayacaktır. Sürüm ve yapı bayraklarını gözden geçirin.
- Orta katman proxy’ler: WAF, DDoS koruması veya ek proxy katmanları UDP’yi bozabilir. Trafik zincirindeki her bileşenin HTTP/3 uyumluluğunu doğrulayın.

SEO ve iş etkisi

Daha hızlı bağlantı kurulumu ve paket kaybına dayanıklı aktarım, özellikle mobil kullanıcıların ilk etkileşim süresini düşürür. Bu, LCP ve FID/INP gibi metriklere olumlu yansır, hemen çıkma oranlarını azaltır ve dönüşüm oranlarını artırabilir. Arama motorları doğrudan “HTTP/3 kullanıyor musunuz?” diye puan vermez; fakat hız ve kullanıcı deneyimi sinyalleri arka planda sıralamayı etkiler. Kısacası HTTP/3, teknik borcunuzu azaltan ve yatırımı hızlı geri ödeyen bir yükseltmedir.

Sonuç

HTTP/3 (QUIC), günümüzün değişken ve gürültülü ağ koşullarında web’i daha hızlı ve dayanıklı kılan önemli bir adımdır. Cloudflare gibi CDN’lerle dakikalar içinde aktif edebilir, Nginx üzerinde ise doğru sürüm ve TLS kitaplığıyla uçtan uca kontrol sağlayabilirsiniz. UDP 443’ü açmayı, TLS 1.3’ü etkinleştirmeyi ve Alt-Svc ilanını eklemeyi unutmayın. Doğrulama testlerinden sonra performans metriklerinizi izleyin; çoğu sitede ölçülebilir iyileşme göreceksiniz.

20 Eylül 2025 Cumartesi

Parolasız Gelecek: WebAuthn Passkey Entegrasyonu ile Güvenli Giriş (Next.js Örneği)

Parolasız Gelecek: WebAuthn Passkey Entegrasyonu ile Güvenli Giriş (Next.js Örneği)

Parolalar, yıllardır sızıntıların ve kimlik avı saldırılarının baş aktörü oldu. Passkey (WebAuthn + FIDO2) yaklaşımı, biyometrik doğrulama (Touch ID, Windows Hello, Android Biometrics) veya donanım anahtarı ile tek dokunuşla güvenli oturum açmayı mümkün kılar. Bu yazıda, güncel tarayıcılar ve işletim sistemleriyle uyumlu passkey altyapısını nasıl kuracağınızı, geliştirici bakışıyla adım adım anlatıyorum. Örnek teknoloji olarak Next.js kullanacağız ancak anlatım, API uç noktaları olan her yığını kapsar.

Passkey Nedir? Passkey, kullanıcının cihazında güvenli bir şekilde saklanan asimetrik anahtar çifti ile çalışır. Sunucu yalnızca genel anahtarı tutar, gizli anahtar cihazdan çıkmaz. Oturum açma akışı; origin, RP ID (Relying Party ID) ve kullanıcı doğrulaması gibi bağlamlarla sınırlandırıldığı için kimlik avı ve kimlik bilgisi doldurma saldırılarına karşı son derece dirençlidir.

Neden Passkey?

- Kimlik avına dayanıklı: İmzalama süreci etki alanına (origin) bağlıdır, sahte sitelerde işe yaramaz.
- Kullanıcı deneyimi: Tek dokunuş, yüz tanıma ya da PIN ile saniyeler içinde giriş.
- Çoklu cihaz desteği: iCloud Keychain, Google Password Manager gibi kasalarla cihazlar arası senkronizasyon.

Mimari Özet ve Gereksinimler

Sunucu tarafında challenge üretir, saklar ve istemciye gönderirsiniz. İstemci bu challenge ile cihazdaki authenticator üzerinden kayıt (registration) veya giriş (assertion) akışını tamamlar; dönen yanıtı sunucuya iletir ve sunucu WebAuthn kurallarına göre doğrular. Sunucunuzda TLS zorunlu olmalı (localhost geliştirmenin istisnasıdır). RP ID genellikle alan adınızın köküdür (ör. example.com). Veritabanında kullanıcı, keyId, publicKey, alg, signCount gibi alanlar saklanır.

Kayıt (Registration) Akışı Adımları

1) Kullanıcı e-postasını veya benzersiz kimliğini alıp sunucunuza gönderin. Sunucu, PublicKeyCredentialCreationOptions üretir: challenge (base64url), rp.id, rp.name, user.id, user.name, pubKeyAlgo listesi (örn. -7 ES256, -257 RS256), authenticatorSelection (residentKey, userVerification) ve timeout gibi alanları içerir.

2) İstemci tarafında navigator.credentials.create() ile native WebAuthn API’sini çağırın. Dönen yanıt, attestationObject ve clientDataJSON içerir.

3) Bu verileri base64url ile sunucuya POST edin. Sunucu, attestation’ı doğrular; origin, challenge eşleşmesi, RP ID hash kontrolü ve sertifika zinciri doğrulaması yapar. Başarılıysa kullanıcıya ait publicKey, keyId ve signCount’ı veritabanına kaydeder.

Giriş (Authentication) Akışı Adımları

1) Sunucu, PublicKeyCredentialRequestOptions üretir: challenge, rpId, allowCredentials (kayıtlı anahtarlar) ve userVerification.

2) İstemci, navigator.credentials.get() çağırır ve authenticator’dan imzalı assertion alır: authenticatorData, clientDataJSON ve signature.

3) Sunucu; challenge, origin, rpIdHash doğrular; publicKey ile imzayı denetler ve signCount güncellenir. Ardından oturum açma token’ı veya session başlatılır.

Next.js ile Minimal Örnek Mantığı

- /api/webauthn/register/options: Kullanıcı kimliği ile çağrılır, creationOptions döner ve server-side Session’a challenge yazılır.
- /api/webauthn/register/verify: İstemciden gelen attestation yanıtını doğrular ve anahtarı kaydeder.
- /api/webauthn/login/options: Kullanıcının kayıtlı keyId’lerine göre requestOptions üretir, challenge’ı saklar.
- /api/webauthn/login/verify: Assertion’ı doğrular, session veya JWT üretir.

Doğrulama adımları için community tarafından desteklenen simplewebauthn gibi kütüphaneler süreci kolaylaştırır. İstemci tarafında base64url dönüştürmeleri (ArrayBuffer ⇄ base64url) dikkat ister; hataların çoğu burada çıkar.

En İyi Uygulamalar ve Güvenlik İpuçları

- Origin ve RP ID tutarlılığı: www ile çıplak alan adı karışıklığı yaşamamak için tek tercih belirleyin.
- User Verification: “required” seçeneği güvenliği yükseltir (biyometrik/PIN zorunlu).
- Resident Key (Discoverable Credentials): Kullanıcı adı sormadan doğrudan passkey ile giriş akışı sağlar; UX’i iyileştirir.
- Attestation politikası: Genellikle “none” yeterlidir; donanım güveni gerekiyorsa “indirect” veya belirli CA’lar ile doğrulayın.
- signCount ve Clone Detection: signCount düşerse potansiyel klon tespitine karşı hesabı işaretleyin.
- Yedekleme: Kullanıcıya birden fazla passkey ekletin ve acil kurtarma için e-posta linki veya destek kanalı sunun (parolaya geri dönüş önermeyin).

Sık Karşılaşılan Hatalar

- TypeError: create() ya da get() için beklenen ArrayBuffer yerine base64 string gönderildi. Çözüm: Base64url’ü Uint8Array’e çevirin.
- DOMException NotAllowedError: Kullanıcı iptal etti veya sayfa güvenli değil. HTTPS kullanın ve UI’de açık bilgi verin.
- InvalidStateError: Aynı kullanıcı için aynı authenticator’da ikinci kez kayıt deneniyor. allowCredentials stratejisini gözden geçirin.

SEO ve Üretim Notları

Passkey, WebAuthn ve FIDO2 anahtar kelimelerini sayfadaki başlık ve açıklamalara ekleyin. Schema.org Person ve WebSite şemaları ile login sayfasını işaretlemek, arama motorlarına daha net sinyaller verir. Üretimde HSTS etkinleştirin, doğru CORS politikası tanımlayın ve CSRF koruması kullanın. Reverse proxy arkasında çalışıyorsanız X-Forwarded-Proto başlığının doğruluğunu garanti edin; aksi halde origin eşleşmeleri başarısız olur.

Sonuç

Passkey entegrasyonu, modern web uygulamalarında güvenliği yükseltirken kullanıcı deneyimini ciddi ölçüde iyileştirir. Next.js veya benzeri bir yığınla, birkaç API uç noktası ve iyi kurgulanmış bir doğrulama katmanı ile parolasız girişe geçebilirsiniz. Küçük bir POC ile başlayın, kayıt ve giriş akışlarını uçtan uca test edin, ardından çoklu cihaz ve kurtarma senaryolarını ekleyerek üretime taşıyın. Parolasız gelecek, bugün erişilebilir durumda.

19 Eylül 2025 Cuma

Web Uygulamana Passkey (WebAuthn) Ekleme: Node.js ile Şifresiz Giriş Rehberi 2025

Giriş

Şifresiz kimlik doğrulama, son yılların en önemli güvenlik dönüşümlerinden biri. Passkey (FIDO2/WebAuthn) sayesinde kullanıcılar, karmaşık şifrelerle uğraşmadan biyometri, güvenli donanım anahtarları veya cihaz kilidi ile giriş yapabiliyor. Bu rehberde, modern bir web uygulamasına Node.js ve tarayıcı WebAuthn API kullanarak passkey ekleme sürecini adım adım anlatıyorum. Odak noktamız: kolay kurulum, sağlam güvenlik ve güncel tarayıcı desteği.

Ön Koşullar

- Alan adınız ve HTTPS etkin (localhost için https://localhost da çalışır, ancak gerçek kullanıcılar için zorunlu HTTPS gereklidir).
- Node.js 18+ (WebCrypto ve fetch desteği için önerilir).
- Basit bir kullanıcı veritabanı (PostgreSQL, MongoDB ya da SQLite).
- Sunucu tarafında WebAuthn doğrulaması için bir kütüphane (örneğin simplewebauthn).
- RP ID (Relying Party ID) olarak genellikle kök alan adınız: example.com. Origin ise https://app.example.com gibi tam kaynak adresidir.

Mimari ve Akış

WebAuthn iki ana akıştan oluşur: Kayıt (Registration) ve Giriş (Authentication). Her ikisi de sunucunun ürettiği rastgele bir challenge değeriyle başlar.

1) Kayıt: Sunucu kullanıcıyı tanımlar, PublicKeyCredentialCreationOptions oluşturur ve challenge döner. Tarayıcı navigator.credentials.create() ile kullanıcıdan attestation üretir. Sunucu attestation’ı doğrular, credentialId ve publicKey gibi özet bilgileri veritabanına kaydeder.

2) Giriş: Sunucu PublicKeyCredentialRequestOptions döner. Tarayıcı navigator.credentials.get() ile assertion (imzalı kanıt) üretir. Sunucu imzayı ve sayaç değerini doğrular, oturumu açar.

Frontend: Kayıt Akışı

Kullanıcı giriş yaptıktan (veya e-posta doğruladıktan) sonra passkey kaydı başlatın. İstemci tarafında tipik istek:

const regOpts = await fetch('/webauthn/register/options').then(r => r.json());
const credential = await navigator.credentials.create({ publicKey: regOpts });
await fetch('/webauthn/register/verify', { method: 'POST', body: JSON.stringify(credential) });

SEO açısından önemli noktalar: “passkey kaydı”, “WebAuthn tarayıcı API”, “şifresiz kimlik doğrulama” gibi anahtar kelimeleri akışa doğal yerleştirin.

Backend: Kayıt Doğrulama

Sunucuda challenge değerini kullanıcıya özel ve tek kullanımlık olarak üretin. @simplewebauthn/server kullanıyorsanız generateRegistrationOptions ile seçenekleri üretir, verifyRegistrationResponse ile attestation doğrularsınız. Doğrulama sonrası şu alanları saklayın:

- credentialId: Kimlik.
- publicKey: İmza doğrulaması için gereken anahtar.
- signCount: Yeniden oynatma saldırılarına karşı sayaç.
- transports ve aaguid gibi meta bilgiler.

Frontend: Giriş (Authentication) Akışı

Kullanıcı giriş sayfasına geldiğinde, eğer passkey kayıtlıysa “Tek tıkla şifresiz giriş” sunabilirsiniz:

const authOpts = await fetch('/webauthn/auth/options?username=ali').then(r => r.json());
const assertion = await navigator.credentials.get({ publicKey: authOpts });
await fetch('/webauthn/auth/verify', { method: 'POST', body: JSON.stringify(assertion) });

Chrome’un Conditional UI özelliğiyle, kullanıcı adı alanına odaklanıldığında passkey önerisi otomatik çıkar. Bunun için mediation: 'conditional' kullanın ve sayfada autocomplete="username" bulunan bir alan bulundurun.

Güvenlik ve En İyi Uygulamalar

- Origin ve rpId kesin eşleşmeli. Ters proxy (Cloudflare, Nginx) arkası dağıtıma dikkat edin.
- Challenge benzersiz ve kısa ömürlü olsun; kullanıldıktan sonra silin.
- userVerification değerini tehdit modelinize göre belirleyin: required (yüksek güvenlik) veya preferred (daha iyi uyumluluk).
- residentKey (discoverable credentials) ile kullanıcı adı girmeden giriş deneyimi sağlayabilirsiniz. Bunun için residentKey: 'required' ve authenticatorSelection.requireResidentKey = true.
- Çoklu cihaz senkronizasyonu (iCloud Keychain, Google Password Manager) sayesinde passkey’ler cihazlar arasında taşınabilir; ancak kurumsal ortamda donanım güvenlik anahtarlarını (YubiKey vb.) zorunlu kılmak isteyebilirsiniz.

Test ve Hata Ayıklama

Chrome DevTools > More Tools > Virtual Authenticator ile fiziksel cihaz olmadan test yapabilirsiniz. Buradan platform/harici anahtar seçebilir, kullanıcı doğrulamasını simüle edebilirsiniz. Sık görülen hatalar: “Invalid RP ID” (alan adınız uyuşmuyor), “DOMException: The operation is insecure” (HTTPS yok), “Challenge mismatch” (eski veya farklı kullanıcıya ait challenge).

Veri Modeli ve Uyum

Bir kullanıcı birden fazla passkey’e sahip olabilir. Veritabanında users, credentials şeklinde ayrıştırın. Girişte birden çok allowCredentials döndürebilir veya discoverable credential ile boş gönderip tarayıcının seçmesini izleyebilirsiniz. KV veya bellek içi önbellek ile challenge yönetimini hızlandırın. KV kullansanız bile kalıcılık için veritabanına yedekleyin.

Dağıtım İpuçları

Sunucunuz HTTP/2 veya HTTP/3 arkasındaysa, Origin başlığının ve CF-Connecting-IP / X-Forwarded-Proto gibi başlıkların framework’ünüz tarafından doğru işlendiğinden emin olun. Serverless (Vercel, Cloudflare Workers) üzerinde de WebAuthn rahatlıkla çalışır; sadece rpId ve origin sabit ve tutarlı olmalı.

Sonuç

Passkey (WebAuthn) ile şifresiz kimlik doğrulama, hem güvenliği hem kullanıcı deneyimini ciddi biçimde iyileştiriyor. Node.js ve WebAuthn API sayesinde birkaç uç noktayı doğru kurgulayarak dakikalar içinde prototip çıkabilir, Conditional UI ile tek dokunuşla giriş deneyimi sunabilirsiniz. Doğru RP ID, benzersiz challenge yönetimi ve kapsamlı doğrulama kontrolleriyle, 2025’in güvenlik standartlarına uygun modern bir “şifresiz giriş” altyapısını bugün projelerinize ekleyin.