您的位置:首页 > 其它

AWS系列:深入了解IAM和访问控制

2017-09-26 17:05 555 查看
写在前面:访问控制,换句话说,能在什么情况下访问哪些资源或者操作,是绝大部分应用程序需要仔细斟酌的问题。作为一个志存高远的云服务提供者,AWS自然也在访问控制上下了很大的力气,一步步完善,才有了今日的IAM:Identity and Access Management。如果你要想能够游刃有余地使用AWS的各种服务,在安全上的纰漏尽可能地少,那么,首先需要先深入了解 IAM。

基本概念

按照 AWS 的定义:

IAM enables you to control who can do what in your AWS account.

它提供了用户(users)管理、群组(groups)管理、角色(roles)管理和权限(permissions)管理等供AWS的客户来管理自己账号下面的资源。

1、首先说用户(users)。在AWS里,一个IAM user和unix下的一个用户几乎等价。你可以创建任意数量的用户,为其分配登录AWS management console所需要的密码,以及使用AWS CLI(或其他使用AWS SDK的应用)所需要的密钥。你可以赋予用户管理员的权限,使其能够任意操作AWS的所有服务,也可以依照Principle of least privilege,只授权合适的权限。下面是使用AWS CLI创建一个用户的示例:

saws> aws iam create-user --user-name tyrchen
{
"User": {
"CreateDate": "2015-11-03T23:05:05.353Z",
"Arn": "arn:aws:iam::<ACCOUNT-ID>:user/tyrchen",
"UserName": "tyrchen",
"UserId": "AIDAISBVIGXYRRQLDDC3A",
"Path": "/"
}
}

当然,这样创建的用户是没有任何权限的,甚至无法登录,你可以用下面的命令进一步为用户关联群组,设置密码和密钥:

saws> aws iam add-user-to-group --user-name --group-name
saws> aws iam create-login-profile --user-name --password
saws> aws iam create-access-key --user-name

2、群组(groups)也等同于常见的unix group。将一个用户添加到一个群组里,可以自动获得这个群组所具有的权限。在一家小的创业公司里,其AWS账号下可能会建立这些群组:

Admins:拥有全部资源的访问权限
Devs:拥有大部分资源的访问权限,但可能不具备一些关键性的权限,如创建用户
Ops:拥有部署的权限
Stakeholders:拥有只读权限,一般给manager查看信息之用
创建一个群组很简单:

saws> aws iam create-group --group-name stakeholders
{
"Group": {
"GroupName": "stakeholders",
"GroupId": "AGPAIVGNNEGMEPLHXY6JU",
"Arn": "arn:aws:iam::<ACCOUNT-ID>:group/stakeholders",
"Path": "/",
"CreateDate": "2015-11-03T23:15:47.021Z"
}
}

然而,这样的群组没有任何权限,我们还需要为其添加policy:

saws> aws iam attach-group-policy --group-name --policy-arn

在前面的例子和这个例子里,我们都看到了ARN这个关键字。ARN是Amazon Resource Names的缩写,在AWS里,创建的任何资源有其全局唯一的ARN。ARN是一个很重要的概念,它是访问控制可以到达的最小粒度。在使用AWS SDK时,我们也需要ARN来操作对应的资源。

policy是描述权限的一段JSON文本,比如AdministratorAccess这个policy,其内容如下:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}

用户或者群组只有添加了相关的policy,才会有相应的权限。

3、角色(roles)类似于用户,但没有任何访问凭证(密码或者密钥),它一般被赋予某个资源(包括用户),使其临时具备某些权限。比如说一个EC2实例需要访问DynamoDB,我们可以创建一个具有访问DynamoDB权限的角色,允许其被EC2 Service代入(AssumeRule),然后创建EC2的instance-profile使用这个角色。这样,这个EC2实例就可以访问DynamoDB了。当然,这样的权限控制也可以通过在EC2的文件系统里添加AWS配置文件设置某个用户的密钥(AccessKey)来获得,但使用角色更安全更灵活。角色的密钥是动态创建的,更新和失效都无须特别处理。想象一下如果你有成百上千个EC2实例,如果使用某个用户的密钥来访问AWS
SDK,那么,只要某台机器的密钥泄漏,这个用户的密钥就不得不手动更新,进而手动更新所有机器的密钥。这是很多使用AWS多年的老手也会犯下的严重错误。

