ACK场景下应用程序安全访问云资源最佳实践

简介: 在实际的容器安全实践中,怎么样避免应用程序永久访问密钥。本文会介绍基于云原生的产品能力来实现无AK方案。

1. 业务背景

当应用程序部署在ACK容器服务中,并且要访问后端云资源(如OSS\SLS等)。最简单的方式就是给应用分配AK。但AK会出现泄露风险,可以看看我们客户一般是怎么使用AK的:

1、硬编码在代码里:很多客户对访问密钥的安全意识不够或者没有意识到风险,为了使用方便,会直接把访问密钥的二元组写在代码里,供程序使用;在现在反编译和内存分析的技术已经很成熟的当下,硬编码的方式无异于明文存储密钥,有巨大的泄露风险。

2.第三方密钥存储:还有很多客户了解访问密钥的重要性,会使用第三方的密钥存储系统或者配置文件,将原始的密钥加密起来。这种方式的做法加延了密钥泄露的链路,但本质上只是原始密钥泄露的风险转移到另外一把密钥上,比如第三方密钥存储系统的访问key,或者是加密的密钥,归根结底还是会存在“最后一把密钥”的安全泄露风险。


所以在实际的容器安全实践中,怎么样避免应用程序永久访问密钥。接下来会介绍基于云原生的产品能力来实现无AK方案。

2. 名词解释

2.1 STS是什么

阿里云临时安全令牌(Security Token Service,STS)是阿里云提供的一种临时访问权限管理服务。

2.2 OAuth2是什么

摘自维基百科:OAuth ("Open Authorization") is an open standard for access delegation, commonly used as a way for internet users to grant websites or applications access to their information on other websites but without giving them the passwords.


关于OAuth2这种授权协议是什么,这里借鉴网上最常见的例子:某人A去机构B,科室C办事,前台就会拦住A,检查A是否预约,之后进行登记,然后给A一个通行证,A就可以正常通行了。这里的前台检查是否预约,之后给通行证就是授权的过程。这里面的A就可以看做业务系统(或者说是第三方应用),科室C就是三方软件想访问的资源。因此授权协议,就是为了保证第三方在获取了授权之后,才能访问资源。这个授权过程在软件系统方面的应用情况是怎么样的呢?

整个流程简图如下:

当用户第一次访问三方系统,此时会引导进行授权,授权服务检查登录信息,生成授权页面,用户同意授权之后,授权服务生成授权码,并通过重定向请求第三方应用服务端,服务端使用code向授权服务请求access_token,生成后返回给第三方服务器端。这就是典型且业界内比较推荐的授权码模式。因此授权码模式的核心就是,生成授权码,通过授权码换取令牌,使用令牌去访问资源。


2.3 OIDC是什么

摘自维基百科:It is an authentication layer on top of the OAuth 2.0 authorization framework.[82] It allows computing clients to verify the identity of an end user based on the authentication performed by an authorization server, as well as to obtain the basic profile information about the end user in an interoperable and REST-like manner. In technical terms, OpenID Connect specifies a RESTful HTTP API, using JSON as a data format.

OIDC是一套业界内比较认可的用于用户身份认证的协议,在我们生活中比较常见的场景是使用支付宝授权登录某个三方应用。OIDC使用授权协议(通常为OAuth 2)来进行身份认证,因为可以简单的理解为OIDC是通过OAuth 2来实现的。


前面讲了OAuth2是用于授权,OIDC是授权加上身份验证。OAuth2中角色为:资源拥有者,第三方软件,授权服务,受保护资源。OIDC既然是基于OAuth2实现的,自然角色上存在些关联。

OIDC的角色为:EU(End User)也就是对应用户,RP(Relying Party)代表认证服务的依赖方,也就是OAuth2里面的第三方,OP(OpenID Provider)代表身份认证的服务方。角色上存在关联关系,自然在实现流程上就很相似,下面看一下OIDC 的通信流程图:


3. 方案实现

整个方案涉及到的产品主要是两块,一块是K8S集群,一块是RAM访问控制,下面从静态配置这部分来介绍方案。


