Modul în care ExpressVPN își păstrează serverele web patate și sigure

Serverul ExpressVPN se ridică din cenușă.

Acest articol explică abordarea ExpressVPN managementul patch-urilor de securitate pentru infrastructura care rulează site-ul ExpressVPN (nu serverele VPN). În general, abordarea noastră pentru securitate este:

  1. Faceți sisteme foarte dificil de piratat.
  2. Minimizați daunele potențiale dacă un sistem este piratat ipotetic și recunoaște faptul că unele sisteme nu pot fi făcute perfect sigure. De obicei, aceasta începe în faza de proiectare arhitecturală, în care reducem la minimum accesul unei aplicații.
  3. Minimizați timpul că un sistem poate rămâne compromis.
  4. Valida aceste puncte cu pentesti obișnuiți, atât interni cât și externi.

Securitatea este înrădăcinată în cultura noastră și este principala preocupare care ne ghidează toată activitatea. Există multe alte subiecte, cum ar fi practicile noastre de dezvoltare a software-ului de securitate, securitatea aplicațiilor, procesele și instruirea angajaților etc., dar acestea nu sunt în sfera de aplicare pentru acest post.

Aici vă explicăm cum realizăm următoarele:

  1. Asigurați-vă că toate serverele sunt complet plasate și niciodată la mai mult de 24 de ore în spatele publicațiilor CVE-urilor.
  2. Asigurați-vă că niciun server nu este folosit vreodată mai mult de 24 de ore, punând astfel o limită superioară timpului pe care un atacator îl poate persista.

Îndeplinim ambele obiective prin intermediul unui sistem automat care reconstruiește serverele, începând cu sistemul de operare și toate cele mai recente corecții și le distruge cel puțin o dată la 24 de ore.

Intenția noastră pentru acest articol este de a fi utilă pentru alți dezvoltatori care se confruntă cu provocări similare și de a oferi transparență operațiunilor ExpressVPN clienților și mass-media.

Cum folosim cărți de joc Ansible și Cloudformation

Infrastructura web ExpressVPN este găzduită pe AWS (spre deosebire de serverele noastre VPN care rulează pe hardware dedicat), iar noi utilizăm intensiv funcțiile sale pentru a face posibilă reconstrucția.

Întreaga noastră infrastructură web este furnizată de Cloudformation și încercăm să automatizăm cât mai multe procese. Cu toate acestea, considerăm că funcționarea cu șabloane brute Cloudformation este destul de neplăcută datorită nevoii de repetare, lizibilității slabe în general și constrângerilor sintaxei JSON sau YAML.

Pentru a atenua acest lucru, folosim un DSL numit cloudformation-ruby-dsl care ne permite să scriem definiții de șablon în Ruby și să exportăm șabloane Cloudformation în JSON.

În special, DSL ne permite să scriem scripturi de date ale utilizatorilor ca scripturi obișnuite care sunt convertite automat în JSON (și nu parcurgem procesul dureros de a face fiecare linie a scriptului într-un șir JSON valid).

Un rol generic Ansible numit cloudformation-infrastructură are grijă de redarea șablonului efectiv într-un fișier temporar, care este apoi utilizat de modulul Ansformation Cloudible:

– nume: ‘render {{component}} stack cloudformation json’
coajă: ‘rubin "{{nume_ șablon | implicit (componentă)}}. rb" expand – name -stack {{stack}} –region {{aws_region}} > {{tempfile_path}} ‘
args:
chdir: ../cloudformation/templates
schimbat_când: fals

– nume: ‘crea / actualizează {{componentă}} stivă’
cloudformation:
stack_name: ‘{{stack}} – {{xv_env_name}} – {{component}}’
stare: prezent
regiune: ‘{{aws_region}}’
șablon: ‘{{tempfile_path}}’
template_parameters: ‘{{template_parameters | Mod implicit({}) }}’
stack_policy: ‘{{stack_policy}}’
înregistrare: cf_result

În jurnalul de redare, numim rolul de infrastructură cloudformare de mai multe ori cu diferite variabile de componentă pentru a crea mai multe stive de Cloudformation. De exemplu, avem o stivă de rețea care definește VPC și resursele aferente și o stivă de aplicații care definește grupul Scalare automată, configurare de lansare, cârlige de viață, etc..

Apoi folosim un truc oarecum urât, dar util pentru a transforma ieșirea modulului de cloudformare în variabile Ansible pentru rolurile ulterioare. Trebuie să utilizăm această abordare, deoarece Ansible nu permite crearea de variabile cu nume dinamice:

– includ: _tempfile.yml
– copie:
continut: ‘{{componenta | regex_replace ("-", "_")}} _ stack: {{cf_result.stack_outputs | to_json}} ‘
dest: ‘{{tempfile_path}}. json’
nu_log: adevărat
schimbat_când: fals

– include_vars: ‘{{tempfile_path}}. json’

Actualizarea grupului de scalare automată EC2

Site-ul ExpressVPN este găzduit pe mai multe instanțe EC2 într-un grup de scalare automată din spatele unui echilibru de încărcare a aplicației care ne permite să distrugem serverele fără niciun timp de oprire, deoarece balansatorul de sarcină poate scurge conexiunile existente înainte ca o instanță să se termine.

Cloudformation orchestrează întreaga reconstruire și declanșăm cartea de redare Ansible descrisă mai sus la fiecare 24 de ore pentru a reconstrui toate instanțele, folosind atributul AutoScalingRollingUpdate UpdatePolicy al resursei AWS :: AutoScaling :: AutoScalingGroup.

Când pur și simplu este declanșat în mod repetat, fără modificări, atributul UpdatePolicy nu este utilizat – este invocat doar în circumstanțe speciale, așa cum este descris în documentație. Una dintre aceste circumstanțe este o actualizare la configurația de lansare a Scalei automate – un șablon pe care un grup Scalare automată îl folosește pentru a lansa instanțe EC2 – care include scriptul de date al utilizatorului EC2 care rulează la crearea unei noi instanțe:

resursa ‘AppLaunchConfiguration’, Tipul: ‘AWS :: AutoScaling :: LaunchConfiguration’,
Proprietăți: {
Nume cheie: param („AppServerKey”),
ImageId: param („AppServerAMI”),
InstanceType: param („AppServerInstanceType”),
Grupuri de securitate: [
param ( ‘SecurityGroupApp’),
],
IamInstanceProfile: param (‘RebuildIamInstanceProfile’),
InstanceMonitoring: adevărat,
BlockDeviceMappings: [
{
Nume Device: ‘/ dev / sda1’, # volum rădăcină
Ebs: {
VolumeSize: param („AppServerStorageSize”),
VolumeType: param („AppServerStorageType”),
DeleteOnTermination: true,
},
},
],
UserData: base64 (interpolați (fișier (‘scripturi / app_user_data.sh’))),
}

Dacă facem vreo actualizare la scriptul de date al utilizatorului, chiar și un comentariu, configurația de lansare va fi considerată modificată și Cloudformation va actualiza toate instanțele din grupul Scalare automată pentru a se conforma noii configurații de lansare..

Datorită cloudformation-ruby-dsl și funcției sale de utilitate interpolată, putem folosi referințe Cloudformation în scriptul app_user_data.sh:

readonly rebuild_timestamp ="{{param (‘RebuildTimestamp’)}}"

Această procedură asigură că configurația noastră de lansare este nouă de fiecare dată când se declanșează reconstrucția.

Cârlige de viață

Folosim cârlige de ciclu de viață Auto Scaling pentru a ne asigura că instanțele noastre sunt complet provizionate și să trecem controalele de sănătate necesare înainte de a merge live.

Utilizarea cârligelor de ciclu de viață ne permite să avem același ciclu de viață al instanței atât atunci când declanșăm actualizarea cu Cloudformation, cât și când are loc un eveniment de scalare automată (de exemplu, când o instanță nu reușește o verificare de sănătate EC2 și se încheie). Nu folosim semnalul cfn și politica de actualizare a scalării automate WaitOnResourceSignals, deoarece acestea sunt aplicate doar atunci când Cloudformation declanșează o actualizare.

Când un grup de scalare automată creează o nouă instanță, cârligul de ciclu de viață EC2_INSTANCE_LAUNCHING este declanșat și automat pune instanța într-o stare în așteptare: Wait.

După ce instanța este complet configurată, începe să lovească propriile obiective ale verificării de sănătate cu curl din scriptul de date al utilizatorului. Odată ce verificările de sănătate raportează că aplicația este sănătoasă, emitem o acțiune CONTINUE pentru acest cârlig de ciclu de viață, astfel încât instanța se atașează la echilibratorul de sarcină și începe să difuzeze traficul.

Dacă verificările de sănătate nu reușesc, emitem o acțiune ABANDON care pune capăt instanței defecte, iar grupul de scalare automată lansează o altă.

Pe lângă faptul că nu reușesc să treacă controalele de sănătate, scriptul nostru de date al utilizatorului poate eșua în alte puncte – de exemplu, dacă problemele de conectivitate temporară împiedică instalarea software-ului.

Vrem ca crearea unei noi instanțe să eșueze imediat ce ne dăm seama că nu va deveni niciodată sănătoasă. Pentru a realiza acest lucru, setăm o capcană ERR în scriptul de date al utilizatorului împreună cu setul -o errtrace pentru a apela o funcție care trimite o acțiune ABANDON pentru ciclul de viață, astfel încât o instanță defectă să poată termina cât mai curând posibil.

Scripturi de date ale utilizatorului

