2025-09-05
AWS CDK Link Shortener 3. Bölüm: Gelişmiş Özellikler ve Güvenlik
Custom domain'ler, toplu işlemler, URL expiration ve kapsamlı güvenlik önlemlerinin implementasyonu. Production link shortener servisleri için defense-in-depth güvenlik stratejileri.
AWS CDK Link Shortener 3. Bölüm: Gelişmiş Özellikler ve Güvenlik
Production bir link shortener oluşturmak sadece kısa URL’ler yaratmaktan daha fazlasını gerektirir - meşru ölçeği kaldırabilirken kötüye kullanımı engelleyen kapsamlı güvenlik önlemleri gerektirir. Link shortener’lar, onları zarlı içerik dağıtmak, güvenlik filtrelerini atlatmak ve phishing kampanyaları yürütmek için kullanan kötü niyetli aktörler için çekici hedeflerdir.
Modern link shortener servisleri, input validasyonu, rate limiting, authentication ve gerçek zamanlı monitoring’i birleştiren defense-in-depth koruma gerektirir. Bu yaklaşım hem servisinizi hem de kısaltılmış linklere tıklayan kullanıcıları korur.
1. Bölüm ve 2. Bölüm’de temel yapı ve redirect fonksiyonalitesini oluşturduk. Şimdi bir oyuncak projeden production servise ayıran gelişmiş özellikleri ve güvenlik önlemlerini ekleyelim.
Custom Short Domain’ler: Vanity URL’lerden Fazlası
Güvenliğe dalmadan önce, custom domain’leri ele alalım. Pazarlama ekibin er ya da geç yourdomain.com/abc123 yerine acme.co/promo gibi markalı kısa URL’ler isteyecek. İşte nasıl çalışır hale getireceğiz:
// lib/custom-domain-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
export class CustomDomainStack extends Stack {
public readonly customDomainName: apigateway.DomainName;
constructor(scope: Construct, id: string, props: StackProps & {
domainName: string;
hostedZoneId: string;
certificateArn: string; // Pre-created ACM certificate
restApi: apigateway.RestApi;
}) {
super(scope, id, props);
// Import existing hosted zone
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
hostedZoneId: props.hostedZoneId,
zoneName: props.domainName,
});
// Import existing certificate (must be in us-east-1 for API Gateway)
const certificate = acm.Certificate.fromCertificateArn(
this,
'Certificate',
props.certificateArn
);
// Create custom domain name for API Gateway
this.customDomainName = new apigateway.DomainName(this, 'CustomDomain', {
domainName: props.domainName,
certificate: certificate,
securityPolicy: apigateway.SecurityPolicy.TLS_1_2,
endpointType: apigateway.EndpointType.EDGE,
});
// Map the custom domain to our API
this.customDomainName.addBasePathMapping(props.restApi, {
basePath: '', // Root path
});
// Create Route53 alias record
new route53.ARecord(this, 'CustomDomainAlias', {
zone: hostedZone,
target: route53.RecordTarget.fromAlias(
new targets.ApiGatewayDomain(this.customDomainName)
),
});
}
}
Sahadan ipucu: API Gateway edge-optimized endpoint’ler için ACM sertifikanı diğer kaynaklarınızın nerede olduğuna bakılmaksızın her zaman us-east-1’de oluştur. Sertifikamın neden “mevcut değil” olduğunu debug ederken iki saatimi harcadım bu gereksinimi fark etmeden önce.
Bulk İşlemler: Scale’i Zarif Bir Şekilde Karşılamak
Pazarlama ekipleri bulk işlemleri seviyor. İşte Lambda concurrency limitlerini patlatmayacak production-test edilmiş bir implementasyon:
// lambda/bulk-create.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { nanoid } from 'nanoid';
const dynamodb = new DynamoDBClient({});
const sqs = new SQSClient({});
interface BulkCreateRequest {
urls: Array<{
originalUrl: string;
customSlug?: string;
expiresAt?: string;
tags?: string[];
}>;
userId: string;
}
export async function handler(
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
try {
const request: BulkCreateRequest = JSON.parse(event.body || '{}');
// Validate batch size to prevent resource exhaustion
if (request.urls.length > 1000) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Batch boyutu 1000 URL\'yi geçemez'
}),
};
}
// For large batches, use SQS for async processing
if (request.urls.length > 100) {
const jobId = nanoid();
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.BULK_PROCESSING_QUEUE_URL,
MessageBody: JSON.stringify({
jobId,
userId: request.userId,
urls: request.urls,
}),
MessageAttributes: {
jobType: {
DataType: 'String',
StringValue: 'BULK_CREATE'
}
}
}));
return {
statusCode: 202,
body: JSON.stringify({
jobId,
message: 'Bulk oluşturma job\'u kuyruğa eklendi',
estimatedCompletionTime: Math.ceil(request.urls.length / 10) + ' dakika'
}),
};
}
// Process small batches synchronously
const results = await Promise.allSettled(
request.urls.map(async (urlData) => {
const shortCode = urlData.customSlug || nanoid(8);
// Validate URL before creating
if (!isValidUrl(urlData.originalUrl)) {
throw new Error(`Geçersiz URL: ${urlData.originalUrl}`);
}
// Check for malicious content (more on this later)
await validateUrlSafety(urlData.originalUrl);
return await createShortUrl({
shortCode,
originalUrl: urlData.originalUrl,
userId: request.userId,
expiresAt: urlData.expiresAt,
tags: urlData.tags || [],
});
})
);
const successful = results
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<any>).value);
const failed = results
.filter(result => result.status === 'rejected')
.map(result => (result as PromiseRejectedResult).reason.message);
return {
statusCode: 200,
body: JSON.stringify({
successful: successful.length,
failed: failed.length,
errors: failed,
urls: successful,
}),
};
} catch (error) {
console.error('Bulk create error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' }),
};
}
}
function isValidUrl(url: string): boolean {
try {
const parsedUrl = new URL(url);
return ['http:', 'https:'].includes(parsedUrl.protocol);
} catch {
return false;
}
}
async function validateUrlSafety(url: string): Promise<void> {
// Implementation coming up in security section
// This is where we check against malicious domains
}
URL Expiration ve Scheduling: Zamana Dayalı Özellikler
Pazarlama kampanyalarının expiration tarihleri olması gerek. İşte pahalı cleanup job’ları çalıştırmadan URL expiration’ın nasıl implement edileceği:
// lambda/redirect-with-expiration.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, GetItemCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
export async function handler(
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
const shortCode = event.pathParameters?.shortCode;
if (!shortCode) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Kısa kod bulunamadı' }),
};
}
try {
const response = await dynamodb.send(new GetItemCommand({
TableName: process.env.URLS_TABLE_NAME,
Key: marshall({ shortCode }),
}));
if (!response.Item) {
return {
statusCode: 404,
headers: {
'Content-Type': 'text/html',
},
body: createNotFoundPage(),
};
}
const item = unmarshall(response.Item);
// Check expiration
if (item.expiresAt && new Date(item.expiresAt) < new Date()) {
// URL expired - optionally log this for analytics
await recordExpiredAccess(shortCode, item.userId);
return {
statusCode: 410, // Gone
headers: {
'Content-Type': 'text/html',
},
body: createExpiredPage(item.originalUrl),
};
}
// Check if URL is scheduled for future activation
if (item.activateAt && new Date(item.activateAt) > new Date()) {
return {
statusCode: 404, // Not yet active
headers: {
'Content-Type': 'text/html',
},
body: createNotYetActivePage(),
};
}
// Update click count asynchronously (fire and forget)
updateClickCount(shortCode, event).catch(console.error);
return {
statusCode: 302,
headers: {
Location: item.originalUrl,
'Cache-Control': 'no-cache', // Important for expired URLs
},
body: '',
};
} catch (error) {
console.error('Redirect error:', error);
return {
statusCode: 500,
headers: {
'Content-Type': 'text/html',
},
body: createErrorPage(),
};
}
}
async function recordExpiredAccess(shortCode: string, userId: string): Promise<void> {
// Record that someone tried to access an expired URL
// Useful for analytics and potential abuse detection
try {
await dynamodb.send(new UpdateItemCommand({
TableName: process.env.ANALYTICS_TABLE_NAME,
Key: marshall({
pk: `USER#${userId}`,
sk: `EXPIRED#${shortCode}#${Date.now()}`,
}),
UpdateExpression: 'SET #count = if_not_exists(#count, :zero) + :inc',
ExpressionAttributeNames: {
'#count': 'expiredAccessCount',
},
ExpressionAttributeValues: marshall({
':zero': 0,
':inc': 1,
}),
}));
} catch (error) {
console.error('Failed to record expired access:', error);
}
}
function createExpiredPage(originalUrl: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<title>Link Süresi Dolmuş</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }
.container { max-width: 500px; margin: 0 auto; }
</style>
</head>
<body>
<div class="container">
<h1>Link Süresi Dolmuş</h1>
<p>Bu linkin süresi dolmuş ve artık kullanılamıyor.</p>
<p>Orijinal hedef: <code>${originalUrl}</code></p>
<a href="/">Ana sayfaya git</a>
</div>
</body>
</html>
`;
}
Güvenlik: Defense in Depth
Şimdi bu yazının en önemli kısmı. Güvenlik sonradan düşünülecek bir şey değil - servisinizin malware dağıtım platformu haline gelmesini engelleyen şey. İşte katmanlı güvenlik yaklaşımımız:
Katman 1: Input Validation ve URL Güvenliği
// lambda/url-validator.ts
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';
const dynamodb = new DynamoDBClient({});
// Malicious domain blacklist (this should be regularly updated)
const MALICIOUS_DOMAINS = new Set([
// Add known malicious domains here
// In production, load this from DynamoDB or Parameter Store
]);
// URL patterns that are commonly abused
const SUSPICIOUS_PATTERNS = [
/bit\.ly/i, // Nested shorteners
/tinyurl\.com/i, // Nested shorteners
/localhost/i, // Local development
/192\.168\./i, // Private networks
/127\.0\.0\.1/i, // Localhost
/10\./i, // Private networks
/172\.16\./i, // Private networks
];
export async function validateUrlSafety(url: string): Promise<{
isValid: boolean;
reason?: string;
}> {
try {
const parsedUrl = new URL(url);
// Check protocol
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
return {
isValid: false,
reason: 'Sadece HTTP ve HTTPS URL\'lerine izin veriliyor'
};
}
// Check for private/local addresses
if (SUSPICIOUS_PATTERNS.some(pattern => pattern.test(url))) {
return {
isValid: false,
reason: 'URL şüpheli pattern\'ler içeriyor'
};
}
// Check against malicious domain blacklist
if (MALICIOUS_DOMAINS.has(parsedUrl.hostname.toLowerCase())) {
return {
isValid: false,
reason: 'Domain blacklist\'te'
};
}
// Check against dynamic blacklist in DynamoDB
const blacklistCheck = await dynamodb.send(new GetItemCommand({
TableName: process.env.BLACKLIST_TABLE_NAME,
Key: marshall({
domain: parsedUrl.hostname.toLowerCase()
}),
}));
if (blacklistCheck.Item) {
return {
isValid: false,
reason: 'Domain blacklist\'te'
};
}
// Optional: Check against external reputation services
const reputationCheck = await checkUrlReputation(url);
if (!reputationCheck.isValid) {
return reputationCheck;
}
return { isValid: true };
} catch (error) {
return {
isValid: false,
reason: 'Geçersiz URL formatı'
};
}
}
async function checkUrlReputation(url: string): Promise<{
isValid: boolean;
reason?: string;
}> {
// In production, integrate with services like:
// - Google Safe Browsing API
// - VirusTotal API
// - URLVoid API
// For now, return valid
return { isValid: true };
}
Katman 2: Kimlik Doğrulama ve Yetkilendirme
// lambda/authorizer.ts
import { APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult } from 'aws-lambda';
import { verify } from 'jsonwebtoken';
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
const dynamodb = new DynamoDBClient({});
interface JWTPayload {
sub: string;
email: string;
role: string;
exp: number;
}
export async function handler(
event: APIGatewayTokenAuthorizerEvent
): Promise<APIGatewayAuthorizerResult> {
try {
const token = event.authorizationToken?.replace('Bearer ', '');
if (!token) {
throw new Error('Token bulunamadı');
}
// Verify JWT token
const decoded = verify(token, process.env.JWT_SECRET!) as JWTPayload;
// Get user details from DynamoDB
const userResponse = await dynamodb.send(new GetItemCommand({
TableName: process.env.USERS_TABLE_NAME,
Key: marshall({ userId: decoded.sub }),
}));
if (!userResponse.Item) {
throw new Error('Kullanıcı bulunamadı');
}
const user = unmarshall(userResponse.Item);
// Check if user is active
if (user.status !== 'ACTIVE') {
throw new Error('Kullanıcı aktif değil');
}
// Generate policy based on user role
const policy = generatePolicy(decoded.sub, 'Allow', event.methodArn, user.role);
// Add user context to be available in Lambda functions
policy.context = {
userId: decoded.sub,
email: decoded.email,
role: user.role,
planType: user.planType || 'free',
};
return policy;
} catch (error) {
console.error('Authorization failed:', error);
throw new Error('Unauthorized');
}
}
function generatePolicy(
principalId: string,
effect: 'Allow' | 'Deny',
resource: string,
role: string
): APIGatewayAuthorizerResult {
const policyDocument = {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
},
],
};
// Role-based permissions
if (role === 'admin') {
// Admins can access all endpoints
policyDocument.Statement[0].Resource = '*';
} else if (role === 'premium') {
// Premium users get access to advanced features
policyDocument.Statement.push({
Action: 'execute-api:Invoke',
Effect: 'Allow',
Resource: resource.replace('/create', '/bulk-create'),
});
}
return {
principalId,
policyDocument,
};
}
Katman 3: Rate Limiting ve Kötüye Kullanım Korunması
// lambda/rate-limiter.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, UpdateItemCommand, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
const dynamodb = new DynamoDBClient({});
interface RateLimitConfig {
requestsPerMinute: number;
requestsPerHour: number;
requestsPerDay: number;
}
const RATE_LIMITS: Record<string, RateLimitConfig> = {
free: {
requestsPerMinute: 10,
requestsPerHour: 100,
requestsPerDay: 1000,
},
premium: {
requestsPerMinute: 100,
requestsPerHour: 1000,
requestsPerDay: 10000,
},
admin: {
requestsPerMinute: 1000,
requestsPerHour: 10000,
requestsPerDay: 100000,
},
};
export async function checkRateLimit(
userId: string,
planType: string = 'free',
clientIp?: string
): Promise<{
allowed: boolean;
resetTime?: number;
remainingRequests?: number;
}> {
const config = RATE_LIMITS[planType] || RATE_LIMITS.free;
const now = Date.now();
// Create time windows
const minuteWindow = Math.floor(now / (60 * 1000));
const hourWindow = Math.floor(now / (60 * 60 * 1000));
const dayWindow = Math.floor(now / (24 * 60 * 60 * 1000));
try {
// Check and update rate limits atomically
const updateResult = await dynamodb.send(new UpdateItemCommand({
TableName: process.env.RATE_LIMIT_TABLE_NAME,
Key: marshall({
userId,
window: 'COMBINED'
}),
UpdateExpression: `
SET
#minute = if_not_exists(#minute, :zero),
#hour = if_not_exists(#hour, :zero),
#day = if_not_exists(#day, :zero),
#minuteWindow = if_not_exists(#minuteWindow, :currentMinute),
#hourWindow = if_not_exists(#hourWindow, :currentHour),
#dayWindow = if_not_exists(#dayWindow, :currentDay)
ADD
#minute :inc,
#hour :inc,
#day :inc
`,
ConditionExpression: `
(attribute_not_exists(#minuteWindow) OR #minuteWindow = :currentMinute OR #minute < :minuteLimit) AND
(attribute_not_exists(#hourWindow) OR #hourWindow = :currentHour OR #hour < :hourLimit) AND
(attribute_not_exists(#dayWindow) OR #dayWindow = :currentDay OR #day < :dayLimit)
`,
ExpressionAttributeNames: {
'#minute': 'requestsThisMinute',
'#hour': 'requestsThisHour',
'#day': 'requestsThisDay',
'#minuteWindow': 'minuteWindow',
'#hourWindow': 'hourWindow',
'#dayWindow': 'dayWindow',
},
ExpressionAttributeValues: marshall({
':zero': 0,
':inc': 1,
':currentMinute': minuteWindow,
':currentHour': hourWindow,
':currentDay': dayWindow,
':minuteLimit': config.requestsPerMinute,
':hourLimit': config.requestsPerHour,
':dayLimit': config.requestsPerDay,
}),
ReturnValues: 'ALL_NEW',
}));
const item = unmarshall(updateResult.Attributes!);
return {
allowed: true,
remainingRequests: Math.min(
config.requestsPerMinute - item.requestsThisMinute,
config.requestsPerHour - item.requestsThisHour,
config.requestsPerDay - item.requestsThisDay
),
};
} catch (error: any) {
if (error.name === 'ConditionalCheckFailedException') {
// Rate limit exceeded
const getResult = await dynamodb.send(new GetItemCommand({
TableName: process.env.RATE_LIMIT_TABLE_NAME,
Key: marshall({ userId, window: 'COMBINED' }),
}));
if (getResult.Item) {
const item = unmarshall(getResult.Item);
// Calculate reset time based on which limit was hit
let resetTime = now + (60 * 1000); // Default to 1 minute
if (item.requestsThisDay >= config.requestsPerDay) {
resetTime = (dayWindow + 1) * 24 * 60 * 60 * 1000;
} else if (item.requestsThisHour >= config.requestsPerHour) {
resetTime = (hourWindow + 1) * 60 * 60 * 1000;
}
return {
allowed: false,
resetTime,
remainingRequests: 0,
};
}
}
throw error;
}
}
export function createRateLimitResponse(resetTime: number): APIGatewayProxyResult {
return {
statusCode: 429,
headers: {
'X-RateLimit-Reset': Math.ceil(resetTime / 1000).toString(),
'Retry-After': Math.ceil((resetTime - Date.now()) / 1000).toString(),
},
body: JSON.stringify({
error: 'Rate limit aşıldı',
message: 'Çok fazla request. Lütfen daha sonra tekrar deneyin.',
resetTime: new Date(resetTime).toISOString(),
}),
};
}
Katman 4: AWS WAF Korunması
// lib/waf-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as logs from 'aws-cdk-lib/aws-logs';
export class WAFStack extends Stack {
public readonly webAcl: wafv2.CfnWebACL;
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
// Create CloudWatch log group for WAF logs
const logGroup = new logs.LogGroup(this, 'WAFLogGroup', {
logGroupName: `/aws/wafv2/link-shortener`,
retention: logs.RetentionDays.ONE_MONTH,
});
this.webAcl = new wafv2.CfnWebACL(this, 'LinkShortenerWAF', {
scope: 'CLOUDFRONT', // Use REGIONAL for ALB/API Gateway
defaultAction: { allow: {} },
rules: [
// Rule 1: AWS Managed Rules - Core Rule Set
{
name: 'AWSManagedRulesCore',
priority: 1,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesCore',
},
},
// Rule 2: Rate limiting for URL creation
{
name: 'RateLimitCreation',
priority: 2,
statement: {
rateBasedStatement: {
limit: 1000, // requests per 5-minute window
aggregateKeyType: 'IP',
scopeDownStatement: {
byteMatchStatement: {
searchString: '/create',
fieldToMatch: { uriPath: {} },
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
positionalConstraint: 'CONTAINS',
},
},
},
},
action: {
block: {
customResponse: {
responseCode: 429,
customResponseBodyKey: 'RateLimitExceeded',
},
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'RateLimitCreation',
},
},
// Rule 3: Block known bot networks
{
name: 'AWSManagedRulesBot',
priority: 3,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesBotControlRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesBot',
},
},
// Rule 4: IP reputation list
{
name: 'AWSManagedRulesIPReputation',
priority: 4,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesAmazonIpReputationList',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesIPReputation',
},
},
// Rule 5: Custom geo-blocking (if needed)
{
name: 'GeoBlocking',
priority: 5,
statement: {
geoMatchStatement: {
// Block requests from specific countries if needed
countryCodes: [], // Add country codes to block
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'GeoBlocking',
},
},
],
customResponseBodies: {
RateLimitExceeded: {
contentType: 'APPLICATION_JSON',
content: JSON.stringify({
error: 'Rate limit aşıldı',
message: 'IP adresinden çok fazla request. Lütfen daha sonra tekrar deneyin.',
}),
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'LinkShortenerWAF',
},
});
// Enable logging
new wafv2.CfnLoggingConfiguration(this, 'WAFLogging', {
resourceArn: this.webAcl.attrArn,
logDestinationConfigs: [logGroup.logGroupArn],
loggingFilter: {
defaultBehavior: 'KEEP',
filters: [
{
behavior: 'DROP',
conditions: [
{
actionCondition: {
action: 'ALLOW',
},
},
],
requirement: 'MEETS_ANY',
},
],
},
});
}
}
Gelişmiş Analytics ve Monitoring
Güvenlik sadece kötü aktörleri engellemek değil - sisteminde neler olduğunu anlamakla ilgili:
// lambda/security-monitor.ts
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const cloudwatch = new CloudWatchClient({});
const sns = new SNSClient({});
export interface SecurityEvent {
type: 'RATE_LIMIT_EXCEEDED' | 'MALICIOUS_URL_BLOCKED' | 'SUSPICIOUS_BULK_REQUEST';
userId?: string;
clientIp: string;
userAgent?: string;
details: Record<string, any>;
timestamp: number;
}
export async function recordSecurityEvent(event: SecurityEvent): Promise<void> {
try {
// Send metric to CloudWatch
await cloudwatch.send(new PutMetricDataCommand({
Namespace: 'LinkShortener/Security',
MetricData: [
{
MetricName: event.type,
Value: 1,
Unit: 'Count',
Timestamp: new Date(event.timestamp),
Dimensions: [
{
Name: 'EventType',
Value: event.type,
},
...(event.userId ? [{
Name: 'UserId',
Value: event.userId,
}] : []),
],
},
],
}));
// For critical events, send SNS alert
if (shouldAlertOn(event)) {
await sns.send(new PublishCommand({
TopicArn: process.env.SECURITY_ALERTS_TOPIC_ARN,
Subject: `Güvenlik Alarmı: ${event.type}`,
Message: JSON.stringify({
eventType: event.type,
timestamp: new Date(event.timestamp).toISOString(),
clientIp: event.clientIp,
userId: event.userId,
details: event.details,
}, null, 2),
}));
}
console.log('Security event recorded:', {
type: event.type,
userId: event.userId,
clientIp: event.clientIp,
timestamp: event.timestamp,
});
} catch (error) {
console.error('Failed to record security event:', error);
// Don't throw - security monitoring failures shouldn't break the main flow
}
}
function shouldAlertOn(event: SecurityEvent): boolean {
// Define which events should trigger immediate alerts
const alertEvents: SecurityEvent['type'][] = [
'MALICIOUS_URL_BLOCKED',
'SUSPICIOUS_BULK_REQUEST',
];
return alertEvents.includes(event.type);
}
// Create a dashboard for security metrics
export async function createSecurityDashboard(): Promise<void> {
// This would be part of your CDK infrastructure code
// Implementation depends on your specific monitoring needs
}
Hepsini Bir Araya Getirmek: Güvenlik-First API
İşte tüm bu güvenlik katmanlarının production endpoint’te nasıl bir araya geldiği:
// lambda/secure-create-url.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { validateUrlSafety } from './url-validator';
import { checkRateLimit, createRateLimitResponse } from './rate-limiter';
import { recordSecurityEvent } from './security-monitor';
import { nanoid } from 'nanoid';
export async function handler(
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
const startTime = Date.now();
try {
// Extract user context from authorizer
const userId = event.requestContext.authorizer?.userId;
const planType = event.requestContext.authorizer?.planType || 'free';
const clientIp = event.requestContext.identity?.sourceIp;
if (!userId) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Kimlik doğrulama gerekli' }),
};
}
// Check rate limits first (fail fast)
const rateLimitCheck = await checkRateLimit(userId, planType, clientIp);
if (!rateLimitCheck.allowed) {
await recordSecurityEvent({
type: 'RATE_LIMIT_EXCEEDED',
userId,
clientIp: clientIp!,
userAgent: event.headers['User-Agent'],
details: { planType, resetTime: rateLimitCheck.resetTime },
timestamp: Date.now(),
});
return createRateLimitResponse(rateLimitCheck.resetTime!);
}
// Parse and validate request
const request = JSON.parse(event.body || '{}');
if (!request.originalUrl) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'originalUrl gerekli',
remainingRequests: rateLimitCheck.remainingRequests
}),
};
}
// Validate URL safety
const safetyCheck = await validateUrlSafety(request.originalUrl);
if (!safetyCheck.isValid) {
await recordSecurityEvent({
type: 'MALICIOUS_URL_BLOCKED',
userId,
clientIp: clientIp!,
userAgent: event.headers['User-Agent'],
details: {
originalUrl: request.originalUrl,
reason: safetyCheck.reason
},
timestamp: Date.now(),
});
return {
statusCode: 400,
body: JSON.stringify({
error: 'URL doğrulama başarısız',
reason: safetyCheck.reason,
remainingRequests: rateLimitCheck.remainingRequests
}),
};
}
// Create short URL
const shortCode = request.customSlug || nanoid(8);
// TODO: Save to DynamoDB (implementation from previous parts)
const shortUrl = await createShortUrl({
shortCode,
originalUrl: request.originalUrl,
userId,
expiresAt: request.expiresAt,
tags: request.tags || [],
});
const responseTime = Date.now() - startTime;
// Record successful creation
console.log(`URL başarıyla oluşturuldu: ${shortCode} -> ${request.originalUrl} (${responseTime}ms)`);
return {
statusCode: 201,
headers: {
'X-RateLimit-Remaining': rateLimitCheck.remainingRequests?.toString() || '0',
'X-Response-Time': responseTime.toString(),
},
body: JSON.stringify({
shortCode,
shortUrl: `${process.env.DOMAIN_NAME}/${shortCode}`,
originalUrl: request.originalUrl,
createdAt: new Date().toISOString(),
expiresAt: request.expiresAt,
remainingRequests: rateLimitCheck.remainingRequests,
}),
};
} catch (error) {
console.error('Error creating short URL:', error);
const responseTime = Date.now() - startTime;
return {
statusCode: 500,
headers: {
'X-Response-Time': responseTime.toString(),
},
body: JSON.stringify({
error: 'Internal server error',
requestId: event.requestContext.requestId
}),
};
}
}
Temel Güvenlik Konuları
Production-ready link shortener güvenliği implement ederken, birkaç kritik faktör dikkatli inceleme gerektirir:
1. Güvenlik-İlk Mimari Güvenlik önlemlerini başlangıçtan tasarlamak sonradan retrofit etmekten daha etkilidir. Erken güvenlik entegrasyonu mimari çatışmaları önler ve ölçekleme sırasında teknik borç azaltır.
2. Serverless Rate Limiting Zorlukları Geleneksel token bucket algoritmaları invocation’lar arası durumsuzluk nedeniyle serverless ortamlara iyi çevrilmez. Zaman tabanlı pencereli DynamoDB atomik counter’lar daha iyi serverless rate limiting sağlar, ancak write capacity unit’ler izleme gerektirir.
3. Adaptif URL Doğrulama Kötü niyetli domain listeleri tehdit aktörleri yeni domain’ler kaydettikçe sürekli güncelleme gerektirir. Hızlı blocklist güncellemelerini destekleyen sistemler inşa etmek kapsamlı başlangıç kapsamı denemeye göre daha sürdürülebilir.
4. Pattern Tabanlı Monitoring Tekil güvenlik olayları genellikle sınırlı içgörü sağlar. Monitoring sistemleri davranışsal pattern’leri tespit etmeye odaklanmalı: tekil IP’lerden tekrarlı istekler, olağandışı redirect hacimleri veya yeni oluşturulan hesaplardan bulk işlemler.
5. Custom Domain Planlama Markalı kısa URL’ler servisler olgunlaştıkça tipik olarak gereksinim haline gelir. Başlangıç geliştirme sırasında custom domain desteği implement etmek mevcut sistemleri retrofit etmeyle kıyaslandığında sonraki genişletmeyi kolaylaştırır.
Sırada Ne Var?
4. Bölüm’de production deployment stratejilerini, gerçekten issue’ları debug etmekte yardımcı olan monitoring’i ve ayda yüzlerce dolar tasarruf ettirebilecek maliyet optimizasyon tekniklerini ele alacağız.
Ayrıca trafik spike yönetimi, veritabanı ölçekleme pattern’leri ve güvenilir production görünürlüğü sağlayan monitoring konfigürasyonları dahil operasyonel hususları keşfedeceğiz.
Burada kurulan güvenlik temeli, gelişen tehditlere karşı korumayı sürdürken önemli trafik hacimlerini yönetmek için ölçekleme destekler. Etkili deployment pipeline’ları ve kapsamlı monitoring bu güvenlik önlemlerinin ölçekte etkili kalmasını sağlar.
Mevcut AWS Fiyatlandırma Avantajları
Son AWS fiyatlandırma güncellemeleriyle (2024-2025), CloudFront’u WAF ile birleştirmek daha maliyet-etkili hale geldi:
- CloudFront fiyat azaltımı: Veri transferinde %25’e kadar maliyet tasarrufu
- WAF entegrasyonu: CloudFront-WAF ilişkilendirmesi için ek ücret yok
- Bölgesel optimizasyon: WAF fiyatlandırması bölgeye göre değişir, us-east-1 tipik olarak en düşük oranları sunar
- İstek filtreleme: WAF kötü niyetli istekleri Lambda fonksiyonlarınıza ulaşmadan önce bloke eder, compute maliyetlerini azaltır
Bu iyileştirmeler production link shortener servisleri için kapsamlı güvenlik katmanları implement etmeyi daha ekonomik hale getirir.
AWS CDK Link Kısaltıcı: Sıfırdan Production'a
AWS CDK, Node.js Lambda ve DynamoDB ile production-grade bir link kısaltma servisi kurulumu hakkında 5 bölümlük kapsamlı seri. Gerçek production hikayeleri, performans optimizasyonu ve maliyet yönetimi dahil.
Serideki tüm yazılar
İlgili yazılar
Amazon Cognito'nun gelişmiş özellikleri üzerine kapsamlı teknik kılavuz: özel authentication akışları, federation pattern'leri, multi-tenancy mimarileri, migration stratejileri ve production-grade güvenlik implementasyonu.
AWS Secrets Manager ve Systems Manager Parameter Store'u karşılaştıran kapsamlı teknik rehber - hangi servisi ne zaman kullanmalı ve gerçek dünya implementation pattern'leri.
TypeScript ile organizasyonunuzun internal sistemleri için custom Model Context Protocol serverları nasıl geliştirip, güvenli hale getirip, deploy edeceğinizi öğren. Authentication, monitoring ve Kubernetes deployment örnekleriyle.
Amazon SNS ve SQS kullanarak güvenli cross-account event dağıtımı nasıl yapılır öğrenin. IAM policy'leri, KMS şifreleme, AWS CDK implementasyonu ve production'da karşılaşılan yaygın sorunları kapsıyor.
AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Gerçek mimari kararlar, ilk kurulum ve büyük ölçekte URL kısaltıcıları inşa etmenin dersleri.