Enable Kerberos authentication for SSH interaction

Kerberos authentication is configured by setting the server-side
kerberos keytab, which identifies the kerberos principal used for
SSH connections (typically host/canonical.host.name).

For servers that have been appropriately configured, this
file typically exists at /etc/krb5.keytab.

Kerberos authentication can be enabled by adding a line to the
gerrit.config under the ssh section as follows:

[ssh]
        kerberosKeytab = /etc/krb5.keytab

If the file is readable and contains a keytab, kerberos authentication
is enabled. Accounts must already exist in Gerrit and be active for
authentication to succeed.

If the canonical host is not the name used by the service, the name of
the principal can be defined appropriately:

[ssh]
        kerberosKeytab = /etc/krb5.keytab
        kerberosPrincipal = host/other.host.name

Change-Id: I03744b6391962bdabf647689ec3a2b8d1ab37078
This commit is contained in:
Alex Blewitt
2013-04-01 12:46:48 -04:00
parent e035784cd2
commit 7efb06ff40
4 changed files with 141 additions and 5 deletions

View File

@@ -2403,6 +2403,34 @@ Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96.
+ +
By default, all supported MACs are available. By default, all supported MACs are available.
[[sshd.kerberosKeytab]]sshd.kerberosKeytab::
+
Enable kerberos authentication for SSH connections. To permit
kerberos authentication, the server must have a host principal
(see `sshd.kerberosPrincipal`) which is acquired from a keytab.
This must be provisioned by the kerberos administrators, and is
typically installed into `/etc/krb5.keytab` on host machines.
+
The keytab must contain at least one `host/` principal, typically
using the host's canonical name. If it does not use the
canonical name, the `sshd.kerberosPrincipal` should be configured
with the correct name.
+
By default, not set and so kerberos authentication is not enabled.
[[sshd.kerberosPrincipal]]sshd.kerberosPrincipal::
+
If kerberos authentication is enabled with `sshd.kerberosKeytab`,
instead use the given principal name instead of the default.
If the principal does not begin with `host/` a warning message is
printed and may prevent successful authentication.
+
This may be useful if the host is behind an IP load balancer or
other SSH forwarding systems, since the principal name is constructed
by the client and must match for kerberos authentication to work.
+
By default, `host/canonical.host.name`
[[suggest]] Section suggest [[suggest]] Section suggest
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -0,0 +1,68 @@
// Copyright (C) 2013 Goldman Sachs
//
// 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.
package com.google.gerrit.sshd;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
import org.apache.sshd.server.session.ServerSession;
/**
* Authenticates users with kerberos (gssapi-with-mic).
*/
@Singleton
class GerritGSSAuthenticator extends GSSAuthenticator {
private final AccountCache accounts;
private final SshScope sshScope;
private final SshLog sshLog;
private final GenericFactory userFactory;
@Inject
GerritGSSAuthenticator(final AccountCache accounts, final SshScope sshScope,
final SshLog sshLog, final IdentifiedUser.GenericFactory userFactory) {
this.accounts = accounts;
this.sshScope = sshScope;
this.sshLog = sshLog;
this.userFactory = userFactory;
}
@Override
public boolean validateIdentity(final ServerSession session,
final String identity) {
final SshSession sd = session.getAttribute(SshSession.KEY);
int at = identity.indexOf('@');
String username;
if (at == -1) {
username = identity;
} else {
username = identity.substring(0, at);
}
AccountState state = accounts.getByUsername(username);
Account account = state == null ? null : state.getAccount();
boolean active = account != null && account.isActive();
if (active) {
return SshUtil.success(username, session, sshScope, sshLog, sd,
SshUtil.createUser(sd, userFactory, account.getId()));
} else {
return false;
}
}
}

View File

