编者按:本文系InfoQ中文站向陈天的约稿,这是AWS系列文章的第一篇。以后会有更多文章刊出,但并无前后依赖的关系,每篇都自成一体。读者若要跟随文章来学习AWS,应该至少注册了一个AWS账号,事先阅读过当期所介绍服务的简介,并在AWS management console中尝试使用过该服务。否则,阅读的效果不会太好。
写在前面:访问控制,换句话说, 谁 能在 什么 情况下访问 哪些 资源或者操作,是绝大部分应用程序需要仔细斟酌的问题。作为一个志存高远的云服务提供者,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账号下可能会建立这些群组:
创建一个群组很简单:
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) 如下:
这11条best practices很清晰,我就不详细解释了。
按照上面的原则,如果一个用户只需要访问AWS management console,那么不要为其创建密钥;反之,如果一个用户只使用AWS CLI,则不要为其创建密码。一切不必要的,都是多余的——这就是安全之道。
上述内容你若理顺,IAM 就算入了门。但真要把握好IAM的精髓,需要深入了解policy,以及如何撰写policy。
前面我们看到,policy是用JSON来描述的,主要包含Statement,也就是这个policy拥有的权限的陈述,一言以蔽之,即: 谁 在什么 条件 下能对哪些 资源 的哪些 操作 进行 处理 。也就是所谓的撰写policy的 PARCE 原则:
我们看一个允许对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呢?请看下面的例子:
{ "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文件。
感谢魏星对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群 )。