Como o ExpressVPN mantém seus servidores da Web atualizados e protegidos

O servidor ExpressVPN sobe das cinzas.


Este artigo explica a abordagem da ExpressVPN para gerenciamento de patches de segurança para a infraestrutura executando o site da ExpressVPN (não os servidores VPN). Em geral, nossa abordagem à segurança é:

  1. Tornar os sistemas muito difícil de invadir.
  2. Minimize o dano potencial se um sistema hipoteticamente for invadido e reconhecer o fato de que alguns sistemas não podem ser perfeitamente seguros. Normalmente, isso começa na fase de design da arquitetura, onde minimizamos o acesso de um aplicativo.
  3. Minimize a quantidade de tempo que um sistema pode permanecer comprometido.
  4. Validar esses pontos com protestos regulares, internos e externos.

A segurança está arraigada em nossa cultura e é a principal preocupação que guia todo o nosso trabalho. Existem muitos outros tópicos, como nossas práticas de desenvolvimento de software de segurança, segurança de aplicativos, processos e treinamento de funcionários etc., mas esses estão fora do escopo desta publicação.

Aqui explicamos como conseguimos o seguinte:

  1. Verifique se todos os servidores estão totalmente corrigidos e nunca mais de 24 horas atrás das publicações dos CVEs.
  2. Certifique-se de que nenhum servidor seja usado por mais de 24 horas, colocando assim um limite superior na quantidade de tempo que um invasor pode ter persistência.

Realizamos os dois objetivos através de um sistema automatizado que reconstrói servidores, começando com o sistema operacional e todos os patches mais recentes, e os destrói pelo menos uma vez a cada 24 horas.

Nosso objetivo neste artigo é ser útil para outros desenvolvedores que enfrentam desafios semelhantes e dar transparência às operações da ExpressVPN para nossos clientes e a mídia.

Como usamos playbooks Ansible e Cloudformation

A infraestrutura da Web do ExpressVPN está hospedada na AWS (em oposição aos nossos servidores VPN executados em hardware dedicado) e fazemos uso intenso de seus recursos para possibilitar a reconstrução.

Toda a nossa infraestrutura da web é fornecida com o Cloudformation e tentamos automatizar o maior número de processos possível. No entanto, achamos que o trabalho com modelos brutos de Cloudformation é bastante desagradável devido à necessidade de repetição, baixa legibilidade geral e restrições da sintaxe JSON ou YAML.

Para atenuar isso, usamos uma DSL chamada cloudformation-ruby-dsl que nos permite escrever definições de modelo em Ruby e exportar modelos de Cloudformation em JSON.

Em particular, o DSL nos permite escrever scripts de dados do usuário como scripts regulares que são convertidos para JSON automaticamente (e não passam pelo processo doloroso de transformar cada linha do script em uma sequência JSON válida).

Uma função Ansible genérica chamada cloudformation-infrastructure cuida da renderização do modelo real em um arquivo temporário, que é então usado pelo módulo Ansible da cloudformation:

– nome: ‘renderizar {{component}} pilha cloudformation json’
shell: ‘ruby "{{template_name | padrão (componente)}}. rb" expanda –stack-name {{stack}} – região {{aws_region}} > {{tempfile_path}} ‘
args:
chdir: ../cloudformation/templates
modified_when: false

– nome: ‘criar / atualizar {{component}} pilha’
cloudformation:
stack_name: ‘{{stack}} – {{xv_env_name}} – {{component}}’
estado: presente
região: ‘{{aws_region}}’
modelo: ‘{{tempfile_path}}’
template_parameters: ‘{{template_parameters | padrão({}) }}’
stack_policy: ‘{{stack_policy}}’
registrar: cf_result

No manual, chamamos a função de infra-estrutura de nuvem várias vezes com variáveis ​​de componentes diferentes para criar várias pilhas de Cloudformation. Por exemplo, temos uma pilha de rede que define a VPC e os recursos relacionados e uma pilha de aplicativos que define o grupo Auto Scaling, configuração de inicialização, ganchos do ciclo de vida, etc..

Em seguida, usamos um truque um tanto feio, mas útil, para transformar a saída do módulo de formação em nuvem em variáveis ​​Ansible para funções subsequentes. Temos que usar essa abordagem, já que o Ansible não permite a criação de variáveis ​​com nomes dinâmicos:

– incluem: _tempfile.yml
– cópia de:
conteúdo: ‘{{component | regex_replace ("-", "_")}} _ stack: {{cf_result.stack_outputs | to_json}} ‘
dest: ‘{{tempfile_path}}. json’
no_log: true
modified_when: false

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

Atualizando o grupo EC2 Auto Scaling

O site da ExpressVPN está hospedado em várias instâncias do EC2 em um grupo de Auto Scaling atrás de um Application Load Balancer, o que nos permite destruir servidores sem nenhum tempo de inatividade, pois o balanceador de carga pode drenar as conexões existentes antes que uma instância seja encerrada..

A Cloudformation orquestra toda a reconstrução e acionamos o manual Ansible descrito acima a cada 24 horas para reconstruir todas as instâncias, usando o atributo AutoScalingRollingUpdate UpdatePolicy do recurso AWS :: AutoScaling :: AutoScalingGroup.

Quando simplesmente acionado repetidamente sem nenhuma alteração, o atributo UpdatePolicy não é usado – é invocado apenas em circunstâncias especiais, conforme descrito na documentação. Uma dessas circunstâncias é uma atualização da configuração de inicialização do Auto Scaling – um modelo que um grupo de Auto Scaling usa para iniciar instâncias do EC2 – que inclui o script de dados do usuário do EC2 executado na criação de uma nova instância:

recurso ‘AppLaunchConfiguration’, Tipo: ‘AWS :: AutoScaling :: LaunchConfiguration’,
Propriedades: {
KeyName: param (‘AppServerKey’),
ImageId: param (‘AppServerAMI’),
InstanceType: param (‘AppServerInstanceType’),
SecurityGroups: [
param (‘SecurityGroupApp’),
],
IamInstanceProfile: param (‘RebuildIamInstanceProfile’),
InstanceMonitoring: true,
BlockDeviceMappings: [
{
Nome do dispositivo: ‘/ dev / sda1’, # volume raiz
Ebs: {
VolumeSize: param (‘AppServerStorageSize’),
VolumeType: param (‘AppServerStorageType’),
DeleteOnTermination: true,
}
}
],
UserData: base64 (interpolar (arquivo (‘scripts / app_user_data.sh’))),
}

Se fizermos alguma atualização no script de dados do usuário, mesmo um comentário, a configuração de inicialização será considerada alterada e o Cloudformation atualizará todas as instâncias no grupo Auto Scaling para cumprir com a nova configuração de inicialização.

Graças ao cloudformation-ruby-dsl e sua função de utilitário interpolar, podemos usar as referências do Cloudformation no script app_user_data.sh:

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

Este procedimento garante que nossa configuração de inicialização seja nova sempre que a reconstrução for acionada.

Ganchos de ciclo de vida

Usamos ganchos do ciclo de vida do Auto Scaling para garantir que nossas instâncias sejam totalmente provisionadas e passem nas verificações de integridade necessárias antes de serem ativadas.

O uso de ganchos do ciclo de vida nos permite ter o mesmo ciclo de vida da instância quando acionamos a atualização com o Cloudformation e quando ocorre um evento de dimensionamento automático (por exemplo, quando uma instância falha em uma verificação de integridade do EC2 e é encerrada). Não usamos cfn-signal e a política de atualização com escala automática WaitOnResourceSignals porque elas são aplicadas apenas quando o Cloudformation aciona uma atualização.

Quando um grupo de dimensionamento automático cria uma nova instância, o gancho do ciclo de vida EC2_INSTANCE_LAUNCHING é acionado e coloca automaticamente a instância no estado Pendente: Aguardar.

Após a instância estar totalmente configurada, ela começa a atingir seus próprios pontos de extremidade de verificação de integridade com ondulação no script de dados do usuário. Depois que as verificações de integridade informam que o aplicativo está íntegro, emitimos uma ação CONTINUE para esse gancho do ciclo de vida, para que a instância seja anexada ao balanceador de carga e comece a veicular o tráfego.

Se as verificações de integridade falharem, emitimos uma ação ABANDON que encerra a instância defeituosa e o grupo de dimensionamento automático inicia outra.

Além de não passar nas verificações de integridade, nosso script de dados do usuário pode falhar em outros pontos – por exemplo, se problemas de conectividade temporários impedirem a instalação do software.

Queremos que a criação de uma nova instância falhe assim que percebermos que ela nunca ficará saudável. Para conseguir isso, configuramos uma interceptação de ERR no script de dados do usuário, juntamente com set -o errtrace para chamar uma função que envia uma ação do ciclo de vida ABANDON para que uma instância defeituosa possa terminar o mais rápido possível.

Scripts de dados do usuário

O script de dados do usuário é responsável por instalar todo o software necessário na instância. Usamos o Ansible com êxito para provisionar instâncias e o Capistrano para implantar aplicativos por um longo período de tempo, por isso também os usamos aqui, permitindo a diferença mínima entre implantações e reconstruções regulares.

O script de dados do usuário faz check-out do repositório de aplicativos do Github, que inclui scripts de provisionamento Ansible, depois executa Ansible e Capistrano apontou para localhost.

Ao fazer o check-out do código, precisamos ter certeza de que a versão atualmente implementada do aplicativo é implantada durante a reconstrução. O script de implantação do Capistrano inclui uma tarefa que atualiza um arquivo no S3 que armazena o SHA de confirmação implantado no momento. Quando a reconstrução acontece, o sistema seleciona a confirmação que deve ser implantada nesse arquivo.

As atualizações de software são aplicadas executando a atualização autônoma em primeiro plano com o comando unattended-upgrade -d. Depois de concluída, a instância é reiniciada e inicia as verificações de integridade.

Lidando com segredos

O servidor precisa de acesso temporário a segredos (como a senha do cofre Ansible) que são buscados no repositório de parâmetros do EC2. O servidor pode acessar apenas segredos por um curto período de tempo durante a reconstrução. Depois que eles são buscados, substituímos imediatamente o perfil de instância inicial por um diferente, que só tem acesso aos recursos necessários para a execução do aplicativo.

Queremos evitar o armazenamento de segredos na memória persistente da instância. O único segredo que salvamos no disco é a chave SSH do Github, mas não a senha. Não salvamos a senha do cofre Ansible,.

No entanto, precisamos passar essas senhas para SSH e Ansible, respectivamente, e isso só é possível no modo interativo (ou seja, o utilitário solicita que o usuário insira as senhas manualmente) por um bom motivo – se uma senha é parte de um comando, é salvos no histórico do shell e podem ser visíveis para todos os usuários no sistema se eles executarem ps. Usamos o utilitário expect para automatizar a interação com essas ferramentas:

Espero << EOF
cd $ {repo_dir}
spawn make ansible_local env = $ {deploy_env} stack = $ {stack} nome do host = $ {server_hostname}
definir tempo limite 2
esperar ‘Senha do Vault’
mandar "$ {senha_do_vault} \ r"
definir tempo limite 900
Espero {
"inacessível = 0 falhou = 0" {
saída 0
}
eof {
saída 1
}
tempo esgotado {
saída 1
}
}
EOF

Disparando a reconstrução

Como acionamos a reconstrução executando o mesmo script Cloudformation usado para criar / atualizar nossa infraestrutura, precisamos garantir que não atualizamos acidentalmente parte da infraestrutura que não deve ser atualizada durante a reconstrução.

Conseguimos isso definindo uma política restritiva de pilha em nossas pilhas do Cloudformation, para que apenas os recursos necessários para a reconstrução sejam atualizados:

{
"Declaração" : [
{
"Efeito" : "Permitir",
"Açao" : "Atualização: Modificar",
"Diretor": "*",
"Recurso" : [
"LogicalResourceId / * AutoScalingGroup"
]
}
{
"Efeito" : "Permitir",
"Açao" : "Atualização: Substituir",
"Diretor": "*",
"Recurso" : [
"LogicalResourceId / * LaunchConfiguration"
]
}
]
}

Quando precisamos fazer atualizações de infraestrutura reais, precisamos atualizar manualmente a política de pilha para permitir atualizações explicitamente a esses recursos.

Como os nomes de host e IPs do servidor mudam todos os dias, temos um script que atualiza nossos inventários Ansible locais e configurações SSH. Ele descobre as instâncias por meio da API da AWS por tags, renderiza o inventário e os arquivos de configuração dos modelos ERB e adiciona os novos IPs ao SSH known_hosts.

O ExpressVPN segue os mais altos padrões de segurança

A reconstrução de servidores nos protege de uma ameaça específica: invasores que acessam nossos servidores por meio de uma vulnerabilidade de kernel / software.

No entanto, essa é apenas uma das muitas maneiras pelas quais mantemos nossa infraestrutura segura, incluindo, sem limitação, a realização de auditorias regulares de segurança e a indisponibilidade de sistemas críticos pela Internet..

Além disso, garantimos que todos os nossos códigos e processos internos sigam os mais altos padrões de segurança.

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