Chef InSpec介绍
任何业务都依赖于基础设施环境。近年来,基础设施领域已发生了巨大的变化。从最初的传统数据中心到数据中心托管服务,而如今,基础设施即服务和云平台在企业中更受欢迎。因此,大多数企业正在将其工作负载从本地数据中心迁移到云平台。
为了部署和管理这些复杂的基础设施配置,很多企业使用代码即基础设施(IaC)。当您使用IaC在AWS云平台上创建成百上千个资源时,如何知道代码按预期方式部署了这些资源?这些资源是否遵守合规性和安全性措施?我们并不确定,因此需要基础设施测试程序。
Chef InSpec是一个用于测试和审核应用程序和基础设施的开源代码框架。Chef InSpec的工作原理是将系统的实际状态与您编写的Chef InSpec代码表达的期望状态进行比较。Chef InSpec可以检测违规并以报告的形式显示。
Chef InSpec入门
Chef InSpec安装
Chef InSpec提供了适配多种操作系统环境的安装包,这里使用一键脚本
1 | curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec |
安装完成后可以看到inspec命令可以正常运行,并提示inspec命令可以使用的参数帮助信息
Chef InSpec配置介绍
使用Chef InSpec时主要需要弄清楚下面的三部分:
- Command-line interface
- Profile
- Resource
InSpec CLI使用local,SSH,WinRM,Docker或AWS等连接方式,可针对不同基础设施目标运行测试和审核。常用命令说明如下:
命令 | 说明 |
---|---|
archive | 将配置文件压缩成tar.gz(默认)或zip文件 |
check | 验证指定路径上的所有测试 |
exec | 针对指定目标运行所有测试文件。加载给定的配置文件,并在需要时获取其依赖项。然后连接到目标并执行配置文件中包含的所有控件。输出测试结果。 |
help | 查看特定命令的帮助信息 |
shell | 打开一个交互式的调试终端 |
version | 打印此工具的版本 |
Chef InSpec支持创建复杂的测试和合规性配置文件,每个profile文件都是独立的结构,具有自己的执行流程。用户可以自己编写profile文件,还可以在社区支持的Chef Supermarket和GitHub上找到别人写的profile。Profile文件通常具有下面的目录结构:
1 | examples/profile |
Chef InSpec拥有80多种可供使用的资源。如果您需要的资源尚未提供,也可以编写自己的自定义资源。Chef InSpec支持的资源列表如下:https://docs.chef.io/inspec/resources/
Chef InSpec测试AWS资源
典型的使用环境如下图所示,在数据中心内使用Chef InSpec对AWS进行基础设施资源测试。
从本地数据中心连接AWS环境需要使用Access Key和Secret Access Key(AKSK)。在企业环境中,通常会将IAM User集中在一个Master账号中进行管理,用户使用IAM User登陆AWS环境,然后再用Assume Role的方式操作其它AWS账户。
使用Chef InSpec连接AWS前,首先需要创建IAM用户,并生成AKSK。另外在UseCase账号中,需要创建Assume Role,给予适当的权限并允许Master账号中的IAM User切换到Use Case账号。
在本地数据中心连接AWS环境的配置示例如下:
1 | # cat .aws/credentials |
使用如下命令创建示例profile,该命令会自动生成profile目录结构。
1 | # inspec init profile --platform aws example |
示例代码的测试内容在controls/example.rb文件中
1 | # copyright: 2018, The Authors |
该测试的主要内容包含:
- 如果传入指定的VPC ID,则检查指定的VPC是否存在
- 检查所有VPC的安全组内是否包含允许22端口访问的策略
- 检查所有区域内的状态为available的VPC
执行如下命令运行该profile对AWS环境进行测试:
1 | # inspec exec example -t aws://cn-north-1/usecase |
InSpec命令行
exec
在指定位置运行所有测试文件。
1 | inspec exec profile [option] |
该子命令加载给定的配置文件,如果需要,获取它们的依赖项,然后连接到目标并执行配置文件中包含的任何控件。一个或多个报告器用于生成输出。
exec允许使用不同位置的测试文件
使用Chef Supermarket:
1 | inspec exec supermarket://username/linux-baseline |
使用本地配置(执行controls中的所有test文件)
1 | inspec exec /path/to/profile |
使用本地配置(执行单个test文件)
1 | inspec exec /path/to/a_test.rb |
使用git仓库
通过SSH链接
1 | inspec exec git@github.com:dev-sec/linux-baseline.git |
使用HTTPS链接
1 | inspec exec https://github.com/dev-sec/linux-baseline.git |
私有仓库
1 | inspec exec https://api_token@github.com/dev-sec/linux-baseline.git |
exec 参数
-t
,--target=TARGET
使用 URI 的简单定位选项;
InSpec项目结构
配置文件应具有以下结构:
1 | examples/profile |
说明:
inspec.yml
包括配置文件描述(必需)controls
是所有测试所在的目录(必填)libraries
是所有 Chef InSpec 资源扩展所在的目录(可选)files
是包含配置文件可以访问的附加文件的目录(可选)README.md
应该用来解释概要、它的范围和用途
inspec.yml配置文件
配置文件如下所示
1 | #指定配置文件的唯一名称。必需的。 |
验证profile是否缺少文件
1 | inspec check |
如下所示
supports,平台支持
使用文件中的supports
设置inspec.yml
来指定配置文件所针对的一个(或多个)平台。支持的平台列表可能包含以下内容:
platform-family
限制到特定平台系列。platform-name
限制特定平台名称。platform-name
支持星号 (*
) 通配符使用。release
限制特定平台版本,并与platform-name
.release
支持星号 (*
) 通配符使用。platform
限制平台名称或平台系列。
举例:
例如,要针对任何运行 Debian Linux 的设备
1 | name: ssh |
要仅针对 Ubuntu 版本 20.04,请使用:
1 | name: ssh |
要针对整个 Windows 2019 平台系列,包括数据中心和核心服务器,请使用:
1 | name: ssh |
要针对在 Amazon AWS 上运行的任何内容,请使用:
1 | name: ssh |
depends,配置文件依赖项
在配置文件可以使用来自另一个配置文件的控件之前,需要inspec.yml
在该部分的包含配置文件的文件中指定要包含的配置文件depends
。对于要包含的每个配置文件,应包括从中获取配置文件的位置和配置文件的名称。例如:
1 | depends: |
path,该path
设置定义了位于磁盘上的配置文件。此设置通常在开发配置文件和调试配置文件时使用。
1 | depends: |
url
设置指定位于基于 HTTP 或 HTTPS 的 URL 的配置文件。配置文件必须可通过 HTTP GET 操作访问,并且必须是有效的配置文件存档(zip、tar 或 tar.gz 格式)。
1 | depends: |
git
1 | depends: |
surpermarkt
1 | depends: |
controls 配置
包括配置文件中的所有控件
使用配置文件中的include_controls命令,每次执行包含配置文件时,都将执行命名配置文件中的所有控件。
在上面的例子中,每次my-app-profile
执行时,所有的控件my-baseline
也都被执行。因此,将执行以下控制:
- myapp-1
- myapp-2
- myapp-3
- baseline-1
- baseline-2
这是一个很好的提醒,当包含来自其他配置文件的控件时,为控件设置良好的命名约定有助于避免混淆!
从配置文件中跳过控件
如果包含的配置文件中的一项控制不适用于您的环境怎么办?幸运的是,不必为了删除控件而维护包含的配置文件的稍微修改的副本。该skip_control
命令告诉 Chef InSpec 不要运行特定控件。
在上面的示例中,除了来自配置文件的控制之外my-app-profile
,my-baseline
每次执行时都会my-app-profile
执行来自配置文件的所有控件。baseline-2``my-baseline
修改控件
假设仍应运行包含配置文件中的特定控件,但影响不合适?也许测试应该仍然运行,但如果它失败了,它应该被视为低严重性而不是高严重性?当包含一个控件时,它也可以被修改!
使用包含的配置文件中的资源
默认情况下,列出的依赖项中的所有自定义资源都可用于您的配置文件。如果您的两个依赖项提供了同名的资源,则可以使用require_resource
DSL 函数来消除两者的歧义:
1 | require_resource(profile: 'my_dep', resource: 'my_res', |
InSpec语法
语句块
配置文件输入
Chef InSpec 配置文件可能包含可在测试期间访问的其他文件。配置文件使您能够将测试的逻辑与您的测试检查的数据分开,例如,您需要打开的端口列表。
要访问这些文件,它们必须存储在files
配置文件的根目录中。它们是通过相对于该文件夹的名称访问的,带有 inspec.profile.file(...)
.这是一个读取和测试端口列表的示例。文件夹结构为:
1 | examples/profile |
services.yml内容
1 | - service_name: httpd-alpha |
example.rb
现在可以访问此文件
1 | my_services = yaml(content: inspec.profile.file('services.yml')).params |
有关使用配置文件的更完整示例,请参阅 Learn Chef Rally 上的探索 Chef InSpec 资源。
“should” vs. “expect” syntax
Chef InSpec 将继续支持这两种编写测试的方法。考虑这个file
测试:
1 | describe file('/tmp/test.txt') do |
expect
这可以用语法重写
1 | describe file('/tmp/test.txt') do |
上述两个示例的输出如下所示:
1 | File /tmp/test.txt |
Chef InSpec 建议使用该should
语法,因为它往往更容易为那些技术不高的用户阅读。
此外,您可以使用subject
关键字来进一步控制您的输出,如果您选择:
1 | describe 'test file' do |
关键词
1 | describe Entity(Property) do |
通用匹配器参考
be
这个匹配器后面可以跟许多不同的比较运算符。始终确保使用数字而不是字符串进行这些比较。
1 | describe file('/proc/cpuinfo') do |
CMP
与eq
,不同cmp
的是,用于限制较少的比较的匹配器。它将尝试将实际值与您要与之进行比较的类型相匹配。这是为了让用户不必编写类型转换和解析。
1 | describe sshd_config do |
cmp
行为方式如下:
- 比较字符串和数字
1 | describe sshd_config do |
- 字符串比较不区分大小写
1 | describe auditd_conf do |
- 识别嵌入在字符串中的版本
1 | describe package('curl') do |
- 将只有一个条目的数组与一个值进行比较
1 | describe passwd.uids(0) do |
- 字符串的单值数组也可以与正则表达式进行比较
1 | describe auditd_conf do |
- 改进了八进制比较的打印
1 | describe file('/proc/cpuinfo') do |
eq
测试两个值是否完全相等。
1 | describe sshd_config do |
eq
如果类型不匹配则失败。在比较数字的配置条目时,请记住这一点:
1 | its('Port') { should eq '22' } # ok |
对于限制较少的比较,请使用cmp
.
include
验证值是否包含在列表中。
1 | describe passwd do |
be_in
验证对象是否包含在列表中。
1 | describe resource do |
match
检查字符串是否匹配正则表达式。
1 | describe sshd_config do |
语句
以下资源测试 |ssh| 服务器配置。例如,一个简单的控件可以描述为:
1 | describe sshd_config do |
在各种用例中,例如在不同部门实施 IT 合规性,使用元数据扩展控制变得很方便。每个控件都可以定义一个附加impact
的title
或desc
。一个示例如下所示:
1 | control 'sshd-8' do |
元数据说明:
- ‘sshd-8’是控件的名称
- ref是对外部文档的引用
- describe是一个包含至少一个测试的块。一个control块必须至少包含一个describe块,但可以包含任意多个
- sshd_config是 Chef InSpec 资源。有关 Chef InSpec 资源的完整列表,请参阅 Chef InSpec 资源文档。
- its(‘Port’)是Chef InSpecde1;{ should eq ‘22’ }是单元测试。一个describe块必须至少包含一个匹配器,但可以包含任意数量的匹配器。
检查至少一个条件是否通过describe.one
使用 Chef InSpec,您可以检查一组检查中的至少一个是否为真。例如,如果您在两个不同的位置配置一个设置,那么您可能想要测试是否设置了配置 A 或配置 B。用块做这个任务describe.one
。 describe.one
定义一组describe
块,其中只有一个块需要通过。
1 | describe.one do |
describe.one
使用说明
- 如果其中一个
describe.one
嵌套describe
块的所有断言都通过了,则该块通过。一个describe.one
块需要整个describe
块才能通过,而不仅仅是一个断言。 - Chef InSpec 将始终评估
describe.one
.describe
它不会在评估通过的块时短路。 - 不支持将一个块嵌套
describe.one
在另一个块内。describe.one
demo
以下示例显示了使用单个control
模块的简单合规性测试。
测试系统事件日志
以下测试显示了如何审核运行 Windows 2012 R2 且启用了密码复杂性的计算机:
1 | control 'windows-account-102' do |
测试 PostgreSQL 密码是否为空
以下测试展示了如何审计运行 PostgreSQL 的机器以确保密码不为空。
复制
1 | control 'postgres-7' do |
测试 MySQL 密码是否在 ENV 中
以下测试显示了如何审计运行 MySQL 的机器以确保密码不存储在ENV
:
复制
1 | control 'mysql-3' do |
测试是否/etc/ssh
是目录
以下测试显示了如何审计机器以确保它/etc/ssh
是一个目录:
复制
1 | control 'basic-1' do |
测试 Apache 是否运行
以下测试显示了如何审计机器以确保 Apache 已启用并正在运行:
复制
1 | control 'apache-1' do |
测试是否安装了不安全的软件包
以下测试显示了如何审计机器是否存在不安全的包:
复制
1 | control 'cis-os-services-5.1.3' do |
测试 Windows 注册表项
以下测试显示如何审核机器以确保启用安全 DLL 搜索模式:
复制
1 | control 'windows-base-101' do |
用于only_if
排除特定控件
此示例说明如果使用 不满足条件,如何允许跳过某些控件only_if
。在本例中,如果redis-cli
命令不存在,则不会执行控制。可选消息可以说明它被跳过的原因。
复制
1 | control 'nutcracker-connect-redis-001' do |
此示例检查是否安装了某些 pip 包,但前提是 ‘/root/.aws’ 存在:
复制
1 | control 'pip-packages-installed' do |
将其与其他条件混合,例如检查文件是否存在,有助于使用 Chef InSpec 测试不同的测试路径。通过这种方式,您可以跳过某些控件,这些控件由于服务器的准备方式而 100% 失败,但是您知道相同的控件套件稍后会在不同的情况下被不同的团队重用。
关于only_if
:
only_if
适用于整个control
. 如果only_if
块的结果评估为 false,则作为块的一部分提及的任何 Chef InSpec 资源describe
都不会运行。此外,描述块的内容将不会运行。但是,only_if 语句之前的裸 Ruby 表达式和裸 Chef InSpec 资源(不与描述块关联)将运行
为了显示:
复制
1 | control "whatruns" do |
- 每个块只
only_if
允许一个。control
如果存在多个only_if
块,则仅使用最后一个only_if
块 - 如果在控制块之外使用,
only_if
则跳过当前文件中的所有控件 - 要实现复杂的逻辑,请在块中使用 Ruby ‘or’ (
||
) 和 ‘and’ (&&
)only_if
:
复制
1 | only_if('ready for launch') do |
控件的附加元数据
以下示例说明了添加标签和引用的各种方法control
复制
1 | control 'ssh-1' do |
在 InSpec 中使用 Ruby
Chef InSpec 语言是一种基于 Ruby 的语言。这使您可以灵活地使用控件中的 Ruby 代码:
复制
1 | json_obj = json('/file.json') |
Ruby 允许很多自由,但应该限制控件,以便它们保持可移植性和易于理解。请参阅我们的个人资料风格指南。
核心和自定义资源被编写为继承自 Inspec.resource
.
Ruby 语法
Ruby数据类型
整数
1 | 123 # Fixnum 十进制 |
浮点型
Ruby 支持浮点数。它们是带有小数的数字。浮点数是类 Float 的对象,且可以是下列中任意一个。
1 | 123.4 # 浮点值 |
算术操作
加减乘除操作符:+-/;指数操作符为*
指数不必是整数,例如
1 | #指数算术,结果 16 |
字符串类型
Ruby 字符串简单地说是一个 8 位字节序列,它们是类 String 的对象。
双引号标记的字符串允许替换和使用反斜线符号,单引号标记的字符串不允许替换,且只允许使用 \ 和 ' 两个反斜线符号。进行转义
1 | #!/usr/bin/ruby -w |
结果
1 | escape using "\" |
可以使用序列 #{ expr } 替换任意 Ruby 表达式的值为一个字符串。在这里,expr 可以是任意的 Ruby 表达式。
1 | #!/usr/bin/ruby -w |
输出结果为:
1 | Ruby |
数据结构
Ruby支持以下三种数据结构:
- 数组
- 哈希类型
- 范围类型
数组
数组字面量通过[]中以逗号分隔定义,且支持range定义。
- 数组通过[]索引访问
- 通过赋值操作插入、删除、替换元素
- 通过+,-号进行合并和删除元素,且集合做为新集合出现
- 通过<<号向原数据追加元素
- 通过*号重复数组元素
- 通过|和&符号做并集和交集操作(注意顺序)
1 | #!/usr/bin/ruby |
创建数组
有多种方式创建或初始化数组。一种方式是通过 new 类方法:
1 | names = Array.new |
您可以在创建数组的同时设置数组的大小:
1 | names = Array.new(20) |
数组 names 的大小或长度为 20 个元素。您可以使用 size 或 length 方法返回数组的大小:
1 | #!/usr/bin/ruby |
赋值
1 | names = Array.new(4, "mac") |
哈希类型
Ruby 哈希是在大括号内放置一系列键/值对,键和值之间使用逗号和序列 => 分隔。尾部的逗号会被忽略。
1 | #!/usr/bin/ruby |
这将产生以下结果:
1 | red is 3840 |
创建哈希
与数组一样,有各种不同的方式来创建哈希。您可以通过 new 类方法创建一个空的哈希:
1 | months = Hash.new |
赋值
1 | mh={'a'=>'apple','b'=>'banan'} |
范围类型
一个范围表示一个区间。
范围是通过设置一个开始值和一个结束值来表示。范围可使用 s..e 和 s…e 来构造,或者通过 Range.new 来构造。使用 .. 构造的范围从开始值运行到结束值(包含结束值)。使用 … 构造的范围从开始值运行到结束值(不包含结束值)。当作为一个迭代器使用时,范围会返回序列中的每个值。范围 (1..5) 意味着它包含值 1, 2, 3, 4, 5,范围 (1…5) 意味着它包含值 1, 2, 3, 4 。
条件判断
1 | if conditional [then] |
通常我们省略保留字 then 。若想在一行内写出完整的 if 式,则必须以 then 隔开条件式和程式区块。如下所示:
1 | if a == 4 then a = 7 end |
实例代码
1 | #!/usr/bin/ruby |
循环
while语句
有两种模式的while语句
1 | while conditional [do] |
demo
1 | while 1<2 : |
until语句
1 | until conditional [do] |
for循环
1 | for variable [, variable ...] in expression [do] |
先计算表达式得到一个对象,然后针对 expression 中的每个元素分别执行一次 code。
1 | #!/usr/bin/ruby |