Improve SSH server. Rename node directory to private.
[lgpl/argeo-commons.git] / org.argeo.cms.lib.sshd / src / org / argeo / cms / ssh / CmsSshServer.java
index ab62654f23a32f521171904b42a394c278070317..f5609a37d5f70e2786153c567f96e0d0759a5684 100644 (file)
@@ -1,16 +1,36 @@
 package org.argeo.cms.ssh;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.forward.PortForwardingEventListener;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.scp.server.ScpCommandFactory;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
+import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator;
 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
 import org.apache.sshd.server.jaas.JaasPasswordAuthenticator;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
@@ -25,7 +45,6 @@ import org.argeo.cms.CmsSshd;
 
 public class CmsSshServer implements CmsSshd {
        private final static CmsLog log = CmsLog.getLog(CmsSshServer.class);
-       private static final String DEFAULT_SSH_HOST_KEY_PATH = CmsConstants.NODE + '/' + CmsConstants.NODE + ".ser";
 
        private CmsState cmsState;
        private SshServer sshd = null;
@@ -41,30 +60,50 @@ public class CmsSshServer implements CmsSshd {
 
                host = cmsState.getDeployProperty(CmsDeployProperty.HOST.getProperty());
 
-               Path hostKeyPath = cmsState.getDataPath(DEFAULT_SSH_HOST_KEY_PATH);
+               KeyPair nodeKeyPair = loadNodeKeyPair();
 
                try {
+                       // authorized keys
+                       String authorizedKeysStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_AUTHORIZEDKEYS.getProperty());
+                       Path authorizedKeysPath = authorizedKeysStr != null ? Paths.get(authorizedKeysStr)
+                                       : AuthorizedKeysAuthenticator.getDefaultAuthorizedKeysFile();
+                       if (authorizedKeysStr != null && !Files.exists(authorizedKeysPath)) {
+                               Files.createFile(authorizedKeysPath);
+                               Set<PosixFilePermission> posixPermissions = new HashSet<>();
+                               posixPermissions.add(PosixFilePermission.OWNER_READ);
+                               posixPermissions.add(PosixFilePermission.OWNER_WRITE);
+                               Files.setPosixFilePermissions(authorizedKeysPath, posixPermissions);
+
+                               if (nodeKeyPair != null)
+                                       try {
+                                               String openSsshPublicKey = PublicKeyEntry.toString(nodeKeyPair.getPublic());
+                                               try (Writer writer = Files.newBufferedWriter(authorizedKeysPath, StandardCharsets.US_ASCII,
+                                                               StandardOpenOption.APPEND)) {
+                                                       writer.write(openSsshPublicKey);
+                                               }
+                                       } catch (IOException e) {
+                                               log.error("Cannot add node public key to SSH authorized keys", e);
+                                       }
+                       }
+
+                       // create server
                        sshd = SshServer.setUpDefaultServer();
                        sshd.setPort(port);
                        if (host != null)
                                sshd.setHost(host);
-                       if (hostKeyPath == null)
-                               throw new IllegalStateException("An SSH server key must be set");
-                       sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath));
-                       // sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i",
-                       // "-l" }));
-//                     String[] shellCommand = OS.LOCAL.getDefaultShellCommand();
-                       // FIXME transfer args
-//             sshd.setShellFactory(new ProcessShellFactory(shellCommand));
-                       sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
-                       sshd.setCommandFactory(new ScpCommandFactory());
+
+                       // host key
+                       if (nodeKeyPair != null) {
+                               sshd.setKeyPairProvider(KeyPairProvider.wrap(nodeKeyPair));
+                       } else {
+                               Path hostKeyPath = cmsState.getDataPath(DEFAULT_SSH_HOST_KEY_PATH);
+                               if (hostKeyPath == null) // TODO deal with no data area?
+                                       throw new IllegalStateException("An SSH server key must be set");
+                               sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath));
+                       }
 
                        // tunnels
                        sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
