How to expose a (monolithic) service securely using AWS services
By Saverio Menin
Spesso ci capita di dover rendere accessibili dall’esterno della nostra rete software aziendali magari monolitici residenti su un server. Una delle domande che mi sono posto è stata: Come posso pubblicare l’applicazione rendendola sicura da attacchi? La soluzione che ho intrapreso è stata quella di usare alcuni servizi di AWS per poter astrarre la pubblicazione del servizio e portare ad un layer superiore l’accesso al sistema. I servizi che ho utilizzato sono:
- AWS Load Balancer
- AWS Cognito
- AWS Route53
La soluzione
AWS Route 53
https://aws.amazon.com/it/route53/ E’ utilizzato per gestire il DNS dell’applicativo. Il DNS punterà al Load Balancer
AWS Load Balancer
https://aws.amazon.com/it/elasticloadbalancing/ D’ il servizio che fa da bilanciatore del carico e permette quindi di gestire il traffico e il suo instradamento. Altra caratteristica è che possono essere agganciati altri servizi tra cui:
- AWS Waf: per gestire un firewall applicativo con un set di regole che possono andare dal IP filtering a regole di analisi delel request (https://aws.amazon.com/it/waf/).
- AWS Cognito che permette di creare un layer autorizzativo all’interno del Load Balancer
Essendo un servizio gestito la mitigazione degli attacchi DDos è “intrinseca” e con AWS Waf possiamo dare molta più sicurezza al singolo endpoint.
AWS Cognito
https://aws.amazon.com/it/cognito/ E’ stato utilizzato per diventare il nostro sistema di login. In questo modo finchè non c’è una login valida NON esponiamo il nostro server al pubblico. Questo aumenta molto la sicurezza perché un qualsiasi attacco arriverebbe solo al Load Balancer che fornirebbe un 403 (se non opportunamente autorizzato). Modificando (se possiamo e abbiamo le competenze) il software, possiamo usare il JWT token staccato da Cognito (che mi garantisce la login e l’autotizzazione) per loggarsi all’applicativo. Abbiamo quindi spezzato la login da un monolite e portata su servizi gestiti senza esporre nulla in modo pubblico.
Di seguito esempi di script per generare l’infrastruttura
Terraform
# Dichiarazione del provider AWS
provider "aws" {
region = "us-west-2" # Inserisci la tua regione desiderata
}
# Creazione del record DNS in Route 53
resource "aws_route53_record" "example_dns_record" {
zone_id = "ZONE_ID" # Inserisci l'ID della zona Route 53 in cui creare il record
name = "example.com" # Inserisci il nome di dominio desiderato
type = "A"
ttl = "300"
alias {
name = "LOAD_BALANCER_DNS_NAME" # Inserisci il nome DNS del bilanciatore di carico
zone_id = "LOAD_BALANCER_ZONE_ID" # Inserisci l'ID della zona del bilanciatore di carico
evaluate_target_health = true
}
}
# Creazione del bilanciatore di carico
resource "aws_lb" "example_load_balancer" {
name = "example-load-balancer"
subnets = ["SUBNET_ID_1", "SUBNET_ID_2"] # Inserisci gli ID delle tue subnet
security_groups = ["SECURITY_GROUP_ID"] # Inserisci l'ID del tuo gruppo di sicurezza
load_balancer_type = "application"
# Definisci i tuoi listener, target group, ecc.
# ...
}
# Creazione dell'istanza EC2
resource "aws_instance" "example_instance" {
ami = "AMI_ID" # Inserisci l'ID dell'AMI desiderato
instance_type = "t2.micro"
# Definisci le tue impostazioni dell'istanza EC2, come la subnet, il gruppo di sicurezza, ecc.
# ...
}
# Creazione di un dominio Cognito
resource "aws_cognito_user_pool_domain" "example_cognito_domain" {
domain = "example-cognito-domain"
user_pool_id = "USER_POOL_ID" # Inserisci l'ID del tuo User Pool di Cognito
}
# Aggiungi il record DNS a Route 53
output "dns_record" {
value = aws_route53_record.example_dns_record.fqdn
}
CloudFormation
AWSTemplateFormatVersion: "2010-09-09"
Resources:
ExampleDNSRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: ZONE_ID # Inserisci l'ID della zona Route 53 in cui creare il record
Name: example.com # Inserisci il nome di dominio desiderato
Type: A
AliasTarget:
HostedZoneId: LOAD_BALANCER_ZONE_ID # Inserisci l'ID della zona del bilanciatore di carico
DNSName: LOAD_BALANCER_DNS_NAME # Inserisci il nome DNS del bilanciatore di carico
EvaluateTargetHealth: true
ExampleLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: example-load-balancer
Subnets:
- SUBNET_ID_1 # Inserisci gli ID delle tue subnet
- SUBNET_ID_2
SecurityGroups:
- SECURITY_GROUP_ID # Inserisci l'ID del tuo gruppo di sicurezza
Type: application
# Definisci i tuoi listener, target group, ecc.
ExampleInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: AMI_ID # Inserisci l'ID dell'AMI desiderato
InstanceType: t2.micro
# Definisci le tue impostazioni dell'istanza EC2, come la subnet, il gruppo di sicurezza, ecc.
ExampleCognitoDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: example-cognito-domain
UserPoolId: USER_POOL_ID # Inserisci l'ID del tuo User Pool di Cognito
Outputs:
DnsRecord:
Value: !Ref ExampleDNSRecord
Description: DNS Record
CDK
import * as cdk from 'aws-cdk-lib';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cognito from 'aws-cdk-lib/aws-cognito';
export class MyStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Creazione del record DNS in Route 53
const zone = route53.HostedZone.fromHostedZoneAttributes(this, 'Zone', {
hostedZoneId: 'ZONE_ID', // Inserisci l'ID della zona Route 53 in cui creare il record
zoneName: 'example.com' // Inserisci il nome di dominio desiderato
});
new route53.ARecord(this, 'DnsRecord', {
zone: zone,
recordName: 'example.com',
target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(loadBalancer))
});
// Creazione del bilanciatore di carico
const vpc = new ec2.Vpc(this, 'Vpc', {
// Definisci le tue impostazioni per la VPC
});
const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LoadBalancer', {
vpc: vpc,
internetFacing: true,
// Definisci i tuoi listener, target group, ecc.
});
// Creazione dell'istanza EC2
const instance = new ec2.Instance(this, 'Instance', {
vpc: vpc,
// Definisci le tue impostazioni dell'istanza EC2, come l'AMI, il tipo di istanza, ecc.
});
// Creazione di un dominio Cognito
const userPool = new cognito.UserPool(this, 'UserPool', {
// Definisci le tue impostazioni per il User Pool di Cognito
});
const userPoolDomain = new cognito.CfnUserPoolDomain(this, 'UserPoolDomain', {
domain: 'example-cognito-domain',
userPoolId: userPool.userPoolId
});
}
}
const app = new cdk.App();
new MyStack(app, 'MyStack');
app.synth();
Conclusione
La soluzione ci ha permesso di diminuire la complessità di rete ma avere sicurezza e una maggior gestione senza necessariamente avere punti vulnerabili nel nostro perimetro.