通常用于身份认证。
简介
Kerberos 协议 (详见 RFC1510 介绍) 是一种网络身份验证的协议 (注意只有验证无授权),用户只需输入一次身份验证信息,就可凭借票据访问多个接入的服务,从而实现 SSO 。
基本概念。
Principal
标识身份,由Primary
、Instance
(可选)、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
验证TGT
与Authenticator
,为客户端提供Service Tickets
。
如下介绍部分重点内容。
Principal
分成了用户 Principal
和 服务 Principal
两种形式:
- 用户,格式为
Name[/Instance]@REALM
其中Instance
可选,通常用于更好的限定用户的类型; - 服务,格式为
Service/Hostname@REALM
第一部分为服务名,例如hive
、hadoop
等,主机名通常可以做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
进行运维,包含了 kadmin
和 kadmin.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;
}
当前只是找到了规避方案,但是根因不太确定。