在2014年初,咱们将线上使用的 Hadoop 1.0 集群切换到 Hadoop 2.2.0 稳定版, 与此同时部署了 Hadoop 的安全认证。本文主要介绍在 Hadoop 2.2.0 上部署安全认证的方案调研实施以及相应的解决方法。html
背景
集群安全措施相对薄弱
最先部署Hadoop集群时并无考虑安全问题,随着集群的不断扩大, 各部门对集群的使用需求增长,集群安全问题就显得颇为重要。说到安全问题,通常包括以下方面:java
-
用户认证(Authentication)
便是对用户身份进行核对, 确认用户便是其声明的身份, 这里包括用户和服务的认证。node -
用户受权(Authorization)
便是权限控制,对特定资源, 特定访问用户进行受权或拒绝访问。用户受权是创建再用户认证的基础上, 没有可靠的用户认证谈不上用户受权。git
未开启安全认证时,Hadoop 是以客户端提供的用户名做为用户凭证, 通常便是发起任务的Unix 用户。通常线上机器部署服务会采用统一帐号,当以统一帐号部署集群时,全部执行 Hadoop 任务的用户都是集群的超级管理员,容易发生误操做。即使是以管理员帐号部署集群,恶意用户在客户端仍然能够冒充管理员帐号执行。github
集群总体升级到 hadoop 2.0
2013年10月份 Hadoop 2.2.0 发布,做为 Apache Hadoop 2.X 的 GA 版本。咱们考虑将集群总体升级 Hadoop 2.2.0,进入 yarn 时代。与此同时,咱们计划在升级过程当中一并把集群安全工做作到位,主要基于如下考虑:web
- 与升级工做同样,安全一样是基础工做,把安全搞好会方便咱们后续的工做,不然会成为下一个阻碍。
- 所谓基础工做,就是越日后改动越难的工做,目前不作,未来依赖更多,开展代价更大。
综上,咱们的需求是在低版本hadoop升级到Yarn的过程当中部署Hadoop安全认证,作好认证以后咱们能够在此之上开启适当的权限控制(hdfs, 队列)。shell
方案调研
在方案调研以前先明确如下安全实践的原则,以下:apache
- 作为一个后端服务平台,部署安全的主要目的是防止用户误操做致使的事故(好比误删数据,误操做等)
- 作安全是为了开放,开放的前提是保证基本的安全,数据安全与平台安全
- 在保证安全的前提下,尽可能简化运维
分析咱们遇到的问题,这里咱们须要调研:后端
- 帐号拆分与相应管理方案
- 开启 Hadoop 安全认证
- 客户端针对安全认证的相应调整
帐号拆分与相应管理方案
集群帐号管理
原先咱们使用单一帐号做为集群管理员,且这一帐号为线上统一登陆帐号, 这存在极大的安全隐患。咱们须要使用特殊帐号来管理集群。这里涉及的问题是,咱们须要几个运维帐号呢?
一种简单的作法是使用一个特殊运维帐号(好比 hadoop), CDH 和 Apache官方 都推荐按服务划分分帐号来启动集群:缓存
User:Group | Daemons |
---|---|
hdfs:hadoop | NameNode, Secondary NameNode, Checkpoint Node, Backup Node, DataNode |
yarn:hadoop | ResourceManager, NodeManager |
mapred:hadoop | MapReduce JobHistory Server |
考虑到精细化控制能够有效避免误操做,这里咱们遵循官方的建议使用多帐号。
在从单一运维帐号迁移到多个帐号部署时,须要考虑相关文件权限问题,包括本地以及hdfs两部分,这能够在安所有署上线时完成相应改动。
用户帐号管理
美团不少小组都有使用 Hadoop 来进行大数据处理需求, 故须要必定程度的多租户环境, 这里主要考虑其中的数据和操做的权限问题。hdfs 自己仅提供类 Unix 的权限系统, 默认的组概念也相对鸡肋。鉴于此,在多用户的管理上能够有简单粗暴的方案:
不一样组有各自的根目录,使用不一样的帐号,对组内文件有所有权限。不一样组之间相互不能访问数据(除非手动修改)。
在一个集中的数据仓库环境下,又要生产各个部门的统计数据的话,上述策略不够灵活。目前Cloudera 有一个精细化权限控制的解决方案 sentry, 支持 Role based 的权限管理。因为其定制化较高,不方便使用, 故暂未考虑。
开启 Hadoop 安全认证
Hadoop 的安全认证是基于 Kerberos 实现的。 Kerberos 是一个网络身份验证协议,用户只需输入身份验证信息,验证经过获取票据便可访问多个接入 Kerberos 的服务, 机器的单点登陆也能够基于此协议完成的。 Hadoop 自己并不建立用户帐号,而是使用 Kerberos 协议来进行用户身份验证,从Kerberos凭证中的用户信息获取用户帐号, 这样一来跟实际用户运行的帐号也无关。
这里咱们从 YARN 上的 MR 任务提交过程简单说明一下:
- 用户执行任务前,先经过KDC认证本身,获取TGT(Ticket Granting Ticket)。KDC是 Kerberos 认证的中心服务,存储用户和服务的认证信息。
- 用户经过 TGT 向 KDC 请求访问服务的Ticket, KDC 生成 session key 后一并发给客户端。
- 客户端经过 service ticket 向服务认证本身,完成身份认证。
- 完成身份认证后客户端向服务请求若干token供后续任务执行认证使用(好比 HDFS NameNode Delegation Token, YARN ResourceManager Delegation Token)
- 客户端连同获取到的 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_lifetime
和 renew_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集群的服务来讲,上述假设就不成立了。这时候咱们能够
-
扩大
ticket_lifetime
和renew_lifetime
时限
扩大凭证存活时限能够解决此问题,但因为Kerberos跟咱们线上用户登录认证绑定,会带来安全隐患,故不方便修改。 -
按期从新进行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的认证过程以下:
- An application instantiates a LoginContext.
- The LoginContext consults a Configuration to load all of the LoginModules configured for that application.
- The application invokes the LoginContext's login method.
- 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.
- The LoginContext returns the authentication status to the application.
- 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
配置认证:
- 经过配置生成 LoginContext
- 调用 LoginContext.login 方法完成登录, 经过 ticket cache 中凭证完成登录
- 判断是否须要其余用户身份(proxy user)执行
- 将
HADOOP_TOKEN_FILE_LOCATION
中的 token 加入 Credentials 集合当中 - 另起一个线程作周期性的凭证更新
spawnAutoRenewalThreadForUserCreds
步骤5能够看出当咱们存在凭证后并不须要主动作周期性地凭证更新。
而 loginUserFromKeytab 方法使用 hadoop-kerberos
配置认证:
- 经过配置生成 LoginContext
- 调用 LoginContext.login 方法完成登录, 使用keytab完成登录
loginUserFromKeytab 没有对凭证作周期的更新, 那怎么保证凭证不会过时呢?
- 在访问集群执行相关操做前, 能够调用
checkTGTAndReloginFromKeytab
来尝试更新凭证(其实是从新登录了) - 在凭证过时时,建立 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 记录了此问题,主要缘由是因为凭证没法更新致使, 通常不须要特殊处理。
【转自】:http://tech.meituan.com/hadoop-security-practice.html
美团点评技术团队