分布式架构-可靠通讯-服务安全

发布时间 2023-03-22 19:13:45作者: 只会一点java

系列目录

分布式架构-可靠通讯-零信任网络

分布式架构-可靠通讯-服务安全

引子

上一节“分布式架构-可靠通讯-零信任网络”里,我们探讨了与微服务运作特点相适应的零信任安全模型。本节,我们将从实践和编码的角度出发,介绍在前微服务时代(以 Spring Cloud 为例)云原生时代(以 Istio over Kubernetes 为例)分别是如何实现安全传输、认证和授权的,通过这两者的对比,探讨在微服务架构下,应如何将业界的安全技术标准引入并实际落地,实现零信任网络下安全的服务访问。

注:本节内容快速看完即可。关注核心思路即可。主要看云原生Istio为例,是如何实现服务安全校验的。

一、建立信任

零信任网络里不存在默认的信任关系,一切服务调用、资源访问成功与否,均需以调用者与提供者间已建立的信任关系为前提。目前通用方案就是使用PKI:公开密钥基础设施(Public Key Infrastructure),基本结构如下图:

PKI基本结构由证书认证机构(certificate authority, CA)、证书持有者(certificate holder)、依赖方(relying party)三方构成:

  • 1.CA是一个独立的可信第三方,为证书持有者签发数字证书,数字证书中声明了证书持有者的身份和公钥。CA在签发证书前应对证书持有者的身份信息进行核实验证,并根据其核验结果为其签发证书。
  • 2.证书持有者向CA申请数字证书,并向CA提供必要的信息以证明其身份及能力,获得由CA签发的证书;证书持有者在与依赖方进行交互时,需向依赖方提供由CA签发的数字证书证明其有效身份。
  • 3.依赖方是证书的验证方,依赖方与证书持有者进行交互(如建立通信连接)时,需获取证书持有者的数字证书,验证数字证书的真实性和有效性。依赖方可以指定其信任的CA列表,若证书持有者提供的数字证书不是受信CA签发的数字证书,依赖方将不认可该证书所声明的信息。

 

       PKI 是构建传输安全层(Transport Layer Security,TLS)的必要基础。在任何网络设施都不可信任的假设前提下,无论是 DNS 服务器、代理服务器、负载均衡器还是路由器,传输路径上的每一个节点都有可能监听或者篡改通信双方传输的信息。要保证通信过程不受到中间人攻击的威胁,启用 TLS 对传输通道本身进行加密,让发送者发出的内容只有接受者可以解密是唯一具备可行性的方案。除了随服务节点动态扩缩而来的运维压力外,比起公众互联网中主流单向的 TLS 认证,在零信任网络中,往往要启用双向 TLS 认证(Mutual TLS Authentication,常简写为 mTLS),即不仅要确认服务端的身份,还需要确认调用者的身份。

  • 单向 TLS 认证:只需要服务端提供证书,客户端通过服务端证书验证服务器的身份,但服务器并不验证客户端的身份。单向 TLS 于公开的服务,即任何客户端都被允许连接到服务进行访问,它保护客户端免遭冒牌服务器的欺骗

  • 双向 TLS 认证:客户端、服务端双方都要提供证书,双方各自通过对方提供的证书来验证对方的身份。双向 TLS 用于私密的服务,即服务只允许特定身份的客户端访问,它除了保护客户端不连接到冒牌服务器外,也保护服务端不遭到非法用户的越权访问

对于以上提到的围绕 TLS 而展开的密钥生成、证书分发、签名请求(Certificate Signing Request,CSR)、更新轮换等是一套操作起来非常繁琐的流程,稍有疏忽就会产生安全漏洞,所以尽管理论上可行,但实践中如果没有自动化的基础设施的支持,仅靠应用程序和运维人员的努力,是很难成功实施零信任安全模型的。下面我们聚焦于“认证”和“授权”两个最基本的安全需求,看它们在微服务架构下,有或者没有基础设施支持时,各是如何实现的。

二、认证

根据认证的目标对象可以把认证分为两种类型:

服务认证:以机器作为认证对象,即访问服务的流量来源是另外一个服务,称为服务认证(Peer Authentication,直译过来是“节点认证”)。

请求认证:以人类作为认证对象,即访问服务的流量来自于最终用户,称为请求认证(Request Authentication)。

2.1 服务认证

2.1.1 云原生架构Istio-服务认证

如果每一个服务提供者、调用者均受 Istio 管理,那 mTLS 就是最理想的认证方案。你只需要参考以下简单的 PeerAuthentication CRD配置,即可对某个Kubernetes 名称空间范围内所有的流量均启用 mTLS:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: authentication-mtls
  namespace: bookstore-servicemesh
