Cómo ExpressVPN mantiene sus servidores web parcheados y seguros

[ware_item id=33][/ware_item]

El servidor ExpressVPN se levanta de las cenizas.


Este artículo explica el enfoque de ExpressVPN para gestión de parches de seguridad para la infraestructura que ejecuta el sitio web ExpressVPN (no los servidores VPN). En general, nuestro enfoque de seguridad es:

  1. Hacer sistemas muy difícil de hackear.
  2. Minimiza el daño potencial si un sistema hipotéticamente es pirateado y reconoce el hecho de que algunos sistemas no pueden ser perfectamente seguros. Normalmente, esto comienza en la fase de diseño arquitectónico, donde minimizamos el acceso de una aplicación.
  3. Minimiza la cantidad de tiempo que un sistema puede quedar comprometido.
  4. Validar estos puntos con pentests regulares, tanto internos como externos.

La seguridad está arraigada en nuestra cultura y es la principal preocupación que guía todo nuestro trabajo. Hay muchos otros temas, como nuestras prácticas de desarrollo de software de seguridad, seguridad de aplicaciones, procesos y capacitación de empleados, etc., pero están fuera del alcance de esta publicación.

Aquí explicamos cómo logramos lo siguiente:

  1. Asegúrese de que todos los servidores estén completamente parcheados y nunca más de 24 horas detrás de las publicaciones de CVE.
  2. Asegúrese de que ningún servidor se use por más de 24 horas, poniendo así un límite superior en la cantidad de tiempo que un atacante puede tener persistencia.

Logramos ambos objetivos a través de un sistema automatizado que reconstruye servidores, comenzando con el sistema operativo y todos los parches más recientes, y los destruye al menos una vez cada 24 horas.

Nuestra intención para este artículo es ser útil para otros desarrolladores que enfrentan desafíos similares y dar transparencia a las operaciones de ExpressVPN a nuestros clientes y a los medios..

Cómo usamos Playbooks de Ansible y Cloudformation

La infraestructura web de ExpressVPN está alojada en AWS (a diferencia de nuestros servidores VPN que se ejecutan en hardware dedicado) y hacemos un uso intensivo de sus funciones para hacer posible la reconstrucción.

Toda nuestra infraestructura web está provista con Cloudformation, e intentamos automatizar tantos procesos como podamos. Sin embargo, descubrimos que trabajar con plantillas de Cloudformation sin procesar es bastante desagradable debido a la necesidad de repetición, mala legibilidad general y las limitaciones de la sintaxis JSON o YAML.

Para mitigar esto, utilizamos un DSL llamado cloudformation-ruby-dsl que nos permite escribir definiciones de plantilla en Ruby y exportar plantillas de Cloudformation en JSON.

En particular, el DSL nos permite escribir scripts de datos de usuario como scripts normales que se convierten a JSON automáticamente (y no pasan por el doloroso proceso de convertir cada línea del script en una cadena JSON válida).

Un rol genérico de Ansible llamado infraestructura de formación en la nube se encarga de representar la plantilla real en un archivo temporal, que luego es utilizado por el módulo Ansible de formación en la nube:

- nombre: 'render {{component}} stack cloudformation json'
concha: 'ruby "{{template_name | predeterminado (componente)}}. rb" expandir --stack-name {{stack}} --region {{aws_region}} > {{tempfile_path}} '
args:
chdir: ../cloudformation/templates
cambiado_cuando: falso

- nombre: 'crear / actualizar {{componente}} pila'
formacion de nubes
stack_name: '{{stack}} - {{xv_env_name}} - {{componente}}'
estado: presente
región: '{{aws_region}}'
plantilla: '{{tempfile_path}}'
template_parameters: '{{template_parameters | defecto({}) }}'
stack_policy: '{{stack_policy}}'
registrarse: cf_result

En el libro de jugadas, llamamos al rol de infraestructura de formación en la nube varias veces con diferentes variables de componentes para crear varias pilas de Cloudformation. Por ejemplo, tenemos una pila de red que define la VPC y los recursos relacionados y una pila de aplicaciones que define el grupo de Auto Scaling, la configuración de inicio, los ganchos del ciclo de vida, etc..

Luego usamos un truco algo feo pero útil para convertir la salida del módulo de formación en la nube en variables Ansible para roles posteriores. Tenemos que usar este enfoque ya que Ansible no permite la creación de variables con nombres dinámicos:

- incluyen: _tempfile.yml
- Copiar:
contenido: '{{componente | regex_replace ("-", "_ _")}} _ stack: {{cf_result.stack_outputs | to_json}} '
dest: '{{tempfile_path}}. json'
no_log: verdadero
cambiado_cuando: falso

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

Actualización del grupo EC2 Auto Scaling

El sitio web de ExpressVPN está alojado en varias instancias EC2 en un grupo de Auto Scaling detrás de un Application Load Balancer que nos permite destruir servidores sin ningún tiempo de inactividad ya que el equilibrador de carga puede drenar las conexiones existentes antes de que una instancia finalice.

Cloudformation organiza la reconstrucción completa, y activamos el libro de jugadas Ansible descrito anteriormente cada 24 horas para reconstruir todas las instancias, haciendo uso del atributo AutoScalingRollingUpdate UpdatePolicy del recurso AWS :: AutoScaling :: AutoScalingGroup.

Cuando simplemente se activa repetidamente sin ningún cambio, el atributo UpdatePolicy no se usa, solo se invoca en circunstancias especiales como se describe en la documentación. Una de esas circunstancias es una actualización de la configuración de inicio de Auto Scaling, una plantilla que utiliza un grupo de Auto Scaling para iniciar instancias EC2, que incluye el script de datos de usuario EC2 que se ejecuta en la creación de una nueva instancia:

recurso 'AppLaunchConfiguration', Tipo: 'AWS :: AutoScaling :: LaunchConfiguration',
Propiedades: {
KeyName: param ('AppServerKey'),
ImageId: param ('AppServerAMI'),
InstanceType: param ('AppServerInstanceType'),
SecurityGroups: [
param ('SecurityGroupApp'),
],
IamInstanceProfile: param ('RebuildIamInstanceProfile'),
Monitoreo de instancias: verdadero,
BlockDeviceMappings: [
{
DeviceName: '/ dev / sda1', # volumen raíz
Ebs: {
VolumeSize: param ('AppServerStorageSize'),
VolumeType: param ('AppServerStorageType'),
DeleteOnTermination: verdadero,
},
},
],
UserData: base64 (interpolate (file ('scripts / app_user_data.sh'))),
}

Si realizamos alguna actualización en el script de datos del usuario, incluso un comentario, la configuración de inicio se considerará modificada y Cloudformation actualizará todas las instancias en el grupo de Auto Scaling para cumplir con la nueva configuración de inicio.

Gracias a cloudformation-ruby-dsl y su función de utilidad de interpolación, podemos usar referencias de Cloudformation en el script app_user_data.sh:

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

Este procedimiento garantiza que nuestra configuración de inicio sea nueva cada vez que se active la reconstrucción.

Ganchos de ciclo de vida

Utilizamos ganchos de ciclo de vida de Auto Scaling para asegurarnos de que nuestras instancias estén completamente aprovisionadas y pasen las comprobaciones de estado necesarias antes de que se activen.

El uso de enlaces de ciclo de vida nos permite tener el mismo ciclo de vida de la instancia tanto cuando activamos la actualización con Cloudformation como cuando ocurre un evento de escala automática (por ejemplo, cuando una instancia falla una comprobación de estado EC2 y se termina). No utilizamos cfn-signal y la política de actualización de autoescalado WaitOnResourceSignals porque solo se aplican cuando Cloudformation activa una actualización.

Cuando un grupo de escalado automático crea una nueva instancia, se activa el enlace del ciclo de vida EC2_INSTANCE_LAUNCHING, y automáticamente coloca la instancia en un estado Pendiente: Esperar.

Una vez que la instancia está completamente configurada, comienza a alcanzar sus propios puntos finales de comprobación de estado con curl desde el script de datos del usuario. Una vez que las comprobaciones de estado informan que la aplicación está en buen estado, emitimos una acción CONTINUAR para este enlace de ciclo de vida, por lo que la instancia se conecta al equilibrador de carga y comienza a servir el tráfico.

Si las comprobaciones de estado fallan, emitimos una acción ABANDON que finaliza la instancia defectuosa y el grupo de escalado automático inicia otra..

Además de no pasar las comprobaciones de estado, nuestro script de datos de usuario puede fallar en otros puntos, por ejemplo, si problemas de conectividad temporales impiden la instalación del software.

Queremos que la creación de una nueva instancia falle tan pronto como nos demos cuenta de que nunca será saludable. Para lograr eso, establecemos una trampa ERR en el script de datos del usuario junto con set -o errtrace para llamar a una función que envía una acción de ciclo de vida ABANDON para que una instancia defectuosa pueda terminar lo antes posible.

Guiones de datos del usuario

El script de datos del usuario es responsable de instalar todo el software requerido en la instancia. Hemos utilizado con éxito Ansible para aprovisionar instancias y Capistrano para implementar aplicaciones durante mucho tiempo, por lo que también las estamos usando aquí, lo que permite la mínima diferencia entre implementaciones y reconstrucciones regulares.

