Hoe ExpressVPN zijn webservers gepatcht en veilig houdt

ExpressVPN-server komt uit de as.


In dit artikel wordt de aanpak van ExpressVPN uitgelegd beheer van beveiligingspatches voor de infrastructuur waarop de ExpressVPN-website draait (niet de VPN-servers). Over het algemeen is onze benadering van beveiliging:

  1. Maak systemen erg moeilijk te hacken.
  2. Minimaliseer de potentiële schade als een systeem hypothetisch wordt gehackt en het feit erkent dat sommige systemen niet perfect veilig kunnen worden gemaakt. Meestal begint dit in de fase van het architectonisch ontwerp, waar we de toegang van een applicatie minimaliseren.
  3. Minimaliseer de hoeveelheid tijd dat een systeem gecompromitteerd kan blijven.
  4. valideren deze punten met regelmatige pentesten, zowel intern als extern.

Veiligheid is ingebakken in onze cultuur en is de primaire zorg die al ons werk leidt. Er zijn veel andere onderwerpen, zoals onze ontwikkelingsmethoden voor beveiligingssoftware, applicatiebeveiliging, personeelsprocessen en training, enz., Maar deze vallen buiten het bereik van dit bericht.

Hier leggen we uit hoe we het volgende bereiken:

  1. Zorg ervoor dat alle servers volledig zijn gepatcht en nooit meer dan 24 uur achter publicaties van CVE’s.
  2. Zorg ervoor dat geen enkele server langer dan 24 uur wordt gebruikt, waardoor een bovengrens wordt gesteld aan de hoeveelheid tijd die een aanvaller doorzettingsvermogen kan hebben.

We bereiken beide doelen via een geautomatiseerd systeem dat servers opnieuw opbouwt, beginnend met het besturingssysteem en alle nieuwste patches, en ze minstens eenmaal per 24 uur vernietigt.

Onze bedoeling voor dit artikel is nuttig te zijn voor andere ontwikkelaars die met vergelijkbare uitdagingen worden geconfronteerd en om transparantie te bieden in de activiteiten van ExpressVPN aan onze klanten en de media.

Hoe we Ansible-playbooks en cloudformation gebruiken

De webinfrastructuur van ExpressVPN wordt gehost op AWS (in tegenstelling tot onze VPN-servers die op speciale hardware draaien) en we maken intensief gebruik van de functies ervan om herbouw mogelijk te maken.

Onze hele webinfrastructuur is voorzien van Cloudformation en we proberen zoveel mogelijk processen te automatiseren. We vinden het werken met onbewerkte Cloudformation-sjablonen echter nogal onaangenaam vanwege de noodzaak van herhaling, algemene slechte leesbaarheid en de beperkingen van de syntaxis van JSON of YAML.

Om dit te verminderen, gebruiken we een DSL genaamd cloudformation-ruby-dsl waarmee we sjabloondefinities kunnen schrijven in Ruby en Cloudformation-sjablonen kunnen exporteren in JSON.

In het bijzonder stelt de DSL ons in staat om gebruikersdatascripts te schrijven als normale scripts die automatisch naar JSON worden geconverteerd (en niet door het pijnlijke proces van het maken van elke regel van het script in een geldige JSON-string).

Een generieke Ansible-rol genaamd cloudformation-infrastructuur zorgt ervoor dat de werkelijke sjabloon wordt omgezet in een tijdelijk bestand, dat vervolgens wordt gebruikt door de cloudformation Ansible-module:

– naam: ‘render {{component}} stapel cloudformation json’
shell: ‘robijn "{{template_name | standaard (component)}}. rb" uitbreiden –stack-naam {{stack}} –region {{aws_region}} > {{tempfile_path}} ‘
args:
chdir: ../cloudformation/templates
change_when: false

– name: ‘create / update {{component}} stack’
cloudformation:
stack_name: ‘{{stack}} – {{xv_env_name}} – {{component}}’
staat: aanwezig
regio: ‘{{aws_region}}’
sjabloon: ‘{{tempfile_path}}’
template_parameters: ‘{{template_parameters | standaard({}) }}’
stack_policy: ‘{{stack_policy}}’
registreren: cf_result

In het playbook noemen we de cloudformation-infrastructuurrol verschillende keren met verschillende componentvariabelen om verschillende Cloudformation-stacks te maken. We hebben bijvoorbeeld een netwerkstapel die de VPC en gerelateerde bronnen definieert en een app-stapel die de Auto Scaling-groep definieert, startconfiguratie, lifecycle hooks, enz..

We gebruiken vervolgens een enigszins lelijke maar nuttige truc om de uitvoer van de cloudformatiemodule om te zetten in Ansible-variabelen voor volgende rollen. We moeten deze aanpak gebruiken, omdat Ansible geen variabelen met dynamische namen toestaat:

– include: _tempfile.yml
– kopie:
inhoud: ‘{{component | regex_replace ("-", "_")}} _ stapel: {{cf_result.stack_outputs | to_json}} ‘
dest: ‘{{tempfile_path}}. json’
no_log: waar
change_when: false

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

De EC2 Auto Scaling-groep bijwerken

De ExpressVPN-website wordt gehost op meerdere EC2-exemplaren in een Auto Scaling-groep achter een Application Load Balancer waarmee we servers kunnen vernietigen zonder downtime, omdat de load balancer bestaande verbindingen kan leegmaken voordat een exemplaar wordt beëindigd.

Cloudformation orkestreert de volledige herbouw en we activeren het hierboven beschreven Ansible-playbook om alle instanties opnieuw te bouwen, met behulp van het kenmerk AutoScalingRollingUpdate UpdatePolicy van de AWS :: AutoScaling :: AutoScalingGroup-resource.

Wanneer het eenvoudig herhaaldelijk zonder wijzigingen wordt geactiveerd, wordt het kenmerk UpdatePolicy niet gebruikt – het wordt alleen aangeroepen onder speciale omstandigheden zoals beschreven in de documentatie. Een van die omstandigheden is een update van de startconfiguratie van Auto Scaling (een sjabloon die een Auto Scaling-groep gebruikt om EC2-exemplaren te starten), inclusief het EC2-script voor gebruikersgegevens dat wordt uitgevoerd bij het maken van een nieuwe instantie:

bron ‘AppLaunchConfiguration’, Type: ‘AWS :: AutoScaling :: LaunchConfiguration’,
Eigendommen: {
KeyName: param (‘AppServerKey’),
ImageId: param (‘AppServerAMI’),
InstanceType: param (‘AppServerInstanceType’),
Beveiligingsgroepen: [
param ( ‘SecurityGroupApp’),
],
IamInstanceProfile: param (‘RebuildIamInstanceProfile’),
InstanceMonitoring: true,
BlockDeviceMappings: [
{
DeviceName: ‘/ dev / sda1’, # rootvolume
Ebs: {
VolumeSize: param (‘AppServerStorageSize’),
VolumeType: param (‘AppServerStorageType’),
DeleteOnTermination: true,
},
},
],
UserData: base64 (interpolate (file (‘scripts / app_user_data.sh’))),
}

Als we een update van het gebruikersgegevensscript uitvoeren, zelfs een opmerking, wordt de startconfiguratie als gewijzigd beschouwd en zal Cloudformation alle instanties in de Auto Scaling-groep bijwerken om te voldoen aan de nieuwe startconfiguratie.

Dankzij cloudformation-ruby-dsl en de interpolate hulpprogramma-functie kunnen we cloudformation-referenties gebruiken in het script app_user_data.sh:

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

Deze procedure zorgt ervoor dat onze startconfiguratie nieuw is telkens wanneer het opnieuw opbouwen wordt geactiveerd.

Lifecycle haken

We gebruiken Auto Scaling-lifecycle hooks om ervoor te zorgen dat onze instanties volledig zijn ingericht en de vereiste gezondheidscontroles doorstaan ​​voordat ze live gaan.

Door gebruik te maken van lifecycle hooks kunnen we dezelfde instance lifecycle hebben, zowel wanneer we de update met Cloudformation activeren als wanneer een auto-scaling-gebeurtenis plaatsvindt (bijvoorbeeld wanneer een instance een EC2 health check niet doorstaat en wordt beëindigd). We gebruiken geen cfn-signaal en het WaitOnResourceSignals automatische schaalbeleid omdat ze alleen worden toegepast wanneer Cloudformation een update activeert.

Wanneer een groep voor automatisch schalen een nieuwe instantie maakt, wordt de EC2_INSTANCE_LAUNCHING levenscyclushaak geactiveerd en wordt de instantie automatisch in de status In behandeling: wacht geplaatst.

Nadat het exemplaar volledig is geconfigureerd, begint het zijn eigen health check-eindpunten te raken met curl uit het gebruikersgegevensscript. Zodra de health checks aangeven dat de applicatie gezond is, voeren we een CONTINUE actie uit voor deze lifecycle hook, zodat de instantie wordt gekoppeld aan de load balancer en verkeer begint te bedienen.

Als de gezondheidscontroles mislukken, voeren we een ABANDON-actie uit die de defecte instantie beëindigt en start de groep voor automatisch schalen een andere.

Naast het niet doorstaan ​​van gezondheidscontroles, kan ons gebruikersgegevensscript op andere punten mislukken, bijvoorbeeld als tijdelijke verbindingsproblemen de software-installatie verhinderen.

We willen dat het creëren van een nieuwe instantie mislukt zodra we ons realiseren dat deze nooit gezond zal worden. Om dat te bereiken, stellen we een ERR-trap in het gebruikersgegevensscript in, samen met set -o errtrace om een ​​functie aan te roepen die een ABANDON-levenscyclusactie verzendt, zodat een defect exemplaar zo snel mogelijk kan worden beëindigd.

Scripts voor gebruikersgegevens

Het script met gebruikersgegevens is verantwoordelijk voor het installeren van alle vereiste software op het exemplaar. We hebben Ansible met succes gebruikt om instances en Capistrano in te richten om applicaties lange tijd te implementeren, dus we gebruiken ze hier ook, waardoor het minimale verschil tussen reguliere implementaties en herbouwen mogelijk is.

