In che modo ExpressVPN mantiene i suoi server Web protetti e protetti

[ware_item id=33][/ware_item]

Il server ExpressVPN si alza dalle ceneri.


Questo articolo spiega l'approccio di ExpressVPN a gestione delle patch di sicurezza per l'infrastruttura che esegue il sito Web ExpressVPN (non i server VPN). In generale, il nostro approccio alla sicurezza è:

  1. Crea sistemi molto difficile da hackerare.
  2. Ridurre al minimo il danno potenziale se un sistema viene ipoteticamente violato e riconosci il fatto che alcuni sistemi non possono essere resi perfettamente sicuri. In genere, questo inizia nella fase di progettazione architettonica, dove minimizziamo l'accesso di un'applicazione.
  3. Ridurre al minimo la quantità di tempo che un sistema può rimanere compromesso.
  4. Convalidare questi punti con pentest regolari, sia interni che esterni.

La sicurezza è radicata nella nostra cultura ed è la preoccupazione principale che guida tutto il nostro lavoro. Esistono molti altri argomenti come le nostre pratiche di sviluppo del software di sicurezza, la sicurezza delle applicazioni, i processi e la formazione dei dipendenti, ecc., Ma quelli non rientrano nell'ambito di questo post.

Qui spieghiamo come realizziamo quanto segue:

  1. Assicurarsi che tutti i server siano completamente patchati e mai più di 24 ore dietro le pubblicazioni di CVE.
  2. Assicurarsi che nessun server venga mai utilizzato per più di 24 ore, ponendo così un limite massimo alla quantità di tempo che un attaccante può avere persistenza.

Raggiungiamo entrambi gli obiettivi attraverso un sistema automatizzato che ricostruisce i server, a partire dal sistema operativo e da tutte le patch più recenti, e li distrugge almeno una volta ogni 24 ore.

Il nostro intento per questo articolo è di essere utile per altri sviluppatori che affrontano sfide simili e di dare trasparenza alle operazioni di ExpressVPN ai nostri clienti e ai media.

Come utilizziamo i libri di testo Ansible e Cloudformation

L'infrastruttura web di ExpressVPN è ospitata su AWS (al contrario dei nostri server VPN che girano su hardware dedicato) e facciamo un uso intenso delle sue funzionalità per rendere possibile la ricostruzione.

Tutta la nostra infrastruttura Web è dotata di Cloudformation e cerchiamo di automatizzare il maggior numero di processi possibile. Tuttavia, riteniamo che lavorare con i modelli grezzi di Cloudformation sia abbastanza spiacevole a causa della necessità di ripetizione, scarsa leggibilità complessiva e vincoli della sintassi JSON o YAML.

Per mitigarlo, utilizziamo un DSL chiamato cloudformation-ruby-dsl che ci consente di scrivere definizioni di modelli in Ruby ed esportare modelli di Cloudformation in JSON.

In particolare, il DSL ci consente di scrivere script di dati utente come script regolari che vengono convertiti automaticamente in JSON (e non passare attraverso il processo doloroso di trasformare ogni riga dello script in una stringa JSON valida).

Un ruolo Ansible generico chiamato cloudformation-infrastructure si occupa del rendering del modello effettivo in un file temporaneo, che viene quindi utilizzato dal modulo Ansible di cloudformation:

- name: 'render {{component}} stack cloudformation json'
shell: 'ruby "{{template_name | default (componente)}}. rb" espandi --stack-name {{stack}} --region {{aws_region}} > {{tempfile_path}} '
args:
chdir: ../cloudformation/templates
changed_when: false

- nome: "crea / aggiorna lo stack {{component}}"
cloudformation:
stack_name: '{{stack}} - {{xv_env_name}} - {{component}}'
stato: presente
regione: "{{aws_region}}"
modello: '{{tempfile_path}}'
template_parameters: '{{template_parameters | predefinito({}) }}'
stack_policy: '{{stack_policy}}'
registrati: cf_result

Nel playbook, chiamiamo il ruolo di infrastruttura cloudformation più volte con diverse variabili componenti per creare diversi stack Cloudformation. Ad esempio, abbiamo uno stack di rete che definisce il VPC e le risorse correlate e uno stack di app che definisce il gruppo di ridimensionamento automatico, la configurazione di avvio, gli hook del ciclo di vita, ecc..

Quindi utilizziamo un trucco un po 'brutto ma utile per trasformare l'output del modulo di cloudformation in variabili Ansible per i ruoli successivi. Dobbiamo usare questo approccio poiché Ansible non consente la creazione di variabili con nomi dinamici:

- include: _tempfile.yml
- copia:
contenuto: '{{componente | regex_replace ("-", "_")}} _ stack: {{cf_result.stack_outputs | to_json}} '
dest: '{{tempfile_path}}. json'
no_log: true
changed_when: false

- include_vars: '{{tempfile_path}}. json'

