]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java
Self-signed certificate with RSA 3072
[lgpl/argeo-commons.git] / org.argeo.cms.lib.sshd / src / org / argeo / cms / ssh / CmsSshServer.java
1 package org.argeo.cms.ssh;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.Writer;
6 import java.nio.charset.StandardCharsets;
7 import java.nio.file.Files;
8 import java.nio.file.Path;
9 import java.nio.file.Paths;
10 import java.nio.file.StandardOpenOption;
11 import java.nio.file.attribute.PosixFilePermission;
12 import java.security.KeyPair;
13 import java.security.KeyStore;
14 import java.security.KeyStoreException;
15 import java.security.NoSuchAlgorithmException;
16 import java.security.NoSuchProviderException;
17 import java.security.PrivateKey;
18 import java.security.UnrecoverableKeyException;
19 import java.security.cert.CertificateException;
20 import java.util.Collections;
21 import java.util.HashSet;
22 import java.util.Set;
23
24 import org.apache.sshd.common.config.keys.PublicKeyEntry;
25 import org.apache.sshd.common.forward.PortForwardingEventListener;
26 import org.apache.sshd.common.keyprovider.KeyPairProvider;
27 import org.apache.sshd.common.session.Session;
28 import org.apache.sshd.common.util.net.SshdSocketAddress;
29 import org.apache.sshd.scp.server.ScpCommandFactory;
30 import org.apache.sshd.server.SshServer;
31 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
32 import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
33 import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator;
34 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
35 import org.apache.sshd.server.jaas.JaasPasswordAuthenticator;
36 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
37 import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
38 import org.apache.sshd.sftp.server.SftpSubsystemFactory;
39 import org.argeo.api.cms.CmsAuth;
40 import org.argeo.api.cms.CmsConstants;
41 import org.argeo.api.cms.CmsLog;
42 import org.argeo.api.cms.CmsState;
43 import org.argeo.cms.CmsDeployProperty;
44 import org.argeo.cms.CmsSshd;
45
46 public class CmsSshServer implements CmsSshd {
47 private final static CmsLog log = CmsLog.getLog(CmsSshServer.class);
48
49 private CmsState cmsState;
50 private SshServer sshd = null;
51
52 private int port;
53 private String host;
54
55 public void start() {
56 String portStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_PORT.getProperty());
57 if (portStr == null)
58 return; // ignore
59 port = Integer.parseInt(portStr);
60
61 host = cmsState.getDeployProperty(CmsDeployProperty.HOST.getProperty());
62
63 KeyPair nodeKeyPair = loadNodeKeyPair();
64
65 try {
66 // authorized keys
67 String authorizedKeysStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_AUTHORIZEDKEYS.getProperty());
68 Path authorizedKeysPath = authorizedKeysStr != null ? Paths.get(authorizedKeysStr)
69 : AuthorizedKeysAuthenticator.getDefaultAuthorizedKeysFile();
70 if (authorizedKeysStr != null && !Files.exists(authorizedKeysPath)) {
71 Files.createFile(authorizedKeysPath);
72 Set<PosixFilePermission> posixPermissions = new HashSet<>();
73 posixPermissions.add(PosixFilePermission.OWNER_READ);
74 posixPermissions.add(PosixFilePermission.OWNER_WRITE);
75 Files.setPosixFilePermissions(authorizedKeysPath, posixPermissions);
76
77 if (nodeKeyPair != null)
78 try {
79 String openSsshPublicKey = PublicKeyEntry.toString(nodeKeyPair.getPublic());
80 try (Writer writer = Files.newBufferedWriter(authorizedKeysPath, StandardCharsets.US_ASCII,
81 StandardOpenOption.APPEND)) {
82 writer.write(openSsshPublicKey);
83 }
84 } catch (IOException e) {
85 log.error("Cannot add node public key to SSH authorized keys", e);
86 }
87 }
88
89 // create server
90 sshd = SshServer.setUpDefaultServer();
91 sshd.setPort(port);
92 if (host != null)
93 sshd.setHost(host);
94
95 // host key
96 if (nodeKeyPair != null) {
97 sshd.setKeyPairProvider(KeyPairProvider.wrap(nodeKeyPair));
98 } else {
99 Path hostKeyPath = cmsState.getDataPath(DEFAULT_SSH_HOST_KEY_PATH);
100 if (hostKeyPath == null) // TODO deal with no data area?
101 throw new IllegalStateException("An SSH server key must be set");
102 sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath));
103 }
104
105 // tunnels
106 sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
107 sshd.addPortForwardingEventListener(new PortForwardingEventListener() {
108
109 @Override
110 public void establishingExplicitTunnel(Session session, SshdSocketAddress local,
111 SshdSocketAddress remote, boolean localForwarding) throws IOException {
112 log.debug("Establishing tunnel " + local + ", " + remote);
113 }
114
115 @Override
116 public void establishedExplicitTunnel(Session session, SshdSocketAddress local,
117 SshdSocketAddress remote, boolean localForwarding, SshdSocketAddress boundAddress,
118 Throwable reason) throws IOException {
119 log.debug("Established tunnel " + local + ", " + remote + ", " + boundAddress);
120 }
121
122 @Override
123 public void establishingDynamicTunnel(Session session, SshdSocketAddress local) throws IOException {
124 log.debug("Establishing dynamic tunnel " + local);
125 }
126
127 @Override
128 public void establishedDynamicTunnel(Session session, SshdSocketAddress local,
129 SshdSocketAddress boundAddress, Throwable reason) throws IOException {
130 log.debug("Established dynamic tunnel " + local);
131 }
132
133 });
134
135 // Authentication
136 // FIXME use strict, set proper permissions, etc.
137 sshd.setPublickeyAuthenticator(
138 new DefaultAuthorizedKeysAuthenticator("user.name", authorizedKeysPath, true));
139 // sshd.setPublickeyAuthenticator(null);
140 // sshd.setKeyboardInteractiveAuthenticator(null);
141 JaasPasswordAuthenticator jaasPasswordAuthenticator = new JaasPasswordAuthenticator();
142 jaasPasswordAuthenticator.setDomain(CmsAuth.NODE.getLoginContextName());
143 sshd.setPasswordAuthenticator(jaasPasswordAuthenticator);
144
145 boolean gssApi = false;
146 if (gssApi) {
147 Path krb5keyTab = cmsState.getDataPath("private/krb5.keytab");
148 if (Files.exists(krb5keyTab)) {
149 // FIXME experimental
150 GSSAuthenticator gssAuthenticator = new GSSAuthenticator();
151 gssAuthenticator.setKeytabFile(krb5keyTab.toString());
152 gssAuthenticator.setServicePrincipalName("HTTP@" + host);
153 sshd.setGSSAuthenticator(gssAuthenticator);
154 }
155 }
156
157 // shell
158 // TODO make it configurable
159 sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
160 // String[] shellCommand = OS.LOCAL.getDefaultShellCommand();
161 // StringJoiner command = new StringJoiner(" ");
162 // for (String str : shellCommand) {
163 // command.add(str);
164 // }
165 // sshd.setShellFactory(new ProcessShellFactory(command.toString(), shellCommand));
166 sshd.setCommandFactory(new ScpCommandFactory());
167
168 // SFTP
169 sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
170
171 // start
172 sshd.start();
173
174 log.debug(() -> "CMS SSH server started on port " + port + (host != null ? " of host " + host : ""));
175 } catch (IOException e) {
176 throw new RuntimeException("Cannot start SSH server on port " + port, e);
177 }
178
179 }
180
181 public void stop() {
182 if (sshd == null)
183 return;
184 try {
185 sshd.stop();
186 log.debug(() -> "CMS SSH server stopped on port " + port + (host != null ? " of host " + host : ""));
187 } catch (IOException e) {
188 throw new RuntimeException("Cannot stop SSH server", e);
189 }
190
191 }
192
193 protected KeyPair loadNodeKeyPair() {
194 try {
195 char[] keyStorePassword = cmsState.getDeployProperty(CmsDeployProperty.SSL_PASSWORD.getProperty())
196 .toCharArray();
197 Path keyStorePath = Paths.get(cmsState.getDeployProperty(CmsDeployProperty.SSL_KEYSTORE.getProperty()));
198 String keyStoreType = cmsState.getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE.getProperty());
199
200 KeyStore store = KeyStore.getInstance(keyStoreType, "SunJSSE");
201 try (InputStream fis = Files.newInputStream(keyStorePath)) {
202 store.load(fis, keyStorePassword);
203 }
204 return new KeyPair(store.getCertificate(CmsConstants.NODE).getPublicKey(),
205 (PrivateKey) store.getKey(CmsConstants.NODE, keyStorePassword));
206 } catch (IOException | KeyStoreException | NoSuchProviderException | NoSuchAlgorithmException
207 | CertificateException | IllegalArgumentException | UnrecoverableKeyException e) {
208 log.error("Cannot add node public key to SSH authorized keys", e);
209 return null;
210 }
211
212 }
213
214 public void setCmsState(CmsState cmsState) {
215 this.cmsState = cmsState;
216 }
217
218 }