İçeriğe atla

Serverless Servisler Arasında Kimlik ve Şifreleme

SigV4 hangi servisin çağırdığını kanıtlar, kimin adına çağırdığını değil. Doğrulanmış özneyi nasıl taşırsınız ve taşıma katmanı gerçekte neyi şifreler.

Ayhan Sipahi Ayhan Sipahi

103. bölüm katmanı kurdu: paylaşılan bir execute-api interface endpoint üzerinden erişilen, her çağrısı SigV4 ile imzalanan ve route bazlı yetkilendirilen private bir REST API. O katman bir soruyu iyi cevaplıyor, ikincisini ise tamamen açık bırakıyor.

SigV4 hangi servisin çağırdığını kanıtlar. Kimin adına çağırdığı hakkında hiçbir şey söylemez. Bunlar farklı sorulardır ve yalnızca ilkini cevaplayan bir katman, her servisin her kullanıcı adına iş yapabildiği bir katmandır. Bu bölüm o boşluğu kapatıyor, ardından taşıma katmanının neyi şifreleyip neyi şifrelemediğini tam olarak söylüyor.

Doğrulanmış özneyi taşımak#

Somut örnek üzerinden gidelim. Kendi execution role’üyle imza atan bir checkout Lambda’sı, checkout-service olduğunu kanıtlar. Sorduğu sepetin, oturumu açan kişiye ait olduğunu kanıtlamaz. İmzanın içinde özneyi taşıyan hiçbir şey yok.

Çağrılan servisin bedavaya aldığı şeyle başlayalım. Lambda proxy entegrasyonunda AWS_IAM açıkken çağıranın IAM principal’ı, hiçbir mapping template yazmadan event’in içine gelir. AWS bunu doğrudan söylüyor: “When the authorization type is AWS_IAM, the authorized user information includes $context.identity.* properties” (mapping template referansı (yeni sekmede açılır)). İşe yarayan alanlar: “The Amazon Resource Name (ARN) of the effective user identified after authentication” diye tanımlanan $context.identity.userArn, bir de .caller ve .accountId. Proxy entegrasyonunda bunlar event.requestContext.identity altına düşer. Yani çağrılan servis, IAM yetkisinin üstüne kendi kuralını koyabilir: /orders yoluna yalnızca checkout-service role’ü POST atabilir.

Bu, servis kimliğidir. Kullanıcı ayrı bir problemdir ve gateway onu sizin yerinize çözmez.

doğrulandı

olduğu gibi geçti, denetlenmedi

Çağıran Lambda

SigV4 imzası

Kullanıcı assertion header'ı

API Gateway, AWS_IAM

Event içinde identity.userArn

Event içinde custom header

Kontrol 1: bu çağıran servise izin var mı?

Kontrol 2: token'ı JWKS ile doğrula

İsteği işle

Düz x-user-id header’ı bir tuzaktır. İmzalı bir isteğin yanında zararsız görünür ama doğrulanmamış bir iddiadır. Route üzerinde zaten execute-api:Invoke yetkisi olan her servis, o header’a istediği değeri yazabilir; çağrılan servis de hiç doğrulamadığı bir iddiaya göre iş yapar. AWS bu duruma bir isim veriyor: confused deputy, yani “a security issue where an entity that doesn’t have permission to perform an action can coerce a more-privileged entity to perform the action” (confused deputy (yeni sekmede açılır)).

Bir method’un tek bir authorizationType’ı var ve onu zaten harcadınız. Akla gelen çözüm, kullanıcı token’ını da gateway’e doğrulatmaktır. API Gateway buna izin vermiyor. Bir REST API method’u tam olarak tek bir authorizationType taşır; geçerli değerler NONE, AWS_IAM, CUSTOM ve COGNITO_USER_POOLS’tur ve bir authorizer yalnızca tip CUSTOM veya COGNITO_USER_POOLS iken bağlanabilir (Method (yeni sekmede açılır)). Yani AWS_IAM ile Cognito veya Lambda authorizer aynı method üzerinde birbirini dışlar. Gateway’deki o tek yuvayı servis kimliğine ayırdığınız anda, kullanıcı token’ı için gateway tarafında doğrulayıcı kalmaz. Özneyi doğrulamak için geriye kalan tek yer, çağrılan servisin içidir.

