Allow $site_path/etc/peer_keys to authenticate peer daemons The peer_keys file is the standard OpenSSH authorized_keys file format, one SSH key per line. Blank lines and any lines starting with # are ignored. The file is scanned each time it is modified, allowing hosts to be added or removed from a cluster configuration without needing to restart the current node. I'm choosing to put the peer keys into a local disk file rather than into the database, because we might run into a catch-22 case where the peers need to authenticate to each other before they can read the database. E.g. this could happen if we figure out how to embed Apache Cassandra, tunnel its swarm traffic over our own SSH channels, and require a quorum read to bring the server up. The use of this file is experimental. I'm not documenting it yet because I don't know if we'll be supporting it long-term. Change-Id: I6e9b8ae5cd1bb3643688a3ee657055aab73e6a87 Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java index 7ffa37e..1faa672 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -43,6 +43,7 @@ public final File ssh_key; public final File ssh_rsa; public final File ssh_dsa; + public final File peer_keys; public final File site_css; public final File site_header; @@ -75,6 +76,7 @@ ssh_key = new File(etc_dir, "ssh_host_key"); ssh_rsa = new File(etc_dir, "ssh_host_rsa_key"); ssh_dsa = new File(etc_dir, "ssh_host_dsa_key"); + peer_keys = new File(etc_dir, "peer_keys"); site_css = new File(etc_dir, "GerritSite.css"); site_header = new File(etc_dir, "GerritSiteHeader.html");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java index 2818b7d..8fd9d72 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -19,21 +19,33 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PeerDaemonUser; +import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.sshd.SshScope.Context; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import org.apache.commons.codec.binary.Base64; import org.apache.mina.core.future.IoFuture; import org.apache.mina.core.future.IoFutureListener; import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.util.Buffer; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; import java.net.SocketAddress; import java.security.KeyPair; import java.security.PublicKey; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -42,21 +54,26 @@ */ @Singleton class DatabasePubKeyAuth implements PublickeyAuthenticator { + private static final Logger log = + LoggerFactory.getLogger(DatabasePubKeyAuth.class); + private final SshKeyCacheImpl sshKeyCache; - private final SshLog log; + private final SshLog sshLog; private final IdentifiedUser.GenericFactory userFactory; private final PeerDaemonUser.Factory peerFactory; private final Set<PublicKey> myHostKeys; + private volatile PeerKeyCache peerKeyCache; @Inject DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l, final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf, - final KeyPairProvider hostKeyProvider) { + final SitePaths site, final KeyPairProvider hostKeyProvider) { sshKeyCache = skc; - log = l; + sshLog = l; userFactory = uf; peerFactory = pf; myHostKeys = myHostKeys(hostKeyProvider); + peerKeyCache = new PeerKeyCache(site.peer_keys); } private static Set<PublicKey> myHostKeys(KeyPairProvider p) { @@ -79,7 +96,8 @@ final SshSession sd = session.getAttribute(SshSession.KEY); if (PeerDaemonUser.USER_NAME.equals(username)) { - if (myHostKeys.contains(suppliedKey)) { + if (myHostKeys.contains(suppliedKey) + || getPeerKeys().contains(suppliedKey)) { PeerDaemonUser user = peerFactory.create(sd.getRemoteAddress()); return success(username, session, sd, user); @@ -120,6 +138,15 @@ return success(username, session, sd, createUser(sd, key)); } + private Set<PublicKey> getPeerKeys() { + PeerKeyCache p = peerKeyCache; + if (!p.isCurrent()) { + p = p.reload(); + peerKeyCache = p; + } + return p.keys; + } + private boolean success(final String username, final ServerSession session, final SshSession sd, final CurrentUser user) { if (sd.getCurrentUser() == null) { @@ -132,7 +159,7 @@ Context ctx = new Context(sd); Context old = SshScope.set(ctx); try { - log.onLogin(); + sshLog.onLogin(); } finally { SshScope.set(old); } @@ -144,7 +171,7 @@ final Context ctx = new Context(sd); final Context old = SshScope.set(ctx); try { - log.onLogout(); + sshLog.onLogout(); } finally { SshScope.set(old); } @@ -174,4 +201,62 @@ } return null; } + + private static class PeerKeyCache { + private final File path; + private final long modified; + final Set<PublicKey> keys; + + PeerKeyCache(final File path) { + this.path = path; + this.modified = path.lastModified(); + this.keys = read(path); + } + + private static Set<PublicKey> read(File path) { + try { + final BufferedReader br = new BufferedReader(new FileReader(path)); + try { + final Set<PublicKey> keys = new HashSet<PublicKey>(); + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.startsWith("#") || line.isEmpty()) { + continue; + } + + try { + byte[] bin = Base64.decodeBase64(line.getBytes("ISO-8859-1")); + keys.add(new Buffer(bin).getRawPublicKey()); + } catch (RuntimeException e) { + logBadKey(path, line, e); + } catch (SshException e) { + logBadKey(path, line, e); + } + } + return Collections.unmodifiableSet(keys); + } finally { + br.close(); + } + } catch (FileNotFoundException noFile) { + return Collections.emptySet(); + + } catch (IOException err) { + log.error("Cannot read " + path, err); + return Collections.emptySet(); + } + } + + private static void logBadKey(File path, String line, Exception e) { + log.warn("Invalid key in " + path + ":\n " + line, e); + } + + boolean isCurrent() { + return path.lastModified() == modified; + } + + PeerKeyCache reload() { + return new PeerKeyCache(path); + } + } }