@@ -19,6 +19,7 @@ import static com.google.gerrit.server.ssh.SshAddressesModule.IANA_SSH_PORT;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Version; import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.ConfigUtil;
@@ -75,6 +76,8 @@ import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.SshFile; import org.apache.sshd.server.SshFile;
import org.apache.sshd.server.UserAuth; import org.apache.sshd.server.UserAuth;
import org.apache.sshd.server.auth.UserAuthPublicKey; import org.apache.sshd.server.auth.UserAuthPublicKey;
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
import org.apache.sshd.server.auth.gss.UserAuthGSS;
import org.apache.sshd.server.channel.ChannelDirectTcpip; import org.apache.sshd.server.channel.ChannelDirectTcpip;
import org.apache.sshd.server.channel.ChannelSession; import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.kex.DHG1; import org.apache.sshd.server.kex.DHG1;
@@ -85,9 +88,12 @@ import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PublicKey; import java.security.PublicKey;
@@ -129,6 +135,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
@Inject @Inject
SshDaemon(final CommandFactory commandFactory, final NoShell noShell, SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
final PublickeyAuthenticator userAuth, final PublickeyAuthenticator userAuth,
final GerritGSSAuthenticator kerberosAuth,
final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator, final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
@GerritServerConfig final Config cfg, final SshLog sshLog, @GerritServerConfig final Config cfg, final SshLog sshLog,
@SshListenAddresses final List<SocketAddress> listen, @SshListenAddresses final List<SocketAddress> listen,
@@ -159,6 +166,11 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
String.valueOf(maxConnectionsPerUser)); String.valueOf(maxConnectionsPerUser));
} }
final String kerberosKeytab = cfg.getString(
"sshd", null, "kerberosKeytab");
final String kerberosPrincipal = cfg.getString(
"sshd", null, "kerberosPrincipal");
if (SecurityUtils.isBouncyCastleRegistered()) { if (SecurityUtils.isBouncyCastleRegistered()) {
initProviderBouncyCastle(); initProviderBouncyCastle();
} else { } else {
@@ -172,7 +184,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
initFileSystemFactory(); initFileSystemFactory();
initSubsystems(); initSubsystems();
initCompression(); initCompression();
initUserAuth(userAuth); initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
setKeyPairProvider(hostKeyProvider); setKeyPairProvider(hostKeyProvider);
setCommandFactory(commandFactory); setCommandFactory(commandFactory);
setShellFactory(noShell); setShellFactory(noShell);
@@ -457,10 +469,36 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
setSubsystemFactories(Collections.<NamedFactory<Command>> emptyList()); setSubsystemFactories(Collections.<NamedFactory<Command>> emptyList());
} }
@SuppressWarnings("unchecked") private void initUserAuth(final PublickeyAuthenticator pubkey,
private void initUserAuth(final PublickeyAuthenticator pubkey) { final GSSAuthenticator kerberosAuthenticator,
setUserAuthFactories(Arrays String kerberosKeytab, String kerberosPrincipal) {
.<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory())); List<NamedFactory<UserAuth>> authFactories = Lists.newArrayList();
if (kerberosKeytab != null) {
authFactories.add(new UserAuthGSS.Factory());
log.info("Enabling kerberos with keytab " + kerberosKeytab);
if (!new File(kerberosKeytab).canRead()) {
log.error("Keytab " + kerberosKeytab +
" does not exist or is not readable; further errors are possible");
}
kerberosAuthenticator.setKeytabFile(kerberosKeytab);
if (kerberosPrincipal == null) {
try {
kerberosPrincipal = "host/" +
InetAddress.getLocalHost().getCanonicalHostName();
} catch(UnknownHostException e) {
kerberosPrincipal = "host/localhost";
}
}
log.info("Using kerberos principal " + kerberosPrincipal);
if (!kerberosPrincipal.startsWith("host/")) {
log.warn("Host principal does not start with host/ " +
"which most SSH clients will supply automatically");
}
kerberosAuthenticator.setServicePrincipalName(kerberosPrincipal);
setGSSAuthenticator(kerberosAuthenticator);
}
authFactories.add(new UserAuthPublicKey.Factory());
setUserAuthFactories(authFactories);
setPublickeyAuthenticator(pubkey); setPublickeyAuthenticator(pubkey);
} }

View File

@@ -42,6 +42,7 @@ import com.google.inject.servlet.RequestScoped;
import org.apache.sshd.common.KeyPairProvider; import org.apache.sshd.common.KeyPairProvider;
import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import java.net.SocketAddress; import java.net.SocketAddress;
@@ -84,6 +85,7 @@ public class SshModule extends FactoryModule {
.toProvider(StreamCommandExecutorProvider.class).in(SINGLETON); .toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON); bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class); bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON); bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);