Kısıt tasarımı belirliyor: özneyi imzalayın ve imzayı işin yapıldığı yerde doğrulayın. İki biçim çalışır.

Orijinal token’ı taşıyın. Kullanıcının OIDC veya Cognito access token’ını bir custom header’da geçirin ve çağrılan serviste, issuer’ın JWKS endpoint’ine karşı doğrulayın. Node tarafında AWS awslabs aws-jwt-verify (yeni sekmede açılır) kütüphanesini işaret ediyor: “In a Node.js app, AWS recommends the aws-jwt-verify library to validate the parameters in the token that your user passes to your app” (bir JWT’yi doğrulamak (yeni sekmede açılır)). Ucuz bir yol: tek bir bağımlılık, yeni altyapı yok, JWKS execution environment’ta cache’leniyor. Bedeli şu: ham kullanıcı token’ı artık zincirin her adımına yayılıyor ve o token’ın audience ile scope’u kenardaki giriş noktası için üretilmişti; orders bu kapsamın dışında.

Token’ı girişte takas edin. Gelen token’ı, girişte bir kez, hem kullanıcıyı hem de çağıran iş yükünü zincir boyunca taşıyan kısa ömürlü bir iç token ile değiştirin. IETF tam bu biçimi Transaction Tokens adıyla standartlaştırmaya çalışıyor: “designed to maintain and propagate user identity, workload identity and authorization context throughout the Call Chain within a trusted domain” (Transaction Tokens (yeni sekmede açılır)). Durumu net söyleyelim: bu bir IETF taslağı, çalışma grubunun son inceleme aşamasında, henüz RFC değil ve AWS’in yönettiği bir uygulaması yok. Onu, gidilen yön ve kendi yazacağınız iç token’ın neleri taşıması gerektiğinin tarifi gibi okuyun; bu çeyrek açıp kullanacağınız bir özellik değil.

Çağrılan servis iki kontrolü de aynı handler’da yapar:

import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { CognitoJwtVerifier } from 'aws-jwt-verify';

// Execution environment başına bir kez kurulur; JWKS ilk çağrıdan sonra cache'lenir.
const verifier = CognitoJwtVerifier.create({
  userPoolId: process.env.USER_POOL_ID!,
  tokenUse: 'access',
  clientId: process.env.APP_CLIENT_ID!,
});

// Deploy anında sabitlenir. ARN'i parse etmeyin: biçimi, çağıranın
// kimlik bilgilerini nasıl aldığına göre değişir.
const allowedCallers = new Set(
  (process.env.ALLOWED_CALLER_ARNS ?? '').split(',').filter(Boolean),
);

export const handler = async (
  event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
  // Kontrol 1: hangi servis çağırıyor. SigV4 imzasını API Gateway doğruladı.
  const callerArn = event.requestContext.identity.userArn;
  if (!callerArn || !allowedCallers.has(callerArn)) {
    return { statusCode: 403, body: JSON.stringify({ message: 'caller not allowed' }) };
  }

  // Kontrol 2: çağrı hangi kullanıcı adına. Buraya kadar kimse bunu doğrulamadı.
  // Küçük harfli ad: private endpoint'ler header adlarını olduğu gibi geçirir.
  const assertion = event.headers['x-user-assertion'];
  if (!assertion) {
    return { statusCode: 401, body: JSON.stringify({ message: 'missing user assertion' }) };
  }

  try {
    const claims = await verifier.verify(assertion);
    return { statusCode: 200, body: JSON.stringify({ subject: claims.sub }) };
  } catch {
    return { statusCode: 401, body: JSON.stringify({ message: 'invalid user assertion' }) };
  }
};

Hangi servisin çağırdığı IAM’den gelir ve doğrulanmış olarak elinize ulaşır. Çağrının hangi kullanıcı adına yapıldığı ise çağrılan servisin kendi doğruladığı imzalı bir iddiadan gelir.

