diff --git a/setup/start.sh b/setup/start.sh index ac2999fa5..0e44d7ac4 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -148,6 +148,11 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi fi systemctl start nginx +echo "==> Configuring proftpd" +# link nginx config to system config +unlink /etc/proftpd/proftpd.conf 2>/dev/null || rm -rf /etc/proftpd/proftpd.conf +ln -s "${PLATFORM_DATA_DIR}/proftpd.conf" /etc/proftpd/proftpd.conf + # restart mysql to make sure it has latest config if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then # wait for all running mysql jobs diff --git a/setup/start/cloudron-firewall.sh b/setup/start/cloudron-firewall.sh index 4734964ad..ef954b47f 100755 --- a/setup/start/cloudron-firewall.sh +++ b/setup/start/cloudron-firewall.sh @@ -10,7 +10,7 @@ iptables -t filter -F CLOUDRON # empty any existing rules # allow ssh, http, https, ping, dns iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT # caas has ssh on port 202 -iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT +iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,222,443,587,993,4190 -j ACCEPT iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT diff --git a/src/infra_version.js b/src/infra_version.js index 567efaf6a..6090d1cd1 100644 --- a/src/infra_version.js +++ b/src/infra_version.js @@ -6,7 +6,7 @@ exports = module.exports = { // a version change recreates all containers with latest docker config - 'version': '48.12.1', + 'version': '48.14.0', 'baseImages': [ { repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' } diff --git a/src/ldap.js b/src/ldap.js index c2626585d..7e4634f2a 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -19,6 +19,8 @@ var assert = require('assert'), mail = require('./mail.js'), MailError = mail.MailError, mailboxdb = require('./mailboxdb.js'), + path = require('path'), + paths = require('./paths.js'), safe = require('safetydance'); var gServer = null; @@ -481,6 +483,74 @@ function authenticateUserMailbox(req, res, next) { }); } +function authenticateProftpd(req, res, next) { + debug('proftpd addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); + + if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); + var parts = email.split('@'); + if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + var username = parts[0]; + var appDomain = parts[1]; + + // TODO sync this with platform.js proftpd config generation + if (username === 'admin' && appDomain === 'cloudron') { + if (req.credentials !== 'password') return next(new ldap.InvalidCredentialsError(req.dn.toString())); + return res.end(); + } + + // actual user bind + users.verifyWithUsername(username, req.credentials, function (error) { + if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + + debug('proftpd addon auth: success'); + + res.end(); + }); +} + +function userSearchProftpd(req, res, next) { + debug('proftpd user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); + + if (req.filter.attribute !== 'username' || !req.filter.value) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + var parts = req.filter.value.split('@'); + if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + var username = parts[0]; + var appDomain = parts[1]; + + apps.getAll(function (error, result) { + if (error) return next(new ldap.OperationsError(error.toString())); + + var app = result.find(function (a) { return a.fqdn === appDomain; }); + if (!app) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + users.getByUsername(username, function (error, result) { + if (error) return next(new ldap.OperationsError(error.toString())); + + var dn = ldap.parseDN(`cn=${username}@${appDomain},ou=proftpd,dc=cloudron`); + + var obj = { + dn: dn.toString(), + attributes: { + homeDirectory: path.join(paths.APPS_DATA_DIR, app.id, 'data'), + objectclass: ['user'], + objectcategory: 'person', + cn: result.id, + uid: `${result.username}@${appDomain}`, // for bind after search + uidNumber: 1000, // unix uid for ftp access + gidNumber: 1000 // unix gid for ftp access + } + }; + + finalSend([ obj ], req, res, next); + }); + }); +} + function authenticateMailAddon(req, res, next) { debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); @@ -555,6 +625,9 @@ function start(callback) { gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka + gServer.bind('ou=proftpd,dc=cloudron', authenticateProftpd); // proftdp + gServer.search('ou=proftpd,dc=cloudron', userSearchProftpd); + gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare); gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare); diff --git a/src/paths.js b/src/paths.js index 6384ddf31..0c04ac4e5 100644 --- a/src/paths.js +++ b/src/paths.js @@ -25,6 +25,7 @@ exports = module.exports = { UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'), SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'), DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'), + PROFTPD_CONFIG_FILE: path.join(config.baseDir(), 'platformdata/proftpd.conf'), // this is not part of appdata because an icon may be set before install APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'), diff --git a/src/platform.js b/src/platform.js index 09215828e..bc7ce1159 100644 --- a/src/platform.js +++ b/src/platform.js @@ -12,13 +12,12 @@ var addons = require('./addons.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), - config = require('./config.js'), debug = require('debug')('box:platform'), + ejs = require('ejs'), fs = require('fs'), graphs = require('./graphs.js'), infra = require('./infra_version.js'), locker = require('./locker.js'), - mail = require('./mail.js'), paths = require('./paths.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), @@ -29,6 +28,8 @@ var addons = require('./addons.js'), var NOOP_CALLBACK = function (error) { if (error) debug(error); }; +var PROFTPD_CONFIG_EJS = fs.readFileSync(__dirname + '/proftpd.ejs', { encoding: 'utf8' }); + function start(callback) { assert.strictEqual(typeof callback, 'function'); @@ -62,6 +63,7 @@ function start(callback) { startApps.bind(null, existingInfra), graphs.startGraphite.bind(null, existingInfra), addons.startServices.bind(null, existingInfra), + configureProftpd.bind(null, existingInfra), fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)) ], function (error) { if (error) return callback(error); @@ -165,3 +167,19 @@ function startApps(existingInfra, callback) { callback(); } } + +function configureProftpd(existingInfra, callback) { + var data = { + sftpPort: 222, + ldapUrl: 'ldap://172.18.0.1:3002', + ldapUsersBaseDn: 'ou=proftpd,dc=cloudron', + ldapBindUsername: 'cn=admin@cloudron,ou=proftpd,dc=cloudron', + ldapBindPassword: 'password' + }; + + console.log('------ configure proftpd', data, paths.PROFTPD_CONFIG_FILE); + + if (!safe.fs.writeFileSync(paths.PROFTPD_CONFIG_FILE, ejs.render(PROFTPD_CONFIG_EJS, data))) return callback(safe.error); + + callback(null); +} diff --git a/setup/start/proftpd.conf b/src/proftpd.ejs similarity index 63% rename from setup/start/proftpd.conf rename to src/proftpd.ejs index f75bd1cba..55e3d383c 100644 --- a/setup/start/proftpd.conf +++ b/src/proftpd.ejs @@ -6,7 +6,7 @@ UseIPv6 off # If set on you can experience a longer connection delay in many cases. IdentLookups off -ServerName "##SERVER_NAME" +ServerName Cloudron ServerType standalone DeferWelcome off @@ -24,8 +24,8 @@ ListOptions "-l" DenyFilter \*.*/ -# Use this to jail all users in their homes -# DefaultRoot ~ +# Use this to jail all users in their homes, will be homeDirectory LDAP attribute +DefaultRoot ~ # Users require a valid shell listed in /etc/shells to login. # Use this directive to release that constrain. @@ -59,26 +59,26 @@ SystemLog /run/proftpd/proftpd.log WtmpLog off -QuotaEngine off + QuotaEngine off -Ratios off + Ratios off # Delay engine reduces impact of the so-called Timing Attack described in # http://www.securityfocus.com/bid/11430/discuss # It is on by default. -DelayEngine on + DelayEngine on -ControlsEngine off -ControlsMaxClients 2 -ControlsLog /var/log/proftpd/controls.log -ControlsInterval 5 -ControlsSocket /var/run/proftpd/proftpd.sock + ControlsEngine off + ControlsMaxClients 2 + ControlsLog /var/log/proftpd/controls.log + ControlsInterval 5 + ControlsSocket /var/run/proftpd/proftpd.sock @@ -87,43 +87,31 @@ ControlsSocket /var/run/proftpd/proftpd.sock LoadModule mod_ldap.c -# https://forums.proftpd.org/smf/index.php?topic=6368.0 -LDAPServer "##LDAP_URL/??sub" -LDAPBindDN "##LDAP_BIND_DN" "##LDAP_BIND_PASSWORD" -LDAPUsers "##LDAP_USERS_BASE_DN" (username=%u) + # https://forums.proftpd.org/smf/index.php?topic=6368.0 + LDAPServer "<%= ldapUrl %>/??sub" + LDAPBindDN "<%= ldapBindUsername %>" "<%= ldapBindPassword %>" + LDAPUsers "<%= ldapUsersBaseDn %>" (username=%u) -LDAPForceDefaultUID on -LDAPDefaultUID ##LDAP_UID -LDAPForceDefaultGID on -LDAPDefaultGID ##LDAP_GID - -LDAPForceGeneratedHomedir on -LDAPGenerateHomedir on -LDAPGenerateHomedirPrefix /app/data -LDAPGenerateHomedirPrefixNoUsername on - -#LDAPUseTLS off -#LDAPLog /run/proftpd/ldap.log + LDAPLog /var/log/proftpd/ldap.log -SFTPEngine on -Port ##SFTP_PORT -SFTPLog /run/proftpd/sftp.log + SFTPEngine on + Port <%= sftpPort %> + SFTPLog /var/log/proftpd/sftp.log -# Configure both the RSA and DSA host keys, using the same host key -# files that OpenSSH uses. -SFTPHostKey /app/data/.sftpd/ssh_host_rsa_key -SFTPHostKey /app/data/.sftpd/ssh_host_dsa_key + # Configure both the RSA and DSA host keys, using the same host key + # files that OpenSSH uses. + SFTPHostKey /etc/ssh/ssh_host_rsa_key -SFTPAuthMethods password + SFTPAuthMethods password -# Enable compression -SFTPCompression delayed + # Enable compression + SFTPCompression delayed -RequireValidShell off + RequireValidShell off - HideNoAccess yes + HideNoAccess yes diff --git a/src/users.js b/src/users.js index 3e9a297c5..78cc351f0 100644 --- a/src/users.js +++ b/src/users.js @@ -16,6 +16,7 @@ exports = module.exports = { remove: removeUser, get: get, getByResetToken: getByResetToken, + getByUsername: getByUsername, getAllAdmins: getAllAdmins, resetPasswordByIdentifier: resetPasswordByIdentifier, setPassword: setPassword, @@ -385,6 +386,18 @@ function getByResetToken(email, resetToken, callback) { }); } +function getByUsername(username, callback) { + assert.strictEqual(typeof username, 'string'); + assert.strictEqual(typeof callback, 'function'); + + userdb.getByUsername(username.toLowerCase(), function (error, result) { + if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND)); + if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error)); + + get(result.id, callback); + }); +} + function updateUser(userId, data, auditSource, callback) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof data, 'object');