4、最后是权限(permissions)。AWS下的权限都通过policy document描述,就是上面我们给出的那个例子。policy是IAM的核心内容,我们稍后详细介绍。

每年的AWS re:invent大会,都会有一个session:Top 10 AWS IAM Best Practices,感兴趣的读者可以去YouTube搜索。2015年的top
10(top 11)如下:

users: create individual users
permissions: Grant least priviledge
groups: manage permissions with groups
conditions: restrict priviledged access further with conditions
auditing: enable cloudTrail to get logs of API calls
password: configure a strong password policy
rotate: rotate security credentials regularly.
MFA: enable MFA (Multi-Factor Authentication) for priviledged users
sharing: use IAM roles to share access
roles: use IAM roles for EC2 instances
root: reduce or remove use of root
这11条best practices很清晰,我就不详细解释了。

按照上面的原则,如果一个用户只需要访问AWS management console,那么不要为其创建密钥;反之,如果一个用户只使用AWS CLI,则不要为其创建密码。一切不必要的,都是多余的——这就是安全之道。

使用 policy 做访问控制

上述内容你若理顺,IAM 就算入了门。但真要把握好IAM的精髓,需要深入了解policy,以及如何撰写policy。

前面我们看到,policy是用JSON来描述的,主要包含Statement,也就是这个policy拥有的权限的陈述,一言以蔽之,即:在什么条件下能对哪些资源的哪些操作进行处理。也就是所谓的撰写policy的PARCE原则:

Principal:谁
Action:哪些操作
Resource:哪些资源
Condition:什么条件
Effect:怎么处理(Allow/Deny)
我们看一个允许对S3进行只读操作的policy:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:Get*",
"s3:List*"
],
"Resource": "*"
}
]
}

其中,Effect是Allow,允许policy中所有列出的权限,Resource是
*
,代表任意S3的资源,Action有两个:
s3:Get*
s3:List*
,允许列出S3下的资源目录,及获取某个具体的S3 Object。

在这个policy里,Principal和Condition都没有出现。如果对资源的访问没有任何附加条件,是不需要Condition的;而这条policy的使用者是用户相关的principal(users, groups, roles),当其被添加到某个用户身上时,自然获得了principal的属性,所以这里不必指明,也不能指明。