Handler’a sokmayacağınız bir alan var: $context.identity.vpceId. AWS onu yalnızca private API’lerde geldiği notuyla dokümante ediyor; endpoint’i kod içinde kontrol etmek cazip geliyor. Ama AWS’in dokümante ettiği kontrol noktası başka yerde: 103. bölümdeki servis stack’inde zaten kurduğunuz aws:SourceVpce koşullu resource policy. Ağ koşulunu policy’de tutun. Handler’ı iki kimlik kontrolüyle sınırlayın.

Servisler arasındaki şifreleme katmanı#

Taşıma zaten şifreli. Bunun tam olarak neyi kapsadığını netleştirmekte fayda var, çünkü bu noktada akla ilk gelen ifade genelde yanlış oluyor.

Üç şey sağlam. API Gateway yalnızca HTTPS endpoint sunar: “API Gateway doesn’t support unencrypted (HTTP) endpoints” (veri koruma (yeni sekmede açılır)). Private API’ler TLS 1.2’yi taban olarak dayatır. AWS’in cümlesini dikkatli okuyun, çünkü yanlış okumak kolay: “Private APIs only support TLS 1.2. Earlier TLS versions are not supported.” (private REST API’ler (yeni sekmede açılır)). Bu cümle bir taban belirliyor. Varsayılan TLS_1_2 güvenlik politikası hem TLS 1.2’yi hem TLS 1.3’ü kabul ediyor (güvenlik politikaları (yeni sekmede açılır)). Ve yol AWS’in içinde kalır: “traffic to your private API uses secure connections and is isolated from the public internet. Traffic doesn’t leave the Amazon network.”

Bu uçtan uca şifreleme değildir. TLS, API Gateway’de sonlanır. Entegrasyon, kendi el sıkışması olan ikinci bir bağlantıdır. Bu serinin önerdiği tamamen Lambda’lı kurulumda o ikinci bağlantı da şifrelidir, çünkü “Lambda API endpoints only support secure connections over HTTPS” (Lambda veri koruma (yeni sekmede açılır)). Ama AWS, çağıranın kodundan çağrılanın koduna kesintisiz tek bir şifreli kanal olduğunu hiçbir yerde iddia etmiyor; siz de bir tasarım incelemesinde iddia etmeyin. Doğru tarif şu: aralarında bir servis sınırı olan iki ayrı TLS bağlantısı. Asıl risk de ikinci bağlantıda. HTTP entegrasyon tipleri için API Gateway şunu söylüyor: “each integration can specify a protocol (http/https), port and path” (Integration (yeni sekmede açılır)). Yani düz http orada geçerli bir seçim. İsteğin API Gateway’den geçmiş olması, ikinci bağlantının şifreli olduğu anlamına gelmez.

Mutual TLS bu katmanda yok. “Mutual TLS isn’t supported for private APIs” (mutual TLS (yeni sekmede açılır)). API Gateway’de mTLS bir custom domain name gerektirir ve private API’ler bunun dışında bırakılmıştır. Açılacak bir property yok. Uyum gereksinimi doğu-batı çağrılarında mTLS istiyorsa, çözüm onu gateway’in önünde sonlandırmaktır. AWS bunu yazmış (private API’leri mutual TLS ile tüketmek (yeni sekmede açılır)) ama yalnızca bir blog deseni olarak; servis dokümanlarında buna dair bir taahhüt yok. Mimari dokümana girecekse bu etiketle girmeli.

Kullanıcı token’ı custom header’da sızabilir. Bu doğrudan kimlik bölümünden çıkıyor. AWS_IAM altında Authorization header’ı zaten SigV4’ün; dolayısıyla taşınan kullanıcı iddiası x-user-assertion veya x-id-token gibi bir custom header’a binmek zorunda. Şimdi redaction sözünü dikkatli okuyun: “API Gateway redacts authorization headers, API key values, and similar sensitive request parameters from the logged data” (logging kurulumu (yeni sekmede açılır)). AWS authorization header’larını ve API key’leri adıyla anıyor, sonra “similar sensitive request parameters” ifadesini açık bırakıyor. Rastgele bir custom header’ın redaksiyona uğradığını hiçbir yerde yazmıyor. Uğramadığını da yazmıyor. O hâlde token’ınızın log’landığını iddia etmeyin. Asıl önemli olanı iddia edin: AWS size log’lanmadığına dair hiçbir söz vermiyor.