-                       // sshd.setForwardingFilter(ForwardingFilter.asForwardingFilter(null, null,
-                       // TcpForwardingFilter.DEFAULT));
-                       // sshd.setForwarderFactory(DefaultForwarderFactory.INSTANCE);
-//                     TcpForwardingFilter tcpForwardingFilter = sshd.getTcpForwardingFilter();
                        sshd.addPortForwardingEventListener(new PortForwardingEventListener() {
 
                                @Override
@@ -94,22 +133,38 @@ public class CmsSshServer implements CmsSshd {
                        });
 
                        // Authentication
-                       // sshd.setPublickeyAuthenticator(new DefaultAuthorizedKeysAuthenticator(true));
-                       sshd.setPublickeyAuthenticator(null);
+                       // FIXME use strict, set proper permissions, etc.
+                       sshd.setPublickeyAuthenticator(
+                                       new DefaultAuthorizedKeysAuthenticator("user.name", authorizedKeysPath, true));
+                       // sshd.setPublickeyAuthenticator(null);
                        // sshd.setKeyboardInteractiveAuthenticator(null);
                        JaasPasswordAuthenticator jaasPasswordAuthenticator = new JaasPasswordAuthenticator();
                        jaasPasswordAuthenticator.setDomain(CmsAuth.NODE.getLoginContextName());
                        sshd.setPasswordAuthenticator(jaasPasswordAuthenticator);
 
-                       Path krb5keyTab = cmsState.getDataPath("node/krb5.keytab");
-                       if (Files.exists(krb5keyTab)) {
-                               // FIXME experimental
-                               GSSAuthenticator gssAuthenticator = new GSSAuthenticator();
-                               gssAuthenticator.setKeytabFile(cmsState.getDataPath("node/krb5.keytab").toString());
-                               gssAuthenticator.setServicePrincipalName("HTTP@" + host);
-                               sshd.setGSSAuthenticator(gssAuthenticator);
+                       boolean gssApi = false;
+                       if (gssApi) {
+                               Path krb5keyTab = cmsState.getDataPath("private/krb5.keytab");
+                               if (Files.exists(krb5keyTab)) {
+                                       // FIXME experimental
+                                       GSSAuthenticator gssAuthenticator = new GSSAuthenticator();
+                                       gssAuthenticator.setKeytabFile(krb5keyTab.toString());
+                                       gssAuthenticator.setServicePrincipalName("HTTP@" + host);
+                                       sshd.setGSSAuthenticator(gssAuthenticator);
+                               }
                        }
 
+                       // shell
+                       // TODO make it configurable
+                       sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
+//                     String[] shellCommand = OS.LOCAL.getDefaultShellCommand();
+//                     StringJoiner command = new StringJoiner(" ");
+//                     for (String str : shellCommand) {
+//                             command.add(str);
+//                     }
+//                     sshd.setShellFactory(new ProcessShellFactory(command.toString(), shellCommand));
+                       sshd.setCommandFactory(new ScpCommandFactory());
+
                        // SFTP
                        sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
 
@@ -135,6 +190,27 @@ public class CmsSshServer implements CmsSshd {
 
        }
 
+       protected KeyPair loadNodeKeyPair() {
+               try {
+                       char[] keyStorePassword = cmsState.getDeployProperty(CmsDeployProperty.SSL_PASSWORD.getProperty())
+                                       .toCharArray();
+                       Path keyStorePath = Paths.get(cmsState.getDeployProperty(CmsDeployProperty.SSL_KEYSTORE.getProperty()));
+                       String keyStoreType = cmsState.getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE.getProperty());
+
+                       KeyStore store = KeyStore.getInstance(keyStoreType, "SunJSSE");
+                       try (InputStream fis = Files.newInputStream(keyStorePath)) {
+                               store.load(fis, keyStorePassword);
+                       }
+                       return new KeyPair(store.getCertificate(CmsConstants.NODE).getPublicKey(),
+                                       (PrivateKey) store.getKey(CmsConstants.NODE, keyStorePassword));
+               } catch (IOException | KeyStoreException | NoSuchProviderException | NoSuchAlgorithmException
+                               | CertificateException | IllegalArgumentException | UnrecoverableKeyException e) {
+                       log.error("Cannot add node public key to SSH authorized keys", e);
+                       return null;
+               }
+
+       }
+
        public void setCmsState(CmsState cmsState) {
                this.cmsState = cmsState;
        }