diff --git a/baseimage/initializeBaseUbuntuImage.sh b/baseimage/initializeBaseUbuntuImage.sh index 01ba87467..90b18315a 100755 --- a/baseimage/initializeBaseUbuntuImage.sh +++ b/baseimage/initializeBaseUbuntuImage.sh @@ -44,8 +44,6 @@ apt-get -y install \ mysql-server-5.7 \ nginx-full \ openssh-server \ - proftpd-basic \ - proftpd-mod-ldap \ pwgen \ resolvconf \ sudo \ diff --git a/scripts/installer.sh b/scripts/installer.sh index 3e934d270..dbf6c0eea 100755 --- a/scripts/installer.sh +++ b/scripts/installer.sh @@ -56,23 +56,6 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb fi -echo "==> installer: updating proftpd" -while ! command -v proftpd; do - echo "Install proftpd" - if ! apt install -y debconf-utils; then - echo "==> installer: Failed to install debconf-utils. Retry" - sleep 1 - continue - fi - echo "proftpd-basic shared/proftpd/inetd_or_standalone select standalone" | debconf-set-selections - if ! apt install -y proftpd-basic proftpd-mod-ldap; then - echo "==> installer: Failed to install proftpd. Retry" - sleep 1 - continue - fi - systemctl stop proftpd -done - echo "==> installer: updating node" if [[ "$(node --version)" != "v10.15.1" ]]; then mkdir -p /usr/local/node-10.15.1 diff --git a/setup/start.sh b/setup/start.sh index 013619a50..ab3e5c4f6 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -148,11 +148,6 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi fi systemctl start nginx -echo "==> Configuring proftpd" -rm -f /etc/proftpd/proftpd.conf -cp "${script_dir}/start/proftpd.conf" /etc/proftpd/proftpd.conf -systemctl restart proftpd - # 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 ef954b47f..4734964ad 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,222,443,587,993,4190 -j ACCEPT +iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,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/setup/start/proftpd.conf b/setup/start/proftpd.conf deleted file mode 100644 index c0130480e..000000000 --- a/setup/start/proftpd.conf +++ /dev/null @@ -1,116 +0,0 @@ -# Includes DSO modules -Include /etc/proftpd/modules.conf - -# Set off to disable IPv6 support which is annoying on IPv4 only boxes. -UseIPv6 off -# If set on you can experience a longer connection delay in many cases. -IdentLookups off - -ServerName Cloudron -ServerType standalone -DeferWelcome off - -MultilineRFC2228 on -DefaultServer on -ShowSymlinks on - -TimeoutNoTransfer 600 -TimeoutStalled 600 -TimeoutIdle 1200 - -DisplayLogin welcome.msg -DisplayChdir .message true -ListOptions "-l" - -DenyFilter \*.*/ - -# 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. -# RequireValidShell off - -# Port 21 is the standard FTP port. -Port 0 - -# To prevent DoS attacks, set the maximum number of child processes -# to 30. If you need to allow more than 30 concurrent connections -# at once, simply increase this value. Note that this ONLY works -# in standalone mode, in inetd mode you should use an inetd server -# that allows you to limit maximum number of processes per service -# (such as xinetd) -MaxInstances 10 - -# Set the user and group that the server normally runs at. -User www-data -Group www-data - -# Umask 022 is a good standard umask to prevent new files and dirs -# (second parm) from being group and world writable. -Umask 022 022 -# Normally, we want files to be overwriteable. -AllowOverwrite on - -TransferLog /run/proftpd/xferlog -SystemLog /run/proftpd/proftpd.log - -# disable ssh login log -WtmpLog off - - - QuotaEngine 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 - - - - ControlsEngine off - ControlsMaxClients 2 - ControlsLog /var/log/proftpd/controls.log - ControlsInterval 5 - ControlsSocket /var/run/proftpd/proftpd.sock - - - - AdminControlsEngine off - - -LoadModule mod_ldap.c - - # https://forums.proftpd.org/smf/index.php?topic=6368.0 - LDAPServer "ldap://localhost:3002/??sub" - LDAPUsers "ou=proftpd,dc=cloudron" (username=%u) - - LDAPLog /var/log/proftpd/ldap.log - - - - SFTPEngine on - Port 222 - SFTPLog /var/log/proftpd/sftp.log - - # 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 - - # Enable compression - SFTPCompression delayed - - RequireValidShell off - - - - HideNoAccess yes - diff --git a/setup/start/sudoers b/setup/start/sudoers index 84c220a1b..a5d85136a 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -50,5 +50,3 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh -Defaults!/home/yellowtent/box/src/scripts/restartproftpd.sh env_keep="HOME BOX_ENV" -yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartproftpd.sh diff --git a/src/addons.js b/src/addons.js index 4f35c6560..a34276d2d 100644 --- a/src/addons.js +++ b/src/addons.js @@ -212,9 +212,9 @@ const KNOWN_SERVICES = { restart: restartUnbound, defaultMemoryLimit: 0 }, - proftpd: { - status: statusProftpd, - restart: restartProftpd, + sftp: { + status: statusSftp, + restart: restartContainer.bind(null, 'sftp'), defaultMemoryLimit: 0 }, graphite: { @@ -1726,22 +1726,27 @@ function restartUnbound(callback) { callback(null); } -function statusProftpd(callback) { +function statusSftp(callback) { assert.strictEqual(typeof callback, 'function'); - shell.exec('statusProftpd', 'systemctl is-active proftpd', function (error) { - callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE }); + docker.inspect('sftp', function (error, container) { + if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error)); + if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error)); + + docker.memoryUsage('sftp', function (error, result) { + if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error)); + + var tmp = { + status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED, + memoryUsed: result.memory_stats.usage, + memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit) + }; + + callback(null, tmp); + }); }); } -function restartProftpd(callback) { - assert.strictEqual(typeof callback, 'function'); - - shell.sudo('restartProftpd', [ path.join(__dirname, 'scripts/restartproftpd.sh') ], {}, NOOP_CALLBACK); - - callback(null); -} - function statusGraphite(callback) { assert.strictEqual(typeof callback, 'function'); diff --git a/src/infra_version.js b/src/infra_version.js index 6090d1cd1..e9ec9f5d8 100644 --- a/src/infra_version.js +++ b/src/infra_version.js @@ -20,6 +20,7 @@ exports = module.exports = { 'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' }, 'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' }, 'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.2.0@sha256:20e4d2508dcf712eb56481067993ae39bf541d793d44f99f6a41d630ad941d9e' }, - 'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' } + 'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }, + 'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.0.1@sha256:2e620f62cf2868ee29eafdc574eca012990a21898f77ab47f2f86e5788134f20' } } }; diff --git a/src/ldap.js b/src/ldap.js index ca1b67361..f426e0877 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -18,7 +18,6 @@ var assert = require('assert'), MailError = mail.MailError, mailboxdb = require('./mailboxdb.js'), path = require('path'), - paths = require('./paths.js'), safe = require('safetydance'), users = require('./users.js'), UsersError = users.UsersError; @@ -479,12 +478,8 @@ function authenticateUserMailbox(req, res, next) { }); } -function authenticateProftpd(req, res, next) { - debug('proftpd addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); - - var sourceIp = req.connection.ldap.id.split(':')[0]; - if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier')); - if (sourceIp !== '127.0.0.1') return next(new ldap.InsufficientAccessRightsError('Source not authorized')); +function authenticateSftp(req, res, next) { + debug('sftp 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())); @@ -496,18 +491,14 @@ function authenticateProftpd(req, res, next) { users.verifyWithUsername(parts[0], req.credentials, function (error) { if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - debug('proftpd addon auth: success'); + debug('sftp 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); - - var sourceIp = req.connection.ldap.id.split(':')[0]; - if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier')); - if (sourceIp !== '127.0.0.1') return next(new ldap.InsufficientAccessRightsError('Source not authorized')); +function userSearchSftp(req, res, next) { + debug('sftp 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())); @@ -525,7 +516,6 @@ function userSearchProftpd(req, res, next) { if (typeof app.manifest.addons.localstorage.ftp.uid !== 'string') return next(new ldap.UnavailableError('Bad uid')); const uidNumber = parseInt(app.manifest.addons.localstorage.ftp.uid.split('/')[0], 10); - if (!Number.isInteger(uidNumber)) { console.error('addon localstorage ftp uid must be an integer', app); return next(new ldap.UnavailableError('Not supported')); @@ -539,9 +529,9 @@ function userSearchProftpd(req, res, next) { if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized')); var obj = { - dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=proftpd,dc=cloudron`).toString(), + dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), attributes: { - homeDirectory: path.join(paths.APPS_DATA_DIR, app.id, 'data'), + homeDirectory: path.join('/app/data', app.id, 'data'), objectclass: ['user'], objectcategory: 'person', cn: user.id, @@ -631,8 +621,8 @@ 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.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp + gServer.search('ou=sftp,dc=cloudron', userSearchSftp); gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare); gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare); diff --git a/src/platform.js b/src/platform.js index 60ced0cb0..7fcd447a1 100644 --- a/src/platform.js +++ b/src/platform.js @@ -21,6 +21,7 @@ var addons = require('./addons.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), settings = require('./settings.js'), + sftp = require('./sftp.js'), shell = require('./shell.js'), taskmanager = require('./taskmanager.js'), _ = require('underscore'); @@ -59,6 +60,7 @@ function start(callback) { // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored startApps.bind(null, existingInfra), graphs.startGraphite.bind(null, existingInfra), + sftp.startSftp.bind(null, existingInfra), addons.startServices.bind(null, existingInfra), fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)) ], function (error) { @@ -136,7 +138,7 @@ function stopContainers(existingInfra, callback) { } else { assert(typeof infra.images, 'object'); var changedAddons = [ ]; - for (var imageName in infra.images) { + for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName); } diff --git a/src/scripts/restartproftpd.sh b/src/scripts/restartproftpd.sh deleted file mode 100755 index b343cbdc5..000000000 --- a/src/scripts/restartproftpd.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -set -eu -o pipefail - -if [[ ${EUID} -ne 0 ]]; then - echo "This script should be run as root." > /dev/stderr - exit 1 -fi - -if [[ $# == 1 && "$1" == "--check" ]]; then - echo "OK" - exit 0 -fi - -if [[ "${BOX_ENV}" == "cloudron" ]]; then - systemctl restart proftpd -fi - diff --git a/src/sftp.js b/src/sftp.js new file mode 100644 index 000000000..2913d5e29 --- /dev/null +++ b/src/sftp.js @@ -0,0 +1,39 @@ +'use strict'; + +exports = module.exports = { + startSftp: startSftp +}; + +var assert = require('assert'), + infra = require('./infra_version.js'), + paths = require('./paths.js'), + shell = require('./shell.js'); + +function startSftp(existingInfra, callback) { + assert.strictEqual(typeof existingInfra, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const tag = infra.images.sftp.tag; + const memoryLimit = 256; + + if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback(); + + const cmd = `docker run --restart=always -d --name="sftp" \ + --net cloudron \ + --net-alias sftp \ + --log-driver syslog \ + --log-opt syslog-address=udp://127.0.0.1:2514 \ + --log-opt syslog-format=rfc5424 \ + --log-opt tag=sftp \ + -m ${memoryLimit}m \ + --memory-swap ${memoryLimit * 2}m \ + --dns 172.18.0.1 \ + --dns-search=. \ + -p 222:22 \ + -v "${paths.APPS_DATA_DIR}:/app/data" \ + -v "/etc/ssh:/etc/ssh:ro" \ + --label isCloudronManaged=true \ + --read-only -v /tmp -v /run "${tag}"`; + + shell.exec('startSftp', cmd, callback); +} diff --git a/src/test/checkInstall b/src/test/checkInstall index 4ea11f0b0..5518de3d7 100755 --- a/src/test/checkInstall +++ b/src/test/checkInstall @@ -18,7 +18,6 @@ scripts=("${SOURCE_DIR}/src/scripts/clearvolume.sh" \ "${SOURCE_DIR}/src/scripts/restart.sh" \ "${SOURCE_DIR}/src/scripts/restartdocker.sh" \ "${SOURCE_DIR}/src/scripts/restartunbound.sh" \ - "${SOURCE_DIR}/src/scripts/restartproftpd.sh" \ "${SOURCE_DIR}/src/scripts/update.sh" \ "${SOURCE_DIR}/src/scripts/collectlogs.sh" \ "${SOURCE_DIR}/src/scripts/configurecollectd.sh" \