ExpressVPN如何保持其Web服务器的修补程序和安全性

[ware_item id=33][/ware_item]

ExpressVPN服务器从灰烬中崛起。


本文介绍ExpressVPN的方法 运行ExpressVPN网站的基础架构的安全补丁程序管理 (不是VPN服务器)。通常,我们的安全性方法是:

  1. 使系统非常 难以破解.
  2. 最小化潜在损害 如果假设某个系统被黑客入侵,并承认某些系统不能完全安全的事实。通常,这始于架构设计阶段,在此阶段我们将应用程序的访问减至最少.
  3. 减少时间 一个系统可以继续受到威胁.
  4. 验证 这些要点定期进行内部和外部渗透测试.

安全在我们的文化中根深蒂固,是指导我们所有工作的主要关注点。还有许多其他主题,例如我们的安全软件开发实践,应用程序安全性,员工流程和培训等,但这些内容超出了本文的范围.

在这里,我们说明如何实现以下目标:

  1. 确保所有服务器均已打补丁 不超过CVE的发布时间不超过24小时.
  2. 确保没有服务器使用超过24小时, 因此,对攻击者可以持续存在的时间设置了上限.

我们通过 自动化系统,从操作系统和所有最新补丁开始重建服务器,并每24小时至少销毁一次.

本文的目的是对面临类似挑战的其他开发人员有所帮助,并向客户和媒体透明化ExpressVPN的运营.

我们如何使用Ansible剧本和Cloudformation

ExpressVPN的网络基础设施托管在AWS上(而不是在专用硬件上运行的VPN服务器),我们大量利用其功能来进行重建.

我们的整个Web基础架构都配备了Cloudformation,我们尝试使尽可能多的流程自动化。但是,由于需要重复,总体可读性差以及JSON或YAML语法的限制,我们发现使用原始Cloudformation模板非常不愉快.

为了减轻这种情况,我们使用称为cloudformation-ruby-dsl的DSL,使我们能够在Ruby中编写模板定义并在JSON中导出Cloudformation模板。.

尤其是,DSL允许我们将用户数据脚本编写为常规脚本,这些脚本会自动转换为JSON(而无需经历使脚本的每一行变成有效JSON字符串的痛苦过程).

称为cloudformation-infrastructure的通用Ansible角色负责将实际模板呈现为临时文件,然后由cloudformation Ansible模块使用:

- 名称:“渲染{{component}}堆栈cloudformation json”
外壳:'红宝石 "{{template_name | default(component)}}。rb" 扩展--stack-name {{stack}} --region {{aws_region}} > {{tempfile_path}}'
args:
chdir:../cloudformation/templates
更改时间:假

-名称:“创建/更新{{组件}}堆栈”
云形成:
stack_name:'{{stack}}-{{xv_env_name}}-{{component}}'
状态:存在
地区:“ {{aws_region}}”
模板:“ {{tempfile_path}}”
template_parameters:'{{template_parameters | default({})}}'
stack_policy:'{{stack_policy}}'
注册:cf_result

在剧本中,我们多次调用cloudformation-infrastructure角色,并使用不同的组件变量来创建多个Cloudformation堆栈。例如,我们具有定义VPC和相关资源的网络堆栈,以及定义Auto Scaling组,启动配置,生命周期挂钩等的应用程序堆栈。.

然后,我们使用一个有点难看但有用的技巧将cloudformation模块的输出转换为Ansible变量,以供后续角色使用。我们必须使用这种方法,因为Ansible不允许创建具有动态名称的变量:

- 包括:_tempfile.yml
-复制:
内容:'{{组件| regex_replace("-", "_")}} _ stack:{{cf_result.stack_outputs | to_json}}'
目标:“ {{tempfile_path}}。json”
no_log:是
更改时间:假

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

更新EC2 Auto Scaling组

ExpressVPN网站托管在应用程序负载平衡器后面的Auto Scaling组中的多个EC2实例上,这使我们能够在不停机的情况下销毁服务器,因为负载平衡器可以在实例终止之前耗尽现有连接.

Cloudformation协调​​了整个重建过程,我们每24小时触发一次上述Ansible剧本,以重建所有实例,并利用AWS :: AutoScaling :: AutoScalingGroup资源的AutoScalingRollingUpdate UpdatePolicy属性.

如果只是简单地重复触发而没有任何更改,则不会使用UpdatePolicy属性-仅在文档中所述的特殊情况下才调用它。其中一种情况是对Auto Scaling启动配置(Auto Scaling组用于启动EC2实例的模板)的更新,其中包括在创建新实例时运行的EC2用户数据脚本:

资源“ AppLaunchConfiguration”,类型:“ AWS :: AutoScaling :: LaunchConfiguration”,
属性:{
键名:param('AppServerKey'),
ImageId:param('AppServerAMI'),
InstanceType:param('AppServerInstanceType'),
安全组:[
param('SecurityGroupApp'),
],
IamInstanceProfile:param('RebuildIamInstanceProfile'),
实例监视:true,
BlockDeviceMappings:[
{
DeviceName:'/ dev / sda1',#根卷
ebs:{
VolumeSize:param('AppServerStorageSize'),
VolumeType:param('AppServerStorageType'),
DeleteOnTermination:是,
},
},
],
UserData:base64(interpolate(file('scripts / app_user_data.sh'))),
}

如果我们对用户数据脚本进行了任何更新,甚至添加了注释,启动配置也将被视为已更改,Cloudformation将更新Auto Scaling组中的所有实例以符合新的启动配置.