Varsayılan olarak açık bir sızıntı değil bu ve dar hâli zaten daha keskin. Data tracing opsiyoneldir; AWS bunun “can result in logging sensitive data” olduğunu söyler ve “We recommend that you don’t use Data tracing for production APIs” der. Ama AWS_IAM’in sizi kullanmaya mecbur bıraktığı o tek header, AWS’in hakkında söz vermeyi reddettiği tek header. Bir route’u incelemek için tracing’i açtığınızda, o header’daki token’ın temizlendiğini kanıtlayamazsınız. O yüzden kullanıcı iddiası taşıyan hiçbir stage’de data tracing açmayın ve iddiayı kendi structured log’larınızın da dışında tutun. Sonra bir adım daha atın; çünkü konfigürasyon disiplini, canlı bir olayı baskı altında inceleyen kişiye karşı kaybeder. 107. bölüm buna dayanan kontrolü kuruyor: token’ı, tracing’i kim açmış olursa olsun, log’a girerken maskeleyen bir data protection policy.

Sınır taşıma katmanı değilse alanı şifreleyin. Bir PII değerini asla görmemesi gereken bir çağrılan servis veya yanlış yapılandırılmış bir log’dan sağ çıkması gereken bir kontrol varsa, payload düzeyinde şifreleme gerekir. AWS Encryption SDK, “a client-side encryption library designed to make it easy for everyone to encrypt and decrypt data using industry standards and best practices” olarak tanımlanıyor (AWS Encryption SDK (yeni sekmede açılır)) ve KMS wrapping key’leriyle envelope encryption üzerine kurulu. Çağıran alanı şifreler; gateway ve log’lar yalnızca şifreli metni görür; onu ancak wrapping key üzerinde kms:Decrypt yetkisi olan bir principal okuyabilir. Bu derinlemesine savunmadır ve bir iç katmanın buna nadiren ihtiyacı olur. Servis başına değil alan başına düşünün ve yalnızca ikinci bağlantıda veya bir log satırında açık metnin gerçekten kabul edilemez olduğu yerde kullanın.

Sık yapılan hatalar#

  1. İstek imzalı diye x-user-id header’ına güvenmek. SigV4 çağıran servisi kanıtlar, öznesi hakkında hiçbir şey söylemez. İmzalı bir token taşıyın ve çağrılan serviste doğrulayın.
  2. Kullanıcı iddiası taşıyan bir stage’de data tracing açmak. Redaction, authorization header’larını ve API key değerlerini kapsar; AWS_IAM’in sizi mecbur bıraktığı custom header’ı kapsamaz.
  3. Bir tasarım dokümanında bu katman için mutual TLS vaat etmek. Private API’ler bundan dışlanmış durumda ve ayarlanacak bir property yok.
  4. Bu yola uçtan uca şifreli demek. TLS gateway’de sonlanır ve entegrasyon ikinci bir bağlantıdır.

Kimlik iki yönde de yerine oturdu. Buraya kadar her şey tek hesabı varsaydı. Çok takımlı bir yapıda takım sınırı genelde bir hesap sınırına oturur ve bu tek hamle, katmanın altındaki yetkilendirme kurallarını baştan yazar. 105. bölüm katmanı hesaplar arasına ve veri sınırına taşıyor.

Kaynaklar#

Serverless İç Servis Katmanı: Kurmadığınız, Oluşturduğunuz Mesh

Çok takımlı bir serverless yapıda service mesh benzeri doğu-batı iletişimini private API Gateway üzerine kurmak. Kimlik, şifreleme, wire format'lar, maliyet ve endüstrinin gerçekte nereye gittiği.

İlerleme 4/7 yazı tamamlandı

İlgili yazılar