购买Express VPN
享受全球加速服务

ExpressVPN如何保持其Web服务器的安全性

本文介绍了ExpressVPN 针对运行ExpressVPN网站(而不是VPN服务器)的基础架构的安全补丁管理方法。总的来说,我们的安全方法是:

  1. 使系统很难破解
  2. 如果系统假设被黑客入侵并且承认某些系统无法完全安全,则可以最大限度地减少潜在的损害。通常,这从架构设计阶段开始,我们将应用程序的访问权限降至最低。
  3. 最大限度地减少系统可以保持受损的时间。
  4. 使用内部和外部的常规测试来验证这些点。

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

在这里,我们解释如何实现以下目标:

  1. 确保所有服务器都经过完全修补,并且永远不会超过CVE出版物24小时。
  2. 确保超过24小时不使用任何服务器,从而对攻击者持久性的时间量设置上限。

我们通过一个自动化系统来实现这两个目标,这个系统重建服务器,从操作系统和所有最新补丁开始,至少每24小时销毁一次

我们对本文的意图是对面临类似挑战的其他开发人员有用,并为我们的客户和媒体提供ExpressVPN操作的透明度。

ExpressVPN的Web基础架构托管在AWS上(而不是我们在专用硬件上运行的VPN服务器),我们大量使用其功能来进行重建。

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

为了缓解这种情况,我们使用名为cloudformation-ruby-dsl的DSL,它使我们能够在Ruby中编写模板定义并以JSON格式导出Cloudformation模板。

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

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

- name: 'render {{ component }} stack cloudformation json'
  shell: 'ruby "{{ template_name | default(component) }}.rb" expand --stack-name {{ stack }} --region {{ aws_region }} > {{ tempfile_path }}'
  args:
chdir: ../cloudformation/templates
  changed_when: false

- name: 'create/update {{ component }} stack'
  cloudformation:
stack_name: '{{ stack }}-{{ xv_env_name }}-{{ component }}'
state: present
region: '{{ aws_region }}'
template: '{{ tempfile_path }}'
template_parameters: '{{ template_parameters | default({}) }}'
stack_policy: '{{ stack_policy }}'
  register: cf_result

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

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

- include: _tempfile.yml
- copy:
content: '{{ component | regex_replace("-", "_") }}_stack: {{ cf_result.stack_outputs | to_json }}'
dest: '{{ tempfile_path }}.json'
no_log: true
changed_when: false

- 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用户数据脚本:

resource 'AppLaunchConfiguration', Type: 'AWS::AutoScaling::LaunchConfiguration',
Properties: {
KeyName: param('AppServerKey'),
ImageId: param('AppServerAMI'),
InstanceType: param('AppServerInstanceType'),
SecurityGroups: [
param('SecurityGroupApp'),
],
IamInstanceProfile: param('RebuildIamInstanceProfile'),
InstanceMonitoring: true,
BlockDeviceMappings: [
{
DeviceName: '/dev/sda1', # root volume
Ebs: {
VolumeSize: param('AppServerStorageSize'),
VolumeType: param('AppServerStorageType'),
DeleteOnTermination: true,
},
},
],
UserData: base64(interpolate(file('scripts/app_user_data.sh'))),
}

如果我们对用户数据脚本(甚至是注释)进行任何更新,则会认为启动配置已更改,并且Cloudformation将更新Auto Scaling组中的所有实例以符合新的启动配置。

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

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

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

生命周期钩子

我们使用Auto Scaling生命周期钩子来确保我们的实例是完全配置的,并在它们上线之前通过所需的健康检查。

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

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

完全配置实例后,它将开始使用用户数据脚本中的curl命中自己的运行状况检查端点。一旦运行状况检查报告应用程序运行正常,我们就会为此生命周期挂钩发出CONTINUE操作,以便实例连接到负载均衡器并开始提供流量。

如果运行状况检查失败,我们将发出ABANDON操作以终止故障实例,并且自动扩展组将启动另一个实例。

除了未通过运行状况检查外,我们的用户数据脚本可能在其他位置失败 - 例如,如果临时连接问题阻止了软件安装。

一旦我们意识到它永远不会变得健康,我们希望创建一个新实例失败。为此,我们在用户数据脚本中设置了一个ERR陷阱和set -o errtrace,以调用发送ABANDON生命周期操作的函数,以便故障实例尽快终止。

用户数据脚本

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

用户数据脚本从Github检出我们的应用程序存储库,其中包括Ansible供应脚本,然后运行Ansible和Capistrano指向localhost。

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

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

处理秘密

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

我们希望避免在实例的持久内存中存储任何秘密。我们保存到磁盘的唯一秘密是Github SSH密钥,但不是密码。我们也不保存Ansible保管库密码。

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

expect << EOF
cd ${repo_dir}
spawn make ansible_local env=${deploy_env} stack=${stack} hostname=${server_hostname}
set timeout 2
expect 'Vault password'
send "${vault_password}\r"
set timeout 900
expect {
"unreachable=0 failed=0" {
exit 0
}
eof {
exit 1
}
timeout {
exit 1
}
}
EOF

触发重建

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

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

{
"Statement" : [
{
"Effect" : "Allow",
"Action" : "Update:Modify",
"Principal": "*",
"Resource" : [
"LogicalResourceId/*AutoScalingGroup"
]
},
{
"Effect" : "Allow",
"Action" : "Update:Replace",
"Principal": "*",
"Resource" : [
"LogicalResourceId/*LaunchConfiguration"
]
}
]
}

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

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

ExpressVPN遵循最高的安全标准

重建服务器可以保护我们免受特定威胁:攻击者通过内核/软件漏洞访问我们的服务器。

但是,这只是我们保持基础设施安全的众多方式之一,包括但不限于定期进行安全审计并使关键系统无法通过互联网访问。

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

未经允许不得转载:ExpressVPN » ExpressVPN如何保持其Web服务器的安全性

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

ExpressVPN:安全、快速、30天退款保证

立即购买