disposable email

Disposable: la tua mail “usa e getta” su AWS

11 min

Il 90% dei servizi online prevede un processo di registrazione durante il quale fornire il proprio indirizzo di posta elettronica. Questo andrà poi confermato ricevendo una email provvista di link sul quale cliccare. A questo punto avremo accesso al servizio al quale eravamo interessati ma contemporaneamente daremo il via alla ricezione di newsletter, sondaggi, promozioni, ecc.

Un indirizzo “usa e getta” è utile proprio in queste situazioni: ci consente di portare a termine il processo di registrazione e ci evita di ricevere in futuro email indesiderate, mantenendo “pulita” la nostra mailbox principale, che utilizziamo per lavoro o per scrivere agli amici.

NOVITA’!!

Sei alla ricerca di un servizio email “usa e getta” pronto all’uso, dotato di API fantastiche e integrato con Slack?

my24h.email

Esistono diversi servizi online che offrono la possibilità di creare indirizzi email “usa e getta” ed il più famoso è probabilmente Mailinator. Sono però servizi che poco si prestano ad un utilizzo professionale in ambito aziendale e sono spesso caratterizzati da alcune limitazioni. Il dominio di posta utilizzato, per esempio, è spesso riconosciuto per “usa e getta” e non considerato valido durante il processo di registrazione.

Andremo a realizzare Disposable-mail, un servizio di mail monouso personalizzabile con il proprio dominio internet, utilizzando i building block AWS. Il servizio è totalmente serverless: non sono infatti necessarie istanze EC2 ma solo semplici lambda functions.

Disposable-mail funziona così:

  1. Val sul sito
  2. Scegli un indirizzo email e lo digiti
  3. La mailbox che hai scelto viene immediatamente attivata e puoi iniziare a ricevere email

Se vuoi provarlo lo trovi qui: http://disposable.aws.gotocloud.it

Non serve registrarsi o scegliere una password. La mailbox ha validità di un’ora e allo scadere dei 60 minuti tutti i messaggi ricevuti saranno eliminati e la mailbox non sarà più attiva.

Non è possibile inviare email, ma solo riceverle. Gli allegati non vengono visualizzati (ma forse lo saranno nella prossima versione).

Per evitare abusi utilizziamo Google reCAPTCHA nella form di login, in versione hidden.

Requisiti

Per realizzare il tuo servizio di email “usa e getta” sul tuo dominio personale ti serve:

  • un account AWS
  • un dominio internet (meglio un terzo livello) su Route53 o comunque per il quale ti sia possibile configurare la relativa zona DNS
  • una coppia di chiavi (site/private) di Google reCAPTCHA validi per il tuo dominio (reCAPTCHA console)

Tutto qui.

Architettura della soluzione

Come anticipato, Disposable-mail è totalmente serverless e, data la natura di questi building blocks AWS, non ci dovremo preoccupare della loro gestione, la scalabilità è garantita e il costo è “per utilizzo”.

Dal layout si comprende come esistano essenzialmente due workflow: il primo, totalmente automatizzato, di ricezione, verifica, memorizzazione e cancellazione delle email. Il secondo, dato dall’utente che accede all’applicazione React e consulta la propria mailbox. L’elemento comune dei due è rappresentato dal bucket S3 dove vengono memorizzate le email ricevute e le tabelle DynamoDB dove vengono memorizzati i relativi metadati.

Per la ricezione delle email ci affidiamo al servizio Amazon SES (Simple Email Service): ci occuperemo della sua configurazione nel paragrafo successivo. Alcune funzioni lambda si occupano di gestire il flusso delle email. Route53 è utilizzato per ospitare la zona relativa al nostro dominio internet.

La web application è ospitata direttamente in un bucket S3 pubblico e l’endpoint API è gestito dal servizio Amazon API Gateway. Alcune funzioni lambda si occupano di processare le richieste da parte dei client.

Ci affidiamo a CloudWatch per eseguire periodicamente una funzione lambda il cui scopo è disattivare ed eliminare le mailbox scadute.

Primi passi

Il primo passo per la realizzazione della soluzione è la corretta configurazione del servizio Amazon SES. Il template CloudFormation che utilizzeremo per la realizzazione di tutta l’infrastruttura di backend presuppone che il dominio email da noi scelto sia già correttamente configurato e verificato.

Iniziamo la configurazione: dalla console AWS è necessario accedere al servizio SES. Non tutte le Region AWS dispongono di questo servizio: è possibile quindi che si debba utilizzare una Region differente da quella che si utilizza normalmente. L’intera soluzione sarà ospitata, per semplicità, sulla medesima Region.

In Domains viene visualizzato l’elenco di tutti i domini internet per i quali il servizio SES è stato configurato. Dovremo aggiungere il nostro dominio ed effettuare il processo di verifica.

Cliccando su Verify a New Domain andiamo ad aggiungere il nostro dominio specificandone il nome. Come detto in precedenza, meglio utilizzare un dominio di terzo livello perché tutte le mailbox di questo saranno gestite da Disposable-mail.

Una volta aggiunto il dominio dobbiamo occuparci della configurazione delle relativa zona DNS. Si dovranno inserire due record:

  • un record TXT per la verifica da parte di AWS
  • un record MX per abilitare la ricezione delle email

Se la zona DNS è gestita da Route53, tutto quello che dobbiamo fare è cliccare su Use Route 53: la configurazione dei due record sarà automatica. In caso contrario è necessario fare riferimento al proprio provider per modificare la zona come indicato.

Una volta che il dominio sarà stato verificato, saremo in grado di ricevere email tramite Amazon SES.

Backend – Ricezione emails

Come vedremo, il template CloudFormation crea una nuova regola di ricezione email (Rule Set) di tipo “catch-all”, cioè valida per tutti gli indirizzi email del tipo <qualsiasi.cosa>@<dominio.scelto>

Il “Recipient” specificato corrisponde infatti al nostro dominio.

La prima Action si occupa di invocare una funziona lambda e di verificarne il responso. La funzione IncomingMailCheckFunction si occupa di verificare che l’indirizzo di destinazione della mail sia valido e attivo. Indica poi al servizio SES se proseguire (CONTINUE) o interrompere l’esecuzione di ulteriori azioni (STOP_RULE_SET).

La seconda Action, eseguita esclusivamente nel caso in cui la precedente lo abbia consentito, comporta il salvataggio della email nel bucket S3, il cui nome corrisponde a “incoming.disposable.<nome_dominio>” e la notifica dell’avvenuto salvataggio ad un topic SNS “IncomingMailTopic“.

Come avviene la verifica dell’indirizzo email? Una tabella DynamoDB è utilizzata per memorizzare l’elenco degli indirizzi email attivi e la relativa scadenza (TTL). La chiave della tabella è proprio l’indirizzo email (address).

## part of IncomingMailCheckFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ['addresses_table_name'])

def address_exists(address):
  exists = False
  response = table.get_item(Key={'address': address})
  if 'Item' in response:
      item = response['Item']
      if item['TTL'] > int(time.time()):
         exists = True

def lambda_handler(event, context):
  for record in event['Records']:
    to_address = record['ses']['mail']['destination'][0]
    if address_exists(to_address):
       return {'disposition': 'CONTINUE'}
    else:
       return {'disposition': 'STOP_RULE_SET'}
    break;

Per completare l’analisi del flusso di ricezione delle email, vediamo la funzione StoreEmailFunction. Questa funzione si occupa di memorizzate i dati della email ricevuta nel database DynamoDB, nella tabella “emails”. La tabella ha come Primary Key la stringa “destination” che corrisponde all’indirizzo email ed una Sort Key che corrisponde invece al “messageId” della email stessa. La funzione lambda riceve le notifiche dal topic SNS.

## part of StoreEmailFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ['emails_table_name'])

## Store email details in database
def store_email(email, receipt):
   response = table.put_item(
                  Item= {'destination': email['destination'][0],
                         'messageId': email['messageId'],
                         'timestamp': email['timestamp'],
                         'source': email['source'],
                         'commonHeaders': email['commonHeaders'],
                         'bucketName': receipt['action']['bucketName'],
                         'bucketObjectKey': receipt['action']['objectKey'],
                         'isNew': "true"
                  }
   )

def lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    store_email(message['mail'], message['receipt'])

Nella notifica SNS è presente il nome del bucket S3 e l’objectKey utilizzato per memorizzarla. Salvando le informazioni nella tabella DynamoDB, sarà possibile successivamente recuperare il contenuto della email per la visualizzazione.

La funzione CleanUpFunction viene invocata periodicamente da CloudWatch al fine di svuotare e disattivare le mailbox scadute. Si occupa inoltre di eliminare i record di sessione anch’essi scaduti.

Viene effettuato uno scan sulle tabelle addresses e sessions verificando il valore dell’attributo TTL.

## part of CleanUpFunction, see Github repository for 
## the complete functions code.

def cleanup():
    ## get expired addresses
    try:
        response = addresses_table.scan(
            FilterExpression = Attr('TTL').lt(int(time.time())),
            ProjectionExpression = "address"
        )
    except ClientError as e:
        logger.error('## DynamoDB Client Exception')
        logger.error(e.response['Error']['Message'])
    else:
        for I in response['Items']:
            find_emails(I['address'])
            delete_address_item(I['address'])

Backend – API

API Gateway viene utilizzato come endpoint REST (regional) per la web application.

Viene utilizzata l’integrazione lambda ed esistono tre funzioni lambda dedicate alla creazione di una nuova mailbox (CreateEmailFunction), alle consultazione dell’elenco delle email presenti (GetEmailsListFunction) e al download del contenuto di una email specifica (GetEmailFileFunction).

La funzione CreateEmailFunction risponde al metodo GET della risorsa /create e prevede due parametri:

  • address – l’indirizzo email da creare
  • captcha – la chiave necessaria per la verifica di reCAPTCHA

Entrambi i parametri vengono validati e nel caso in cui l’indirizzo email non esista, lo stesso viene creato inserendo il relativo record nella tabella addresses. La funzione restituisce nel body un parametro sessionID che sarà utilizzato dal client per le ulteriori chiamate alle API. Gli ID di sessione validi e la relativa scadenza sono memorizzati nella tabella sessions.

## part of CreateEmailFunction, see Github repository for  
## the complete functions code.

import http.client, urllib.parse
import os
import re

valid_domains = os.environ['valid_domains'].split(',')
recaptcha_key = os.environ['recaptcha_key']

## A very simple function to validate email address
def validate_email(address):
    valid = False
    # Do some basic regex validation 
    match = re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', address)
    if (match != None):
        domain = address.split("@")[-1]
        if domain in valid_domains:
            valid = True
    return valid

## reCAPTCHA validation function
def validate_recaptcha(token):
    conn = http.client.HTTPSConnection('www.google.com')
    headers = {"Content-type": "application/x-www-form-urlencoded"}
    params = urllib.parse.urlencode({'secret': recaptcha_key, 'response': token}) 
    conn.request('POST', '/recaptcha/api/siteverify', params, headers)
    response = conn.getresponse()
    r = response.read().decode() 
    r = json.loads(r)
    return r['success']

La funzione GetEmailsListFunction risponde al metodo GET della risorsa /{destination} dove il parametro {destination} è l’indirizzo della mailbox. La funzione ritorna l’elenco delle email presenti nella mailbox, dopo aver validato il sessionID fornito nella richiesta (queryString).

L’elenco delle email è ottenuto tramite una Query sulla tabella emails.

## part of GetEmailsListFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
emails_table = dynamodb.Table(os.environ['emails_table_name'])
sessions_table = dynamodb.Table(os.environ['sessions_table_name'])

## Get emails list from DB
def get_emails(destination):
    items = None
    try:
        filtering_exp = Key('destination').eq(destination)
        response = emails_table.query(KeyConditionExpression=filtering_exp)        
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])
    else:
        #Remove private bucket details        
        for i in response['Items']:
            i.pop('bucketObjectKey', None)
            i.pop('bucketName', None)
        items = {'Items' : response['Items'], 'Count' : response['Count']}
    return items

