Hadoop 集群交互式用户的永久 Kerberos 票证

Permanent Kerberos tickets for interactive users of Hadoop cluster

我有一个 Hadoop 集群,它使用公司的 Active Directory 作为 Kerberos 领域。节点和最终用户 Linux 工作站都是 Ubuntu 16.04。它们使用 PowerBroker PBIS 加入同一个域,因此工作站和网格节点之间的 SSH 登录是单点登录。最终用户 运行 长运行ning 脚本来自他们的工作站,重复使用 SSH 首先在集群上启动 Spark / Yarn 作业,然后跟踪他们的进度,必须保持 运行在 Kerberos 票证的 10 小时生命周期之外过夜和周末。

我正在寻找一种方法来为用户安装永久的、服务式的 Kerberos 密钥表,从而使他们无需处理 kinit。我知道这意味着任何人 shell 作为特定用户访问网格将能够作为该用户进行身份验证。

我还注意到使用密码执行非 SSO SSH 登录会自动创建从登录时起有效的网络票证。如果可以为 SSO 登录启用此行为,那将解决我的问题。

如果您正在访问 Hive/Hbase 或需要 kerberos 票证的任何其他组件,请让您的 spark 代码在票证过期时重新登录。您必须更新票证才能使用密钥表,而不是依赖缓存中已存在的 TGT。这是通过使用 Hadoop 安全包中的 UserGroupInformation class 完成的。在你的 spark job 中添加下面的代码片段 运行-

val configuration = new Configuration
configuration.addResource("/etc/hadoop/conf/hdfs-site.xml")
UserGroupInformation.setConfiguration(configuration)

UserGroupInformation.getCurrentUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS)
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
  "hadoop.kerberos.principal", " path of hadoop.kerberos.keytab file")
  .doAs(new PrivilegedExceptionAction[Unit]() {
    @Override
    def run(): Unit = {
       //hbase/hive connection
      // logic

    }
  })

上面我们指定了服务主体的名称和我们生成的密钥表文件的路径。只要该 keytab 有效,我们的程序就会对所有操作使用所需的服务主体,无论用户 运行 程序是否已经通过身份验证并收到 TGT。

如果除了spark之外没有其他组件访问,那么就不需要写上面的代码了。只需在您的 spark submit 命令中提供 keytab 和 principal。

spark-submit --master yarn-cluster --keytab "xxxxxx.keytab" --principal "svc-xxxx@xxxx.COM"  xxxx.jar

您只需要求用户将 --principal--keytab 参数添加到他们的 Spark 作业中。然后 Spark(实际上是 YARN)代码会自动为你更新票证。使用这种方法,我们有 运行 数周的工作。

参见示例https://spark.apache.org/docs/latest/security.html#yarn-mode

For long-running apps like Spark Streaming apps to be able to write to HDFS, it is possible to pass a principal and keytab to spark-submit via the --principal and --keytab parameters respectively. The keytab passed in will be copied over to the machine running the Application Master via the Hadoop Distributed Cache (securely - if YARN is configured with SSL and HDFS encryption is enabled). The Kerberos login will be periodically renewed using this principal and keytab and the delegation tokens required for HDFS will be generated periodically so the application can continue writing to HDFS.

当 Yarn 更新 Kerberos 票证时,您可以在 Spark 驱动程序日志中看到。

我采纳了上面的建议,使用 --keytab 参数在我提交给 Spark 的网格节点上指定自定义密钥表。我使用下面的脚本创建自己的每用户密钥表。它一直保持到用户更改密码。

请注意,该脚本做出了简化假设,即 Kerberos 领域与定义用户的 DNS 域和 LDAP 目录相同。这适用于我的设置,请小心使用您的设置。它还希望用户成为该网格节点上的 sudoers。更完善的脚本可能会将密钥表生成和安装分开。

#!/usr/bin/python2.7

from __future__ import print_function

import os
import sys
import stat
import getpass
import subprocess
import collections
import socket
import tempfile

def runSudo(cmd, pw):
    try:
        subprocess.check_call("echo '{}' | sudo -S -p '' {}".format(pw, cmd), shell = True)
        return True
    except subprocess.CalledProcessError:
        return False

def testPassword(pw):
    subprocess.check_call("sudo -k", shell = True)
    if not runSudo("true", pw):
        print("Incorrect password for user {}".format(getpass.getuser()), file = sys.stderr)
        sys.exit(os.EX_NOINPUT)    

class KeytabFile(object):
    def __init__(self, pw):
        self.userName = getpass.getuser()
        self.pw = pw
        self.targetPath = "/etc/security/keytabs/{}.headless.keytab".format(self.userName)
        self.tempFile = None

    KeytabEntry = collections.namedtuple("KeytabEntry", ("kvno", "principal", "encryption"))

    def LoadExistingKeytab(self):
        if not os.access(self.targetPath, os.R_OK):

            # Note: the assumption made here, that the Kerberos realm is same as the DNS domain,
            # may not hold in other setups
            domainName = ".".join(socket.getfqdn().split(".")[1:])

            encryptions = ("aes128-cts-hmac-sha1-96", "arcfour-hmac", "aes256-cts-hmac-sha1-96")
            return [
                self.KeytabEntry(0, "@".join( (self.userName, domainName)), encryption)
                    for encryption in encryptions ]

        def parseLine(keytabLine):
            tokens = keytabLine.strip().split(" ")
            return self.KeytabEntry(int(tokens[0]), tokens[1], tokens[2].strip("()"))

        cmd ="klist -ek {} | tail -n+4".format(self.targetPath)
        entryLines = subprocess.check_output(cmd, shell = True).splitlines()
        return map(parseLine, entryLines)

    class KtUtil(subprocess.Popen):
        def __init__(self):
            subprocess.Popen.__init__(self, "ktutil",
                stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE, shell = True)

        def SendLine(self, line, expectPrompt = True):
            self.stdin.write(bytes(line + "\n"))
            self.stdin.flush()
            if expectPrompt:
                self.stdout.readline()

        def Quit(self):
            self.SendLine("quit", False)
            rc = self.wait()
            if rc != 0:
                raise subprocess.CalledProcessError(rc, "ktutil")


    def InstallUpdatedKeytab(self):
        fd, tempKt = tempfile.mkstemp(suffix = ".keytab")
        os.close(fd)
        entries = self.LoadExistingKeytab()
        ktutil = self.KtUtil()
        for entry in entries:
            cmd = "add_entry -password -p {} -k {} -e {}".format(
                entry.principal, entry.kvno + 1, entry.encryption)

            ktutil.SendLine(cmd)
            ktutil.SendLine(self.pw)

        os.unlink(tempKt)
        ktutil.SendLine("write_kt {}".format(tempKt))
        ktutil.Quit()

        if not runSudo("mv {} {}".format(tempKt, self.targetPath), self.pw):
            os.unlink(tempKt)
            print("Failed to install the keytab to {}.".format(self.targetPath), file = sys.stderr)
            sys.exit(os.EX_CANTCREAT)

        os.chmod(self.targetPath, stat.S_IRUSR)
        # TODO: Also change group to 'hadoop'

if __name__ == '__main__':

    def main():
        userPass = getpass.getpass("Please enter your password: ")
        testPassword(userPass)
        kt = KeytabFile(userPass)
        kt.InstallUpdatedKeytab()

    main()