001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.zookeeper;
019
020import java.io.IOException;
021import java.util.HashMap;
022import java.util.Map;
023import javax.security.auth.login.AppConfigurationEntry;
024import org.apache.hadoop.conf.Configuration;
025import org.apache.hadoop.hbase.HConstants;
026import org.apache.hadoop.security.SecurityUtil;
027import org.apache.hadoop.security.authentication.util.KerberosUtil;
028import org.apache.yetus.audience.InterfaceAudience;
029import org.apache.zookeeper.client.ZooKeeperSaslClient;
030import org.apache.zookeeper.server.ZooKeeperSaslServer;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Provides ZooKeeper authentication services for both client and server processes.
036 */
037@InterfaceAudience.Private
038public final class ZKAuthentication {
039  private static final Logger LOG = LoggerFactory.getLogger(ZKAuthentication.class);
040
041  private ZKAuthentication() {
042  }
043
044  /**
045   * Log in the current zookeeper server process using the given configuration keys for the
046   * credential file and login principal.
047   * <p>
048   * <strong>This is only applicable when running on secure hbase</strong> On regular HBase (without
049   * security features), this will safely be ignored.
050   * </p>
051   * @param conf          The configuration data to use
052   * @param keytabFileKey Property key used to configure the path to the credential file
053   * @param userNameKey   Property key used to configure the login principal
054   * @param hostname      Current hostname to use in any credentials
055   * @throws IOException underlying exception from SecurityUtil.login() call
056   */
057  public static void loginServer(Configuration conf, String keytabFileKey, String userNameKey,
058    String hostname) throws IOException {
059    login(conf, keytabFileKey, userNameKey, hostname, ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY,
060      JaasConfiguration.SERVER_KEYTAB_KERBEROS_CONFIG_NAME);
061  }
062
063  /**
064   * Log in the current zookeeper client using the given configuration keys for the credential file
065   * and login principal.
066   * <p>
067   * <strong>This is only applicable when running on secure hbase</strong> On regular HBase (without
068   * security features), this will safely be ignored.
069   * </p>
070   * @param conf          The configuration data to use
071   * @param keytabFileKey Property key used to configure the path to the credential file
072   * @param userNameKey   Property key used to configure the login principal
073   * @param hostname      Current hostname to use in any credentials
074   * @throws IOException underlying exception from SecurityUtil.login() call
075   */
076  public static void loginClient(Configuration conf, String keytabFileKey, String userNameKey,
077    String hostname) throws IOException {
078    login(conf, keytabFileKey, userNameKey, hostname, ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY,
079      JaasConfiguration.CLIENT_KEYTAB_KERBEROS_CONFIG_NAME);
080  }
081
082  /**
083   * Log in the current process using the given configuration keys for the credential file and login
084   * principal.
085   * <p>
086   * <strong>This is only applicable when running on secure hbase</strong> On regular HBase (without
087   * security features), this will safely be ignored.
088   * </p>
089   * @param conf                 The configuration data to use
090   * @param keytabFileKey        Property key used to configure the path to the credential file
091   * @param userNameKey          Property key used to configure the login principal
092   * @param hostname             Current hostname to use in any credentials
093   * @param loginContextProperty property name to expose the entry name
094   * @param loginContextName     jaas entry name
095   * @throws IOException underlying exception from SecurityUtil.login() call
096   */
097  private static void login(Configuration conf, String keytabFileKey, String userNameKey,
098    String hostname, String loginContextProperty, String loginContextName) throws IOException {
099    if (!isSecureZooKeeper(conf)) {
100      return;
101    }
102
103    // User has specified a jaas.conf, keep this one as the good one.
104    // HBASE_OPTS="-Djava.security.auth.login.config=jaas.conf"
105    if (System.getProperty("java.security.auth.login.config") != null) {
106      return;
107    }
108
109    // No keytab specified, no auth
110    String keytabFilename = conf.get(keytabFileKey);
111    if (keytabFilename == null) {
112      LOG.warn("no keytab specified for: {}", keytabFileKey);
113      return;
114    }
115
116    String principalConfig = conf.get(userNameKey, System.getProperty("user.name"));
117    String principalName = SecurityUtil.getServerPrincipal(principalConfig, hostname);
118
119    // Initialize the "jaas.conf" for keyTab/principal,
120    // If keyTab is not specified use the Ticket Cache.
121    // and set the zookeeper login context name.
122    JaasConfiguration jaasConf =
123      new JaasConfiguration(loginContextName, principalName, keytabFilename);
124    javax.security.auth.login.Configuration.setConfiguration(jaasConf);
125    System.setProperty(loginContextProperty, loginContextName);
126  }
127
128  /**
129   * Returns {@code true} when secure authentication is enabled (whether
130   * {@code hbase.security.authentication} is set to "{@code kerberos}").
131   */
132  public static boolean isSecureZooKeeper(Configuration conf) {
133    // Detection for embedded HBase client with jaas configuration
134    // defined for third party programs.
135    try {
136      javax.security.auth.login.Configuration testConfig =
137        javax.security.auth.login.Configuration.getConfiguration();
138      if (
139        testConfig.getAppConfigurationEntry("Client") == null
140          && testConfig
141            .getAppConfigurationEntry(JaasConfiguration.CLIENT_KEYTAB_KERBEROS_CONFIG_NAME) == null
142          && testConfig
143            .getAppConfigurationEntry(JaasConfiguration.SERVER_KEYTAB_KERBEROS_CONFIG_NAME) == null
144          && conf.get(HConstants.ZK_CLIENT_KERBEROS_PRINCIPAL) == null
145          && conf.get(HConstants.ZK_SERVER_KERBEROS_PRINCIPAL) == null
146      ) {
147
148        return false;
149      }
150    } catch (Exception e) {
151      // No Jaas configuration defined.
152      return false;
153    }
154
155    // Master & RSs uses hbase.zookeeper.client.*
156    return "kerberos".equalsIgnoreCase(conf.get("hbase.security.authentication"));
157  }
158
159  /**
160   * A JAAS configuration that defines the login modules that we want to use for ZooKeeper login.
161   */
162  private static class JaasConfiguration extends javax.security.auth.login.Configuration {
163    private static final Logger LOG = LoggerFactory.getLogger(JaasConfiguration.class);
164
165    public static final String SERVER_KEYTAB_KERBEROS_CONFIG_NAME =
166      "zookeeper-server-keytab-kerberos";
167    public static final String CLIENT_KEYTAB_KERBEROS_CONFIG_NAME =
168      "zookeeper-client-keytab-kerberos";
169
170    private static final Map<String, String> BASIC_JAAS_OPTIONS = new HashMap<>();
171
172    static {
173      String jaasEnvVar = System.getenv("HBASE_JAAS_DEBUG");
174      if ("true".equalsIgnoreCase(jaasEnvVar)) {
175        BASIC_JAAS_OPTIONS.put("debug", "true");
176      }
177    }
178
179    private static final Map<String, String> KEYTAB_KERBEROS_OPTIONS = new HashMap<>();
180
181    static {
182      KEYTAB_KERBEROS_OPTIONS.put("doNotPrompt", "true");
183      KEYTAB_KERBEROS_OPTIONS.put("storeKey", "true");
184      KEYTAB_KERBEROS_OPTIONS.put("refreshKrb5Config", "true");
185      KEYTAB_KERBEROS_OPTIONS.putAll(BASIC_JAAS_OPTIONS);
186    }
187
188    private static final AppConfigurationEntry KEYTAB_KERBEROS_LOGIN =
189      new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(),
190        AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, KEYTAB_KERBEROS_OPTIONS);
191
192    private static final AppConfigurationEntry[] KEYTAB_KERBEROS_CONF =
193      new AppConfigurationEntry[] { KEYTAB_KERBEROS_LOGIN };
194
195    private javax.security.auth.login.Configuration baseConfig;
196    private final String loginContextName;
197    private final boolean useTicketCache;
198    private final String keytabFile;
199    private final String principal;
200
201    public JaasConfiguration(String loginContextName, String principal, String keytabFile) {
202      this(loginContextName, principal, keytabFile, keytabFile == null || keytabFile.length() == 0);
203    }
204
205    private JaasConfiguration(String loginContextName, String principal, String keytabFile,
206      boolean useTicketCache) {
207      try {
208        this.baseConfig = javax.security.auth.login.Configuration.getConfiguration();
209      } catch (SecurityException e) {
210        this.baseConfig = null;
211      }
212      this.loginContextName = loginContextName;
213      this.useTicketCache = useTicketCache;
214      this.keytabFile = keytabFile;
215      this.principal = principal;
216      LOG.info("JaasConfiguration loginContextName={} principal={} useTicketCache={} keytabFile={}",
217        loginContextName, principal, useTicketCache, keytabFile);
218    }
219
220    @Override
221    public AppConfigurationEntry[] getAppConfigurationEntry(String appName) {
222      if (loginContextName.equals(appName)) {
223        if (!useTicketCache) {
224          KEYTAB_KERBEROS_OPTIONS.put("keyTab", keytabFile);
225          KEYTAB_KERBEROS_OPTIONS.put("useKeyTab", "true");
226        }
227        KEYTAB_KERBEROS_OPTIONS.put("principal", principal);
228        KEYTAB_KERBEROS_OPTIONS.put("useTicketCache", useTicketCache ? "true" : "false");
229        return KEYTAB_KERBEROS_CONF;
230      }
231
232      if (baseConfig != null) {
233        return baseConfig.getAppConfigurationEntry(appName);
234      }
235
236      return (null);
237    }
238  }
239}