Het script met gebruikersgegevens checkt onze applicatierepository uit Github, inclusief Ansible-provisioningscripts, voert vervolgens Ansible uit en Capistrano verwijst naar localhost.

Bij het uitchecken van code moeten we er zeker van zijn dat de momenteel geïmplementeerde versie van de applicatie wordt geïmplementeerd tijdens het opnieuw opbouwen. Het Capistrano-implementatiescript bevat een taak die een bestand in S3 bijwerkt waarin de momenteel geïmplementeerde commit SHA wordt opgeslagen. Wanneer het opnieuw opbouwen gebeurt, haalt het systeem de commit op die verondersteld wordt te worden geïmplementeerd uit dat bestand.

Software-updates worden toegepast door unattended-upgrade op de voorgrond uit te voeren met de opdracht unattended-upgrade -d. Eenmaal voltooid, wordt het exemplaar opnieuw opgestart en worden de gezondheidscontroles gestart.

Omgaan met geheimen

De server heeft tijdelijke toegang nodig tot geheimen (zoals het wachtwoord van de Anault-kluis) die worden opgehaald uit de EC2 Parameter Store. De server heeft slechts gedurende korte tijd toegang tot geheimen tijdens het opnieuw opbouwen. Nadat ze zijn opgehaald, vervangen we onmiddellijk het eerste exemplaarprofiel door een ander profiel dat alleen toegang heeft tot bronnen die nodig zijn om de toepassing te laten werken.

We willen voorkomen dat geheimen worden opgeslagen in het permanente geheugen van de instantie. Het enige geheim dat we op schijf opslaan, is de Github SSH-sleutel, maar niet de wachtwoordzin. We slaan het Ansible Vault-wachtwoord ook niet op.

We moeten deze wachtwoorden echter respectievelijk doorgeven aan SSH en Ansible, en het is alleen mogelijk in de interactieve modus (dwz het hulpprogramma vraagt ​​de gebruiker om de wachtwoorden handmatig in te voeren) om een ​​goede reden – als een wachtwoordzin onderdeel is van een opdracht, is het opgeslagen in de shell-geschiedenis en kan zichtbaar zijn voor alle gebruikers in het systeem als ze ps uitvoeren. We gebruiken het hulpprogramma expect om de interactie met deze tools te automatiseren:

verwachten << EOF
cd $ {repo_dir}
spawn make ansible_local env = $ {deploy_env} stack = $ {stack} hostname = $ {server_hostname}
time-out instellen 2
verwacht ‘Vault-wachtwoord’
sturen "$ {Vault_password} \ r"
time-out instellen 900
verwacht {
"onbereikbaar = 0 mislukt = 0" {
uitgang 0
}
eof {
uitgang 1
}
time-out {
uitgang 1
}
}
EOF

De herbouw activeren

Omdat we het opnieuw opbouwen activeren door hetzelfde Cloudformation-script uit te voeren dat wordt gebruikt om onze infrastructuur te maken / bij te werken, moeten we ervoor zorgen dat we niet per ongeluk een deel van de infrastructuur bijwerken die niet geacht wordt te worden bijgewerkt tijdens het opnieuw opbouwen.

We bereiken dit door een restrictief stapelbeleid in te stellen voor onze Cloudformation-stacks, zodat alleen de middelen die nodig zijn voor het opnieuw opbouwen worden bijgewerkt:

{
"Uitspraak" : [
{
"Effect" : "Toestaan",
"Actie" : "Update: Modify",
"principaal": "*",
"hulpbron" : [
"LogicalResourceId / * AutoScalingGroup"
]
},
{
"Effect" : "Toestaan",
"Actie" : "UPDATE: Vervang",
"principaal": "*",
"hulpbron" : [
"LogicalResourceId / * LaunchConfiguration"
]
}
]
}

Wanneer we echte infrastructuurupdates moeten uitvoeren, moeten we het stapelbeleid handmatig bijwerken om updates voor die bronnen expliciet toe te staan.

Omdat onze serverhostnamen en IP’s elke dag veranderen, hebben we een script dat onze lokale Ansible-inventarissen en SSH-configuraties bijwerkt. Het ontdekt de instanties via de AWS API met tags, geeft de inventaris- en configuratiebestanden van ERB-sjablonen weer en voegt de nieuwe IP’s toe aan SSH bekende_hosts.

ExpressVPN volgt de hoogste beveiligingsstandaarden

Het opnieuw bouwen van servers beschermt ons tegen een specifieke dreiging: aanvallers die toegang krijgen tot onze servers via een kwetsbaarheid in de kernel / software.

Dit is echter slechts een van de vele manieren waarop we onze infrastructuur beveiligen, inclusief maar niet beperkt tot regelmatige beveiligingsaudits en kritische systemen ontoegankelijk maken via internet.

Bovendien zorgen we ervoor dat al onze code en interne processen de hoogste beveiligingsstandaarden volgen.

Kim Martin Administrator
Sorry! The Author has not filled his profile.
follow me
    Like this post? Please share to your friends:
    Adblock
    detector
    map