|
@@ -0,0 +1,976 @@
|
|
|
+/*
|
|
|
+ * Licensed to the Apache Software Foundation (ASF) under one
|
|
|
+ * or more contributor license agreements. See the NOTICE file
|
|
|
+ * distributed with this work for additional information
|
|
|
+ * regarding copyright ownership. The ASF licenses this file
|
|
|
+ * to you under the Apache License, Version 2.0 (the
|
|
|
+ * "License"); you may not use this file except in compliance
|
|
|
+ * with the License. You may obtain a copy of the License at
|
|
|
+ *
|
|
|
+ * http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+ *
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
+ * limitations under the License.
|
|
|
+ */
|
|
|
+
|
|
|
+package org.apache.hadoop.security;
|
|
|
+
|
|
|
+import org.apache.commons.io.IOUtils;
|
|
|
+import org.apache.directory.server.kerberos.shared.keytab.Keytab;
|
|
|
+import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry;
|
|
|
+import org.apache.directory.shared.kerberos.components.EncryptionKey;
|
|
|
+import org.apache.hadoop.conf.Configuration;
|
|
|
+import org.apache.hadoop.conf.Configured;
|
|
|
+import org.apache.hadoop.io.Text;
|
|
|
+import org.apache.hadoop.security.token.Token;
|
|
|
+import org.apache.hadoop.security.token.TokenIdentifier;
|
|
|
+import org.apache.hadoop.util.ExitUtil;
|
|
|
+import org.apache.hadoop.util.Shell;
|
|
|
+import org.apache.hadoop.util.StringUtils;
|
|
|
+import org.apache.hadoop.util.Tool;
|
|
|
+import org.apache.hadoop.util.ToolRunner;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import java.io.Closeable;
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileInputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.PrintWriter;
|
|
|
+import java.lang.reflect.InvocationTargetException;
|
|
|
+import java.net.InetAddress;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.LinkedList;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.*;
|
|
|
+import static org.apache.hadoop.security.UserGroupInformation.*;
|
|
|
+import static org.apache.hadoop.security.authentication.util.KerberosUtil.*;
|
|
|
+import static org.apache.hadoop.util.StringUtils.popOption;
|
|
|
+import static org.apache.hadoop.util.StringUtils.popOptionWithArgument;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Kerberos diagnostics
|
|
|
+ *
|
|
|
+ * This operation expands some of the diagnostic output of the security code,
|
|
|
+ * but not all. For completeness
|
|
|
+ *
|
|
|
+ * Set the environment variable {@code HADOOP_JAAS_DEBUG=true}
|
|
|
+ * Set the log level for {@code org.apache.hadoop.security=DEBUG}
|
|
|
+ */
|
|
|
+public class KDiag extends Configured implements Tool, Closeable {
|
|
|
+
|
|
|
+ private static final Logger LOG = LoggerFactory.getLogger(KDiag.class);
|
|
|
+ /**
|
|
|
+ * Location of the kerberos ticket cache as passed down via an environment
|
|
|
+ * variable. This is what kinit will use by default: {@value}
|
|
|
+ */
|
|
|
+ public static final String KRB5_CCNAME = "KRB5CCNAME";
|
|
|
+ public static final String JAVA_SECURITY_KRB5_CONF
|
|
|
+ = "java.security.krb5.conf";
|
|
|
+ public static final String JAVA_SECURITY_KRB5_REALM
|
|
|
+ = "java.security.krb5.realm";
|
|
|
+ public static final String JAVA_SECURITY_KRB5_KDC_ADDRESS
|
|
|
+ = "java.security.krb5.kdc";
|
|
|
+ public static final String SUN_SECURITY_KRB5_DEBUG
|
|
|
+ = "sun.security.krb5.debug";
|
|
|
+ public static final String SUN_SECURITY_SPNEGO_DEBUG
|
|
|
+ = "sun.security.spnego.debug";
|
|
|
+ public static final String SUN_SECURITY_JAAS_FILE
|
|
|
+ = "java.security.auth.login.config";
|
|
|
+ public static final String KERBEROS_KINIT_COMMAND
|
|
|
+ = "hadoop.kerberos.kinit.command";
|
|
|
+
|
|
|
+ public static final String HADOOP_AUTHENTICATION_IS_DISABLED
|
|
|
+ = "Hadoop authentication is disabled";
|
|
|
+ public static final String UNSET = "(unset)";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * String seen in {@code getDefaultRealm()} exceptions if the user has
|
|
|
+ * no realm: {@value}.
|
|
|
+ */
|
|
|
+ public static final String NO_DEFAULT_REALM = "Cannot locate default realm";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The exit code for a failure of the diagnostics: 41 == HTTP 401 == unauth.
|
|
|
+ */
|
|
|
+ public static final int KDIAG_FAILURE = 41;
|
|
|
+ public static final String DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS
|
|
|
+ = "dfs.data.transfer.saslproperties.resolver.class";
|
|
|
+ public static final String DFS_DATA_TRANSFER_PROTECTION
|
|
|
+ = "dfs.data.transfer.protection";
|
|
|
+ public static final String ETC_KRB5_CONF = "/etc/krb5.conf";
|
|
|
+ public static final String ETC_NTP = "/etc/ntp.conf";
|
|
|
+ public static final String HADOOP_JAAS_DEBUG = "HADOOP_JAAS_DEBUG";
|
|
|
+
|
|
|
+ private PrintWriter out;
|
|
|
+ private File keytab;
|
|
|
+ private String principal;
|
|
|
+ private long minKeyLength = 256;
|
|
|
+ private boolean securityRequired;
|
|
|
+ private boolean nofail = false;
|
|
|
+ private boolean nologin = false;
|
|
|
+ private boolean jaas = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Flag set to true if a {@link #verify(boolean, String, String, Object...)}
|
|
|
+ * probe failed.
|
|
|
+ */
|
|
|
+ private boolean probeHasFailed = false;
|
|
|
+
|
|
|
+ public static final String CAT_CONFIG = "CONFIG";
|
|
|
+ public static final String CAT_JAAS = "JAAS";
|
|
|
+ public static final String CAT_JVM = "JVM";
|
|
|
+ public static final String CAT_KERBEROS = "KERBEROS";
|
|
|
+ public static final String CAT_LOGIN = "LOGIN";
|
|
|
+ public static final String CAT_OS = "JAAS";
|
|
|
+ public static final String CAT_SASL = "SASL";
|
|
|
+ public static final String CAT_UGI = "UGI";
|
|
|
+
|
|
|
+ public static final String ARG_KEYLEN = "--keylen";
|
|
|
+ public static final String ARG_KEYTAB = "--keytab";
|
|
|
+ public static final String ARG_JAAS = "--jaas";
|
|
|
+ public static final String ARG_NOFAIL = "--nofail";
|
|
|
+ public static final String ARG_NOLOGIN = "--nologin";
|
|
|
+ public static final String ARG_OUTPUT = "--out";
|
|
|
+ public static final String ARG_PRINCIPAL = "--principal";
|
|
|
+ public static final String ARG_RESOURCE = "--resource";
|
|
|
+
|
|
|
+ public static final String ARG_SECURE = "--secure";
|
|
|
+
|
|
|
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
|
|
|
+ public KDiag(Configuration conf,
|
|
|
+ PrintWriter out,
|
|
|
+ File keytab,
|
|
|
+ String principal,
|
|
|
+ long minKeyLength,
|
|
|
+ boolean securityRequired) {
|
|
|
+ super(conf);
|
|
|
+ this.keytab = keytab;
|
|
|
+ this.principal = principal;
|
|
|
+ this.out = out;
|
|
|
+ this.minKeyLength = minKeyLength;
|
|
|
+ this.securityRequired = securityRequired;
|
|
|
+ }
|
|
|
+
|
|
|
+ public KDiag() {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void close() throws IOException {
|
|
|
+ flush();
|
|
|
+ if (out != null) {
|
|
|
+ out.close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int run(String[] argv) throws Exception {
|
|
|
+ List<String> args = new LinkedList<>(Arrays.asList(argv));
|
|
|
+ String keytabName = popOptionWithArgument(ARG_KEYTAB, args);
|
|
|
+ if (keytabName != null) {
|
|
|
+ keytab = new File(keytabName);
|
|
|
+ }
|
|
|
+ principal = popOptionWithArgument(ARG_PRINCIPAL, args);
|
|
|
+ String outf = popOptionWithArgument(ARG_OUTPUT, args);
|
|
|
+ String mkl = popOptionWithArgument(ARG_KEYLEN, args);
|
|
|
+ if (mkl != null) {
|
|
|
+ minKeyLength = Integer.parseInt(mkl);
|
|
|
+ }
|
|
|
+ securityRequired = popOption(ARG_SECURE, args);
|
|
|
+ nofail = popOption(ARG_NOFAIL, args);
|
|
|
+ jaas = popOption(ARG_JAAS, args);
|
|
|
+ nologin = popOption(ARG_NOLOGIN, args);
|
|
|
+
|
|
|
+ // look for list of resources
|
|
|
+ String resource;
|
|
|
+ while (null != (resource = popOptionWithArgument(ARG_RESOURCE, args))) {
|
|
|
+ // loading a resource
|
|
|
+ LOG.info("Loading resource {}", resource);
|
|
|
+ try (InputStream in =
|
|
|
+ getClass().getClassLoader().getResourceAsStream(resource)) {
|
|
|
+ if (verify(in != null, CAT_CONFIG, "No resource %s", resource)) {
|
|
|
+ Configuration.addDefaultResource(resource);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // look for any leftovers
|
|
|
+ if (!args.isEmpty()) {
|
|
|
+ println("Unknown arguments in command:");
|
|
|
+ for (String s : args) {
|
|
|
+ println(" \"%s\"", s);
|
|
|
+ }
|
|
|
+ println();
|
|
|
+ println(usage());
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ if (outf != null) {
|
|
|
+ println("Printing output to %s", outf);
|
|
|
+ out = new PrintWriter(new File(outf), "UTF-8");
|
|
|
+ }
|
|
|
+ execute();
|
|
|
+ return probeHasFailed ? KDIAG_FAILURE : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String usage() {
|
|
|
+ return "KDiag: Diagnose Kerberos Problems\n"
|
|
|
+ + arg("-D", "key=value", "Define a configuration option")
|
|
|
+ + arg(ARG_JAAS, "",
|
|
|
+ "Require a JAAS file to be defined in " + SUN_SECURITY_JAAS_FILE)
|
|
|
+ + arg(ARG_KEYLEN, "<keylen>",
|
|
|
+ "Require a minimum size for encryption keys supported by the JVM."
|
|
|
+ + " Default value : "+ minKeyLength)
|
|
|
+ + arg(ARG_KEYTAB, "<keytab> " + ARG_PRINCIPAL + " <principal>",
|
|
|
+ "Login from a keytab as a specific principal")
|
|
|
+ + arg(ARG_NOFAIL, "", "Do not fail on the first problem")
|
|
|
+ + arg(ARG_NOLOGIN, "", "Do not attempt to log in")
|
|
|
+ + arg(ARG_OUTPUT, "<file>", "Write output to a file")
|
|
|
+ + arg(ARG_RESOURCE, "<resource>", "Load an XML configuration resource")
|
|
|
+ + arg(ARG_SECURE, "", "Require the hadoop configuration to be secure");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String arg(String name, String params, String meaning) {
|
|
|
+ return String.format(" [%s%s%s] : %s",
|
|
|
+ name, (!params.isEmpty() ? " " : ""), params, meaning) + ".\n";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Execute diagnostics.
|
|
|
+ * <p>
|
|
|
+ * Things it would be nice if UGI made accessible
|
|
|
+ * <ol>
|
|
|
+ * <li>A way to enable JAAS debug programatically</li>
|
|
|
+ * <li>Access to the TGT</li>
|
|
|
+ * </ol>
|
|
|
+ * @return true if security was enabled and all probes were successful
|
|
|
+ * @throws KerberosDiagsFailure explicitly raised failure
|
|
|
+ * @throws Exception other security problems
|
|
|
+ */
|
|
|
+ @SuppressWarnings("deprecation")
|
|
|
+ public boolean execute() throws Exception {
|
|
|
+
|
|
|
+ title("Kerberos Diagnostics scan at %s",
|
|
|
+ new Date(System.currentTimeMillis()));
|
|
|
+
|
|
|
+ // check that the machine has a name
|
|
|
+ println("Hostname = %s",
|
|
|
+ InetAddress.getLocalHost().getCanonicalHostName());
|
|
|
+
|
|
|
+ println("%s = %d", ARG_KEYLEN, minKeyLength);
|
|
|
+ println("%s = %s", ARG_KEYTAB, keytab);
|
|
|
+ println("%s = %s", ARG_PRINCIPAL, principal);
|
|
|
+
|
|
|
+ // Fail fast on a JVM without JCE installed.
|
|
|
+ validateKeyLength();
|
|
|
+
|
|
|
+ // look at realm
|
|
|
+ println("JVM Kerberos Login Module = %s", getKrb5LoginModuleName());
|
|
|
+
|
|
|
+ title("Core System Properties");
|
|
|
+ for (String prop : new String[]{
|
|
|
+ "user.name",
|
|
|
+ "java.version",
|
|
|
+ "java.vendor",
|
|
|
+ JAVA_SECURITY_KRB5_CONF,
|
|
|
+ JAVA_SECURITY_KRB5_REALM,
|
|
|
+ JAVA_SECURITY_KRB5_KDC_ADDRESS,
|
|
|
+ SUN_SECURITY_KRB5_DEBUG,
|
|
|
+ SUN_SECURITY_SPNEGO_DEBUG,
|
|
|
+ SUN_SECURITY_JAAS_FILE
|
|
|
+ }) {
|
|
|
+ printSysprop(prop);
|
|
|
+ }
|
|
|
+ endln();
|
|
|
+
|
|
|
+ title("All System Properties");
|
|
|
+ ArrayList<String> propList = new ArrayList<>(
|
|
|
+ System.getProperties().stringPropertyNames());
|
|
|
+ Collections.sort(propList, String.CASE_INSENSITIVE_ORDER);
|
|
|
+ for (String s : propList) {
|
|
|
+ printSysprop(s);
|
|
|
+ }
|
|
|
+ endln();
|
|
|
+
|
|
|
+ title("Environment Variables");
|
|
|
+ for (String env : new String[]{
|
|
|
+ HADOOP_JAAS_DEBUG,
|
|
|
+ KRB5_CCNAME,
|
|
|
+ HADOOP_USER_NAME,
|
|
|
+ HADOOP_PROXY_USER,
|
|
|
+ HADOOP_TOKEN_FILE_LOCATION,
|
|
|
+ "HADOOP_SECURE_LOG",
|
|
|
+ "HADOOP_OPTS",
|
|
|
+ "HADOOP_CLIENT_OPTS",
|
|
|
+ }) {
|
|
|
+ printEnv(env);
|
|
|
+ }
|
|
|
+ endln();
|
|
|
+
|
|
|
+ title("Configuration Options");
|
|
|
+ for (String prop : new String[]{
|
|
|
+ KERBEROS_KINIT_COMMAND,
|
|
|
+ HADOOP_SECURITY_AUTHENTICATION,
|
|
|
+ HADOOP_SECURITY_AUTHORIZATION,
|
|
|
+ "hadoop.kerberos.min.seconds.before.relogin", // not in 2.6
|
|
|
+ "hadoop.security.dns.interface", // not in 2.6
|
|
|
+ "hadoop.security.dns.nameserver", // not in 2.6
|
|
|
+ HADOOP_RPC_PROTECTION,
|
|
|
+ HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS,
|
|
|
+ HADOOP_SECURITY_CRYPTO_CODEC_CLASSES_KEY_PREFIX,
|
|
|
+ HADOOP_SECURITY_GROUP_MAPPING,
|
|
|
+ "hadoop.security.impersonation.provider.class", // not in 2.6
|
|
|
+ DFS_DATA_TRANSFER_PROTECTION, // HDFS
|
|
|
+ DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS // HDFS
|
|
|
+ }) {
|
|
|
+ printConfOpt(prop);
|
|
|
+ }
|
|
|
+
|
|
|
+ // check that authentication is enabled
|
|
|
+ Configuration conf = getConf();
|
|
|
+ if (isSimpleAuthentication(conf)) {
|
|
|
+ println(HADOOP_AUTHENTICATION_IS_DISABLED);
|
|
|
+ failif(securityRequired, CAT_CONFIG, HADOOP_AUTHENTICATION_IS_DISABLED);
|
|
|
+ // no security, warn
|
|
|
+ LOG.warn("Security is not enabled for the Hadoop cluster");
|
|
|
+ } else {
|
|
|
+ if (isSimpleAuthentication(new Configuration())) {
|
|
|
+ LOG.warn("The default cluster security is insecure");
|
|
|
+ failif(securityRequired, CAT_CONFIG, HADOOP_AUTHENTICATION_IS_DISABLED);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // now the big test: login, then try again
|
|
|
+ boolean krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG);
|
|
|
+ boolean spnegoDebug = getAndSet(SUN_SECURITY_SPNEGO_DEBUG);
|
|
|
+
|
|
|
+ try {
|
|
|
+ UserGroupInformation.setConfiguration(conf);
|
|
|
+ validateKrb5File();
|
|
|
+ printDefaultRealm();
|
|
|
+ validateSasl(HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS);
|
|
|
+ if (conf.get(DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS) != null) {
|
|
|
+ validateSasl(DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS);
|
|
|
+ }
|
|
|
+ validateKinitExecutable();
|
|
|
+ validateJAAS(jaas);
|
|
|
+ validateNTPConf();
|
|
|
+
|
|
|
+ if (!nologin) {
|
|
|
+ title("Logging in");
|
|
|
+ if (keytab != null) {
|
|
|
+ dumpKeytab(keytab);
|
|
|
+ loginFromKeytab();
|
|
|
+ } else {
|
|
|
+ UserGroupInformation loginUser = getLoginUser();
|
|
|
+ dumpUGI("Log in user", loginUser);
|
|
|
+ validateUGI("Login user", loginUser);
|
|
|
+ println("Ticket based login: %b", isLoginTicketBased());
|
|
|
+ println("Keytab based login: %b", isLoginKeytabBased());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } finally {
|
|
|
+ // restore original system properties
|
|
|
+ System.setProperty(SUN_SECURITY_KRB5_DEBUG,
|
|
|
+ Boolean.toString(krb5Debug));
|
|
|
+ System.setProperty(SUN_SECURITY_SPNEGO_DEBUG,
|
|
|
+ Boolean.toString(spnegoDebug));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the authentication method of this configuration "simple"?
|
|
|
+ * @param conf configuration to check
|
|
|
+ * @return true if auth is simple (i.e. not kerberos)
|
|
|
+ */
|
|
|
+ protected boolean isSimpleAuthentication(Configuration conf) {
|
|
|
+ return SecurityUtil.getAuthenticationMethod(conf)
|
|
|
+ .equals(AuthenticationMethod.SIMPLE);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fail fast on a JVM without JCE installed.
|
|
|
+ *
|
|
|
+ * This is a recurrent problem
|
|
|
+ * (that is: it keeps creeping back with JVM updates);
|
|
|
+ * a fast failure is the best tactic.
|
|
|
+ * @throws NoSuchAlgorithmException
|
|
|
+ */
|
|
|
+
|
|
|
+ protected void validateKeyLength() throws NoSuchAlgorithmException {
|
|
|
+ int aesLen = Cipher.getMaxAllowedKeyLength("AES");
|
|
|
+ println("Maximum AES encryption key length %d bits", aesLen);
|
|
|
+ verify(minKeyLength <= aesLen,
|
|
|
+ CAT_JVM,
|
|
|
+ "Java Cryptography Extensions are not installed on this JVM."
|
|
|
+ + " Maximum supported key length %s - minimum required %d",
|
|
|
+ aesLen, minKeyLength);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the default realm.
|
|
|
+ * <p>
|
|
|
+ * Not having a default realm may be harmless, so is noted at info.
|
|
|
+ * All other invocation failures are downgraded to warn, as
|
|
|
+ * follow-on actions may still work.
|
|
|
+ * Failure to invoke the method via introspection is considered a failure,
|
|
|
+ * as it's a sign of JVM compatibility issues that may have other
|
|
|
+ * consequences
|
|
|
+ */
|
|
|
+ protected void printDefaultRealm() {
|
|
|
+ try {
|
|
|
+ String defaultRealm = getDefaultRealm();
|
|
|
+ println("Default Realm = %s", defaultRealm);
|
|
|
+ if (defaultRealm == null) {
|
|
|
+ warn(CAT_KERBEROS, "Host has no default realm");
|
|
|
+ }
|
|
|
+ } catch (ClassNotFoundException
|
|
|
+ | IllegalAccessException
|
|
|
+ | NoSuchMethodException e) {
|
|
|
+ throw new KerberosDiagsFailure(CAT_JVM, e,
|
|
|
+ "Failed to invoke krb5.Config.getDefaultRealm: %s: " +e, e);
|
|
|
+ } catch (InvocationTargetException e) {
|
|
|
+ Throwable cause = e.getCause() != null ? e.getCause() : e;
|
|
|
+ if (cause.toString().contains(NO_DEFAULT_REALM)) {
|
|
|
+ // exception raised if there is no default realm. This is not
|
|
|
+ // always a problem, so downgrade to a message.
|
|
|
+ warn(CAT_KERBEROS, "Host has no default realm");
|
|
|
+ LOG.debug(cause.toString(), cause);
|
|
|
+ } else {
|
|
|
+ error(CAT_KERBEROS, "Kerberos.getDefaultRealm() failed: %s\n%s",
|
|
|
+ cause, StringUtils.stringifyException(cause));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Locate the {@code krb5.conf} file and dump it.
|
|
|
+ *
|
|
|
+ * No-op on windows.
|
|
|
+ * @throws IOException problems reading the file.
|
|
|
+ */
|
|
|
+ private void validateKrb5File() throws IOException {
|
|
|
+ if (!Shell.WINDOWS) {
|
|
|
+ title("Locating Kerberos configuration file");
|
|
|
+ String krbPath = ETC_KRB5_CONF;
|
|
|
+ String jvmKrbPath = System.getProperty(JAVA_SECURITY_KRB5_CONF);
|
|
|
+ if (jvmKrbPath != null && !jvmKrbPath.isEmpty()) {
|
|
|
+ println("Setting kerberos path from sysprop %s: \"%s\"",
|
|
|
+ JAVA_SECURITY_KRB5_CONF, jvmKrbPath);
|
|
|
+ krbPath = jvmKrbPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ String krb5name = System.getenv(KRB5_CCNAME);
|
|
|
+ if (krb5name != null) {
|
|
|
+ println("Setting kerberos path from environment variable %s: \"%s\"",
|
|
|
+ KRB5_CCNAME, krb5name);
|
|
|
+ krbPath = krb5name;
|
|
|
+ if (jvmKrbPath != null) {
|
|
|
+ println("Warning - both %s and %s were set - %s takes priority",
|
|
|
+ JAVA_SECURITY_KRB5_CONF, KRB5_CCNAME, KRB5_CCNAME);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ File krbFile = new File(krbPath);
|
|
|
+ println("Kerberos configuration file = %s", krbFile);
|
|
|
+ dump(krbFile);
|
|
|
+ endln();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Dump a keytab: list all principals.
|
|
|
+ *
|
|
|
+ * @param keytabFile the keytab file
|
|
|
+ * @throws IOException IO problems
|
|
|
+ */
|
|
|
+ private void dumpKeytab(File keytabFile) throws IOException {
|
|
|
+ title("Examining keytab %s", keytabFile);
|
|
|
+ File kt = keytabFile.getCanonicalFile();
|
|
|
+ verifyFileIsValid(kt, CAT_KERBEROS, "keytab");
|
|
|
+ List<KeytabEntry> entries = Keytab.read(kt).getEntries();
|
|
|
+ println("keytab entry count: %d", entries.size());
|
|
|
+ for (KeytabEntry entry : entries) {
|
|
|
+ EncryptionKey key = entry.getKey();
|
|
|
+ println(" %s: version=%d expires=%s encryption=%s",
|
|
|
+ entry.getPrincipalName(),
|
|
|
+ entry.getKeyVersion(),
|
|
|
+ entry.getTimeStamp(),
|
|
|
+ key.getKeyType());
|
|
|
+ }
|
|
|
+ endln();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Log in from a keytab, dump the UGI, validate it, then try and log in again.
|
|
|
+ *
|
|
|
+ * That second-time login catches JVM/Hadoop compatibility problems.
|
|
|
+ * @throws IOException Keytab loading problems
|
|
|
+ */
|
|
|
+ private void loginFromKeytab() throws IOException {
|
|
|
+ UserGroupInformation ugi;
|
|
|
+ String identity;
|
|
|
+ if (keytab != null) {
|
|
|
+ File kt = keytab.getCanonicalFile();
|
|
|
+ println("Using keytab %s principal %s", kt, principal);
|
|
|
+ identity = principal;
|
|
|
+
|
|
|
+ failif(principal == null, CAT_KERBEROS, "No principal defined");
|
|
|
+ ugi = loginUserFromKeytabAndReturnUGI(principal, kt.getPath());
|
|
|
+ dumpUGI(identity, ugi);
|
|
|
+ validateUGI(principal, ugi);
|
|
|
+
|
|
|
+ title("Attempting to relogin");
|
|
|
+ try {
|
|
|
+ // package scoped -hence the reason why this class must be in the
|
|
|
+ // hadoop.security package
|
|
|
+ setShouldRenewImmediatelyForTests(true);
|
|
|
+ // attempt a new login
|
|
|
+ ugi.reloginFromKeytab();
|
|
|
+ } catch (IllegalAccessError e) {
|
|
|
+ // if you've built this class into an independent JAR, package-access
|
|
|
+ // may fail. Downgrade
|
|
|
+ warn(CAT_UGI, "Failed to reset UGI -and so could not try to relogin");
|
|
|
+ LOG.debug("Failed to reset UGI: {}", e, e);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ println("No keytab: attempting to log in is as current user");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Dump a UGI.
|
|
|
+ *
|
|
|
+ * @param title title of this section
|
|
|
+ * @param ugi UGI to dump
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ private void dumpUGI(String title, UserGroupInformation ugi)
|
|
|
+ throws IOException {
|
|
|
+ title(title);
|
|
|
+ println("UGI instance = %s", ugi);
|
|
|
+ println("Has kerberos credentials: %b", ugi.hasKerberosCredentials());
|
|
|
+ println("Authentication method: %s", ugi.getAuthenticationMethod());
|
|
|
+ println("Real Authentication method: %s",
|
|
|
+ ugi.getRealAuthenticationMethod());
|
|
|
+ title("Group names");
|
|
|
+ for (String name : ugi.getGroupNames()) {
|
|
|
+ println(name);
|
|
|
+ }
|
|
|
+ title("Credentials");
|
|
|
+ List<Text> secretKeys = ugi.getCredentials().getAllSecretKeys();
|
|
|
+ title("Secret keys");
|
|
|
+ if (!secretKeys.isEmpty()) {
|
|
|
+ for (Text secret: secretKeys) {
|
|
|
+ println("%s", secret);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ println("(none)");
|
|
|
+ }
|
|
|
+
|
|
|
+ dumpTokens(ugi);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate the UGI: verify it is kerberized.
|
|
|
+ * @param messagePrefix message in exceptions
|
|
|
+ * @param user user to validate
|
|
|
+ */
|
|
|
+ private void validateUGI(String messagePrefix, UserGroupInformation user) {
|
|
|
+ if (verify(user.getAuthenticationMethod() == AuthenticationMethod.KERBEROS,
|
|
|
+ CAT_LOGIN, "User %s is not authenticated by Kerberos", user)) {
|
|
|
+ verify(user.hasKerberosCredentials(),
|
|
|
+ CAT_LOGIN, "%s: No kerberos credentials for %s", messagePrefix, user);
|
|
|
+ verify(user.getAuthenticationMethod() != null,
|
|
|
+ CAT_LOGIN, "%s: Null AuthenticationMethod for %s", messagePrefix,
|
|
|
+ user);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A cursory look at the {@code kinit} executable.
|
|
|
+ *
|
|
|
+ * If it is an absolute path: it must exist with a size > 0.
|
|
|
+ * If it is just a command, it has to be on the path. There's no check
|
|
|
+ * for that -but the PATH is printed out.
|
|
|
+ */
|
|
|
+ private void validateKinitExecutable() {
|
|
|
+ String kinit = getConf().getTrimmed(KERBEROS_KINIT_COMMAND, "");
|
|
|
+ if (!kinit.isEmpty()) {
|
|
|
+ File kinitPath = new File(kinit);
|
|
|
+ println("%s = %s", KERBEROS_KINIT_COMMAND, kinitPath);
|
|
|
+ if (kinitPath.isAbsolute()) {
|
|
|
+ verifyFileIsValid(kinitPath, CAT_KERBEROS, KERBEROS_KINIT_COMMAND);
|
|
|
+ } else {
|
|
|
+ println("Executable %s is relative -must be on the PATH", kinit);
|
|
|
+ printEnv("PATH");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Try to load the SASL resolver.
|
|
|
+ * @param saslPropsResolverKey key for the SASL resolver
|
|
|
+ */
|
|
|
+ private void validateSasl(String saslPropsResolverKey) {
|
|
|
+ title("Resolving SASL property %s", saslPropsResolverKey);
|
|
|
+ String saslPropsResolver = getConf().getTrimmed(saslPropsResolverKey);
|
|
|
+ try {
|
|
|
+ Class<? extends SaslPropertiesResolver> resolverClass =
|
|
|
+ getConf().getClass(
|
|
|
+ saslPropsResolverKey,
|
|
|
+ SaslPropertiesResolver.class,
|
|
|
+ SaslPropertiesResolver.class);
|
|
|
+ println("Resolver is %s", resolverClass);
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ throw new KerberosDiagsFailure(CAT_SASL, e,
|
|
|
+ "Failed to load %s class %s",
|
|
|
+ saslPropsResolverKey, saslPropsResolver);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate any JAAS entry referenced in the {@link #SUN_SECURITY_JAAS_FILE}
|
|
|
+ * property.
|
|
|
+ * @param jaasRequired is JAAS required
|
|
|
+ */
|
|
|
+ private void validateJAAS(boolean jaasRequired) throws IOException {
|
|
|
+ String jaasFilename = System.getProperty(SUN_SECURITY_JAAS_FILE);
|
|
|
+ if (jaasRequired) {
|
|
|
+ verify(jaasFilename != null, CAT_JAAS,
|
|
|
+ "No JAAS file specified in " + SUN_SECURITY_JAAS_FILE);
|
|
|
+ }
|
|
|
+ if (jaasFilename != null) {
|
|
|
+ title("JAAS");
|
|
|
+ File jaasFile = new File(jaasFilename);
|
|
|
+ println("JAAS file is defined in %s: %s",
|
|
|
+ SUN_SECURITY_JAAS_FILE, jaasFile);
|
|
|
+ verifyFileIsValid(jaasFile, CAT_JAAS,
|
|
|
+ "JAAS file defined in " + SUN_SECURITY_JAAS_FILE);
|
|
|
+ dump(jaasFile);
|
|
|
+ endln();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateNTPConf() throws IOException {
|
|
|
+ if (!Shell.WINDOWS) {
|
|
|
+ File ntpfile = new File(ETC_NTP);
|
|
|
+ if (ntpfile.exists()
|
|
|
+ && verifyFileIsValid(ntpfile, CAT_OS,
|
|
|
+ "NTP file: " + ntpfile)) {
|
|
|
+ title("NTP");
|
|
|
+ dump(ntpfile);
|
|
|
+ endln();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Verify that a file is valid: it is a file, non-empty and readable.
|
|
|
+ * @param file file
|
|
|
+ * @param category category for exceptions
|
|
|
+ * @param text text message
|
|
|
+ * @return true if the validation held; false if it did not <i>and</i>
|
|
|
+ * {@link #nofail} has disabled raising exceptions.
|
|
|
+ */
|
|
|
+ private boolean verifyFileIsValid(File file, String category, String text) {
|
|
|
+ return verify(file.exists(), category,
|
|
|
+ "%s file does not exist: %s",
|
|
|
+ text, file)
|
|
|
+ && verify(file.isFile(), category,
|
|
|
+ "%s path does not refer to a file: %s", text, file)
|
|
|
+ && verify(file.length() != 0, category,
|
|
|
+ "%s file is empty: %s", text, file)
|
|
|
+ && verify(file.canRead(), category,
|
|
|
+ "%s file is not readable: %s", text, file);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Dump all tokens of a UGI.
|
|
|
+ * @param ugi UGI to examine
|
|
|
+ */
|
|
|
+ public void dumpTokens(UserGroupInformation ugi) {
|
|
|
+ Collection<Token<? extends TokenIdentifier>> tokens
|
|
|
+ = ugi.getCredentials().getAllTokens();
|
|
|
+ title("Token Count: %d", tokens.size());
|
|
|
+ for (Token<? extends TokenIdentifier> token : tokens) {
|
|
|
+ println("Token %s", token.getKind());
|
|
|
+ }
|
|
|
+ endln();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set the System property to true; return the old value for caching.
|
|
|
+ *
|
|
|
+ * @param sysprop property
|
|
|
+ * @return the previous value
|
|
|
+ */
|
|
|
+ private boolean getAndSet(String sysprop) {
|
|
|
+ boolean old = Boolean.getBoolean(sysprop);
|
|
|
+ System.setProperty(sysprop, "true");
|
|
|
+ return old;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Flush all active output channels, including {@Code System.err},
|
|
|
+ * so as to stay in sync with any JRE log messages.
|
|
|
+ */
|
|
|
+ private void flush() {
|
|
|
+ if (out != null) {
|
|
|
+ out.flush();
|
|
|
+ } else {
|
|
|
+ System.out.flush();
|
|
|
+ }
|
|
|
+ System.err.flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print a line of output. This goes to any output file, or
|
|
|
+ * is logged at info. The output is flushed before and after, to
|
|
|
+ * try and stay in sync with JRE logging.
|
|
|
+ *
|
|
|
+ * @param format format string
|
|
|
+ * @param args any arguments
|
|
|
+ */
|
|
|
+ private void println(String format, Object... args) {
|
|
|
+ flush();
|
|
|
+ String msg = String.format(format, args);
|
|
|
+ if (out != null) {
|
|
|
+ out.println(msg);
|
|
|
+ } else {
|
|
|
+ System.out.println(msg);
|
|
|
+ }
|
|
|
+ flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print a new line
|
|
|
+ */
|
|
|
+ private void println() {
|
|
|
+ println("");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print something at the end of a section
|
|
|
+ */
|
|
|
+ private void endln() {
|
|
|
+ println();
|
|
|
+ println("-----");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print a title entry.
|
|
|
+ *
|
|
|
+ * @param format format string
|
|
|
+ * @param args any arguments
|
|
|
+ */
|
|
|
+ private void title(String format, Object... args) {
|
|
|
+ println();
|
|
|
+ println();
|
|
|
+ println("== " + String.format(format, args) + " ==");
|
|
|
+ println();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print a system property, or {@link #UNSET} if unset.
|
|
|
+ * @param property property to print
|
|
|
+ */
|
|
|
+ private void printSysprop(String property) {
|
|
|
+ println("%s = \"%s\"", property,
|
|
|
+ System.getProperty(property, UNSET));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print a configuration option, or {@link #UNSET} if unset.
|
|
|
+ *
|
|
|
+ * @param option option to print
|
|
|
+ */
|
|
|
+ private void printConfOpt(String option) {
|
|
|
+ println("%s = \"%s\"", option, getConf().get(option, UNSET));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print an environment variable's name and value; printing
|
|
|
+ * {@link #UNSET} if it is not set.
|
|
|
+ * @param variable environment variable
|
|
|
+ */
|
|
|
+ private void printEnv(String variable) {
|
|
|
+ String env = System.getenv(variable);
|
|
|
+ println("%s = \"%s\"", variable, env != null ? env : UNSET);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Dump any file to standard out.
|
|
|
+ * @param file file to dump
|
|
|
+ * @throws IOException IO problems
|
|
|
+ */
|
|
|
+ private void dump(File file) throws IOException {
|
|
|
+ try (FileInputStream in = new FileInputStream(file)) {
|
|
|
+ for (String line : IOUtils.readLines(in)) {
|
|
|
+ println(line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Format and raise a failure.
|
|
|
+ *
|
|
|
+ * @param category category for exception
|
|
|
+ * @param message string formatting message
|
|
|
+ * @param args any arguments for the formatting
|
|
|
+ * @throws KerberosDiagsFailure containing the formatted text
|
|
|
+ */
|
|
|
+ private void fail(String category, String message, Object... args)
|
|
|
+ throws KerberosDiagsFailure {
|
|
|
+ error(category, message, args);
|
|
|
+ throw new KerberosDiagsFailure(category, message, args);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a condition must hold.
|
|
|
+ *
|
|
|
+ * If not, an exception is raised, or, if {@link #nofail} is set,
|
|
|
+ * an error will be logged and the method return false.
|
|
|
+ *
|
|
|
+ * @param condition condition which must hold
|
|
|
+ * @param category category for exception
|
|
|
+ * @param message string formatting message
|
|
|
+ * @param args any arguments for the formatting
|
|
|
+ * @return true if the verification succeeded, false if it failed but
|
|
|
+ * an exception was not raised.
|
|
|
+ * @throws KerberosDiagsFailure containing the formatted text
|
|
|
+ * if the condition was met
|
|
|
+ */
|
|
|
+ private boolean verify(boolean condition,
|
|
|
+ String category,
|
|
|
+ String message,
|
|
|
+ Object... args)
|
|
|
+ throws KerberosDiagsFailure {
|
|
|
+ if (!condition) {
|
|
|
+ // condition not met: fail or report
|
|
|
+ probeHasFailed = true;
|
|
|
+ if (!nofail) {
|
|
|
+ fail(category, message, args);
|
|
|
+ } else {
|
|
|
+ error(category, message, args);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ // condition is met
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print a message as an error
|
|
|
+ * @param category error category
|
|
|
+ * @param message format string
|
|
|
+ * @param args list of arguments
|
|
|
+ */
|
|
|
+ private void error(String category, String message, Object...args) {
|
|
|
+ println("ERROR: %s: %s", category, String.format(message, args));
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Print a message as an warning
|
|
|
+ * @param category error category
|
|
|
+ * @param message format string
|
|
|
+ * @param args list of arguments
|
|
|
+ */
|
|
|
+ private void warn(String category, String message, Object...args) {
|
|
|
+ println("WARNING: %s: %s", category, String.format(message, args));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Conditional failure with string formatted arguments.
|
|
|
+ * There is no chek for the {@link #nofail} value.
|
|
|
+ * @param condition failure condition
|
|
|
+ * @param category category for exception
|
|
|
+ * @param message string formatting message
|
|
|
+ * @param args any arguments for the formatting
|
|
|
+ * @throws KerberosDiagsFailure containing the formatted text
|
|
|
+ * if the condition was met
|
|
|
+ */
|
|
|
+ private void failif(boolean condition,
|
|
|
+ String category,
|
|
|
+ String message,
|
|
|
+ Object... args)
|
|
|
+ throws KerberosDiagsFailure {
|
|
|
+ if (condition) {
|
|
|
+ fail(category, message, args);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Inner entry point, with no logging or system exits.
|
|
|
+ *
|
|
|
+ * @param conf configuration
|
|
|
+ * @param argv argument list
|
|
|
+ * @return an exception
|
|
|
+ * @throws Exception
|
|
|
+ */
|
|
|
+ public static int exec(Configuration conf, String... argv) throws Exception {
|
|
|
+ try(KDiag kdiag = new KDiag()) {
|
|
|
+ return ToolRunner.run(conf, kdiag, argv);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Main entry point.
|
|
|
+ * @param argv args list
|
|
|
+ */
|
|
|
+ public static void main(String[] argv) {
|
|
|
+ try {
|
|
|
+ ExitUtil.terminate(exec(new Configuration(), argv));
|
|
|
+ } catch (ExitUtil.ExitException e) {
|
|
|
+ LOG.error(e.toString());
|
|
|
+ System.exit(e.status);
|
|
|
+ } catch (Exception e) {
|
|
|
+ LOG.error(e.toString(), e);
|
|
|
+ ExitUtil.halt(-1, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Diagnostics failures return the exit code 41, "unauthorized".
|
|
|
+ *
|
|
|
+ * They have a category, initially for testing: the category can be
|
|
|
+ * validated without having to match on the entire string.
|
|
|
+ */
|
|
|
+ public static class KerberosDiagsFailure extends ExitUtil.ExitException {
|
|
|
+ private final String category;
|
|
|
+
|
|
|
+ public KerberosDiagsFailure(String category, String message) {
|
|
|
+ super(KDIAG_FAILURE, category + ": " + message);
|
|
|
+ this.category = category;
|
|
|
+ }
|
|
|
+
|
|
|
+ public KerberosDiagsFailure(String category,
|
|
|
+ String message,
|
|
|
+ Object... args) {
|
|
|
+ this(category, String.format(message, args));
|
|
|
+ }
|
|
|
+
|
|
|
+ public KerberosDiagsFailure(String category, Throwable throwable,
|
|
|
+ String message, Object... args) {
|
|
|
+ this(category, message, args);
|
|
|
+ initCause(throwable);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getCategory() {
|
|
|
+ return category;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|