Aggiornamento del gruppo di ridimensionamento automatico EC2

Il sito Web ExpressVPN è ospitato su più istanze EC2 in un gruppo di ridimensionamento automatico dietro un bilanciamento del carico dell'applicazione che ci consente di distruggere i server senza tempi di inattività poiché il bilanciamento del carico può svuotare le connessioni esistenti prima che un'istanza venga chiusa.

Cloudformation orchestra l'intera ricostruzione e attiviamo il playbook Ansible descritto sopra ogni 24 ore per ricostruire tutte le istanze, facendo uso dell'attributo AutoScalingRollingUpdate UpdatePolicy della risorsa AWS :: AutoScaling :: AutoScalingGroup.

Quando viene semplicemente attivato ripetutamente senza modifiche, l'attributo UpdatePolicy non viene utilizzato, ma viene invocato solo in circostanze speciali come descritto nella documentazione. Una di queste circostanze è un aggiornamento della configurazione di avvio del ridimensionamento automatico, un modello utilizzato da un gruppo di ridimensionamento automatico per avviare istanze EC2, che include lo script dei dati utente EC2 che viene eseguito sulla creazione di una nuova istanza:

risorsa "AppLaunchConfiguration", tipo: "AWS :: AutoScaling :: LaunchConfiguration",
Proprietà: {
KeyName: param ('AppServerKey'),
ImageId: param ('AppServerAMI'),
InstanceType: param ('AppServerInstanceType'),
SecurityGroups: [
param ( 'SecurityGroupApp'),
],
IamInstanceProfile: param ('RebuildIamInstanceProfile'),
InstanceMonitoring: true,
BlockDeviceMappings: [
{
DeviceName: '/ dev / sda1', # volume root
Ebs: {
VolumeSize: param ('AppServerStorageSize'),
VolumeType: param ('AppServerStorageType'),
DeleteOnTermination: true,
},
},
],
UserData: base64 (interpolate (file ('scripts / app_user_data.sh'))),
}

Se eseguiamo aggiornamenti allo script dei dati utente, anche un commento, la configurazione di avvio verrà considerata modificata e Cloudformation aggiornerà tutte le istanze nel gruppo di ridimensionamento automatico per conformarsi alla nuova configurazione di avvio.

Grazie a cloudformation-ruby-dsl e alla sua funzione di utilità interpolare, possiamo usare i riferimenti a Cloudformation nello script app_user_data.sh:

readonly rebuild_timestamp ="{{param ('RebuildTimestamp')}}"

Questa procedura garantisce che la nostra configurazione di avvio sia nuova ogni volta che viene avviata la ricostruzione.

Ganci per il ciclo di vita

Utilizziamo gli hook del ciclo di vita del ridimensionamento automatico per assicurarci che le nostre istanze siano completamente sottoposte a provisioning e passino i controlli di integrità richiesti prima che diventino attive.

L'uso degli hook del ciclo di vita ci consente di avere lo stesso ciclo di vita dell'istanza sia quando attiviamo l'aggiornamento con Cloudformation sia quando si verifica un evento di ridimensionamento automatico (ad esempio, quando un'istanza fallisce un controllo di integrità EC2 e viene terminata). Non utilizziamo i criteri di aggiornamento del ridimensionamento automatico di cfn-signal e WaitOnResourceSignals perché vengono applicati solo quando Cloudformation attiva un aggiornamento.

Quando un gruppo di ridimensionamento automatico crea una nuova istanza, viene attivato l'hook del ciclo di vita EC2_INSTANCE_LAUNCHING che pone automaticamente l'istanza in uno stato Pending: Wait.

Dopo che l'istanza è stata completamente configurata, inizia a colpire i propri endpoint di controllo dello stato con arricciatura dallo script dei dati utente. Una volta che i controlli di integrità segnalano che l'applicazione è integra, emettiamo un'azione CONTINUA per questo hook del ciclo di vita, quindi l'istanza viene collegata al bilanciamento del carico e inizia a servire il traffico.

Se i controlli di integrità falliscono, eseguiamo un'azione ABANDON che termina l'istanza difettosa e il gruppo di ridimensionamento automatico ne avvia un'altra.

Oltre a non riuscire a superare i controlli di integrità, il nostro script dati utente potrebbe non riuscire in altri punti, ad esempio se problemi di connettività temporanea impediscono l'installazione del software.

Vogliamo che la creazione di una nuova istanza fallisca non appena ci rendiamo conto che non diventerà mai salutare. A tale scopo, impostiamo una trap ERR nello script dei dati utente insieme a set -o errtrace per chiamare una funzione che invia un'azione del ciclo di vita ABANDON in modo che un'istanza difettosa possa terminare il più presto possibile.

Script di dati utente

