Merge "Enable Kerberos authentication for SSH interaction"
This commit is contained in:
@@ -2403,6 +2403,34 @@ Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96.
|
||||
+
|
||||
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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.SECONDS;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gerrit.common.Version;
|
||||
import com.google.gerrit.extensions.events.LifecycleListener;
|
||||
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.UserAuth;
|
||||
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.ChannelSession;
|
||||
import org.apache.sshd.server.kex.DHG1;
|
||||
@@ -85,9 +88,12 @@ import org.eclipse.jgit.lib.Config;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
@@ -129,6 +135,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
@Inject
|
||||
SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
|
||||
final PublickeyAuthenticator userAuth,
|
||||
final GerritGSSAuthenticator kerberosAuth,
|
||||
final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
|
||||
@GerritServerConfig final Config cfg, final SshLog sshLog,
|
||||
@SshListenAddresses final List<SocketAddress> listen,
|
||||
@@ -159,6 +166,11 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
String.valueOf(maxConnectionsPerUser));
|
||||
}
|
||||
|
||||
final String kerberosKeytab = cfg.getString(
|
||||
"sshd", null, "kerberosKeytab");
|
||||
final String kerberosPrincipal = cfg.getString(
|
||||
"sshd", null, "kerberosPrincipal");
|
||||
|
||||
if (SecurityUtils.isBouncyCastleRegistered()) {
|
||||
initProviderBouncyCastle();
|
||||
} else {
|
||||
@@ -172,7 +184,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
initFileSystemFactory();
|
||||
initSubsystems();
|
||||
initCompression();
|
||||
initUserAuth(userAuth);
|
||||
initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
|
||||
setKeyPairProvider(hostKeyProvider);
|
||||
setCommandFactory(commandFactory);
|
||||
setShellFactory(noShell);
|
||||
@@ -457,10 +469,36 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
setSubsystemFactories(Collections.<NamedFactory<Command>> emptyList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void initUserAuth(final PublickeyAuthenticator pubkey) {
|
||||
setUserAuthFactories(Arrays
|
||||
.<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory()));
|
||||
private void initUserAuth(final PublickeyAuthenticator pubkey,
|
||||
final GSSAuthenticator kerberosAuthenticator,
|
||||
String kerberosKeytab, String kerberosPrincipal) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.google.inject.servlet.RequestScoped;
|
||||
import org.apache.sshd.common.KeyPairProvider;
|
||||
import org.apache.sshd.server.CommandFactory;
|
||||
import org.apache.sshd.server.PublickeyAuthenticator;
|
||||
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
@@ -84,6 +85,7 @@ public class SshModule extends FactoryModule {
|
||||
.toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
|
||||
bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
|
||||
|
||||
bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
|
||||
bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
|
||||
bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user