前言

在2014年初,我们将线上使用的 Hadoop 1.0 集群切换到 Hadoop 2.2.0 稳定版, 与此同时部署了 Hadoop 的安全认证。本文主要介绍在 Hadoop 2.2.0 上部署安全认证的方案调研实施以及相应的解决方法。

背景

集群安全措施相对薄弱

最早部署Hadoop集群时并没有考虑安全问题,随着集群的不断扩大, 各部门对集群的使用需求增加,集群安全问题就显得颇为重要。说到安全问题,一般包括如下方面:

  • 用户认证(Authentication) 即是对用户身份进行核对, 确认用户即是其声明的身份, 这里包括用户和服务的认证。

  • 用户授权(Authorization) 即是权限控制,对特定资源, 特定访问用户进行授权或拒绝访问。用户授权是建立再用户认证的基础上, 没有可靠的用户认证谈不上用户授权。

未开启安全认证时,Hadoop 是以客户端提供的用户名作为用户凭证, 一般即是发起任务的Unix 用户。一般线上机器部署服务会采用统一账号,当以统一账号部署集群时,所有执行 Hadoop 任务的用户都是集群的超级管理员,容易发生误操作。即便是以管理员账号部署集群,恶意用户在客户端仍然可以冒充管理员账号执行。

集群整体升级到 hadoop 2.0

2013年10月份 Hadoop 2.2.0 发布,作为 Apache Hadoop 2.X 的 GA 版本。我们考虑将集群整体升级 Hadoop 2.2.0,进入 yarn 时代。与此同时,我们计划在升级过程中一并把集群安全工作做到位,主要基于以下考虑: * 与升级工作一样,安全同样是基础工作,把安全搞好会方便我们后续的工作,否则会成为下一个阻碍。 * 所谓基础工作,就是越往后改动越难的工作,目前不做,将来依赖更多,开展代价更大。

综上,我们的需求是在低版本hadoop升级到Yarn的过程中部署Hadoop安全认证,做好认证之后我们可以在此之上开启适当的权限控制(hdfs, 队列)。

方案调研

在方案调研之前先明确以下安全实践的原则,如下: * 做为一个后端服务平台,部署安全的主要目的是防止用户误操作导致的事故(比如误删数据,误操作等) * 做安全是为了开放,开放的前提是保证基本的安全,数据安全与平台安全 * 在保证安全的前提下,尽量简化运维

分析我们遇到的问题,这里我们需要调研: * 账号拆分与相应管理方案 * 开启 Hadoop 安全认证 * 客户端针对安全认证的相应调整

账号拆分与相应管理方案

集群账号管理

原先我们使用单一账号作为集群管理员,且这一账号为线上统一登录账号, 这存在极大的安全隐患。我们需要使用特殊账号来管理集群。这里涉及的问题是,我们需要几个运维账号呢? 一种简单的做法是使用一个特殊运维账号(比如 hadoop), CDHApache官方 都推荐按服务划分分账号来启动集群:

User:GroupDaemons
hdfs:hadoopNameNode, Secondary NameNode, Checkpoint Node, Backup Node, DataNode
yarn:hadoopResourceManager, NodeManager
mapred:hadoopMapReduce JobHistory Server

考虑到精细化控制可以有效避免误操作,这里我们遵循官方的建议使用多账号。 在从单一运维账号迁移到多个账号部署时,需要考虑相关文件权限问题,包括本地以及hdfs两部分,这可以在安全部署上线时完成相应改动。

用户账号管理

美团很多小组都有使用 Hadoop 来进行大数据处理需求, 故需要一定程度的多租户环境, 这里主要考虑其中的数据和操作的权限问题。hdfs 本身仅提供类 Unix 的权限系统, 默认的组概念也相对鸡肋。鉴于此,在多用户的管理上可以有简单粗暴的方案: > 不同组有各自的根目录,使用不同的账号,对组内文件有全部权限。不同组之间相互不能访问数据(除非手动修改)。

在一个集中的数据仓库环境下,又要生产各个部门的统计数据的话,上述策略不够灵活。目前Cloudera 有一个精细化权限控制的解决方案 sentry, 支持 Role based 的权限管理。由于其定制化较高,不方便使用, 故暂未考虑。

开启 Hadoop 安全认证

Hadoop 的安全认证是基于 Kerberos 实现的。 Kerberos 是一个网络身份验证协议,用户只需输入身份验证信息,验证通过获取票据即可访问多个接入 Kerberos 的服务, 机器的单点登录也可以基于此协议完成的。 Hadoop 本身并不创建用户账号,而是使用 Kerberos 协议来进行用户身份验证,从Kerberos凭证中的用户信息获取用户账号, 这样一来跟实际用户运行的账号也无关。

这里我们从 YARN 上的 MR 任务提交过程简单说明一下: Yarn 任务提交步骤

  1. 用户执行任务前,先通过KDC认证自己,获取TGT(Ticket Granting Ticket)。KDC是 Kerberos 认证的中心服务,存储用户和服务的认证信息。
  2. 用户通过 TGT 向 KDC 请求访问服务的Ticket, KDC 生成 session key 后一并发给客户端。
  3. 客户端通过 service ticket 向服务认证自己,完成身份认证。
  4. 完成身份认证后客户端向服务请求若干token供后续任务执行认证使用(比如 HDFS NameNode Delegation Token, YARN ResourceManager Delegation Token)
  5. 客户端连同获取到的 token 一并提交任务,后续任务执行使用 token 进行来自服务的认证

从上可以看出,出于性能的考虑,Hadoop 安全认证体系中仅在用户跟服务通信以及各个服务之间通信适用 Kerberos 认证,在用户认证后任务执行,访问服务,读取/写入数据等均采用特定服务(NameNode, Resource Manager)发起访问token,让需求方凭借 token 访问相应服务和数据。这里 token 的传递,认证以及更新不做深入讨论。

关于开启 Hadoop 安全认证, Cloudera 有详细的文档介绍。由于自身环境以及部署运维的考虑,最终的部署方案有些许出入, 一一说明。

Kerberos 部署

Hadoop 安全认证需要一个 Kerberos 集群, 部署 Kerberos 需要部署KDC。 由于我们的环境中使用 freeIPA 进行主机认证相关的权限控制,已经集成 Kerberos 服务, 故不需要另外部署。 Kerberos 相关的运维操作, 比如添加用户,服务,导出keytab,均可以通过 ipa 相关接口来进行。

Container 的选择

从上图可以看出用户发起的任务是在特定的容器(Container)内执行的, 一开始我们考虑使用DefaultContainer 而不是官方推荐的 LinuxContainer, 缺点是对任务之间的物理隔离以及防范恶意任务方面会有缺陷, 不过方便部署,使用LinuxContainer需要在集群各台机器上部署用户账号。 实际测试发现由于MAPREDUCE-5208的引入,在 hadoop 2.2.0 上开启安全认证后无法使用 DefaultContainer。 这里不希望对代码有过多定制化的修改,我们考虑还是使用 LinuxContainer, 需要解决一下问题: * 用户账号创建 我们需要在集群内添加所有可能的任务发起用户账号。借助 freeipa 的统一的用户管理 , 我们只需要在 freeipa 上添加相应用户即可。 * container-executor 和 container-executor.cfg 的部署 container-executor 作为Yarn 的 container 执行程序,有一系列的权限要求: > Be owned by root > Be owned by a group that contains only the user running the YARN daemons > Be setuid > Be group readable and executable

配置 container-executor.cfg 不仅需要是owned by root,且其所在目录同样需要 owned by root。这两者都给自动化部署带来不便,鉴于此部分比较独立且基本不会改变,我们可以将其加入集群机器的 puppet 管理当中。

DataNode 启动方式

CDH 推荐的datanode 的启动方式需要使用低端口并且使用jsvc发布, 在运维方面也不太方便。这里我们通过配置ignore.secure.ports.for.testing=true来启动datanode, 规避这些约束。

客户端针对安全认证的相应调整

集群开启安全认证之后, 依赖集群的客户端(脚本, 服务)都需要做相应修改,不过改动基本无异。大部分服务都已包括对 Kerberos 认证的相应处理, 基本不需要修改。

这里首先得说明一下开启安全认证后的认证方式: * 使用密码认证 使用用户密码通过kinit认证, 获取到的TGT存在本地凭证缓存当中, 供后续访问服务认证使用。一般在交互式访问中使用。 * 使用 keytab 认证 用户通过导出的keytab 可以免密码进行用户认证, 后续步骤一致。一般在应用程序中配置使用。

Kerberos 凭证(ticket) 有两个属性, ticket_lifetimerenew_lifetime。其中 ticket_lifetime 表明凭证生效的时限,一般为24小时。在凭证失效前部分凭证可以延期失效时间(即Renewable), renew_lifetime 表明凭证最长可以被延期的时限,一般为一个礼拜。当凭证过期之后,对安全认证的服务的后续访问则会失败。这里第一个问题就是如何处理凭证过期。

凭证过期处理策略

在最早的 Security features for Hadoop 设计中提出这样的假设: > A Hadoop job will run no longer than 7 days (configurable) on a MapReduce cluster or accessing HDFS from the job will fail.

对于一般的任务, 24小时甚至延迟到一周的凭证时限是足够充分的。所以大部分时间我们只需要在执行操作之前使用 kinit 认证一遍,再起后台任务进行周期性凭证更新即可。

while true ; do kinit -R; sleep $((3600 * 6)) ; done &

不过对于需要常驻的访问Hadoop集群的服务来说,上述假设就不成立了。这时候我们可以 1. 扩大 ticket_lifetimerenew_lifetime 时限 扩大凭证存活时限可以解决此问题,但由于Kerberos跟我们线上用户登陆认证绑定,会带来安全隐患,故不方便修改。

  1. 定期重新进行kinit 认证更新凭证 不仅仅是定期延长认证时间,可以直接定期重新认证以延长凭证有限期限。一般我们需要导出 keytab 来进行定期认证的操作。

Hadoop 将 Kerberos 认证部分进行了一定的封装,实际上并不需要那么复杂, 这里重点可以看看 UserGroupInformation 这个类。

UserGroupInformation

UserGroupInformation 这个类在 JAAS 框架上封装了 Hadoop 的用户信息, 更确切地说是对 Subject 做了一层封装。

  UserGroupInformation(Subject subject) {
    this.subject = subject;
    this.user = subject.getPrincipals(User.class).iterator().next();
    this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty();
    this.isKrbTkt = !subject.getPrivateCredentials(KerberosTicket.class).isEmpty();
  }

JAAS 是 Java 认证和授权服务(Java Authentication and Authorization Service)的缩写, 主要包含以下几个实体:

  • Subject Subject 是一个不可继承的实体类,它标志一个请求的来源, 包含相关的凭证标识(Principal) 和 公开和私有的凭据(Credential)。
  • Principal 凭证标识,认证成功后,一个 Subject 可以被关联多个Principal。
  • Credential 凭据,有公有凭据以及私有凭据。

JAAS的认证过程如下:

  1. An application instantiates a LoginContext.
  2. The LoginContext consults a Configuration to load all of the LoginModules configured for that application.
  3. The application invokes the LoginContext’s login method.
  4. The login method invokes all of the loaded LoginModules. Each LoginModule attempts to authenticate the subject. Upon success, LoginModules associate relevant Principals and credentials with a Subject object that represents the subject being authenticated.
  5. The LoginContext returns the authentication status to the application.
  6. If authentication succeeded, the application retrieves the Subject from the LoginContext.

需要认证的代码片段可以包装在 doPrivileged 当中, 可以直接使用 Subject.doAs 方法,支持嵌套。

在安全模式下,UGI 支持不同LoginContext 配置, 均是通过 HadoopConfiguration 类动态产生: * hadoop-user-kerberos 使用kerberos缓存凭证登陆的配置, useTicketCache 置为 true. * hadoop-keytab-kerberos 使用keytab登陆的配置, useKeyTab 置为 true.

UGI 当中有多处认证, getLoginUser 方法使用 hadoop-user-kerberos 配置认证: 1. 通过配置生成 LoginContext 2. 调用 LoginContext.login 方法完成登陆, 通过 ticket cache 中凭证完成登陆 3. 判断是否需要其他用户身份(proxy user)执行 4. 将 HADOOP_TOKEN_FILE_LOCATION 中的 token 加入 Credentials 集合当中 5. 另起一个线程做周期性的凭证更新 spawnAutoRenewalThreadForUserCreds

步骤5可以看出当我们存在凭证后并不需要主动做周期性地凭证更新。

而 loginUserFromKeytab 方法使用 hadoop-kerberos 配置认证: 1. 通过配置生成 LoginContext 2. 调用 LoginContext.login 方法完成登陆, 使用keytab完成登陆

loginUserFromKeytab 没有对凭证做周期的更新, 那怎么保证凭证不会过期呢? 1. 在访问集群执行相关操作前, 可以调用 checkTGTAndReloginFromKeytab 来尝试更新凭证(实际上是重新登陆了) 2. 在凭证过期时,创建 IPC 失败会触发调用 reloginFromKeytab 来重新登陆

Client.java

    private synchronized void handleSaslConnectionFailure(
        final int currRetries, final int maxRetries, final Exception ex,
        final Random rand, final UserGroupInformation ugi) throws IOException,
        InterruptedException {
      ugi.doAs(new PrivilegedExceptionAction<Object>() {
        @Override
        public Object run() throws IOException, InterruptedException {
          final short MAX_BACKOFF = 5000;
          closeConnection();
          disposeSasl();
          if (shouldAuthenticateOverKrb()) {
            if (currRetries < maxRetries) {
              if(LOG.isDebugEnabled()) {
                LOG.debug("Exception encountered while connecting to "
                    + "the server : " + ex);
              }
              // try re-login
              if (UserGroupInformation.isLoginKeytabBased()) {
                UserGroupInformation.getLoginUser().reloginFromKeytab();
              } else {
                UserGroupInformation.getLoginUser().reloginFromTicketCache();
              }

可见如果是使用 keytab 认证的话,认证是长期有效的。

从上述代码中可以看到,不论是否是keytab认证,创建IPC失败均会尝试重新登陆。

基于keytab 的Kerberos认证方式

为了让用户免于记忆密码,我们可以考虑导出并交付keytab给相关用户(前提是用户数量可控, 比如是以虚拟用户为单位)。 这样,用户的Hadoop任务认证方式可以有: * 直接使用 keytab kinit 之后访问 * 或者调用 loginUserFromKeytab 完成登录,然后将代码片段包裹在 UGI 的 doAs 方法当中执行

上线部署

确定了部署方案之后, 我们在升级 hadoop 版本的同时完成了安全认证的部署。在部署和使用中我们遇到若干问题,这里一一说明。

JCE 部署

开启安全认证时发现 Kerberos 认证不通过:

Client failed to SASL authenticate: javax.security.sasl.SaslException: GSS initiate failed [Caused by GSSException: Failure unspecified at GSS-API level (Mechanism level: Checksum failed)]

由于我们部署的Kerberos默认使用 AES-256 加密, 需要在Hadoop环境(集群以及客户端)上安装 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy File, 否则Kerberos认证不通过。可以通过此 gist 验证改动是否生效。此步骤可以添加到puppet当中。

SNN getimage 返回 NPE

开启安全认证发现 SNN 持续由于 getimage 报错NPE 退出, 相关错误如下。

2013-12-29 23:56:19,572 DEBUG org.apache.hadoop.security.authentication.server.AuthenticationFilter: Request [http://XXX.com:50070/getimage?getimage=1&txid=8627&storageInfo=-47:200271
8265:0:CID-3dce02cb-a1c2-4ab8-8b12-f23bbefd7bcc] triggering authentication
2013-12-29 23:56:19,580 WARN org.apache.hadoop.security.authentication.server.AuthenticationFilter: Authentication exception: GSSException: Failure unspecified at GSS-API level (Mechanism level: Specified
 version of key is not available (44))
org.apache.hadoop.security.authentication.client.AuthenticationException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Specified version of key is not available (44))
        at org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler.authenticate(KerberosAuthenticationHandler.java:360)
        at org.apache.hadoop.security.authentication.server.AuthenticationFilter.doFilter(AuthenticationFilter.java:349)

根据报错信息 Specified version of key is not available 发现是由于同一个 HTTP 凭证被导出多遍导致之前的keytab中的凭证失效了,重新生成部署所需的 keytab 即可。 这里的提醒就是不要重复导出相同的凭证, 以防止已经分发使用的keytab中的凭证失效。

Balancer 执行过长导致认证过期

在部署安全认证之后, 我们对hdfs数据进行 balance 就需要预先认证一下再执行, 这样就会遇到我们之前说的认证期限的问题。 这里有两种方式可以解决此问题: * 添加外部定时任务重新认证, 刷新凭证缓存, 延迟凭证有效期限。 * 可以写一个小代码对 balance 的入口 org.apache.hadoop.hdfs.server.balancer.Balancer 进行一点封装,将其封装在一个 doAs 当中, 类似 hue 中的 SudoFsShell 一样的思路

sssd 服务认证异常

sssd 是指我们用于线上登陆认证的一个底层服务,在过去一段时间内经常出现问题退出,导致用户登录动作hang住,进而导致相关任务执行失败。部署Hadoop安全认证之后相关 kerberos 认证也走这个服务,增大了服务异常退出的概率。目前看起来sssd服务问题是由于系统版本过低sssd服务代码有bug导致,解决方案最方便的是升级系统或切换服务到新的机器。

“KDC can’t fulfill requested option while renewing credentials”

应用执行日志偶尔会报如下错误:

2014-03-12 21:30:03,593 WARN  security.UserGroupInformation (UserGroupInformation.java:run(794)) - Exception encountered while running the renewal command. Aborting renew thread. org.apache.hadoop.util.Shell$ExitCodeException: kinit(v5): KDC can't fulfill requested option while renewing credentials

表示 UGI的凭证更新线程失败退出了。目前HADOOP-10041 记录了此问题,主要原因是由于凭证无法更新导致, 一般不需要特殊处理。

参考资料