## Verify session
def session_is_valid(sessionid):
    valid = False
    try:
        response = sessions_table.get_item(
            Key={
            'sessionId': sessionid
            }
        )
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])
    else:
        if 'Item' in response:
            item = response['Item']
            if item['TTL'] > int(time.time()):
                valid = True
    return valid

def lambda_handler(event, context):

    headers = {
        "access-control-allow-headers": 
           "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "access-control-allow-methods": 
           "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT",
        "access-control-allow-origin": "*"
    }
     
    if session_is_valid(event["queryStringParameters"]["sessionid"]):
        destination = event["pathParameters"]["destination"]
        items = get_emails(destination)
        if items != None:
           result = {"statusCode": 200, "body": json.dumps(items), "headers": headers }

    return result    

La funzione GetEmailFileFunction risponde al metodo GET della risorsa /{destination}/{messageId} dove il parametro {destination} è l’indirizzo della mailbox e {messageId} è l’identificativo univoco della mail. Anche questa funzione verifica il sessionID fornito nella richiesta (queryString).

Restituisce il contenuto della mail leggendolo dal bucket S3 e si occupa di segnare come “letta” il relativo record della tabella emails.

## part of GetEmailFileFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
emails_table = dynamodb.Table(os.environ['emails_table_name'])
s3 = boto3.client('s3')

## Get emails details, including s3 bucket and s3 bucket object key.
def get_email_file(destination, messageId):
    result = None
    try:
        response = emails_table.get_item(
            Key={
            'destination': destination,
            'messageId' : messageId
            }
        )
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])
    else:
        if 'Item' in response:
            result = response['Item']
    return result

## Set item ad readed in DynamoDB table
def set_as_readed(destination, messageId):
    try:
        response = emails_table.update_item(
            Key={
            'destination': destination,
            'messageId' : messageId
            },
            UpdateExpression="SET isNew = :updated",                   
            ExpressionAttributeValues={':updated': 'false'}
        )
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])

def lambda_handler(event, context):

    headers = {
        "access-control-allow-headers": 
           "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "access-control-allow-methods": 
           "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT",
        "access-control-allow-origin": "*"
    }

    email_file = get_email_file(destination, messageId) 
    if email_file != None:
        data = s3.get_object(Bucket=email_file['bucketName'], Key=email_file['bucketObjectKey'])
        contents = data['Body'].read().decode('utf-8')
        headers.update({"content-type": "message/rfc822"})
        result = {
           "statusCode": 200,
            "headers": headers,
            "body":  contents 
        }
        if email_file['isNew'] == 'true':
           set_as_readed(destination, messageId)

    return result

Al fine di abilitare CORS è stato aggiunto un metodo OPTION per ciascuna risorsa gestita da API Gateway. In aggiunta, le funzioni lambda restituiscono degli header specifici.

Il template CloudFormation si occupa inoltre di creare la distribuzione delle API e il relativo stage in modo da ottenere un endpoint valido per la configurazione della nostra web application.

Installazione backend

Una volta terminata la configurazione del servizio SES per il proprio dominio come spiegato nella sezione Primi Passi, utilizzeremo un template di CloudFormation per realizzare l’intera infrastruttura di backend. Il template prevede la creazione del bucket S3, delle tabelle DynamoDB, di tutte le funzioni lambda e delle API. Prevede inoltre la creazione di tutti i ruoli IAM, i permessi e la configurazione dei vari servizi, incluso il topic SNS.

AWSTemplateFormatVersion: 2010-09-09
Description: Disposable Email

Parameters:
    DomainName:
        Description: Domain name to be used
        Type: String
    ReCaptchaPrivateKey:
        Description: ReCaptcha private key to validate mailbox creation requests  
        Type: String
        Default: ""
    AddressesTableName:
        Description: DynamoDB table to store valid addresses
        Type: String
        Default: "disposable_addresses_table"
    EmailsTableName:
        Description: DynamoDB table to store emails 
        Type: String
        Default: "disposable_emails_table"
    SessionsTableName:
        Description: DynamoDB table to store client sessions 
        Type: String
        Default: "disposable_sessions_table"  
    RuleSetName:
        Description: SES RuleSet name to be used 
        Type: String
        Default: "default-rule-set"
    MailboxTTL:
        Description: Life (seconds) of mailbox
        Type: String
        Default: "3600"
    SessionTTL:
        Description: Session expiration in seconds
        Type: String
        Default: "600"
    SourceBucket:
        Description: Lambda functions source S3 Bucket
        Type: String
        Default: "cfvn" 

I sorgenti delle funzioni lambda non sono presenti direttamente nel template ma in file python esterni. Durante la creazione dello stack, i file ZIP delle funzioni lambda devono essere ospitati in un bucket S3. E’ possibile specificare un bucket differente da quello “ufficiale” (specificato come parametro “SourceBucket“) se si intende modificarli prima della creazione dello stack.

Per realizzare l’intera infrastruttura di backend puoi utilizzare la console di CloudFormation oppure AWS CLI:

aws cloudformation create-stack --stack-name <name> --template-url <url> --parameters <parameters> --capabilities CAPABILITY_IAM
DescrizioneEsempio
<name> il nome che vuoi dare allo stackdisposable_stack
<url>url del templatehttps://cfvn.s3-eu-west-1.amazonaws.com/disposable.yml

<parameters> è invece l’elenco dei parametri da fornire al template. I due parametri principali e obbligatori sono DomainName e ReCaptchaPrivateKey.

ParameterKey=DomainName,ParameterValue=<your_domain> 
ParameterKey=ReCaptchaPrivateKey,ParameterValue=<your_private_captcha_key>

Una volta creato la stack, ci interessa conoscere l’endpoint da utilizzare per la configurazione della web application. Questo è fornito da CloudFormation come output. E’ possibile vederlo nella console di CloudFormation oppure direttamente da AWS CLI:

aws cloudformation describe-stacks --stack-name <name> --query Stacks[0].Outputs[0].OutputValue

Frontend

L’applicazione React / Material-UI è molto semplice: si compone di tre semplici componenti: LoginForm per consentire la creazione e l’accesso ad una mailbox, EmailList per la visualizzazione dell’elenco dei messaggi presenti e EmailViewer per la visualizzazione del contenuto di una mail.

Il componente principale App prevede nel proprio stato l’indirizzo email e l’id di sessione, entrambi inizialmente vuoti. Viene costruito il componente LoginForm che gestirà il processo di login.

<LoginForm changeAddress={this.changeAddress.bind(this)} 
           apiEndpoint={APIEndpoint}
           recaptcha_key={ReCaptcha_SiteKey}
           email_domain={email_domain}/>

Il componente LoginForm incorpora il componente reCAPTCHA e si occupa di chiamare l’API /create sull’evento onSubmit della form.

handleSubmit(event) {
        const recaptchaValue = this.recaptchaRef.current.getValue();
        if (recaptchaValue === '') {
           window.location.reload();
        } else { 
            console.log("Captcha value:", recaptchaValue);
        
          fetch(this.props.apiEndpoint + 'create?address=' 
             + encodeURI(this.state.address + this.props.email_domain)
             + '&captcha=' + recaptchaValue)
          .then(r =>  r.json().then(data => ({status: r.status, body: data})))
          .then(r => {
            console.log(r);
            console.log('Response from API: ' + r.body.message);
            if (r.status === 200) {
              this.props.changeAddress(this.state.address + this.props.email_domain, r.body.sessionid);  
            }
        })
        .catch(console.log);
        }
        event.preventDefault();
    }

A login avvenuta, si ottiene un sessionID. Il componente App si occupa di costruire il componente EmailList.

<EmailList address={this.state.address} 
           sessionid={this.state.sessionid}
           changeAddress={this.changeAddress.bind(this)} 
           apiEndpoint={APIEndpoint}/>

Il componente EmailList si occupa di chiamare l’API /{destination} per ottenere l’elenco delle email.

getList(force) {
        fetch(this.props.apiEndpoint + this.state.address +
              '?sessionid=' + encodeURI(this.props.sessionid))
        .then(response => {
            const statusCode = response.status;
            const data = response.json();
            return Promise.all([statusCode, data]);
          })
        .then(res => {
            console.log(res);
            if (res[0] === 400) {
                this.logout();
            } else {      
                res[1].Items.sort(function(a,b){
                    if (a.timestamp > b.timestamp) { return -1 } else { return 1 }
                });         
                if ((this.listIsChanged(res[1].Items) || force)) {
                    this.setState({ emails: res[1].Items });
                    if ((this.state.selectedId === '') && (res[1].Items.length > 0)) {
                        this.setState({ selectedId: res[1].Items[0].messageId });   
                    }
                }
            }
        })
        .catch(console.log)
    }

Lo stesso si occupa inoltre di creare un componente EmailViewer per la visualizzazione del messaggio selezionato.

<EmailViewer address={this.state.address} 
             messageId={this.state.selectedId} 
             sessionid={this.state.sessionid} 
             apiEndpoint={this.props.apiEndpoint}/> 

La visualizzazione del messaggio email è la parte più complessa dell’applicazione client. Ottenuto il contenuto dalla API /{destination}/{messageId} è necessario effettuare il parsing dello stesso.

A tale scopo è utilizzata la libreria emailjs-mime-parser che consente di accedere ai vari oggetti che costituiscono la mail. La funzione getMailContents si occupa di scegliere il body corretto da mostrare, dando precedenza ai contenuti HTML.

Per visualizzare il contenuto HTML si utilizza un “trucco” molto pericoloso:

<div className="body" dangerouslySetInnerHTML={{__html: this.state.messageData}} />

Installazione Frontend

Per realizzare la web application React, una volta clonato il repository, è necessario configurare alcuni parametri che sono presenti all’inizio di App.js:

//  - your APIEndpoint
const APIEndpoint = <your_API_endpoint>;

//  - your ReCaptcha Site Key 
const ReCaptcha_SiteKey = <your_reCAPTCHA_site_key>;
 
//  - your email domain
const email_domain = <your_domain>;

Usa npm start per testare l’applicazione e npm run build per prepare i file da posizionare sul tuo hosting preferito oppure, come ho fatto io, direttamente in un bucket S3 pubblico. Ricordati di abilitare il dominio “localhost” tra quelli consentiti da reCAPTCHA per poter testare localmente la tua applicazione.

Conclusione

In questo repository GitHub trovi sia i sorgenti dell’applicazione React che il template CloudFormation per la realizzazione del backend AWS.

Disposable-mail nasce come esempio di realizzazione di una soluzione cloud native. Ci sono sicuramente molti aspetti da perfezionare e sarebbe necessario renderla più sicura. Si tratta comunque di una buona base di partenza per la realizzazione della propria webmail “usa e getta”.

Ci siamo divertiti? Alla prossima!

VPN

Come realizzare un semplice VPN server su AWS

7 min

Realizzare un semplice VPN server su AWS può aiutarci a risolvere qualche piccolo problemino quotidiano: mi è servito, ad esempio, quando ho preso la rivoluzionaria decisione di rinunciare alla linea ADSL casalinga a favore di una ben più performante connettività LTE 4G. Vivendo in una zona non ancora raggiunta (e chissà quando accadrà) dalla fibra, ho deciso infatti di approfittare di una delle offerte “a tanti GB inclusi”  superando di gran lunga le prestazioni della tradizionale linea di terra e risparmiando anche un pò ogni mese.

Si è però subito posto un problema: come noto, le connettività mobile sono quasi sempre di tipo NAT, cioè l’indirizzo IP pubblico che si usa per la propria presenza su internet è in realtà condiviso tra più dispositivi e utenti. Di per se questo tipo di connettività non rappresenta un limite per accedere ai servizi web ma, al contrario, rende di fatto impossibile contattare il proprio dispositivo 4G da un altro dispositivo posto sulla rete pubblica. Come posso quindi connettermi, ovunque mi trovi, ai dispositivi della mia LAN casalinga se NATtati dietro una connettività mobile?