因为涉及到K8S容器服务和RAM访问控制,所以配置也在两边同时进行:

  1. 在RAM访问控制侧,需要配置一个新身份提供商(IDP),所以这里需要在RAM访问控制侧添加基于OIDC的身份提供商定义,比如OIDC的issuer url,aud等,同时配置一个该IDP的某一身份能扮演的角色,在角色扮演的授信方限制OIDC的相关身份信息,比如audience,subject等,同时定义该角色能访问的资源和相关action;

  2. 在K8S集群侧,配置相关OIDC的属性,比如自身的OIDC标识,颁发ID_Token使用的公私钥等,同时给目标Pod定义响应的Service Account,把该Pod需要扮演的角色名称等配置好,这样在Pod启动后在对应卷目录下就可以获取到动态替换的ID_Token,结合在RAM侧定义的角色ARN,通过RAM/STS服务获取到阿里云临时安全令牌(Security Token Service,STS),从而最终访问阿里云其他云产品服务;

从最终实现体来看,就是K8S里的一个Service Account定义,里面包含RoleName和OIDC ID_Token,RoleName映射到RAM里面的角色名称,然后该角色扮演的授信体,通过ID_Token里的aud,sub来匹配。


4. 操作步骤

4.1 启用RRSA功能

本步骤配置容器服务的RRSA功能,若已配置参数可跳过该步骤。

  1. 登录容器服务管理控制台

  2. 在控制台左侧导航栏中,单击集群

  3. 集群列表页面中,单击目标集群名称或者目标集群右侧操作列下的详情

  4. 在集群详情页面,单击基本信息页签,单击RRSA OIDC提供商URL右侧的启用RRSA

  5. 在弹出的启用RRSA对话框中,单击确定。当集群状态由更新中变为运行中时,说明该集群的RRSA特性已变更完成,RRSA OIDC提供商URL右侧会显示OIDC提供商的URL链接。

4.2 使用RRSA功能

集群开启RRSA功能后,按照以下步骤来赋予集群内应用通过RRSA功能获取访问云资源OpenAPI的临时凭证的能力。

  1. 创建RAM角色

需要为应用所使用的服务账户(Service Account)创建一个RAM角色。后续应用将获取一个扮演这个RAM角色的临时凭证。更多信息,请参见创建可信实体为阿里云账号的RAM角色

  1. 修改RAM角色信任策略。

您需要修改RAM角色的信任策略,确保使用指定的服务账户的应用有权限获取一个扮演这个RAM角色的临时凭证。更多信息,请参见修改RAM角色的信任策略


{
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "oidc:aud": "sts.aliyuncs.com",
          /*替换为当前集群的OIDC提供商URL,该URL可以在集群详情的基本信息页签获取。*/
          "oidc:iss": "",
          /*替换为应用所在的命名空间,如:default。*/
          /*替换为应用使用的服务账户,需要提前创建,如:app。*/
          "oidc:sub": "system:serviceaccount::"
        }
      },
      "Effect": "Allow",
      "Principal": {
        "Federated": [
          /*替换为阿里云主账号UID。替换为ACK集群ID。*/
          "acs:ram:::oidc-provider/ack-rrsa-"
        ]
      }
    }
  ],
  "Version": "1"
}


  1. 为RAM角色授权。您可以通过为这个RAM角色授权的方式,指定这个RAM角色可以访问的云资源。更多信息,请参见为RAM角色授权


  1. 部署应用

部署应用时,需要修改应用的模板内容,自动生成OIDC Token。具体操作,请参见部署服务账户令牌卷投影。

模板示例:

apiVersion: v1
kind: Pod
metadata:
  name: test-rrsa
spec:
  containers:
  - image: alpine:3.14
    command:
    - sh
    - -c
    - 'sleep inf'
    name: test
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: oidc-token
  serviceAccountName: build-robot
  volumes:
  - name: oidc-token     # 新增的配置项。
    projected:
      sources:
      - serviceAccountToken:
          path: oidc-token
          expirationSeconds: 7200    # oidc token过期时间(单位:秒)。
          audience: "sts.aliyuncs.com"

