记一次Kerberos认证失败的排查过程

2,736 阅读2分钟

我们的presto开启Kerberos认证后,通过jdbc访问的时候提示:"Unable to obtain password from user".对于这种错误提示,一般就是principal和keytab文件配置的有问题,但是因为我们对presto认证模块做了修改,业务部门坚持他们没有配错,所以我的亲自动手找出证据了,首先看堆栈信息:

Caused by: javax.security.auth.login.LoginException: Unable to obtain password from user

	at com.sun.security.auth.module.Krb5LoginModule.promptForPass(Krb5LoginModule.java:903)
	at com.sun.security.auth.module.Krb5LoginModule.attemptAuthentication(Krb5LoginModule.java:766)
	at com.sun.security.auth.module.Krb5LoginModule.login(Krb5LoginModule.java:617)

看到这个堆栈我就放心一大半了,报错的是底层Krb5LoginModule,跟我们修改的模块没有关系,我们从login方法开始debug,上代码:

public boolean login() throws LoginException {
		...
        try {
            //堆栈显示的是这里
            attemptAuthentication(false);
            succeeded = true;
            cleanState();
            return true;
        } catch (LoginException e) {
            ...
        }
    }

接着看看attemptAuthentication方法:

private void attemptAuthentication(boolean getPasswdFromSharedState)
        throws LoginException {
       			...
                if (ktab == null) {
                    promptForPass(getPasswdFromSharedState);
                    builder = new KrbAsReqBuilder(principal, password);
                    if (isInitiator) {
                        // XXX Even if isInitiator=false, it might be
                        // better to do an AS-REQ so that keys can be
                        // updated with PA info
                        cred = builder.action().getCreds();
                    }
                    if (storeKey) {
                        encKeys = builder.getKeys(isInitiator);
                        // When encKeys is empty, the login actually fails.
                        // For compatibility, exception is thrown in commit().
                    }
                } else {
                    builder = new KrbAsReqBuilder(principal, ktab);
                    if (isInitiator) {
                        cred = builder.action().getCreds();
                    }
                }
                builder.destroy();

                ...
    }

明显可以看到ktab为null的时候才会调用promptForPass(getPasswdFromSharedState),ktab为什么是null,程序中配置了keytab文件呀,往上看看:

if (useKeyTab) {
                    if (!unboundServer) {
                        KerberosPrincipal kp =
                                new KerberosPrincipal(principal.getName());
                        ktab = (keyTabName == null)
                                ? KeyTab.getInstance(kp)
                                : KeyTab.getInstance(kp, new File(keyTabName));
                    } else {
                        ktab = (keyTabName == null)
                                ? KeyTab.getUnboundInstance()
                                : KeyTab.getUnboundInstance(new File(keyTabName));
                    }
                    if (isInitiator) {
                        if (Krb5Util.keysFromJavaxKeyTab(ktab, principal).length
                                == 0) {
                            ktab = null;
                            if (debug) {
                                System.out.println
                                    ("Key for the principal " +
                                     principal  +
                                     " not available in " +
                                     ((keyTabName == null) ?
                                      "default key tab" : keyTabName));
                            }
                        }
                    }
                }

上面if-else中对ktab的处理没有问题,只有一种可能Krb5Util.keysFromJavaxKeyTab(ktab, principal).length== 0,我们继续看看为啥length会等于0。

看下keysFromJavaxKeyTab方法:

public static EncryptionKey[] keysFromJavaxKeyTab(
            KeyTab ktab, PrincipalName cname) {
        return snapshotFromJavaxKeyTab(ktab).readServiceKeys(cname);
    }

继续:

public static sun.security.krb5.internal.ktab.KeyTab
            snapshotFromJavaxKeyTab(KeyTab ktab) {
        return KerberosSecrets.getJavaxSecurityAuthKerberosAccess()
                .keyTabTakeSnapshot(ktab);
    }

继续:

public sun.security.krb5.internal.ktab.KeyTab keyTabTakeSnapshot(
            KeyTab ktab) {
        return ktab.takeSnapshot();
    }

继续:

sun.security.krb5.internal.ktab.KeyTab takeSnapshot() {
        try {
            return sun.security.krb5.internal.ktab.KeyTab.getInstance(file);
        } catch (AccessControlException ace) {
            if (file != null) {
                // It's OK to show the name if caller specified it
                throw ace;
            } else {
                AccessControlException ace2 = new AccessControlException(
                        "Access to default keytab denied (modified exception)");
                ace2.setStackTrace(ace.getStackTrace());
                throw ace2;
            }
        }
    }

debug显示这里的file是程序中配置的keytab文件路径,继续:

public static KeyTab getInstance(File file) {
        if (file == null) {
            return getInstance();
        } else {
            return getInstance0(file.getPath());
        }
    }

继续:

private synchronized static KeyTab getInstance0(String s) {
        long lm = new File(s).lastModified();
        KeyTab old = map.get(s);
        if (old != null && old.isValid() && old.lastModified == lm) {
            return old;
        }
        KeyTab ktab = new KeyTab(s);
        if (ktab.isValid()) {               // A valid new keytab
            map.put(s, ktab);
            return ktab;
        } else if (old != null) {           // An existing old one
            return old;
        } else {
            return ktab;                    // first read is invalid
        }
    }

可以看到最终是在getInstance0方法中构造的keytab对象,让我们看看是怎么构造的:

private KeyTab(String filename) {
        tabName = filename;
        try {
            lastModified = new File(tabName).lastModified();
            try (KeyTabInputStream kis =
                    new KeyTabInputStream(new FileInputStream(filename))) {
                load(kis);
            }
        } catch (FileNotFoundException e) {
            entries.clear();
            isMissing = true;
        } catch (Exception ioe) {
            entries.clear();
            isValid = false;
        }
    }

构造一个inputStream,然后加载文件内容,到这里就很明显了,debug显示文件filename不存在,一般情况就是应用程序中写的相对路径不对导致的。