Per questo motivo e per altri che vedremo, può far comodo realizzare una semplice rete VPN.

Il cuore di una rete VPN è un server centrale, raggiungibile da tutti i client che saranno autenticati dallo stesso. Una volta connessi, i dispositivi posti sulla VPN potranno dialogare in modo sicuro tra loro superando le limitazioni dei livelli di rete sottostanti, come se si trovassero su un’unica – virtuale e privata – LAN.

Vediamo come realizzare un semplice VPN server su AWS utilizzando OpenVPN. Il diagramma che seguiremo è il seguente.

Realizziamo il server

Avremo bisogno di un’istanza EC2: per rapidità useremo un’istanza On-Demand. Ci occuperemo di installare e configurare tutto il software necessario; successivamente andremo a realizzare una nostra AMI in modo da poter ricreare rapidamente il nostro server.

Avviamo la nostra EC2 basata su Ubuntu Server 18.04 LTS: sarà molto semplice configurare il nostro server OpenVPN senza aver la necessità di utilizzare AMI del MarketPlace.

Un’istanza t2.micro Free-Tier è assolutamente adatta alla scopo; configuriamo un Security Group in modo che sia raggiungibile tramite protocollo SSH (TCP/22).

Una volta avviata, colleghiamoci alla console utilizzando l’utente ubuntu.

ssh -i <keypairfile.pem> [email protected]<ec2_public_ip>

Aggiorniamo subito la nostra istanza e recuperiamo lo script per l’installazione di OpenVPN

sudo apt-get update
sudo apt-get upgrade -y
wget https://git.io/vpn -O openvpn-install.sh
chmod +x openvpn-install.sh

A questo punto, prima di lanciare lo script di installazione è necessario fare una premessa: per contattare il nostro server OpenVPN sarà necessario fornire ai client l’indirizzo di quest’ultimo. L’indirizzo può essere un IP statico oppure un record DNS. La soluzione più semplice consiste nell’assegnare un Elastic IP alla nostra istanza. Tuttavia ho preferito rendere la nostra configurazione indipendente anche dall’indirizzo IP assegnato al server, utilizzando Route 53 di AWS per definire un hostname pubblico a cui i nostri client faranno riferimento.

Eseguiamo lo script

sudo ./openvpn-install.sh

La prima cosa che ci verrà chiesta è proprio l’indirizzo del server OpenVPN, che lo script tenta di determinare automaticamente. Se abbiamo assegnato un Elastic IP alla nostra istanza AWS, confermiamo l’indirizzo. Se invece intendiamo utilizzare un host in una zona Route 53, specifichiamo il nome host completo. Nel mio caso: vpn.aws.gotocloud.it.

Confermiamo poi tutte le successive richieste con i valori di default proposti e avviamo l’installazione.

Al termine dell’installazione un messaggio ci ricorda che il file di configurazione ed il certificato da utilizzare su uno dei nostri client (laptop, smartphone, raspberry, etc..)  è disponibile in /home/ubuntu/client.ovpn

Assicuriamoci di avviare OpenVPN.

sudo systemctl enable openvpn
sudo systemctl start openvpn

Alcune informazioni: lo script crea la rete 10.8.0.0/24 ed assegna l’indirizzo ip 10.8.0.1 al nostro OpenVPN server. Gli indirizzi IP successivi saranno assegnato ai client. Utilizzeremo questi indirizzo per comunicare tramite la nostra VPN.

OpenVPN utilizza di default la porta UDP 1194.  Dobbiamo quindi provvedere a modificare il SecurityGroup dell’instanza EC2 su cui stiamo lavorando aggiungendo questa policy.

Se abbiamo deciso di utilizzare un indirizzo Elastic IP, la configurazione del server è terminata. Se invece abbiamo deciso di utilizzare un nome host di una zona DNS ospitata su Route 53, dovremo fare in modo di aggiornare il relativo record A con l’indirizzo pubblico che AWS ha assegnato alla nostra istanza, ripetendo l’operazione ad ogni avvio. Per farlo utilizziamo uno script Python.

Integrazione con Route53

Per prima cosa installiamo le dipendenze tra cui Boto3 che è il SDK di AWS per Python. Dato che lo script verrà eseguito come root all’avvio, anche le relative dipendenze vanno installate nell’ambiente di root.

sudo apt install python-pip -y
sudo su
pip install boto3
pip install requests
exit

Andiamo ora a creare lo script “update_route53_zone.py” con il contenuto seguente.

import requests
import json
import boto3

META_DATA_URL = 'http://169.254.169.254/latest/meta-data/public-ipv4'

# Route53 host to be updated
zone_name = "aws.gotocloud.it."
A_record_name = "vpn"

# Get Public IP Address
r = requests.get(META_DATA_URL)
public_ip = r.text

# Prepare data for Route53 changes
data = {}
data['Comment'] = "Update openvpn DNS record"
data['Changes'] = []
data['Changes'].append({
    'Action': 'UPSERT',
    'ResourceRecordSet': {
        "Name": A_record_name + '.' + zone_name,
        "Type": "A",
        "TTL": 60,
        "ResourceRecords": [
          {
            "Value": public_ip
          }
        ]
      }
})

# Get hosted zone list to find zone_id
client = boto3.client('route53')
zones = client.list_hosted_zones()
zone_id = ''
for z in zones['HostedZones']:
	if (z['Name'] == zone_name):
		zone_id = z['Id']
		break 

# Change Route53 record
if zone_id != '':
	response = client.change_resource_record_sets(HostedZoneId=zone_id, ChangeBatch=data)
	print(response)

Lo script ottiene l’indirizzo IP pubblico dell’instanza e aggiorna la zona DNS specificata inserendo o aggiornando l’host indicato come “A_record_name”. Per poter procedere all’aggiornamento della zona DNS, è necessario assegnare all’instanza un ruolo dotato delle policy corrette.

Le API di Route 53 utilizzate sono “ChangeResourceRecordSets” e “ListHostedZones”.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "route53:ChangeResourceRecordSets",
            "Resource": "arn:aws:route53:::hostedzone/*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "route53:ListHostedZones",
            "Resource": "*"
        }
    ]
}

Una volta assegnato il ruolo, eseguendo lo script come root, verrà creato o aggiornato il record DNS. Per fare in modo che questo avvenga ad ogni avvio, utilizziamo CRON.

sudo cp update_route53_zone.py /bin
sudo crontab -e

Andremo ad inserire:

@reboot python /bin/update_route53_zone.py &

I client

Per aggiungere utenti/dispositivi alla nostra VPN utilizziamo ancora lo script openvpn-install.sh. E’ consigliato creare un utente per ogni dispositivo che andremo a connettere alla nostra VPN specificandone un nome che ci permetta di riconoscerlo facilmente. Inoltre lo script può essere utilizzato per revocare un utente.

E’ il momento del client: nella sezione Community Download di OpenVPN troviamo i client per varie versioni di Windows. Su MacOS consiglio di utilizzare TunnelBlick. IOS e Android prevedono l’app ufficiale di OpenVPN nei rispettivi store. Per configurare  Raspbian (Raspberry PI) ho invece seguito le indicazioni presenti in questo thread.

Indipendentemente dal client utilizzato, ci servirà il file OVPN generato dallo script di configurazione che dovremo quindi trasferire dal server (SFTP). Normalmente la configurazione del client è abbastanza semplice: con MacOS e TunnelBlick, ad esempio, basterà un doppio click sul file OVPN perchè questo venga importato. Rimarrà solo da avviare la VPN.

Se tutto è andato per il meglio, dal nostro dispositivo in VPN sarà possibile pingare con successo l’indirizzo del nostro VPN server 10.8.0.1.

Raspberry PI

Per il setup casalingo ho invece scelto di installare il client OpenVPN su un dispositivo Raspberry PI da utilizzare poi anche come bridge verso gli altri dispositivi presenti sulla mia LAN. L’installazione su Raspbian è semplice.

sudo apt-get install openvpn

Andremo a copiare il file OVPN nella directory corretta, cambiando l’estensione in .CONF. Procederemo poi ad avviare il servizio.

sudo cp pi4.ovpn /etc/openvpn/pi4.conf
sudo systemctl enable openvpn
sudo systemctl start openvpn

Il nostro PI è ora connesso alla VPN e lo potremo verificare pingando, anche in queso caso, l’indirizzo IP 10.8.0.1 del server. Al contrario, se il PI ospita per esempio un repository per i nostri file (Owncloud) , questo sarà raggiungibile da tutti i dispositivi connessi in VPN (ad esempio il nostro smartphone) tramite l’indirizzo IP assegnato al PI sulla rete 10.8.0.0/24

Se vogliamo inoltre che il PI si comporti da VPN gateway, pubblicando servizi ospitati sul nostra LAN da altri dispositivi (ad esempio, il modulo IP della nostra centrale antifurto) potremo utilizzare un reverse proxy come NGINX. Un esempio di configurazione è il seguente: tutte le richieste pervenute al nostro PI sulla porta 10000 vengono girate sull’indirizzo LAN della nostra centrale antifurto, rendendola di fatto accessibile tramite VPN da qualunque dispositivo connesso alla stessa.

stream {
    upstream paradox {
        server 192.168.22.3:10000;
    }

    server {
             listen 10000;
             proxy_pass paradox;
    }
}

Navigazione tramite VPN

Se avete mantenuto la configurazione di default così come indicato in questo articolo, vi sarete resi conto che, una volta avviata la VPN dal nostro client (ad esempio dal nostro laptop), questa viene utilizzata anche per la navigazione WEB: in pratica il nostro server EC2 si comporta come nostro personale gateway per andare sulla rete pubblica. Di conseguenza ci presenteremo su internet con l’indirizzo IP del server OpenVPN nella Region AWS dove è stato creato. Possiamo scegliere in che parte del mondo far finta di essere, che è poi una delle prerogative dei tanti servizi VPN a pagamento!

Se invece vogliamo evitare questo comportamento e, al contrario, intendiamo utilizzare la nostra VPN solo per raggiungere i dispositivi connessi alla stessa, dovremo modificare la configurazione di OpenVPN.

Modifichiamo il file /etc/openvpn/server/server.conf e andiamo a commentare queste direttive.

#push "redirect-gateway def1 bypass-dhcp"
#push "dhcp-option DNS 1.1.1.1"
#push "dhcp-option DNS 1.0.0.1"

Ora riavviamo il server VPN e il nostro traffico web non sarà più incanalato tramite VPN.

Conclusioni

Esistono moltissime alternative alla soluzione proposta per risolvere i problemi di raggiungibilità dietro NAT, connessione sicura di più dispositivi over internet e navigazione originata da una region diversa dalla nostra. La soluzione OpenVPN/AWS ha però diversi vantaggi. Vediamoli.

  • OpenVPN è gratuito ed esistono client per tutti i principali sistemi operativi
  • Utilizzando AWS è possibile creare rapidamente il proprio OpenVPN server in diverse Region del mondo (US/EU/etc..) utilizzando CloudFormation.
  • La configurazione è semplice e personalizzabile secondo le proprie esigenze
  • Costa meno di altri servizi VPN: pagheremo la sola istanza EC2 (on-demand, reserved o addirittura Spot se possiamo tollerare interruzioni del servizio)
  • Non ha limiti di utilizzo
  • E’ personale e autogestito: nessun log o dato fornito a un provider VPN
  • E’ possibile utilizzarlo come bastion host per collegarsi alle nostre risorse AWS

Ci siamo divertiti? Alla prossima!

the million dollar homepage

AWS Serverless – The million dollar homepage

6 min

La "The Million Dollar Homepage" è una delle pagine che ha fatto la storia di Internet e del Web: un sito composto da una semplice immagine, suddivisa in un milione di blocchi da vendere ciascuno ad un solo, onesto, dollaro. Ne conosciamo tutti il seguito e l'enorme successo.

Immaginiamo, per qualche minuto, di chiamarci Alex Tew, di essere uno studente universitario inglese e di dover necessariamente trovare un modo per finanziarci gli studi. Improvvisamente boom! Un'idea!

Se fossimo nel 2005, anno in cui Alex realizza e pubblica "The Million Dollar Homepage", non potremmo appoggiarci ai servizi cloud di Amazon in quanto AWS verrà fondata l'anno successivo. Pur tenendo conto della semplicità del progetto, dovremo obbligatoriamente occuparci di tutte le attività di gestione dell'infrastruttura come il provisioning del server o del cluster, l'applicazione di patch e la manutenzione del sistema operativo.   

Fortunatamente siamo invece nel 2019 e abbiamo un ecosistema di soluzioni serverless che AWS ci mette a disposizione. Come realizzare la stessa pagina da un milione di dollari in modo agile e economico? 

In questo articolo proveremo per gioco a identificare le soluzioni serverless che fanno al caso nostro e definiremo l'architettura della nostra webpage: che ci serva come esercizio per approfondire la conoscenza dei building block di AWS. 

Prima però un piccolo chiarimento: quando si parla di paradigma serverless non significa che non ci siano server a supporto dei servizi che andremo ad utilizzare, ovviamente. Semplicemente non ne percepiamo la presenza e non ci è richiesto di occuparcene. 

Requisiti

Iniziamo col fare una lista delle funzionalità della nostra web app:

  • Dovremo visualizzare il banner "da un milione di dollari", composto dalle immagini scelte dai clienti e dai rimanenti spazi liberi disponibili per l'acquisto
  • Daremo la possibilità di acquistare uno degli spazio liberi: l'acquirente dovrà registrarsi sul sito e procedere al pagamento per lo spazio scelto.  
  • Confermato il pagamento, l'acquirente potrà procedere all'upload dell'immagine scelta che verrà collocata nello spazio libero acquistato. Dovrà inoltre indicare il proprio slogan e l'URL di riferimento
  • Vogliamo che la nostra web app sia pronta a scalare per poter rispondere in tempi rapidi alle richieste di visualizzazione di migliaia di utenti sparsi per il mondo

Vedremo nelle prossime sezioni come implementare, step by step, i componenti della nostra soluzione.

 

Inizio easy

Decidiamo come prima versione del sito di pubblicare esclusivamente contenuti statici.

Avremo una o più pagine HTML, CSS, JS e ovviamente immagini tra le quali il nostro banner milionario.

Abbiamo bisogno di istanziare un server web? Assolutamente no. Utilizziamo Amazon S3.

Amazon S3 è il servizio di storage di oggetti che offre scalabilità, disponibilità dei dati, sicurezza e prestazioni. 

Creiamo un bucket dove collocare i nostri file, abilitiamo la proprietà "hosting siti web statici" e creiamo una bucket policy per consentire l'accesso pubblico in sola lettura.

Finito, siamo online! Se vogliamo essere originali e abbiamo registrato un dominio internet per la nostra applicazione, ci basterà configurare un record CNAME nella relativa zona DNS per poterlo utilizzare e "mascherare" l'endpoint HTTP S3. Attenzione: in questo caso il nome del bucket dovrà corrispondere al record CNAME.

image

Automatizziamo

Diciamo che il nostro sito inizia ad essere conosciuto. I clienti ci contattano via email per acquistare uno spazio libero e noi dobbiamo fornirgli le informazioni per effettuare il pagamento. Una volta verificato il pagamento e ottenuta l'immagine del cliente, la andiamo ad applicare alla million dollars page. Infine pubblichiamo la nuova pagina del sito. Purtroppo abbiamo anche garantito al nostro cliente di poter cambiare la propria immagine tutte le volte che lo desidera, quindi il processo si ripete più volte al giorno per diversi clienti. Ci rendiamo conto di non riuscire a gestire "manualmente" tutte queste operazioni e decidiamo che è venuto il momento di automatizzare.

Per prima cosa dovremo essere in grado di gestire gli ordini dei clienti.

E' il momento di introdurre un pò di elementi: ci viene in aiuto Cognito.

Amazon Cognito permette di aggiungere strumenti di registrazione, accesso e controllo degli accessi alle app Web integrandosi con provider di identità social quali Facebook e Google.

Possiamo quindi gestire semplicemente tutto il processo di registrazione, login, logout e recupero password dei nostri clienti. A questi andremo a fornire una form per l'acquisto di uno spazio. Come realizziamo il backend?

Andremo ad introdurre l'Amazon API Gateway come elemento esposto su internet e collegheremo gli endpoint Lambda in grado di processare l'ordine e salvarlo su un database.

Amazon API Gateway semplifica  la creazione, la pubblicazione e la manutenzione delle proprie API. 

Amazon Lambda consente di eseguire codice senza dover effettuare il provisioning e gestione di un server.

Quale database? Se vogliamo rimanere serverless (e certo che si!) abbiamo due possibilità: DynamoDB o Aurora Serverless. Preferiamo NoSQL e scegliamo quindi DynamoDB.

DynamoDB è un database non relazionale completamente gestito per applicazioni che necessitano di prestazioni elevate su qualsiasi scala.

Ok, come si presenta ora l'architettura della nostra soluzione? Vediamola.

image

Abbiamo aggiunto quindi la possibilità di inserire un ordine. Al termine dell'inserimento mostreremo al nostro cliente la pagina per il pagamento dello stesso. Ipotizziamo, per rapidità, di usare PayPal. Ci servirà una API per poter verificare la transazione tramite l'endpoint di PayPal al termine del processo di pagamento: andremo quindi a realizzare un'altra Lambda function per questo scopo che, una volta verificato, cambierà lo stato dell'ordine in "Pagato".

image

A questo punto tutto il processo di invio dell'ordine e pagamento è automatizzato. Per comodità vogliamo ricevere una notifica email ad avvenuto pagamento di un ordine, con tutti i dettagli dello stesso e l'indirizzo a cui poter contattare il nostro acquirente. Per questo motivo ho aggiunto il servizio SES (Simple Email Service) alla nostra architettura, che sarà richiamato direttamente dalla nostra Lambda function.

Amazon SES è un servizio di invio di e-mail basato sul cloud progettato per consentire agli esperti di marketing e agli sviluppatori di applicazioni di inviare e-mail di marketing, notifiche ed e-mail transazionali.

Sebbene una buona parte del lavoro sia stata fatta ci rimane ancora un passo da automatizzare: dare la possibilità al nostro cliente di aggiornare direttamente la propria immagine che andrà a far parte del nostro collage milionario.

Upload dell'immagine

Dobbiamo dare al nostro cliente la possibilità di effettuare in qualunque momento l'upload di un'immagine, che andrà ad occupare lo spazio precedentemente acquistato sul nostro million dollar banner. Il nostro bucket S3 è accessibile pubblicamente in sola lettura, ovviamente. Come possiamo autorizzare e gestire l'upload di un file per un utente specifico?

Ci viene in aiuto una funzionalità di S3 chiamata "presigned URL": andiamo a generare una URL temporanea (con scadenza) che il client potrà utilizzare per  effettuare l'upload di un file nel nostro bucket senza dover fornire allo stesso altre credenziali di accesso o ruoli specifici.

Useremo una funzione Lambda anche in questo caso. Questa si occuperà di richiedere la presigned URL al servizio S3, ma solo dopo aver verificato che l'utente abbia effettivamente acquisto una spazio del nostro banner. Ottenuta l'URL, verrà restituita al client per procedere al upload.

Finito? Non ancora. L'immagine scelta dal cliente è ora nel nostro bucket. Come possiamo prenderla in carico, ridimensionarla e rigenerare il nostro banner includendo questa nuova immagine nella posizione corretta? La risposta è ancora una funzione Lambda che sarà triggerata direttamente dal servizio S3 ogni qualvolta viene effettuato l'upload di una nuova immagine.

Ecco il quadro completo.

image

Conclusioni

Andiamo ad analizzare la nostra architettura: abbiamo utilizzato esclusivamente servizi serverless, semplici da manutenere e con costi sensibilmente ridotti rispetto ad una soluzione "classica", in quanto non ci è richiesto un provisioning iniziale della capacità. I costi sono trasparenti e in base all'utilizzo. Non ci dovremo preoccupare di gestire server e sistemi operativi. Non dovremo nemmeno occuparci di scalare i componenti della nostra architettura in caso di grande successo, molte visualizzazioni, molti clienti che frequentemente aggiornano la propria immagine: questo perché i servizi utilizzati scalano (o possono essere configurati per scalare) automaticamente. Security e affidabilità? Anche questi in carico a Amazon. Potremo semplicemente concentrarci sulla nostra applicazione senza preoccuparci dell'intera infrastruttura che la supporta.

Non male vero? 

Abbiamo inoltre qualche altro asso nella manica: potremo monitorare la nostra soluzione grazie ad Amazon CloudWatch, includendo metriche e log.

Amazon CloudWatch è un servizio di monitoraggio e osservabilità. Fornisce dati e analisi concrete per monitorare le applicazioni e rispondere ai cambiamenti di prestazioni a livello di sistema.

Potremo decidere di abilitare l'encryption "at rest" sia su S3 che su DynamoDB e utilizzare VPC private. Potremmo decidere di utilizzare Amazon CloudFront come front-end del servizio S3, distribuendo la nostra applicazione sulle 200 edge location di AWS. 

Amazon CloudFront è una rete per la distribuzione rapida di contenuti o CDN (Content Delivery Network) che permette la distribuzione di dati, video, applicazioni e API a livello globale agli utenti con latenza minima, velocità di trasferimento elevata.

Ci siamo divertiti? Alla prossima!

image
digital trasformation - Il mio primo blog

Digital transformation – Il mio primo Blog

2 min

Sarò onesto: inizio questo blog con la voglia di condividere un percorso

Ho fatto della mia passione per la tecnologia e per il coding una professione, lavorando per un'azienda che è cresciuta molto e molto in fretta. Come sviluppatore e successivamente software architect, sono stato spesso messo di fronte a nuove sfide e cambiamenti; ho dovuto adeguare rapidamente le mie conoscenze ed il mio know-how. In un certo senso, il periodo della "digital transformation" l'ho già vissuto, quando semplicemente si parlava di "informatizzazione". 

Il percorso che voglio condividere oggi mi porta ad entusiasmarmi per argomenti quali Cloud Computing, AI, Machine Learning e Computer Vision. Argomenti per i quali la continua formazione è indispensabile e con i quali mi piace sporcarmi le mani per creare e condividere know-how.

La mia mission è proprio riuscire, con il know-how acquisito,  ad identificare la tecnologia, il design architetturale o il cloud provider più adatto alle specifiche esigenze durante il processo di digital trasformation o di rinnovamento di una soluzione. 

Il mio ambizioso obiettivo è di scrivere un articolo a settimana. A seconda dell'argomento i post saranno in lingua inglese oppure in italiano. Preferisco realizzare brevi sessioni "hands-on" su soluzioni o tecnologie alternandole ad altrettanto brevi whitepaper e guide. Brevi perché focalizzate su uno specifico argomento.

Se sei interessato a contribuire a questo Blog, contattami. Mi farà molto piacere.

 

Ok! Direi che è arrivato il momento di iniziare.

 

 

Due parole su di me

Mi chiamo Vittorio Nardone. Ho solo 44 anni e mi sono occupato di progettare e realizzare soluzioni software durante gli ultimi 18 anni della mia vita. Soluzioni dedicate principalmente alle Forze di Polizia. 

Ho due bellissimi bambini di 4 e 8 anni ed una - futura prossima - moglie che mi sostiene sempre, in tutto quello che faccio. Anche se a volte sembra non avere senso. Come quando ho deciso di comprarmi una Harley Davidson.

Le mie esperienze professionali sono qui.