您的应用就可以使用容器内挂载的OIDC Token(上面示例中的/var/run/secrets/tokens/oidc-token文件内容)调用STS的AssumeRoleWithOIDC接口来获取一个扮演前面创建的RAM角色的STS临时凭证,然后使用临时凭证访问云资源OpenAPI。


4.3 应用程序使用RRSA OIDC Token认证

阿里云提供Go / Java语言的SDK,当然也可以直接使用AssumeRoleWithOIDC接口(支持匿名访问API)。这样就避免出现最后一把AK。


示例代码:

#!/usr/bin/env python
# coding=utf-8

import json
import logging
import sys
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest
from aliyunsdkcore.auth.credentials import StsTokenCredential
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_sts20150401.client import Client as Sts20150401Client
from alibabacloud_sts20150401 import models as sts_20150401_models
from alibabacloud_tea_util import models as util_models


class RAM(object):
    # 使用STS Token构造一个RAM对象
    def __init__(self, sts_access_key, sts_access_secret, sts_token, region_id):
        self.sts_access_key = sts_access_key
        self.sts_access_secret = sts_access_secret
        self.sts_token = sts_token
        self.region_id = region_id
        # 使用sts
        self.credentials = StsTokenCredential(self.sts_access_key, self.sts_access_secret, self.sts_token)
        self.clt = AcsClient(region_id=self.region_id, credential=self.credentials)

    def ListUsers(self):
        '''查询全部RAM用户列表
        '''
        request = CommonRequest()
        request.set_accept_format('json')
        request.set_domain('ram.aliyuncs.com')
        request.set_method('POST')
        request.set_protocol_type('https')  # https | http
        request.set_version('2015-05-01')
        request.set_action_name('ListUsers')

        response = self.clt.do_action(request)
        logging.debug(str(response, encoding='utf-8'))
        return json.loads(response)


class OIDC(object):
    def __init__(self, region_id):
        # 支持匿名访问的 API,不需要 AccessKey ID 等鉴权配置
        config = open_api_models.Config()
        # 访问的域名
        config.endpoint = region_id
        self.clt = Sts20150401Client(config)

    def get_sts_credentials(self, oidc_provider_arn, role_arn, oidc_token, role_session_name):
        assume_role_with_oidcrequest = sts_20150401_models.AssumeRoleWithOIDCRequest(
            oidcprovider_arn=oidc_provider_arn,
            role_arn=role_arn,
            oidctoken=oidc_token,
            role_session_name=role_session_name
        )
        runtime = util_models.RuntimeOptions()
        try:
            resp = self.clt.assume_role_with_oidcwith_options(assume_role_with_oidcrequest, runtime)
            return (resp.body.credentials.access_key_id, resp.body.credentials.access_key_secret,
                    resp.body.credentials.security_token)
        except Exception as error:
            print(error)


def read_oidc_token(file_path):
    with open(file_path, 'r') as f:
        ff = f.read()
    return ff


if __name__ == '__main__':
    OIDCProviderArn = "acs:ram:::oidc-provider/ack-rrsa-{cluster_id}"
    REGION_ID = "sts.cn-shenzhen.aliyuncs.com"
    RoleArn = "acs:ram:::role/"
    OIDCToken = read_oidc_token("/var/run/secrets/tokens/oidc-token")
    RoleSessionName = "xxx"
    sts = OIDC(REGION_ID)

    sts_ak, sts_sk, sts_token = sts.get_sts_credentials(OIDCProviderArn, RoleArn, OIDCToken, RoleSessionName)
    try:
        ram = RAM(sts_ak, sts_sk, sts_token, REGION_ID)
        r = ram.ListUsers()
        print(r)
    except Exception as ex:
        sys.exit()


5. 方案延展

前两天跟一家MNC客户交流容器场景身份权限。客户的规划是这样的:

租户A、B里面的应用全部集中部署在中央集群账号里面。应用程序需要访问租户A\B里面的云资源。那这种情况就用不了本方案提到的无AK方案了,只能使用STS的方式。


作者介绍
目录