Kerberos 基本介绍

2022-09-21 warehouse

通常用于身份认证。

简介

Kerberos 协议 (详见 RFC1510 介绍) 是一种网络身份验证的协议 (注意只有验证无授权),用户只需输入一次身份验证信息,就可凭借票据访问多个接入的服务,从而实现 SSO 。

基本概念。

  • Principal 标识身份,由 PrimaryInstance(可选)、Realm 三部分组成,其中带有 Instance 的一般是服务端的。
  • Keytab 包含了多个 Principal 与密码的文件,用户可以利用该文件进行身份认证。
  • Ticket Cache 在客户端与 KDC 交互完成后,包含身份认证信息的文件,短期有效,需要不断更新。
  • Realm 表示一个认证管理域,同一个域的认证方式相同,例如 ACL 设置、Database 位置等。

另外,Key Distribution Center, KDC 是 Kerberos 的核心组件,主要由三个部分组成:

  • Kerberos Database 包含了一个 Realm 中所有的 principal、密码与其它相关信息,默认使用 Berkeley DB 存储。
  • Authentication Service, AS 进行用户信息认证,为客户端提供 Ticket Granting Tickets, TGT
  • Ticket Granting Service, TGS 验证 TGTAuthenticator,为客户端提供 Service Tickets

如下介绍部分重点内容。

Principal

分成了用户 Principal 和 服务 Principal 两种形式:

  • 用户,格式为 Name[/Instance]@REALM 其中 Instance 可选,通常用于更好的限定用户的类型;
  • 服务,格式为 Service/Hostname@REALM 第一部分为服务名,例如 hivehadoop 等,主机名通常可以做 DNS 解析获取,

过期时间

Ticket 的生命周期包括了两个:A) Ticket Lifetime 票据生命周期;B) Renewable Lifetime 可续期的生命周期;一般来说,Renewable 要大于 Ticket 的时间。

例如有 ticket_lifetime=1d renew_lifetime=7d 配置,那么就意味着,如果 1d 内没有续期将无法续期;续期一次后有效期更新为 1d;登陆超过 7d 后不再允许续期,需要重新登陆。

renew_lifetime 配置为 0 时,表示禁用票据续订功能。

安装部署

如下介绍基本的操作。

----- 如果是客户端则无需安装server服务
# yum install krb5-server krb5-workstation krb5-libs

----- 默认的配置文件,可以通过 KRB5_KDC_PROFILE 环境变量覆盖
# cat /var/kerberos/krb5kdc/kdc.conf
[realms] # KDC管辖的realm相关配置
EXAMPLE.COM = {
     #master_key_type = aes256-cts
     acl_file = /var/kerberos/krb5kdc/kadm5.acl  # 管理员权限的访问列表
     dict_file = /usr/share/dict/words # 直接使用Linux的文件,作为密码的黑名单
     admin_keytab = /var/kerberos/krb5kdc/kadm5.keytab  # 免交互式认证的文件存储位置
     supported_enctypes = aes256-cts:normal aes128-cts:normal arcfour-hmac:normal camellia256-cts:normal camellia128-cts:normal
}

----- 配置哪些principal具有管理kdc数据库的权限
# cat /var/kerberos/krb5kdc/kadm5.acl
*/admin@EXAMPLE.COM *

其中默认的服务配置文件为 /etc/krb5.conf,其内容如下。

[libdefaults]
    dns_lookup_realm = false
    ticket_lifetime = 24h
    renew_lifetime = 7d
    forwardable = true
    rdns = false
    pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
    spake_preauth_groups = edwards25519
    default_realm = EXAMPLE.COM
    default_ccache_name = KEYRING:persistent:%{uid}

# 标识不同KDC的位置,通过IP:Port配置,如果是域名则需要确认DNS可用
[realms]
EXAMPLE.COM = {
    kdc = kerberos.example.com
    admin_server = kerberos.example.com
}

# 将主机名进行映射,用来将不同的host或者域名映射到不同的realm上
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM

接着需要初始化。

----- 创建Kerberos的数据库,默认在 /var/kerberos/krb5kdc 目录下生成 principal 相关文件
kdb5_util create -s -r EXAMPLE.COM
* -r  krb5.conf 中有多个时需要指定 realm 值。
* -s 如果不添加该参数每次KDC重启都需要输入密码,这里将登陆认证信息缓存,这样重启无需密码

----- 添加管理Database的Principal
/usr/sbin/kadmin.local -q "addprinc root/admin"

----- 启动服务,其中kadmin服务主要是为kadmin命令行维护对应策略
systemctl start krb5kdc
systemctl start kadmin

----- 登录kadmin.local导出kadmin服务的keytab文件
kadmin.local:  ktadd -k /var/kerberos/krb5kdc/krb5.keytab root/admin

