All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hadoop.security.authentication.util.ZKSignerSecretProvider Maven / Gradle / Ivy

The newest version!
/**
 * Licensed 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. See accompanying LICENSE file.
 */
package org.apache.hadoop.security.authentication.util;

import com.google.common.annotations.VisibleForTesting;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.servlet.ServletContext;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.ACLProvider;
import org.apache.curator.framework.imps.DefaultACLProvider;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs.Perms;
import org.apache.zookeeper.client.ZooKeeperSaslClient;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A SignerSecretProvider that synchronizes a rolling random secret between
 * multiple servers using ZooKeeper.
 * 

* It works by storing the secrets and next rollover time in a ZooKeeper znode. * All ZKSignerSecretProviders looking at that znode will use those * secrets and next rollover time to ensure they are synchronized. There is no * "leader" -- any of the ZKSignerSecretProviders can choose the next secret; * which one is indeterminate. Kerberos-based ACLs can also be enforced to * prevent a malicious third-party from getting or setting the secrets. It uses * its own CuratorFramework client for talking to ZooKeeper. If you want to use * your own Curator client, you can pass it to ZKSignerSecretProvider; see * {@link org.apache.hadoop.security.authentication.server.AuthenticationFilter} * for more details. *

* Details of the configurations are listed on Configuration Page */ @InterfaceStability.Unstable @InterfaceAudience.Private public class ZKSignerSecretProvider extends RolloverSignerSecretProvider { private static final String CONFIG_PREFIX = "signer.secret.provider.zookeeper."; /** * Constant for the property that specifies the ZooKeeper connection string. */ public static final String ZOOKEEPER_CONNECTION_STRING = CONFIG_PREFIX + "connection.string"; /** * Constant for the property that specifies the ZooKeeper path. */ public static final String ZOOKEEPER_PATH = CONFIG_PREFIX + "path"; /** * Constant for the property that specifies the auth type to use. Supported * values are "none" and "sasl". The default value is "none". */ public static final String ZOOKEEPER_AUTH_TYPE = CONFIG_PREFIX + "auth.type"; /** * Constant for the property that specifies the Kerberos keytab file. */ public static final String ZOOKEEPER_KERBEROS_KEYTAB = CONFIG_PREFIX + "kerberos.keytab"; /** * Constant for the property that specifies the Kerberos principal. */ public static final String ZOOKEEPER_KERBEROS_PRINCIPAL = CONFIG_PREFIX + "kerberos.principal"; /** * Constant for the property that specifies whether or not the Curator client * should disconnect from ZooKeeper on shutdown. The default is "true". Only * set this to "false" if a custom Curator client is being provided and the * disconnection is being handled elsewhere. */ public static final String DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN = CONFIG_PREFIX + "disconnect.on.shutdown"; /** * Constant for the ServletContext attribute that can be used for providing a * custom CuratorFramework client. If set ZKSignerSecretProvider will use this * Curator client instead of creating a new one. The providing class is * responsible for creating and configuring the Curator client (including * security and ACLs) in this case. */ public static final String ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE = CONFIG_PREFIX + "curator.client"; private static final String JAAS_LOGIN_ENTRY_NAME = "ZKSignerSecretProviderClient"; private static Logger LOG = LoggerFactory.getLogger( ZKSignerSecretProvider.class); private String path; /** * Stores the next secret that will be used after the current one rolls over. * We do this to help with rollover performance by actually deciding the next * secret at the previous rollover. This allows us to switch to the next * secret very quickly. Afterwards, we have plenty of time to decide on the * next secret. */ private volatile byte[] nextSecret; private final Random rand; /** * Stores the current version of the znode. */ private int zkVersion; /** * Stores the next date that the rollover will occur. This is only used * for allowing new servers joining later to synchronize their rollover * with everyone else. */ private long nextRolloverDate; private long tokenValidity; private CuratorFramework client; private boolean shouldDisconnect; private static int INT_BYTES = Integer.SIZE / Byte.SIZE; private static int LONG_BYTES = Long.SIZE / Byte.SIZE; private static int DATA_VERSION = 0; public ZKSignerSecretProvider() { super(); rand = new SecureRandom(); } /** * This constructor lets you set the seed of the Random Number Generator and * is meant for testing. * @param seed the seed for the random number generator */ @VisibleForTesting public ZKSignerSecretProvider(long seed) { super(); rand = new Random(seed); } @Override public void init(Properties config, ServletContext servletContext, long tokenValidity) throws Exception { Object curatorClientObj = servletContext.getAttribute( ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE); if (curatorClientObj != null && curatorClientObj instanceof CuratorFramework) { client = (CuratorFramework) curatorClientObj; } else { client = createCuratorClient(config); servletContext.setAttribute( ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE, client); } this.tokenValidity = tokenValidity; shouldDisconnect = Boolean.parseBoolean( config.getProperty(DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN, "true")); path = config.getProperty(ZOOKEEPER_PATH); if (path == null) { throw new IllegalArgumentException(ZOOKEEPER_PATH + " must be specified"); } try { nextRolloverDate = System.currentTimeMillis() + tokenValidity; // everyone tries to do this, only one will succeed and only when the // znode doesn't already exist. Everyone else will synchronize on the // data from the znode client.create().creatingParentsIfNeeded() .forPath(path, generateZKData(generateRandomSecret(), generateRandomSecret(), null)); zkVersion = 0; LOG.info("Creating secret znode"); } catch (KeeperException.NodeExistsException nee) { LOG.info("The secret znode already exists, retrieving data"); } // Synchronize on the data from the znode // passing true tells it to parse out all the data for initing pullFromZK(true); long initialDelay = nextRolloverDate - System.currentTimeMillis(); // If it's in the past, try to find the next interval that we should // be using if (initialDelay < 1l) { int i = 1; while (initialDelay < 1l) { initialDelay = nextRolloverDate + tokenValidity * i - System.currentTimeMillis(); i++; } } super.startScheduler(initialDelay, tokenValidity); } /** * Disconnects from ZooKeeper unless told not to. */ @Override public void destroy() { if (shouldDisconnect && client != null) { client.close(); } super.destroy(); } @Override protected synchronized void rollSecret() { super.rollSecret(); // Try to push the information to ZooKeeper with a potential next secret. nextRolloverDate += tokenValidity; byte[][] secrets = super.getAllSecrets(); pushToZK(generateRandomSecret(), secrets[0], secrets[1]); // Pull info from ZooKeeper to get the decided next secret // passing false tells it that we don't care about most of the data pullFromZK(false); } @Override protected byte[] generateNewSecret() { // We simply return nextSecret because it's already been decided on return nextSecret; } /** * Pushes proposed data to ZooKeeper. If a different server pushes its data * first, it gives up. * @param newSecret The new secret to use * @param currentSecret The current secret * @param previousSecret The previous secret */ private synchronized void pushToZK(byte[] newSecret, byte[] currentSecret, byte[] previousSecret) { byte[] bytes = generateZKData(newSecret, currentSecret, previousSecret); try { client.setData().withVersion(zkVersion).forPath(path, bytes); } catch (KeeperException.BadVersionException bve) { LOG.debug("Unable to push to znode; another server already did it"); } catch (Exception ex) { LOG.error("An unexpected exception occurred pushing data to ZooKeeper", ex); } } /** * Serialize the data to attempt to push into ZooKeeper. The format is this: *

* [DATA_VERSION, newSecretLength, newSecret, currentSecretLength, currentSecret, previousSecretLength, previousSecret, nextRolloverDate] *

* Only previousSecret can be null, in which case the format looks like this: *

* [DATA_VERSION, newSecretLength, newSecret, currentSecretLength, currentSecret, 0, nextRolloverDate] *

* @param newSecret The new secret to use * @param currentSecret The current secret * @param previousSecret The previous secret * @return The serialized data for ZooKeeper */ private synchronized byte[] generateZKData(byte[] newSecret, byte[] currentSecret, byte[] previousSecret) { int newSecretLength = newSecret.length; int currentSecretLength = currentSecret.length; int previousSecretLength = 0; if (previousSecret != null) { previousSecretLength = previousSecret.length; } ByteBuffer bb = ByteBuffer.allocate(INT_BYTES + INT_BYTES + newSecretLength + INT_BYTES + currentSecretLength + INT_BYTES + previousSecretLength + LONG_BYTES); bb.putInt(DATA_VERSION); bb.putInt(newSecretLength); bb.put(newSecret); bb.putInt(currentSecretLength); bb.put(currentSecret); bb.putInt(previousSecretLength); if (previousSecretLength > 0) { bb.put(previousSecret); } bb.putLong(nextRolloverDate); return bb.array(); } /** * Pulls data from ZooKeeper. If isInit is false, it will only parse the * next secret and version. If isInit is true, it will also parse the current * and previous secrets, and the next rollover date; it will also init the * secrets. Hence, isInit should only be true on startup. * @param isInit see description above */ private synchronized void pullFromZK(boolean isInit) { try { Stat stat = new Stat(); byte[] bytes = client.getData().storingStatIn(stat).forPath(path); ByteBuffer bb = ByteBuffer.wrap(bytes); int dataVersion = bb.getInt(); if (dataVersion > DATA_VERSION) { throw new IllegalStateException("Cannot load data from ZooKeeper; it" + "was written with a newer version"); } int nextSecretLength = bb.getInt(); byte[] nextSecret = new byte[nextSecretLength]; bb.get(nextSecret); this.nextSecret = nextSecret; zkVersion = stat.getVersion(); if (isInit) { int currentSecretLength = bb.getInt(); byte[] currentSecret = new byte[currentSecretLength]; bb.get(currentSecret); int previousSecretLength = bb.getInt(); byte[] previousSecret = null; if (previousSecretLength > 0) { previousSecret = new byte[previousSecretLength]; bb.get(previousSecret); } super.initSecrets(currentSecret, previousSecret); nextRolloverDate = bb.getLong(); } } catch (Exception ex) { LOG.error("An unexpected exception occurred while pulling data from" + "ZooKeeper", ex); } } @VisibleForTesting protected byte[] generateRandomSecret() { byte[] secret = new byte[32]; // 32 bytes = 256 bits rand.nextBytes(secret); return secret; } /** * This method creates the Curator client and connects to ZooKeeper. * @param config configuration properties * @return A Curator client * @throws Exception thrown if an error occurred */ protected CuratorFramework createCuratorClient(Properties config) throws Exception { String connectionString = config.getProperty( ZOOKEEPER_CONNECTION_STRING, "localhost:2181"); RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); ACLProvider aclProvider; String authType = config.getProperty(ZOOKEEPER_AUTH_TYPE, "none"); if (authType.equals("sasl")) { LOG.info("Connecting to ZooKeeper with SASL/Kerberos" + "and using 'sasl' ACLs"); String principal = setJaasConfiguration(config); System.setProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, JAAS_LOGIN_ENTRY_NAME); System.setProperty("zookeeper.authProvider.1", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider"); aclProvider = new SASLOwnerACLProvider(principal); } else { // "none" LOG.info("Connecting to ZooKeeper without authentication"); aclProvider = new DefaultACLProvider(); // open to everyone } CuratorFramework cf = CuratorFrameworkFactory.builder() .connectString(connectionString) .retryPolicy(retryPolicy) .aclProvider(aclProvider) .build(); cf.start(); return cf; } private String setJaasConfiguration(Properties config) throws Exception { String keytabFile = config.getProperty(ZOOKEEPER_KERBEROS_KEYTAB).trim(); if (keytabFile == null || keytabFile.length() == 0) { throw new IllegalArgumentException(ZOOKEEPER_KERBEROS_KEYTAB + " must be specified"); } String principal = config.getProperty(ZOOKEEPER_KERBEROS_PRINCIPAL) .trim(); if (principal == null || principal.length() == 0) { throw new IllegalArgumentException(ZOOKEEPER_KERBEROS_PRINCIPAL + " must be specified"); } // This is equivalent to writing a jaas.conf file and setting the system // property, "java.security.auth.login.config", to point to it JaasConfiguration jConf = new JaasConfiguration(JAAS_LOGIN_ENTRY_NAME, principal, keytabFile); Configuration.setConfiguration(jConf); return principal.split("[/@]")[0]; } /** * Simple implementation of an {@link ACLProvider} that simply returns an ACL * that gives all permissions only to a single principal. */ private static class SASLOwnerACLProvider implements ACLProvider { private final List saslACL; private SASLOwnerACLProvider(String principal) { this.saslACL = Collections.singletonList( new ACL(Perms.ALL, new Id("sasl", principal))); } @Override public List getDefaultAcl() { return saslACL; } @Override public List getAclForPath(String path) { return saslACL; } } /** * Creates a programmatic version of a jaas.conf file. This can be used * instead of writing a jaas.conf file and setting the system property, * "java.security.auth.login.config", to point to that file. It is meant to be * used for connecting to ZooKeeper. */ @InterfaceAudience.Private public static class JaasConfiguration extends Configuration { private final javax.security.auth.login.Configuration baseConfig = javax.security.auth.login.Configuration.getConfiguration(); private static AppConfigurationEntry[] entry; private String entryName; /** * Add an entry to the jaas configuration with the passed in name, * principal, and keytab. The other necessary options will be set for you. * * @param entryName The name of the entry (e.g. "Client") * @param principal The principal of the user * @param keytab The location of the keytab */ public JaasConfiguration(String entryName, String principal, String keytab) { this.entryName = entryName; Map options = new HashMap(); options.put("keyTab", keytab); options.put("principal", principal); options.put("useKeyTab", "true"); options.put("storeKey", "true"); options.put("useTicketCache", "false"); options.put("refreshKrb5Config", "true"); String jaasEnvVar = System.getenv("HADOOP_JAAS_DEBUG"); if (jaasEnvVar != null && "true".equalsIgnoreCase(jaasEnvVar)) { options.put("debug", "true"); } entry = new AppConfigurationEntry[]{ new AppConfigurationEntry(getKrb5LoginModuleName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)}; } @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { return (entryName.equals(name)) ? entry : ((baseConfig != null) ? baseConfig.getAppConfigurationEntry(name) : null); } private String getKrb5LoginModuleName() { String krb5LoginModuleName; if (System.getProperty("java.vendor").contains("IBM")) { krb5LoginModuleName = "com.ibm.security.auth.module.Krb5LoginModule"; } else { krb5LoginModuleName = "com.sun.security.auth.module.Krb5LoginModule"; } return krb5LoginModuleName; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy