ARTEMIS-1264 Foundation work for authentication with Kerberos using KRB_ cypher suites.

Core client with netty connector and acceptor doing kerberos
jaas.doAs around sslengine init such that the SSL handshake can do kerberos ticket
generaton and validation.
The kerberos authenticated user is then validated with the security manager before
being populated into the message userId.
The feature is enabled with the kerb5Config property. When lowercase it is the
principal. With a leading uppercase char it is the login.config entry to use.
This commit is contained in:
gtully 2017-06-30 13:56:24 +01:00 committed by Clebert Suconic
parent faae59e6d4
commit cda1e018e1
10 changed files with 339 additions and 25 deletions

View File

@ -27,6 +27,9 @@ import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactor
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.utils.ClassloadingUtil;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
/**
* Stores static mappings of class names to ConnectorFactory instances to act as a central repo for ConnectorFactory
* objects.
@ -95,4 +98,29 @@ public class TransportConfigurationUtil {
}
return false;
}
public static Configuration kerb5Config(String principal, boolean initiator) {
final Map<String, String> krb5LoginModuleOptions = new HashMap<>();
krb5LoginModuleOptions.put("isInitiator", String.valueOf(initiator));
krb5LoginModuleOptions.put("principal", principal);
krb5LoginModuleOptions.put("useKeyTab", "true");
krb5LoginModuleOptions.put("storeKey", "true");
krb5LoginModuleOptions.put("doNotPrompt", "true");
krb5LoginModuleOptions.put("renewTGT", "true");
krb5LoginModuleOptions.put("refreshKrb5Config", "true");
krb5LoginModuleOptions.put("useTicketCache", "true");
String ticketCache = System.getenv("KRB5CCNAME");
if (ticketCache != null) {
krb5LoginModuleOptions.put("ticketCache", ticketCache);
}
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
return new AppConfigurationEntry[]{
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
krb5LoginModuleOptions)};
}
};
}
}

View File

@ -28,6 +28,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.handler.ssl.SslHandler;
@ -45,6 +46,8 @@ import org.apache.activemq.artemis.utils.Env;
import org.apache.activemq.artemis.utils.IPV6Util;
import org.jboss.logging.Logger;
import javax.net.ssl.SSLPeerUnverifiedException;
public class NettyConnection implements Connection {
private static final Logger logger = Logger.getLogger(NettyConnection.class);
@ -487,6 +490,17 @@ public class NettyConnection implements Connection {
//never allow this
@Override
public final ActiveMQPrincipal getDefaultActiveMQPrincipal() {
ChannelHandler channelHandler = channel.pipeline().get("ssl");
if (channelHandler != null && channelHandler instanceof SslHandler) {
SslHandler sslHandler = (SslHandler) channelHandler;
try {
return new ActiveMQPrincipal(sslHandler.engine().getSession().getPeerPrincipal().getName(), "");
} catch (SSLPeerUnverifiedException ignored) {
if (logger.isTraceEnabled()) {
logger.trace(ignored.getMessage(), ignored);
}
}
}
return null;
}

View File

@ -28,6 +28,7 @@ import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -43,6 +44,8 @@ import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
@ -95,6 +98,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.client.ActiveMQClientLogger;
import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle;
import org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQClientProtocolManager;
import org.apache.activemq.artemis.core.remoting.impl.TransportConfigurationUtil;
import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport;
import org.apache.activemq.artemis.core.server.ActiveMQComponent;
import org.apache.activemq.artemis.spi.core.remoting.AbstractConnector;
@ -205,6 +209,10 @@ public class NettyConnector extends AbstractConnector {
private boolean verifyHost;
private String sniHost;
private String kerb5Config;
private boolean useDefaultSslContext;
private boolean tcpNoDelay;
@ -328,6 +336,10 @@ public class NettyConnector extends AbstractConnector {
verifyHost = ConfigurationHelper.getBooleanProperty(TransportConstants.VERIFY_HOST_PROP_NAME, TransportConstants.DEFAULT_VERIFY_HOST, configuration);
sniHost = ConfigurationHelper.getStringProperty(TransportConstants.SNIHOST_PROP_NAME, TransportConstants.DEFAULT_SNIHOST_CONFIG, configuration);
kerb5Config = ConfigurationHelper.getStringProperty(TransportConstants.SSL_KRB5_CONFIG_PROP_NAME, TransportConstants.DEFAULT_SSL_KRB5_CONFIG, configuration);
useDefaultSslContext = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_DEFAULT_SSL_CONTEXT_PROP_NAME, TransportConstants.DEFAULT_USE_DEFAULT_SSL_CONTEXT, configuration);
} else {
keyStoreProvider = TransportConstants.DEFAULT_KEYSTORE_PROVIDER;
@ -509,13 +521,36 @@ public class NettyConnector extends AbstractConnector {
public void initChannel(Channel channel) throws Exception {
final ChannelPipeline pipeline = channel.pipeline();
if (sslEnabled && !useServlet) {
SSLEngine engine;
if (verifyHost) {
engine = context.createSSLEngine(host, port);
} else {
engine = context.createSSLEngine();
Subject subject = null;
if (kerb5Config != null && kerb5Config.length() > 0) {
LoginContext loginContext = null;
if (Character.isUpperCase(kerb5Config.charAt(0))) {
// use as login.config scope
loginContext = new LoginContext(kerb5Config);
} else {
// inline keytab config using kerb5Config as principal
loginContext = new LoginContext("", null, null,
TransportConfigurationUtil.kerb5Config(kerb5Config, true));
}
loginContext.login();
subject = loginContext.getSubject();
verifyHost = true;
}
SSLEngine engine = Subject.doAs(subject, new PrivilegedExceptionAction<SSLEngine>() {
@Override
public SSLEngine run() {
if (verifyHost) {
return context.createSSLEngine(sniHost != null ? sniHost : host, port);
} else {
return context.createSSLEngine();
}
}
});
engine.setUseClientMode(true);
engine.setWantClientAuth(true);

View File

@ -27,6 +27,8 @@ public class TransportConstants {
public static final String SSL_ENABLED_PROP_NAME = "sslEnabled";
public static final String SSL_KRB5_CONFIG_PROP_NAME = "sslKrb5Config";
public static final String HTTP_ENABLED_PROP_NAME = "httpEnabled";
public static final String HTTP_CLIENT_IDLE_PROP_NAME = "httpClientIdleTime";
@ -99,6 +101,8 @@ public class TransportConstants {
public static final String VERIFY_HOST_PROP_NAME = "verifyHost";
public static final String SNIHOST_PROP_NAME = "sniHost";
public static final String BACKLOG_PROP_NAME = "backlog";
public static final String USE_DEFAULT_SSL_CONTEXT_PROP_NAME = "useDefaultSslContext";
@ -145,6 +149,10 @@ public class TransportConstants {
public static final boolean DEFAULT_SSL_ENABLED = false;
public static final String DEFAULT_SSL_KRB5_CONFIG = null;
public static final String DEFAULT_SNIHOST_CONFIG = null;
public static final boolean DEFAULT_USE_GLOBAL_WORKER_POOL = true;
public static final boolean DEFAULT_USE_EPOLL = true;

View File

@ -94,7 +94,7 @@
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-integ</artifactId>
<artifactId>apacheds-test-framework</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
<exclusions>
@ -105,18 +105,11 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-integ</artifactId>
<version>${directory-version}</version>
<groupId>org.apache.directory.jdbm</groupId>
<artifactId>apacheds-jdbm2</artifactId>
<version>${directory-jdbm2-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-commons</artifactId>

View File

@ -20,10 +20,13 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -68,6 +71,7 @@ import org.apache.activemq.artemis.api.core.management.CoreNotificationType;
import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryImpl;
import org.apache.activemq.artemis.core.protocol.ProtocolHandler;
import org.apache.activemq.artemis.core.remoting.impl.AbstractAcceptor;
import org.apache.activemq.artemis.core.remoting.impl.TransportConfigurationUtil;
import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport;
import org.apache.activemq.artemis.core.security.ActiveMQPrincipal;
import org.apache.activemq.artemis.core.server.ActiveMQComponent;
@ -154,6 +158,8 @@ public class NettyAcceptor extends AbstractAcceptor {
private final boolean verifyHost;
private final String kerb5Config;
private final boolean tcpNoDelay;
private final int backlog;
@ -217,6 +223,8 @@ public class NettyAcceptor extends AbstractAcceptor {
sslEnabled = ConfigurationHelper.getBooleanProperty(TransportConstants.SSL_ENABLED_PROP_NAME, TransportConstants.DEFAULT_SSL_ENABLED, configuration);
kerb5Config = ConfigurationHelper.getStringProperty(TransportConstants.SSL_KRB5_CONFIG_PROP_NAME, TransportConstants.DEFAULT_SSL_KRB5_CONFIG, configuration);
remotingThreads = ConfigurationHelper.getIntProperty(TransportConstants.NIO_REMOTING_THREADS_PROPNAME, -1, configuration);
remotingThreads = ConfigurationHelper.getIntProperty(TransportConstants.REMOTING_THREADS_PROPNAME, remotingThreads, configuration);
@ -423,7 +431,7 @@ public class NettyAcceptor extends AbstractAcceptor {
public synchronized SslHandler getSslHandler() throws Exception {
final SSLContext context;
try {
if (keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider))
if (kerb5Config == null && keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider))
throw new IllegalArgumentException("If \"" + TransportConstants.SSL_ENABLED_PROP_NAME +
"\" is true then \"" + TransportConstants.KEYSTORE_PATH_PROP_NAME + "\" must be non-null " +
"unless an alternative \"" + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME + "\" has been specified.");
@ -433,13 +441,32 @@ public class NettyAcceptor extends AbstractAcceptor {
ise.initCause(e);
throw ise;
}
SSLEngine engine;
if (verifyHost) {
engine = context.createSSLEngine(host, port);
} else {
engine = context.createSSLEngine();
Subject subject = null;
if (kerb5Config != null && kerb5Config.length() > 0) {
LoginContext loginContext = null;
if (Character.isUpperCase(kerb5Config.charAt(0))) {
// use as login.config scope
loginContext = new LoginContext(kerb5Config);
} else {
loginContext = new LoginContext("", null, null,
TransportConfigurationUtil.kerb5Config(kerb5Config, false));
}
loginContext.login();
subject = loginContext.getSubject();
}
SSLEngine engine = Subject.doAs(subject, new PrivilegedExceptionAction<SSLEngine>() {
@Override
public SSLEngine run() {
if (verifyHost) {
return context.createSSLEngine(host, port);
} else {
return context.createSSLEngine();
}
}
});
engine.setUseClientMode(false);
if (needClientAuth)

View File

@ -164,7 +164,9 @@
<javac-compiler-id>javac-with-errorprone</javac-compiler-id>
<directory-version>1.5.7</directory-version>
<directory-version>2.0.0-M15</directory-version>
<directory-jdbm2-version>2.0.0-M1</directory-jdbm2-version>
<cdi-api.version>1.2</cdi-api.version>
<geronimo-annotation_1.2_spec.version>1.0</geronimo-annotation_1.2_spec.version>
</properties>

View File

@ -249,13 +249,19 @@
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-integ</artifactId>
<artifactId>apacheds-test-framework</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-ldap-schema-data</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-integ</artifactId>
<artifactId>apacheds-server-annotations</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
<exclusions>
@ -328,6 +334,18 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-minikdc</artifactId>
<version>2.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.jdbm</groupId>
<artifactId>apacheds-jdbm2</artifactId>
<version>${directory-jdbm2-version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,163 @@
/*
* 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.activemq.artemis.tests.integration.ssl;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientConsumer;
import org.apache.activemq.artemis.api.core.client.ClientMessage;
import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.apache.hadoop.minikdc.MiniKdc;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class CoreClientOverOneWaySSLKerb5Test extends ActiveMQTestBase {
public static final SimpleString QUEUE = new SimpleString("QueueOverKrb5SSL");
public static final String CLIENT_PRINCIPAL = "client";
public static final String SNI_HOST = "sni.host";
public static final String SERVICE_PRINCIPAL = "host/" + SNI_HOST;
private MiniKdc kdc;
private ActiveMQServer server;
private TransportConfiguration tc;
@Test
public void testOneWaySSLWithGoodClientCipherSuite() throws Exception {
// hard coded match, default_keytab_name in minikdc-krb5.conf template
File userKeyTab = new File("target/test.krb5.keytab");
kdc.createPrincipal(userKeyTab, CLIENT_PRINCIPAL, SERVICE_PRINCIPAL);
createCustomSslServer();
tc.getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true);
tc.getParams().put(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, getSuitableCipherSuite());
tc.getParams().put(TransportConstants.SNIHOST_PROP_NAME, SNI_HOST); // static service name rather than dynamic machine name
tc.getParams().put(TransportConstants.SSL_KRB5_CONFIG_PROP_NAME, "client"); // lower case used as principal with default keytab
final ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc));
ClientSessionFactory sf = null;
try {
sf = createSessionFactory(locator);
ClientSession session = sf.createSession(false, true, true);
session.createQueue(CoreClientOverOneWaySSLKerb5Test.QUEUE, RoutingType.ANYCAST, CoreClientOverOneWaySSLKerb5Test.QUEUE);
ClientProducer producer = session.createProducer(CoreClientOverOneWaySSLKerb5Test.QUEUE);
final String text = RandomUtil.randomString();
ClientMessage message = createTextMessage(session, text);
producer.send(message);
ClientConsumer consumer = session.createConsumer(CoreClientOverOneWaySSLKerb5Test.QUEUE);
session.start();
ClientMessage m = consumer.receive(1000);
Assert.assertNotNull(m);
Assert.assertEquals(text, m.getReadOnlyBodyBuffer().readString());
System.err.println("m:" + m + ", user:" + m.getValidatedUserID());
Assert.assertNotNull("got validated user", m.getValidatedUserID());
Assert.assertTrue("krb id in validated user", m.getValidatedUserID().contains(CLIENT_PRINCIPAL));
} catch (Exception e) {
e.printStackTrace();
Assert.fail();
} finally {
if (sf != null) {
sf.close();
}
locator.close();
}
}
public String getSuitableCipherSuite() throws Exception {
return "TLS_KRB5_WITH_3DES_EDE_CBC_SHA";
}
// Package protected ---------------------------------------------
@Override
@Before
public void setUp() throws Exception {
super.setUp();
kdc = new MiniKdc(MiniKdc.createConf(), temporaryFolder.newFolder("kdc"));
kdc.start();
}
@Override
@After
public void tearDown() throws Exception {
try {
kdc.stop();
} finally {
super.tearDown();
}
}
private void createCustomSslServer() throws Exception {
Map<String, Object> params = new HashMap<>();
params.put(TransportConstants.SSL_ENABLED_PROP_NAME, true);
params.put(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, getSuitableCipherSuite());
params.put(TransportConstants.SSL_KRB5_CONFIG_PROP_NAME, SERVICE_PRINCIPAL);
ConfigurationImpl config = createBasicConfig().addAcceptorConfiguration(new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params, "nettySSL"));
config.setPopulateValidatedUser(true); // so we can verify the kerb5 id is present
config.setSecurityEnabled(true);
server = createServer(false, config);
server.start();
waitForServerToStart(server);
final String roleName = "ALLOW_ALL";
Role role = new Role(roleName, true, true, true, true, true, true, true, true, true, true);
Set<Role> roles = new HashSet<>();
roles.add(role);
HierarchicalRepository<Set<Role>> securityRepository = server.getSecurityRepository();
securityRepository.addMatch(QUEUE.toString(), roles);
ActiveMQJAASSecurityManager securityManager = (ActiveMQJAASSecurityManager) server.getSecurityManager();
final String user = CLIENT_PRINCIPAL + "@" + kdc.getRealm();
securityManager.getConfiguration().addUser(user, "");
securityManager.getConfiguration().addRole(user, roleName);
tc = new TransportConfiguration(NETTY_CONNECTOR_FACTORY);
}
}

View File

@ -0,0 +1,26 @@
#
# 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.
#
[libdefaults]
default_realm = {0}
udp_preference_limit = 1
default_keytab_name = FILE:target/test.krb5.keytab
[realms]
{0} = '{'
kdc = {1}:{2}
'}'