感谢cloudformation-ruby-dsl及其插值实用程序功能,我们可以在app_user_data.sh脚本中使用Cloudformation引用:

只读rebuild_timestamp ="{{param('RebuildTimestamp')}}"

此过程可确保每次触发重建时我们的启动配置都是新的.

生命周期挂钩

我们使用Auto Scaling生命周期挂钩来确保实例已完全配置,并在运行之前通过了必需的运行状况检查.

使用生命周期挂钩可以使我们在使用Cloudformation触发更新时以及发生自动扩展事件时(例如,当实例未通过EC2运行状况检查并终止时)具有相同的实例生命周期。我们不使用cfn-signal和WaitOnResourceSignals自动缩放更新策略,因为它们仅在Cloudformation触发更新时才应用.

当自动伸缩组创建新实例时,将触发EC2_INSTANCE_LAUNCHING生命周期挂钩,并自动将实例置于Pending:Wait状态.

实例完全配置后,它将开始使用用户数据脚本中的curl命中其自身的运行状况检查端点。运行状况检查报告应用程序运行状况良好之后,我们将为此生命周期挂钩发出CONTINUE操作,因此该实例将附加到负载均衡器并开始为流量提供服务.

如果运行状况检查失败,我们将执行ABANDON操作,该操作将终止有故障的实例,然后自动伸缩组将启动另一个操作.

除了无法通过运行状况检查外,我们的用户数据脚本还可能在其他地方失败,例如,如果临时连接问题阻止了软件安装.

我们希望一旦发现新实例永远不会变得健康就失败了。为此,我们在用户数据脚本中设置了一个ERR陷阱,同时设置了-o errtrace来调用一个函数,该函数发送一个ABANDON生命周期操作,以便有故障的实例可以尽快终止.

用户数据脚本

用户数据脚本负责在实例上安装所有必需的软件。我们已经成功地使用Ansible来配置实例,并使用Capistrano长期部署了应用程序,因此我们也在这里使用它们,从而使常规部署和重建之间的差异最小.

用户数据脚本从Github中检出我们的应用程序存储库,其中包括Ansible设置脚本,然后运行Ansible和Capistrano指向本地主机.

签出代码时,我们需要确保在重建过程中已部署了应用程序的当前部署版本。 Capistrano部署脚本包括一个任务,该任务更新S3中存储当前部署的提交SHA的文件。重建发生时,系统会从该文件中选择应该部署的提交.

通过使用无人值守升级-d命令在前台运行无人值守升级来应用软件更新。完成后,实例将重新启动并开始运行状况检查.

处理秘密

服务器需要临时访问从EC2参数存储中获取的机密(例如Ansible保管库密码)。在重建期间,服务器只能在短时间内访问机密。提取它们后,我们立即将初始实例配置文件替换为另一个实例配置文件,该配置文件只能访问应用程序运行所需的资源.

我们希望避免在实例的永久内存中存储任何秘密。我们保存到磁盘的唯一秘密是Github SSH密钥,而不是其密码短语。我们也没有保存Ansible保管库密码.

但是,我们需要将这些密码分别传递给SSH和Ansible,并且只有在交互模式下(即实用程序提示用户手动输入密码)是有充分理由的-如果密码是命令的一部分,则是保存在Shell历史记录中,并且如果他们运行ps,则对系统中的所有用户可见。我们使用Expect实用程序自动与这些工具进行交互:

期望 << 紧急行动
cd $ {repo_dir}
生成make ansible_local env = $ {deploy_env} stack = $ {stack} hostname = $ {server_hostname}
设置超时2
需要“保险柜密码”
发送 "$ {vault_password} \ r"
设置超时900
期望{
"不可达= 0失败= 0" {
出口0
}
eof {
1号出口
}
暂停 {
1号出口
}
}
紧急行动

触发重建

由于我们通过运行用于创建/更新基础架构的相同Cloudformation脚本来触发重建,因此我们需要确保我们不会意外地更新不应在重建期间更新的基础架构的某些部分.

我们通过在Cloudformation堆栈上设置限制性堆栈策略来实现此目的,因此仅更新重建所需的资源:

{
"声明" :[
{
"影响" : "允许",
"行动" : "更新:修改",
"主要": "*",
"资源资源" :[
"LogicalResourceId / * AutoScalingGroup"
]
},
{
"影响" : "允许",
"行动" : "更新:替换",
"主要": "*",
"资源资源" :[
"LogicalResourceId / *启动配置"
]
}
]
}

当我们需要进行实际的基础结构更新时,我们必须手动更新堆栈策略以允许显式更新这些资源.

因为我们的服务器主机名和IP每天都在变化,所以我们有一个脚本来更新我们的本地Ansible库存和SSH配置。它通过标签的AWS API来发现实例,从ERB模板呈现清单和配置文件,并将新IP添加到SSHknown_hosts.

ExpressVPN遵循最高的安全标准

重建服务器可保护我们免受特定威胁的侵害:攻击者通过内核/软件漏洞获得对我们服务器的访问权限.

但是,这只是我们确保基础架构安全的众多方式之一,包括但不限于进行定期安全审核以及使关键系统无法从Internet访问.

此外,我们确保所有代码和内部流程均遵循最高的安全标准.

ExpressVPN如何保持其Web服务器的修补程序和安全性
admin Author
Sorry! The Author has not filled his profile.