Scriptul de date al utilizatorului este responsabil de instalarea întregului software necesar pe instanță. Am folosit cu succes Ansible pentru instanțele de aprovizionare și Capistrano pentru a implementa aplicații pentru o lungă perioadă de timp, așa că le folosim și aici, permițând diferența minimă între implementările obișnuite și reconstruirile..

Scriptul de date al utilizatorului verifică depozitul nostru de aplicații de la Github, care include scripturi de aprovizionare Ansible, apoi rulează Ansible și Capistrano indicat localhost.

La verificarea codului, trebuie să fim siguri că versiunea implementată în prezent a aplicației este implementată în timpul reconstruirii. Scriptul de implementare Capistrano include o sarcină care actualizează un fișier în S3 care stochează SHA-ul angajat actual implementat. Când se întâmplă reconstrucția, sistemul preia angajamentul care se presupune a fi implementat din acel fișier.

Actualizările software sunt aplicate prin rularea nesupravegheată în prim-plan cu comanda nesupravegheată-upgrade -d. Odată finalizată, instanța repornește și începe verificările de sănătate.

Tratarea secretelor

Serverul are nevoie de acces temporar la secrete (cum ar fi parola pentru bolta Ansible) care sunt preluate din magazinul de parametri EC2. Serverul poate accesa secrete doar pentru o durată scurtă în timpul reconstrucției. După ce au fost preluate, înlocuim imediat profilul instanței inițiale cu unul diferit, care are acces numai la resursele necesare pentru ca aplicația să fie rulată.

Vrem să evităm să stocați secrete în memoria persistentă a instanței. Singurul secret pe care îl salvăm pe disc este cheia SSH Github, dar nu și fraza sa de acces. Nici nu salvăm parola pentru bolta Ansible.

Cu toate acestea, trebuie să trecem aceste fraze de parolă la SSH și respectiv la Ansible și este posibilă numai în modul interactiv (adică utilitarul solicită utilizatorului să introducă manual parolele) dintr-un motiv bun – dacă o frază de acces este o parte a unei comenzi, aceasta este salvat în istoricul de shell și poate fi vizibil pentru toți utilizatorii din sistem dacă rulează ps. Folosim utilitatea așteptată pentru a automatiza interacțiunea cu aceste instrumente:

aştepta << EOF
cd $ {repo_dir}
spawn make ansible_local env = $ {deploy_env} stack = $ {stack} hostname = $ {server_hostname}
setare timp 2
așteptați-vă ‘Parola Vault’
trimite "$ {Vault_password} \ r"
setare de timp 900
asteptati {
"unreachable = 0 eșuat = 0" {
iesirea 0
}
eof {
iesirea 1
}
pauză {
iesirea 1
}
}
EOF

Declanșarea reconstrucției

Întrucât declanșăm reconstruirea rulând același script Cloudformation folosit pentru crearea / actualizarea infrastructurii noastre, trebuie să ne asigurăm că nu actualizăm accidental o parte a infrastructurii care nu se presupune că va fi actualizată în timpul reconstruirii..

Obținem acest lucru prin stabilirea unei politici de stivă restrictive pe stivele noastre de Cloudformation, astfel încât doar resursele necesare pentru reconstrucție sunt actualizate:

{
"Afirmație" : [
{
"Efect" : "Permite",
"Acțiune" : "Actualizare: Modificare",
"Principal": "*",
"Resursă" : [
"LogicalResourceId / * AutoScalingGroup"
]
},
{
"Efect" : "Permite",
"Acțiune" : "Actualizați: Înlocuiți",
"Principal": "*",
"Resursă" : [
"LogicalResourceId / * LaunchConfiguration"
]
}
]
}

Atunci când trebuie să facem actualizări reale de infrastructură, trebuie să actualizăm manual politica de stivă pentru a permite actualizări în mod explicit la aceste resurse.

Deoarece numele de gazdă și IP-urile serverului nostru se schimbă în fiecare zi, avem un script care actualizează inventarele noastre locale Ansible și configurațiile SSH. Descoperă instanțele prin API-ul AWS după etichete, redă fișierele de inventar și configurează din șabloanele ERB și adaugă noile IP-uri la SS_Hosts cunoscute.

ExpressVPN respectă cele mai înalte standarde de securitate

Reconstruirea serverelor ne protejează de o amenințare specifică: atacatorii obținând acces la serverele noastre printr-o vulnerabilitate de kernel / software.

Cu toate acestea, acesta este doar unul dintre numeroasele moduri prin care ne păstrăm securitatea infrastructurii, incluzând, dar fără a se limita la supunerea unor audituri periodice de securitate și de a face ca sistemele critice să fie inaccesibile de pe internet.

În plus, ne asigurăm că toate codurile și procesele noastre interne respectă cele mai înalte standarde de securitate.