----- 使用Keytab登录,同时会缓存在Cache中
kinit -kt /var/kerberos/krb5kdc/krb5.keytab root/admin
kinit -ekt /var/kerberos/krb5kdc/krb5.keytab root/admin
----- 然后可以查看票据,销毁通过 kdestroy 命令
klist

如果是在其它 Client 节点,则需要将 /etc/krb5.conf/var/kerberos/krb5kdc/krb5.keytab 复制过去。

日常运维

主要是通过 kadmin 进行运维,包含了 kadminkadmin.local 两个,前者是访问 kadmin server 进程,而后者则是在 kdc 服务器上直接访问 kdc 数据库,不依赖 kadmin server,功能相同。

$ kadmin.local -p root/admin
----- 查看KDC中拥有的principal列表
kadmin.local: listprincs
----- 创建和清理principal
kadmin.local: addprinc -randkey hive
kadmin.local: delprinc hive
----- 生成Keytab文件
kadmin.local: ktadd -k /opt/hive.keytab hive

注意,生成 keytab 文件时,默认会生成长串随机密码覆盖原密码,这样就只能通过 keytab 登录了,可以通过 -norandkey 参数允许原密码登录。

另外,相关的命令可以直接通过 ? 查看。

GSSException

问题很诡异,配置了 Kerberos 认证之后,隔一段时间会有如下的报错,详细报错信息如下。

Caused by: org.ietf.jgss.GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)
        at sun.security.jgss.krb5.Krb5InitCredential.getInstance(Krb5InitCredential.java:162) ~[?:1.8.0_372]
        at sun.security.jgss.krb5.Krb5MechFactory.getCredentialElement(Krb5MechFactory.java:122) ~[?:1.8.0_372]
        at sun.security.jgss.krb5.Krb5MechFactory.getMechanismContext(Krb5MechFactory.java:189) ~[?:1.8.0_372]
        at sun.security.jgss.GSSManagerImpl.getMechanismContext(GSSManagerImpl.java:224) ~[?:1.8.0_372]
        at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:212) ~[?:1.8.0_372]
        at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:179) ~[?:1.8.0_372]
        at com.sun.security.sasl.gsskerb.GssKrb5Client.evaluateChallenge(GssKrb5Client.java:192) ~[?:1.8.0_372]
        ... 36 more

上述的报错是在 Java 代码中,报错的栈信息在如下的文件中。

// src/share/classes/sun/security/jgss/krb5/Krb5InitCredential.java
static Krb5InitCredential getInstance(GSSCaller caller, Krb5NameElement name, int initLifetime)
            throws GSSException {
    KerberosTicket tgt = getTgt(caller, name, initLifetime);
    if (tgt == null)
        throw new GSSException(GSSException.NO_CRED, -1, "Failed to find any Kerberos tgt");
	... ...
}

private static KerberosTicket getTgt(GSSCaller caller, Krb5NameElement name, int initLifetime)
            throws GSSException {
	... ...
    try {
        final GSSCaller realCaller = (caller == GSSCaller.CALLER_UNKNOWN)
                               ? GSSCaller.CALLER_INITIATE
                               : caller;
        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<KerberosTicket>() {
            public KerberosTicket run() throws Exception {
                // It's OK to use null as serverPrincipal. TGT is almost
                // the first ticket for a principal and we use list.
                return Krb5Util.getTicket(realCaller, clientPrincipal, null, acc);
                    }});
    } catch (PrivilegedActionException e) {
	... ...
    }
}

也就是说,最终出错的是在如下函数,默认只从 AccessControlContext 中获取,如果 ticket==null-Djavax.security.auth.useSubjectCredsOnly=false 时,会通过 HadoopLoginModule 重新登录的方式来获取 Subject,也就是说,会进行一次重试。

// src/share/classes/sun/security/jgss/krb5/Krb5Util.java
static KerberosTicket getTicket(GSSCaller caller,
    String clientPrincipal, String serverPrincipal,
    AccessControlContext acc) throws LoginException {

    // Try to get ticket from acc's Subject
    Subject accSubj = Subject.getSubject(acc);
    KerberosTicket ticket =
        SubjectComber.find(accSubj, serverPrincipal, clientPrincipal,
              KerberosTicket.class);

    // Try to get ticket from Subject obtained from GSSUtil
    if (ticket == null && !GSSUtil.useSubjectCredsOnly(caller)) {
        Subject subject = GSSUtil.login(caller, GSSUtil.GSS_KRB5_MECH_OID);
        ticket = SubjectComber.find(subject,
            serverPrincipal, clientPrincipal, KerberosTicket.class);
    }
    return ticket;
}

当前只是找到了规避方案,但是根因不太确定。