El script de datos del usuario revisa nuestro repositorio de aplicaciones de Github, que incluye scripts de aprovisionamiento de Ansible, luego ejecuta Ansible y Capistrano apuntó a localhost.

Al retirar el código, debemos asegurarnos de que la versión implementada actualmente de la aplicación se implemente durante la reconstrucción. El script de implementación de Capistrano incluye una tarea que actualiza un archivo en S3 que almacena el SHA de confirmación implementado actualmente. Cuando ocurre la reconstrucción, el sistema recoge la confirmación que se supone que se implementará desde ese archivo.

Las actualizaciones de software se aplican ejecutando la actualización desatendida en primer plano con el comando desatendido-actualización -d. Una vez completada, la instancia se reinicia y comienza las comprobaciones de estado.

Tratando con secretos

El servidor necesita acceso temporal a los secretos (como la contraseña del almacén de Ansible) que se obtienen del almacén de parámetros EC2. El servidor solo puede acceder a los secretos durante un breve período de tiempo durante la reconstrucción. Una vez recuperados, reemplazamos inmediatamente el perfil de instancia inicial por uno diferente que solo tiene acceso a los recursos necesarios para que la aplicación se ejecute.

Queremos evitar almacenar secretos en la memoria persistente de la instancia. El único secreto que guardamos en el disco es la clave Github SSH, pero no su frase de contraseña. Tampoco guardamos la contraseña del almacén de Ansible.

Sin embargo, necesitamos pasar estas frases de contraseña a SSH y Ansible respectivamente, y solo es posible en modo interactivo (es decir, la utilidad solicita al usuario que ingrese las frases de contraseña manualmente) por una buena razón: si una frase de contraseña es parte de un comando, es guardado en el historial de shell y puede ser visible para todos los usuarios del sistema si ejecutan ps. Utilizamos la utilidad de esperar para automatizar la interacción con esas herramientas:

esperar << EOF
cd $ {repo_dir}
spawn make ansible_local env = $ {deploy_env} stack = $ {stack} hostname = $ {server_hostname}
establecer tiempo de espera 2
esperar 'contraseña de Vault'
enviar "$ {contraseña_vault} \ r"
establecer tiempo de espera 900
esperar {
"inalcanzable = 0 fallido = 0" {
salida 0
}
eof {
salida 1
}
se acabó el tiempo {
salida 1
}
}
EOF

Disparando la reconstrucción

Dado que activamos la reconstrucción ejecutando el mismo script de Cloudformation que se usa para crear / actualizar nuestra infraestructura, debemos asegurarnos de no actualizar accidentalmente alguna parte de la infraestructura que no debe actualizarse durante la reconstrucción.

Logramos esto al establecer una política de pila restrictiva en nuestras pilas de Cloudformation para que solo se actualicen los recursos necesarios para la reconstrucción:

{
"Declaración" : [
{
"Efecto" : "Permitir",
"Acción" : "Actualización: Modificar",
"Principal": "* *",
"Recurso" : [
"LogicalResourceId / * AutoScalingGroup"
]
},
{
"Efecto" : "Permitir",
"Acción" : "Actualización: Reemplazar",
"Principal": "* *",
"Recurso" : [
"LogicalResourceId / * LaunchConfiguration"
]
}
]
}

Cuando necesitamos hacer actualizaciones de infraestructura reales, tenemos que actualizar manualmente la política de pila para permitir actualizaciones a esos recursos explícitamente.

Debido a que los nombres de host y las direcciones IP de nuestros servidores cambian todos los días, tenemos un script que actualiza nuestros inventarios locales de Ansible y las configuraciones SSH. Descubre las instancias a través de la API de AWS por etiquetas, representa el inventario y los archivos de configuración de las plantillas ERB, y agrega las nuevas IP a SSH known_hosts.

ExpressVPN sigue los más altos estándares de seguridad

La reconstrucción de servidores nos protege de una amenaza específica: los atacantes obtienen acceso a nuestros servidores a través de una vulnerabilidad de kernel / software.

Sin embargo, esta es solo una de las muchas formas en que mantenemos segura nuestra infraestructura, que incluye, entre otros, someterse a auditorías de seguridad periódicas y hacer que los sistemas críticos sean inaccesibles desde Internet.

Además, nos aseguramos de que todos nuestros códigos y procesos internos sigan los más altos estándares de seguridad..

Cómo ExpressVPN mantiene sus servidores web parcheados y seguros
admin Author
Sorry! The Author has not filled his profile.