概述
作为分布式文件系统,HDFS实现了一套兼容POSIX的文件权限模型,包括粗粒度的POSIX UGO模型和细粒度的POSIX ACLs协议。
客户端在每次进行文件操作时,HDFS会从用户身份认证、用户组映射和数据访问鉴权三个环节进行验证: 客户端的操作请求会首先从本地系统获取用户名,然后服务端将用户名匹配上组信息,最后查看所访问的数据是否已经授权给该用户。一旦这个流程中的某个环节出现异常,客户端的操作请求便会失败。
本文以Hadoop 3.1.1版本为例,介绍 HDFS 权限管理的相关内容以及权限分配的应用实践。
用户身份认证
用户身份认证并不属于HDFS的范畴,也就是说HDFS并不负责用户身份的合法性检查,它只是会依赖相关系统来获取用户身份,从而用于后续的鉴权。而对于身份认证,完全取决于其采用的认证系统。目前HDFS有支持两种用户身份认证:简单认证和kerberos认证。
简单认证
kerberos认证 kerberos是一个网络认证协议,其使用密钥加密技术为客户端和服务端应用提供强认证功能。它有一个管理端(AdminServer)用于管理所有需要认证的账号信息,另外还有若干的密钥分发服务器(KDC)用于提供认证和分发密钥服务。 用户从 Kerberos 管理员处获取对应的 Kerberos 账号名或者密钥文件,然后通过 kinit 等方式认证Kerberos,拿到TGT(ticket-granting-ticket)票据。客户端会将TGT信息传输到NN端,NN在获取到认证信息后,将principle首部截取出来作为客户端的用户名。例如使用 todd/foobar@CORP.COMPANY.COM认证成功后, da作为principle的首部会作为该客户端的用户名使用。 使用Kerberos可以极大增强HDFS的安全性,特别是在多租户的生产环境下。
用户组映射
因为HDFS采用与Linux/Unix系统类似的文件权限模型,也就是UGO模型,分成用户、组和其他,所以在拿到用户名后,NN会通过获取该用户所对应的组列表。HDFS中组的获取是通过外部Group Mapping服务来获取,目前社区对用户到组的映射有主要有两种实现方式:1)使用系统自带的方案(即NameNode服务器上的用户组系统),2)使用三方服务如LDAP,通过参数hadoop.security.group.mapping来进行设置。下面重点讲解下这两种实现方式。
基于Linux/Unix系统的用户和组实现 Linux/Unix 系统上的用户和用户组信息存储在/etc/passwd 和 /etc/group 文件中。默认情况下,HDFS 会使用org.apache.hadoop.security.ShellBasedUnixGroupsMapping服务,其原理是在NN上调用Shell 命令groups来获取用户的组列表。 此方案的优点在于组映射服务十分稳定,不易受外部服务的影响。但是用户和用户组管理需要root权限,同时会在服务器上生成大量的用户组,后续管理,特别是自动化运维方面会有较大影响。
基于LDAP数据库的用户和组实现 OpenLDAP是一个开源LDAP的数据库,可以通过phpLDAPadmin等管理工具或相关接口方便地添加用户和修改用户组。HDFS通过配置org.apache.hadoop.security.LdapGroupsMapping来使用 LDAP 服务,可以通过接口来直接获取到某个用户的组列表。使用LDAP的不足在于需要保障LDAP服务的可用性和性能。
数据授权(数据权限管理)
UGO权限管理
- HDFS的文件权限与Linux/Unix系统的UGO模型类似,我们使用FS Shell查看目录,可以看到如下内容:
drwxrwxrwt - yarn hadoop 0 2020-01-20 15:42 /app-logs
drwxr-xr-x - yarn hadoop 0 2020-01-20 15:40 /ats
drwxr-xr-x - hdfs hdfs 0 2020-01-20 15:40 /hdp
drwxr-xr-x - mapred hdfs 0 2020-01-20 15:40 /mapred
drwxrwxrwx - mapred hadoop 0 2020-01-20 15:40 /mr-history
drwxrwxrwx - hdfs hdfs 0 2020-01-20 15:41 /tmp
drwxr-xr-x - hdfs hdfs 0 2020-01-20 15:40 /user
drwxr-xr-x - hdfs hdfs 0 2020-01-20 15:40 /warehouse
- 但是与传统的POSIX模式相比,HDFS没有setuid和setgid实现
- 与Linux/Uninx类似,HDFS在目录中也可以设置粘连位(Sticky bit)。 通过设置粘连位可以让不同的用户共享特定目录的读写权限,而只有子目录的属主才有删除权限。类似linux下/tmp目录一般设置粘连位,hdfs也应该将/tmp目录设置粘连位来共享读写而限制随便删除,通过hdfs dfs -chmod o+t /tmp设置后如下:
drwxrwxrwt - hdfs hdfs 0 2016-10-11 05:14 /tmp
/tmp下面的一些子目录的权限如下:
drwxr-xr-x - hdfs hdfs 0 2020-02-12 15:47 /tmp/entity-file-history
drwxr-x--- - hive hdfs 0 2020-02-14 22:30 /tmp/hive
drwxr-x--- - spark hdfs 0 2020-02-14 22:28 /tmp/spark
- 与Linux/Unix类似,HDFS也提供了umask掩码,用于设置在HDFS中默认新建的文件和目录权限位。为了配合用户组的权限限制,建议将其设置成027。配置如下:
<property>
<name>fs.permissions.umask-mode</name>
<value>027</value>
</property>
注意:这个umask配置是客户端可以配置的,即客户端自己主宰创建文件或目录的权限。
关于022和027的解释:如果umask是022(默认值),那么新文件的模式就是644,新目录的模式就是755,即umask擦除掉了group和other的写权限。如果umask是027,那么新文件的模式就是650,新目录的模式就是750,即umask擦除掉了group的写权限,以及other的读写执行权限。
- HDFS也有与linux/uinx系统root账号类似的超级用户。默认来说,只有启动Namenode进程的用户才有超级用户权限,也就是hdfs用户。但很多操作实际上都需要超级用户的权限(如fsck等),故HDFS也可以配置超级用户组,除了部分操作(如hdfs dfsadmin -report命令)对用户名有限制之外,所有在该用户组里面的用户都可以以超级用户权限来操作HDFS。开启超级用户组的参数如下:
<property>
<name>dfs.permissions.superusergroup</name>
<value>hdfs</value>
</property>
注意:必须在NN节点上将用户添加到superusergroup。
UGO 权限相关操作
- hadoop fs -chmod 750 /user/dw_dev/foo
- hadoop fs -chown dw_dev:hdfs /user/dw_dev/foo
- hadoop fs -chgrp hdfs /user/dw_dev/foo
ACLs权限管理
除了支持传统的POSIX权限模型之外,HDFS还支持POSIX ACLs (Access Control Lists)。ACLs增强了传统的权限模型,通过为特定的用户或组设置不同的权限来控制对HDFS文件的访问,即可以做到更加灵活和细粒度的权限控制:允许用户为用户和组的任意组合定义访问控制,而不是单个用户(所有者)或单个组。
默认情况下对ACLs的支持是关闭的,可以通过设置dfs.namenode.acls.enabled为true来打开。
ACLs应用场景
HDFS使用UGO这种用户组的权限管理模型可以满足大多数场景的安全性要求,但对于一些复杂场景无法胜任,实际企业应用中存在如下问题:
对于一个目录,可能会有两种权限需求,一种是只读,一种是读写,但用户组权限只能设定为其中之一;
ACL规则定义
一条ACL规则由若干ACL条目组成,每个条目指定一个用户或用户组的权限位。ACL条目由类型名,可选名称和权限字符串组成,以:为分隔符。
user::rw-
user:bruce:rwx #effective:r--
group::r-x #effective:r--
group:sales:rwx #effective:r--
mask::r--
other::r--
第一部分由固定的类型名构成,有user,group,other,mask,default等选项。mask条目会过滤掉所有命名的用户和用户组,以及未命名的用户组权限。第二部分可以指定类型名称,如用户名,用户组名等(other类型不需要名称),这部分是可选项,若不指定特定的用户名或用户组,则表示只对该文件属主或目录的用户组生效。第三部分就是权限位。 若该条规则应用到foo文件,foo文件的属主有读写权限,foo文件的用户组有只读和执行权限(对于目录),其他用户也是只读权限;但bruce用户的权限经过mask过滤后只有只读权限,sales组也是只读权限。
user::rwx
group::r-x
other::r-x
default:user::rwx
default:user:bruce:rwx #effective:r-x
default:group::r-x
default:group:sales:rwx #effective:r-x
default:mask::r-x
default:other::r-x
default类型是一个特殊的类型,且只应用在目录上,用于在创建子目录和文件时为其应用该默认的ACL规则。权限复制发生在文件产生之时,在这之后对父级目录的ACL操作,不会影响子目录已存在的ACL规则。 另外每个ACL规则都有mask条目,如果用户在设置ACL时没有显式声明,那么系统会自动地添加一条mask规则。在含有ACL规则的文件上通过chmod变更权限会改变mask值。因为mask要作为一个过滤器来更有效地限制所有的扩展ACL条目,如果仅仅改变组条目,这会导致Other部分的ACL规则出现缺漏。
当设置了ACL规则之后,目录或文件的权限位后面会出现一个“+”号,如下:
drwxrwx---+ - hive hadoop 0 2020-02-13 03:12 /warehouse/tablespace/managed/hive
ACL相关操作
1.查看目录权限
hadoop fs -setfacl /user/dw_dev
2.为目录添加访问权限
hadoop fs -setfacl -m user:public:r-x /user/dw_dev
3.为目录添加可继承的权限
hadoop fs -setfacl -m default:user:public:r-x,default:group:public:r-x,default:other::r-x /user/dw_dev
4.删除目录权限
hdfs dfs -setfacl -b /user/dw_dev
5.删除特定权限,保留其他权限
hdfs dfs -setfacl -x user:public:r-x /user/dw_dev
数据鉴权(数据访问权限检查)
以HDFS Client为例,来分析下HDFS鉴权的过程
在hdfs client分析:hdfs dfs -ls一文中,我们已经知道client在创建出FileSystem对象后,就会去调用fs.listStatus方法去列取目录信息。listStatus底层是通过RPC调用到NameNode的listStatus方法,那么ls的鉴权肯定就是在NameNode的listStatus方法中进行的了。
进入DistributedFileSystem.listStatus方法
//DistributedFileSystem.java
@Override
public FileStatus[] listStatus(Path p) throws IOException {
Path absF = fixRelativePart(p);
return new FileSystemLinkResolver<FileStatus[]>() {
@Override
public FileStatus[] doCall(final Path p)
throws IOException, UnresolvedLinkException {
return listStatusInternal(p);
}
@Override
public FileStatus[] next(final FileSystem fs, final Path p)
throws IOException {
return fs.listStatus(p);
}
}.resolve(this, absF);
}
再进入listStatusInternal(p)
//DistributedFileSystem.java
private FileStatus[] listStatusInternal(Path p) throws IOException {
String src = getPathName(p);
// fetch the first batch of entries in the directory
DirectoryListing thisListing = dfs.listPaths(
src, HdfsFileStatus.EMPTY_NAME);
if (thisListing == null) { // the directory does not exist
throw new FileNotFoundException("File " + p + " does not exist.");
}
...
}
再进入dfs.listPaths(src, HdfsFileStatus.EMPTY_NAME)
//DFSClient.java
public DirectoryListing listPaths(String src, byte[] startAfter)
throws IOException {
return listPaths(src, startAfter, false);
}
再进入listPaths(src, startAfter, false)
//DFSClient.java
public DirectoryListing listPaths(String src, byte[] startAfter,
boolean needLocation) throws IOException {
checkOpen();
TraceScope scope = getPathTraceScope("listPaths", src);
try {
return namenode.getListing(src, startAfter, needLocation);
} catch(RemoteException re) {
throw re.unwrapRemoteException(AccessControlException.class,
FileNotFoundException.class,
UnresolvedPathException.class);
} finally {
scope.close();
}
}
namenode.getListing最终是调用到了NameNodeRPCServer端了
//NameNodeRpcServer.java
public DirectoryListing getListing(String src, byte[] startAfter,
boolean needLocation) throws IOException {
checkNNStartup();
DirectoryListing files = namesystem.getListing(
src, startAfter, needLocation);
if (files != null) {
metrics.incrGetListingOps();
metrics.incrFilesInGetListingOps(files.getPartialListing().length);
}
return files;
}
进入namesystem.getListing
DirectoryListing getListing(String src, byte[] startAfter,
boolean needLocation)
throws IOException {
checkOperation(OperationCategory.READ);
DirectoryListing dl = null;
readLock();
try {
checkOperation(NameNode.OperationCategory.READ);
dl = FSDirStatAndListingOp.getListingInt(dir, src, startAfter,
needLocation);
} catch (AccessControlException e) {
logAuditEvent(false, "listStatus", src);
throw e;
} finally {
readUnlock();
}
logAuditEvent(true, "listStatus", src);
return dl;
}
终于看到AccessControlException出现了,看来getListingInt
static DirectoryListing getListingInt(FSDirectory fsd, final String srcArg,
byte[] startAfter, boolean needLocation) throws IOException {
//创建出FSPermissionChecker对象,此对象包含client端的UGI信息
FSPermissionChecker pc = fsd.getPermissionChecker();
byte[][] pathComponents = FSDirectory
.getPathComponentsForReservedPath(srcArg);
final String startAfterString = new String(startAfter, Charsets.UTF_8);
final String src = fsd.resolvePath(pc, srcArg, pathComponents);
final INodesInPath iip = fsd.getINodesInPath(src, true);
// Get file name when startAfter is an INodePath
if (FSDirectory.isReservedName(startAfterString)) {
byte[][] startAfterComponents = FSDirectory
.getPathComponentsForReservedPath(startAfterString);
try {
String tmp = FSDirectory.resolvePath(src, startAfterComponents, fsd);
byte[][] regularPath = INode.getPathComponents(tmp);
startAfter = regularPath[regularPath.length - 1];
} catch (IOException e) {
// Possibly the inode is deleted
throw new DirectoryListingStartAfterNotFoundException(
"Can't find startAfter " + startAfterString);
}
}
boolean isSuperUser = true;
/**
* 开启鉴权,dfs.permissions.enabled
* 进行path的权限验证
*/
if (fsd.isPermissionEnabled()) {
if (iip.getLastINode() != null && iip.getLastINode().isDirectory()) {
fsd.checkPathAccess(pc, iip, FsAction.READ_EXECUTE);
} else {
fsd.checkTraverse(pc, iip);
}
isSuperUser = pc.isSuperUser();
}
return getListing(fsd, iip, src, startAfter, needLocation, isSuperUser);
}
先来看下fsd.getPermissionChecker()
FSPermissionChecker getPermissionChecker()
throws AccessControlException {
try {
return getPermissionChecker(fsOwnerShortUserName, supergroup,
NameNode.getRemoteUser());
} catch (IOException e) {
throw new AccessControlException(e);
}
}
public static UserGroupInformation getRemoteUser() throws IOException {
UserGroupInformation ugi = Server.getRemoteUser();
return (ugi != null) ? ugi : UserGroupInformation.getCurrentUser();
}
/** Returns the RPC remote user when invoked inside an RPC. Note this
* may be different than the current user if called within another doAs
* @return connection's UGI or null if not an RPC
*/
public static UserGroupInformation getRemoteUser() {
Call call = CurCall.get();
return (call != null && call.connection != null) ? call.connection.user
: null;
}
看到remoteUser信息是从RPC连接中获取到的。
接下来看鉴权的过程fsd.isPermissionEnabled()
在NameNodeServer端获取到了remoteUGI后,就是和path的ugi信息做匹配了,很简单,就不一步步的分析了。
参考文档
HDFS Permissions Guide
Hadoop Groups Mapping
【Linux】理解setuid()、setgid()和sticky位 - puyangsky
Linux SetUID(SUID)文件特殊权限用法详解
Linux SetGID(SGID)文件特殊权限用法详解
Linux Stick BIT(SBIT)文件特殊权限用法详解
HDFS Extended ACLs