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;
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;
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
});
// 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()));
}
+ 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;
}
//
/** Request an HTTP server on this port. */
SSHD_PORT("argeo.sshd.port"),
+ /** Path to admin authorized keys file. */
+ SSHD_AUTHORIZEDKEYS("argeo.sshd.authorizedkeys"),
//
// INTERNATIONALIZATION
//
package org.argeo.cms;
-/** Just a marker interface for the time being.*/
-public interface CmsSshd {
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.internal.runtime.KernelConstants;
+/** Just a marker interface for the time being. */
+public interface CmsSshd {
+ final static String NODE_USERNAME_ALIAS = "user.name";
+ final static String DEFAULT_SSH_HOST_KEY_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".ser";
}
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.StringJoiner;
import java.util.UUID;
private final Map<CmsDeployProperty, String> deployPropertyDefaults;
public CmsStateImpl() {
+ this.deployPropertyDefaults = Collections.unmodifiableMap(createDeployPropertiesDefaults());
+ }
+
+ protected Map<CmsDeployProperty, String> createDeployPropertiesDefaults() {
Map<CmsDeployProperty, String> deployPropertyDefaults = new HashMap<>();
deployPropertyDefaults.put(CmsDeployProperty.NODE_INIT, "../../init");
deployPropertyDefaults.put(CmsDeployProperty.LOCALE, Locale.getDefault().toString());
deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORETYPE, PkiUtils.PKCS12);
deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD, PkiUtils.DEFAULT_KEYSTORE_PASSWORD);
- this.deployPropertyDefaults = Collections.unmodifiableMap(deployPropertyDefaults);
+ // SSH
+ Path authorizedKeysPath = getDataPath(KernelConstants.NODE_SSHD_AUTHORIZED_KEYS_PATH);
+ if (authorizedKeysPath != null) {
+ deployPropertyDefaults.put(CmsDeployProperty.SSHD_AUTHORIZEDKEYS,
+ authorizedKeysPath.toAbsolutePath().toString());
+ }
+ return deployPropertyDefaults;
}
public void start() {
log.debug("## CMS starting... (" + uuid + ")\n" + sb + "\n");
}
- Path nodeBase = getDataPath(CmsConstants.NODE);
+ Path nodeBase = getDataPath(KernelConstants.DIR_PRIVATE);
if (nodeBase != null && !Files.exists(nodeBase)) {// first init
firstInit();
}
}
private void initSecurity() {
+ // private directory permissions
+ Path privateDir = KernelUtils.getOsgiInstancePath(KernelConstants.DIR_PRIVATE);
+ if (privateDir != null) {
+ // TODO rather check whether we can read and write
+ Set<PosixFilePermission> posixPermissions = new HashSet<>();
+ posixPermissions.add(PosixFilePermission.OWNER_READ);
+ posixPermissions.add(PosixFilePermission.OWNER_WRITE);
+ posixPermissions.add(PosixFilePermission.OWNER_EXECUTE);
+ try {
+ Files.setPosixFilePermissions(privateDir, posixPermissions);
+ } catch (IOException e) {
+ log.error("Cannot set permissions on " + privateDir);
+ }
+ }
+
if (getDeployProperty(CmsDeployProperty.JAVA_LOGIN_CONFIG) == null) {
String jaasConfig = KernelConstants.JAAS_CONFIG;
URL url = getClass().getResource(jaasConfig);
getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE));
try (Reader key = Files.newBufferedReader(pemKeyPath, StandardCharsets.US_ASCII);
Reader cert = Files.newBufferedReader(pemCertPath, StandardCharsets.US_ASCII);) {
- PkiUtils.loadPem(keyStore, key, keyStorePassword, cert);
+ PkiUtils.loadPrivateCertificatePem(keyStore, CmsConstants.NODE, key, keyStorePassword, cert);
Files.createDirectories(keyStorePath.getParent());
PkiUtils.saveKeyStore(keyStorePath, keyStorePassword, keyStore);
if (log.isDebugEnabled())
KeyStore trustStore = PkiUtils.getKeyStore(trustStorePath, trustStorePassword,
getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE));
try (Reader cert = Files.newBufferedReader(ipaCaCertPath, StandardCharsets.US_ASCII);) {
- PkiUtils.loadPem(trustStore, null, trustStorePassword, cert);
+ PkiUtils.loadTrustedCertificatePem(trustStore, trustStorePassword, cert);
Files.createDirectories(keyStorePath.getParent());
PkiUtils.saveKeyStore(trustStorePath, trustStorePassword, trustStore);
if (log.isDebugEnabled())
protected List<Dictionary<String, Object>> getUserDirectoryConfigs() {
List<Dictionary<String, Object>> res = new ArrayList<>();
- Path nodeBase = cmsState.getDataPath(KernelConstants.DIR_NODE);
+ Path nodeBase = cmsState.getDataPath(KernelConstants.DIR_PRIVATE);
List<String> uris = new ArrayList<>();
// node roles
try {
if (uri == null) {
String baseDn = (String) properties.get(DirectoryConf.baseDn.name());
- u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
+ u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_PRIVATE + '/' + baseDn + ".ldif");
} else if (realm != null) {
u = null;
} else {
package org.argeo.cms.internal.runtime;
/** Internal CMS constants. */
-interface KernelConstants {
+public interface KernelConstants {
// Directories
- String DIR_NODE = "node";
-// String DIR_REPOS = "repos";
-// String DIR_INDEXES = "indexes";
-// String DIR_TRANSACTIONS = "transactions";
+ String DIR_PRIVATE = "private";
// Files
-// String DEPLOY_CONFIG_PATH = DIR_NODE + '/' + CmsConstants.DEPLOY_BASEDN + ".ldif";
- String NODE_KEY_TAB_PATH = DIR_NODE + "/krb5.keytab";
+ String NODE_KEY_TAB_PATH = DIR_PRIVATE + "/krb5.keytab";
+ String NODE_SSHD_AUTHORIZED_KEYS_PATH = DIR_PRIVATE + "/authorized_keys";
// Security
String JAAS_CONFIG = "/org/argeo/cms/internal/runtime/jaas.cfg";
String JAAS_CONFIG_IPA = "/org/argeo/cms/internal/runtime/jaas-ipa.cfg";
- // Java
-// String JAAS_CONFIG_PROP = "java.security.auth.login.config";
-
- // DEFAULTS JCR PATH
-// String DEFAULT_HOME_BASE_PATH = "/home";
-// String DEFAULT_USERS_BASE_PATH = "/users";
-// String DEFAULT_GROUPS_BASE_PATH = "/groups";
-
// KERBEROS
String DEFAULT_KERBEROS_SERVICE = "HTTP";
// HTTP client
- String COOKIE_POLICY_BROWSER_COMPATIBILITY = "compatibility";
-
- // RWT / RAP
- // String PATH_WORKBENCH = "/ui";
- // String PATH_WORKBENCH_PUBLIC = PATH_WORKBENCH + "/public";
-
-// String JETTY_FACTORY_PID = "org.eclipse.equinox.http.jetty.config";
-// String JETTY_FACTORY_PID = "org.argeo.equinox.jetty.config";
- // default Jetty server configured via JettyConfigurator
-// String DEFAULT_JETTY_SERVER = "default";
-// String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer";
+ // String COOKIE_POLICY_BROWSER_COMPATIBILITY = "compatibility";
- // avoid dependencies
-// String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri";
-// String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault";
}
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
+import java.util.Objects;
import javax.security.auth.x500.X500Principal;
final static String PKCS12 = "PKCS12";
final static String JKS = "JKS";
- static final String DEFAULT_KEYSTORE_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".p12";
+ static final String DEFAULT_KEYSTORE_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".p12";
- static final String DEFAULT_TRUSTSTORE_PATH = KernelConstants.DIR_NODE + "/trusted.p12";
+ static final String DEFAULT_TRUSTSTORE_PATH = KernelConstants.DIR_PRIVATE + "/trusted.p12";
- static final String DEFAULT_PEM_KEY_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".key";
+ static final String DEFAULT_PEM_KEY_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".key";
- static final String DEFAULT_PEM_CERT_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".crt";
+ static final String DEFAULT_PEM_CERT_PATH = KernelConstants.DIR_PRIVATE + '/' + CmsConstants.NODE + ".crt";
static final String IPA_PEM_CA_CERT_PATH = "/etc/ipa/ca.crt";
static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
private final static String SUN_SECURITY_PROVIDER;
+ private final static String SUN_JSSE_SECURITY_PROVIDER;
private final static String BC_SECURITY_PROVIDER;
static {
Security.addProvider(new BouncyCastleProvider());
// TODO report it
BC_SECURITY_PROVIDER = "BC";
SUN_SECURITY_PROVIDER = "SUN";
+ SUN_JSSE_SECURITY_PROVIDER = "SunJSSE";
}
public static X509Certificate generateSelfSignedCertificate(KeyStore keyStore, X500Principal x500Principal,
public static KeyStore getKeyStore(Path keyStoreFile, char[] keyStorePassword, String keyStoreType) {
try {
- KeyStore store = KeyStore.getInstance(keyStoreType, "SunJSSE");
+ KeyStore store = KeyStore.getInstance(keyStoreType, SUN_JSSE_SECURITY_PROVIDER);
if (Files.exists(keyStoreFile)) {
try (InputStream fis = Files.newInputStream(keyStoreFile)) {
store.load(fis, keyStorePassword);
// return bos.toByteArray();
// }
- public static void loadPem(KeyStore keyStore, Reader key, char[] keyPassword, Reader cert) {
+ public static void loadPrivateCertificatePem(KeyStore keyStore, String alias, Reader key, char[] keyPassword,
+ Reader cert) {
+ Objects.requireNonNull(keyStore);
+ Objects.requireNonNull(key);
try {
X509Certificate certificate = loadPemCertificate(cert);
- if (key != null) {
- PrivateKey privateKey = loadPemPrivateKey(key, keyPassword);
- keyStore.setKeyEntry(certificate.getSubjectX500Principal().getName(), privateKey, keyPassword,
- new java.security.cert.Certificate[] { certificate });
- } else {
- TrustedCertificateEntry trustedCertificateEntry = new TrustedCertificateEntry(certificate);
- keyStore.setEntry(certificate.getSubjectX500Principal().getName(), trustedCertificateEntry, null);
- }
+ PrivateKey privateKey = loadPemPrivateKey(key, keyPassword);
+ keyStore.setKeyEntry(alias, privateKey, keyPassword, new java.security.cert.Certificate[] { certificate });
+ } catch (KeyStoreException e) {
+ throw new RuntimeException("Cannot store PEM certificate", e);
+ }
+ }
+
+ public static void loadTrustedCertificatePem(KeyStore keyStore,char[] keyStorePassword, Reader cert) {
+ try {
+ X509Certificate certificate = loadPemCertificate(cert);
+ TrustedCertificateEntry trustedCertificateEntry = new TrustedCertificateEntry(certificate);
+ keyStore.setEntry(certificate.getSubjectX500Principal().getName(), trustedCertificateEntry, null);
} catch (KeyStoreException e) {
throw new RuntimeException("Cannot store PEM certificate", e);
}
public String[] getDefaultShellCommand() {
if (!isMSWindows())
- return new String[] { "/bin/sh", "-l", "-i" };
+ return new String[] { "/bin/bash", "-l", "-i" };
else
return new String[] { "cmd.exe", "/C" };
}