spec:
  mtls:
    mode: STRICT

 

如果你的分布式系统还没有达到完全云原生的程度,其中仍存在部分不受 Istio 管理(即未注入 Sidecar)的服务端或者客户端(这是颇为常见的),你也可以将 mTLS 传输声明为“宽容模式”(Permissive Mode)。宽容模式为普通微服务向服务网格迁移提供了良好的灵活性。

2.1.2 Spring Cloud非云原生-服务认证

举例使用OAtuh2 协议的客户端模式。

  • 客户端:提前配置好客户端密钥,客户端调用服务时,会先使用该密钥向认证服务器申请到 JWT 令牌,然后通过令牌证明自己的身份,最后访问服务。
/**
 * 客户端列表
 */
private static final List<Client> clients = Arrays.asList(
    new Client("bookstore_frontend", "bookstore_secret", new String[]{GrantType.PASSWORD, GrantType.REFRESH_TOKEN}, new String[]{Scope.BROWSER}),
    // 微服务一共有Security微服务、Account微服务、Warehouse微服务、Payment微服务四个客户端
    // 如果正式使用,这部分信息应该做成可以配置的,以便快速增加微服务的类型。clientSecret也不应该出现在源码中,应由外部配置传入
    new Client("account", "account_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}),
    new Client("warehouse", "warehouse_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}),
    new Client("payment", "payment_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}),
    new Client("security", "security_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE})
);

 

  • 每一个对外提供服务的服务端,都扮演着 OAuth 2 中的资源服务器的角色,它们均声明为要求提供客户端模式的凭证,如以下代码所示。客户端要调用受保护的服务,就必须先出示能证明调用者身份的 JWT 令牌,否则就会遭到拒绝,这个操作本质上是授权,但是在授权过程中已实现了服务的身份认证。
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
    return new ClientCredentialsResourceDetails();
}

 

由于每一个微服务都同时具有服务端和客户端两种身份,既消费其他服务,也提供服务供别人消费,所以这些代码在每个微服务中都应有包含(放在公共 infrastructure 工程里)。Spring Security 提供的过滤器会自动拦截请求、驱动认证、授权检查的执行,申请和验证 JWT 令牌等操作无论在开发期对程序员,还是运行期对用户都能做到相对透明。尽管如此,以上做法仍然是一种应用层面的、不加密传输的解决方案。这种方案不适用于零信任安全模型,只能在默认内网节点间具备信任关系的边界安全模型上才能良好工作。

2.2 用户认证

2.2.1 云原生架构Istio-用户认证

当来自最终用户的请求进入服务网格时,Istio 会自动根据配置中的JWKS(JSON Web Key Set)来验证令牌的合法性,如果令牌没有被篡改过且在有效期内,就信任 Payload 中的用户身份,并从令牌的 Iss 字段中获得 Principal。以下是 Istio 用户认证配置,其中jwks字段配的就是 JWKS 全文(实际生产中并不推荐这样做,应该使用jwksUri来配置一个 JWKS 地址,以方便密钥轮换),根据这里配置的密钥信息,Istio 就能够验证请求中附带的 JWT 是否合法。

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: authentication-jwt-token
  namespace: bookstore-servicemesh
spec:
  jwtRules:
    - issuer: "icyfenix@gmail.com"
      # Envoy默认只认“Bearer”作为JWT前缀,之前其他地方用的都是小写,这里专门兼容一下
      fromHeaders:
        - name: Authorization
          prefix: "bearer "
      # 在rsa-key目录下放了用来生成这个JWKS的证书,最初是用java keytool生成的jks格式,一般转jwks都是用pkcs12或者pem格式,为方便使用也一起附带了
      jwks: |
        {
            "keys": [
                {
                    "e": "AQAB",
                    "kid": "bookstore-jwt-kid",
                    "kty": "RSA",
                    "n": "i-htQPOTvNMccJjOkCAzd3YlqBElURzkaeRLDoJYskyU59JdGO-p_q4JEH0DZOM2BbonGI4lIHFkiZLO4IBBZ5j2P7U6QYURt6-AyjS6RGw9v_wFdIRlyBI9D3EO7u8rCA4RktBLPavfEc5BwYX2Vb9wX6N63tV48cP1CoGU0GtIq9HTqbEQs5KVmme5n4XOuzxQ6B2AGaPBJgdq_K0ZWDkXiqPz6921X3oiNYPCQ22bvFxb4yFX8ZfbxeYc-1rN7PaUsK009qOx-qRenHpWgPVfagMbNYkm0TOHNOWXqukxE-soCDI_Nc--1khWCmQ9E2B82ap7IXsVBAnBIaV9WQ"
                }
            ]
        }
      forwardOriginalToken: true

 

2.2.2 Spring Cloud非云原生-用户认证

Spring Cloud,采用 JWT 令牌+在Spring Security过滤器实现。Spring Security 已经做好了认证所需的绝大部分的工作,真正要开发者去编写的代码是令牌的具体实现,即代码中名为RSA256PublicJWTAccessToken的实现类。它的作用是加载 Resource 目录下的公钥证书public.cert(实在是怕“抄作业不改名字”的行为,笔者再一次强调不要将密码、密钥、证书这类敏感信息打包到程序中,示例代码只是为了演示,实际生产应该由运维人员管理密钥),验证请求中的 JWT 令牌是否合法。

@Named
public class RSA256PublicJWTAccessToken extends JWTAccessToken {
    RSA256PublicJWTAccessToken(UserDetailsService userDetailsService) throws IOException {
        super(userDetailsService);
        Resource resource = new ClassPathResource("public.cert");
        String publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        setVerifierKey(publicKey);
    }
}

如果 JWT 令牌合法,Spring Security 的过滤器就会放行调用请求,并从令牌中提取出 Principals,放到自己的安全上下文中。(即SecurityContextHolder.getContext())。

三、授权

 经过认证之后,合法的调用者就有了可信任的身份,此时就已经不再需要区分调用者到底是机器(服务)还是人类(最终用户)了,只根据其身份角色来进行权限访问控制,即我们常说的 RBAC(Role-Based Access Control )。举个具体例子,如果我们准备把一部分微服务视为私有服务,限制它只接受来自集群内部其他服务的请求,另外一部分微服务视为公共服务,允许它可接受来自集群外部的最终用户发出的请求;

2.3.1云原生架构Istio-授权

通过以下配置,限制了来自bookstore-servicemesh名空间的内部流量只允许访问accountsproductspaysettlements四个端点的 GET、POST、PUT、PATCH 方法,而对于来自istio-system名空间(Istio Ingress Gateway 所在的名空间)的外部流量就不作限制,直接放行。

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: authorization-peer
  namespace: bookstore-servicemesh
spec:
  action: ALLOW
  rules:
    - from:
        - source:
            namespaces: ["bookstore-servicemesh"]
      to:
        - operation:
            paths:
              - /restful/accounts/*
              - /restful/products*
              - /restful/pay/*
              - /restful/settlements*
            methods: ["GET","POST","PUT","PATCH"]
    - from:
        - source:
            namespaces: ["istio-system"]

Istio 已经提供了比较完善的目标匹配工具,如上面配置中用到的源from、目标to,还有未用到的条件匹配when,以及其他如通配符、IP、端口、名空间、JWT 字段,等等。要说灵活和功能强大,肯定还是不可能跟在应用中由代码实现的授权相媲美,但对绝大多数场景已经够用了。在便捷性、安全性、无侵入、统一管理等方面,Istio 这种在基础设施上实现授权方案显然就要更具优势

2.3.1非云原生架构SpringCloud-授权

常见的 Spring Security 授权方法有两种,

  • 1.代码配置:一种是使用它的ExpressionUrlAuthorizationConfigurer,即类似如下编码所示的写法来进行集中配置,也是 Spring Security 资料中都有介绍的最主流方式,适合对批量端点进行控制。
http.authorizeRequests()
    .antMatchers("/restful/accounts/**").hasScope(Scope.BROWSER)
    .antMatchers("/restful/pay/**").hasScope(Scope.SERVICE)
  • 2.注解标识:Spring 的全局方法级安全(Global Method Security)以及JSR 250@RolesAllowed注解来做授权控制。这种写法对代码的侵入性更强,要以注解的形式分散写到每个服务甚至是每个方法中,但好处是能以更方便的形式做出更加精细的控制效果。譬如要控制服务中某个方法只允许来自服务或者来自浏览器的调用,那直接在该方法上标注@PreAuthorize注解即可,还支持SpEL 表达式来做条件。表达式中用到的SERVICEBROWSER代表授权范围,就是在声明客户端列表时传入的,具体可参见开头声明客户端列表的代码清单。
/**
 * 根据用户名称获取用户详情
 */
@GET
@Path("/{username}")
@Cacheable(key = "#username")
@PreAuthorize("#oauth2.hasAnyScope('SERVICE','BROWSER')")
public Account getUser(@PathParam("username") String username) {
    return service.findAccountByUsername(username);
}