Lo script dei dati utente è responsabile dell'installazione di tutto il software richiesto sull'istanza. Abbiamo utilizzato con successo Ansible per il provisioning delle istanze e Capistrano per distribuire le applicazioni per lungo tempo, quindi le stiamo usando anche qui, consentendo la minima differenza tra distribuzioni e ricostruzioni regolari.

Lo script dei dati utente controlla il nostro repository di applicazioni da Github, che include script di provisioning Ansible, quindi esegue Ansible e Capistrano punta a localhost.

Quando si esegue il checkout del codice, è necessario essere sicuri che la versione attualmente distribuita dell'applicazione sia distribuita durante la ricostruzione. Lo script di distribuzione Capistrano include un'attività che aggiorna un file in S3 che memorizza il commit SHA attualmente distribuito. Quando si verifica la ricostruzione, il sistema raccoglie il commit che dovrebbe essere distribuito da quel file.

Gli aggiornamenti software vengono applicati eseguendo unattended-upgrade in primo piano con il comando unattended-upgrade -d. Al termine, l'istanza si riavvia e avvia i controlli di integrità.

Gestire i segreti

Il server necessita dell'accesso temporaneo ai segreti (come la password del vault Ansible) che vengono recuperati dall'archivio parametri EC2. Il server può accedere ai segreti solo per un breve periodo durante la ricostruzione. Dopo che sono stati recuperati, sostituiamo immediatamente il profilo dell'istanza iniziale con uno diverso che ha accesso solo alle risorse necessarie per l'esecuzione dell'applicazione.

Vogliamo evitare di memorizzare eventuali segreti nella memoria persistente dell'istanza. L'unico segreto che salviamo su disco è la chiave SSH di Github, ma non la sua passphrase. Non salviamo nemmeno la password del vault Ansible.

Tuttavia, è necessario passare queste passphrase a SSH e Ansible rispettivamente, ed è possibile solo in modalità interattiva (ovvero l'utilità richiede all'utente di inserire le passphrase manualmente) per una buona ragione: se una passphrase fa parte di un comando, lo è salvato nella cronologia della shell e può essere visibile a tutti gli utenti del sistema se eseguono ps. Usiamo l'utilità di attesa per automatizzare l'interazione con tali strumenti:

aspettarsi << EOF
cd $ {repo_dir}
spawn make ansible_local env = $ {deploy_env} stack = $ {stack} hostname = $ {server_hostname}
imposta il timeout 2
aspettarsi 'password Vault'
Spedire "$ {} Vault_password \ r"
impostare il timeout 900
aspettarsi {
"non raggiungibile = 0 fallito = 0" {
uscita 0
}
eof {
uscita 1
}
tempo scaduto {
uscita 1
}
}
EOF

Attivare la ricostruzione

Poiché attiviamo la ricostruzione eseguendo lo stesso script Cloudformation utilizzato per creare / aggiornare la nostra infrastruttura, dobbiamo assicurarci di non aggiornare accidentalmente una parte dell'infrastruttura che non dovrebbe essere aggiornata durante la ricostruzione.

Raggiungiamo questo obiettivo impostando una politica di stack restrittiva sui nostri stack di Cloudformation in modo che vengano aggiornate solo le risorse necessarie per la ricostruzione:

{
"dichiarazione" : [
{
"Effetto" : "permettere",
"Azione" : "Aggiornamento: Modifica",
"Principale": "*",
"Risorsa" : [
"LogicalResourceId / * AutoScalingGroup"
]
},
{
"Effetto" : "permettere",
"Azione" : "Aggiornamento: Sostituire",
"Principale": "*",
"Risorsa" : [
"LogicalResourceId / * LaunchConfiguration"
]
}
]
}

Quando è necessario eseguire gli aggiornamenti effettivi dell'infrastruttura, è necessario aggiornare manualmente la politica dello stack per consentire esplicitamente gli aggiornamenti di tali risorse.

Poiché i nostri nomi host e IP dei server cambiano ogni giorno, abbiamo uno script che aggiorna i nostri inventari Ansible locali e le configurazioni SSH. Rileva le istanze tramite l'API AWS per tag, esegue il rendering dell'inventario e configura i file dai modelli ERB e aggiunge i nuovi IP a SSH known_hosts.

ExpressVPN segue i più alti standard di sicurezza

La ricostruzione dei server ci protegge da una minaccia specifica: gli aggressori che ottengono l'accesso ai nostri server tramite una vulnerabilità kernel / software.

Tuttavia, questo è solo uno dei molti modi in cui proteggiamo la nostra infrastruttura, incluso ma non limitato a sottoporsi a regolari controlli di sicurezza e rendere inaccessibili i sistemi critici da Internet.

Inoltre, ci assicuriamo che tutto il nostro codice e i nostri processi interni seguano i più alti standard di sicurezza.

In che modo ExpressVPN mantiene i suoi server Web protetti e protetti
admin Author
Sorry! The Author has not filled his profile.