所有的IAM managed policy是不需要指明Principal的。这种policy可以单独创建,在需要的时候可以被添加到用户、群组或者角色身上。另一大类policy是Resource policy,它们不能单独创建,只能依附在某个资源之上(所以也叫inline policy),这时候,需要指明Principal。比如,当我希望对一个S3 bucket使能Web hosting时,这个bucket里面的对象自然是要允许外界访问的,所以需要如下的inline policy:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::corp-fs-web-bucket/*"
}
]
}

这里,我们对于
arn:aws:s3:::corp-fs-web-bucket/*
这个资源的
s3:GetObject
,允许任何人访问(
Principal: *
)。

有时候,我们希望能更加精细地控制用户究竟能访问资源下的哪些数据,这个时候,可以使用Condition。比如对一个叫tyrchen的用户,只允许他访问personal-files这个S3 bucket下和他有关的目录:

{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::personal-files"
],
"Condition": {
"StringLike": {
"s3:prefix": [
"tyrchen/*"
]
}
}
},
{
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::personal-files/tyrchen/*"
]
}
]
}

这里我们用到了
StringLike
这个Condition,只有当访问的
s3:prefix
满足
tyrchen/*
时,才为真。我们还可以使用非常复杂的Condition:

"Condition": {
"IPAddress": {"aws:SourceIP": ["10.0.0.0/8", "4.4.4.4/32"]},
"StringEquals": {"ec2:ResourceTag/department": "dev"}
}

在一条Condition下并列的若干个条件间是and的关系,这里IPAddress和StringEquals这两个条件必须同时成立;在某个条件内部则是or的关系,这里10.0.0.0/8和4.4.4.4/32任意一个源IP都可以访问。这个条件最终的意思是:对于一个EC2实例,如果其department标签是dev,且访问的源IP是10网段的内网地址或者4.4.4.4/32这个外网地址,则Condition成立。

讲完了Condition,我们再回到之前的policy。

这条policy里有两个statement,前一个允许列出
arn:aws:s3:::personal-files
下prefix是
tyrchen/*
里的任何对象;后一个允许读写
arn:aws:s3:::personal-files/tyrchen/*
里的对象。注意这个policy不能写成:

{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListObject",
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::personal-files/tyrchen/*"
]
}
]
}

因为这样的话,用户在AWS management console连在personal-files下列出
/tyrchen
这个根目录的机会都没有了,自然也无法进行后续的操作。这样的policy其权限是不完备的。

上面的policy可以被添加到用户tyrchen身上,这样他就可以访问自己的私人目录;如果我们创建一个新用户叫lindsey,也想做类似处理,则需要再创建一个几乎一样的policy,非常不符合DRY(Don’t Repeat Yourself)原则。好在AWS也考虑到了这一点,它支持policy variable,允许用户在policy里使用AWS预置的一些变量,比如
${aws:username}
。上述的policy里,把
tyrchen
替换为
${aws:username}
,就变得通用多了。

以上是policy的一些基础用法,下面讲讲policy的执行规则,它也是几乎所有访问控制方案的通用规则:

默认情况下,一切资源的一切行为的访问都是Deny
如果在policy里显式Deny,则最终的结果是Deny
否则,如果在policy里是Allow,则最终结果是Allow
否则,最终结果是Deny
如下图:





什么情况下我们会用到显式Deny呢?请看下面的例子:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:*",
"s3:*"
],
"Resource": [
"arn:aws:dynamodb:AWS-REGION-IDENTIFIER:ACCOUNT-ID-WITHOUT-HYPHENS:table/EXAMPLE-TABLE-NAME",
"arn:aws:s3:::EXAMPLE-BUCKET-NAME",
"arn:aws:s3:::EXAMPLE-BUCKET-NAME/*"
]
},
{
"Effect": "Deny",
"NotAction": [
"dynamodb:*",
"s3:*"
],
"NotResource": [
"arn:aws:dynamodb:AWS-REGION-IDENTIFIER:ACCOUNT-ID-WITHOUT-HYPHENS:table/EXAMPLE-TABLE-NAME",
"arn:aws:s3:::EXAMPLE-BUCKET-NAME",
"arn:aws:s3:::EXAMPLE-BUCKET-NAME/*"
]
}
]
}

在这个例子里,我们只允许用户访问DynamoDB和S3中的特定资源,除此之外一律不允许访问。我们知道一个用户可以有多重权限,属于多个群组。所以上述policy里的第一个statement虽然规定了用户只能访问的资源,但别的policy可能赋予用户其他资源的访问权限。所以,我们需要第二条statement封死其他的可能。按照之前的policy enforcement的规则,只要看见Deny,就是最终结果,不会考虑其他policy是否有Allow,这样杜绝了一些隐性的后门,符合Principle of least
privilege。

我们再看一个生产环境中可能用得着的例子,来证明IAM不仅「攘内」,还能「安外」。假设我们是一个手游公司,使用AWS Cognito来管理游戏用户。每个游戏用户的私人数据放置于S3之中。我们希望congito user只能访问他们各自的目录,IAM policy可以定义如下:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::awesome-game"],
"Condition": {"StringLike": {"s3:prefix": ["cognito/*"]}}
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::awesome-game/cognito/${cognito-identity.amazonaws.com:sub}",
"arn:aws:s3:::awesome-gamecognito/${cognito-identity.amazonaws.com:sub}/*"
]
}
]
}

最后,讲一下如何创建policy。很简单:

$ aws iam create-policy --policy-name --policy-document

其中policy-document就是如上所示的一个个JSON文件。

参考文档

IAM policy overview
policy variables
policy evaluation logic
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: