diff --git a/setup/start/cloudron-firewall.sh b/setup/start/cloudron-firewall.sh index b73a33082..8e1e8fed6 100755 --- a/setup/start/cloudron-firewall.sh +++ b/setup/start/cloudron-firewall.sh @@ -36,9 +36,13 @@ if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_ done fi +# first setup any user IP block lists +ipset create cloudron_ldap_allowlist hash:net || true +/home/yellowtent/box/src/scripts/setldapallowlist.sh + # ldap server we expose 3004 and also redirect from standard ldaps port 636 -iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 636,3004 -j ACCEPT iptables -t nat -I PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004 +iptables -t filter -A CLOUDRON -m set --match-set cloudron_ldap_allowlist src -p tcp --dport 3004 -j ACCEPT # turn and stun service iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT diff --git a/setup/start/sudoers b/setup/start/sudoers index 259baa91b..2cd9a0e6a 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -53,6 +53,9 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh Defaults!/home/yellowtent/box/src/scripts/setblocklist.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setblocklist.sh +Defaults!/home/yellowtent/box/src/scripts/setldapallowlist.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setldapallowlist.sh + Defaults!/home/yellowtent/box/src/scripts/addmount.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/addmount.sh diff --git a/src/paths.js b/src/paths.js index 235911bfc..9c9503c4f 100644 --- a/src/paths.js +++ b/src/paths.js @@ -48,6 +48,7 @@ exports = module.exports = { SFTP_PUBLIC_KEY_FILE: path.join(baseDir(), 'platformdata/sftp/ssh/ssh_host_rsa_key.pub'), SFTP_PRIVATE_KEY_FILE: path.join(baseDir(), 'platformdata/sftp/ssh/ssh_host_rsa_key'), FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'platformdata/firewall/blocklist.txt'), + LDAP_ALLOWLIST_FILE: path.join(baseDir(), 'platformdata/firewall/ldap_allowlist.txt'), BOX_DATA_DIR: path.join(baseDir(), 'boxdata/box'), MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'), diff --git a/src/scripts/setldapallowlist.sh b/src/scripts/setldapallowlist.sh new file mode 100755 index 000000000..193935209 --- /dev/null +++ b/src/scripts/setldapallowlist.sh @@ -0,0 +1,28 @@ +#!/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 + +[[ "${BOX_ENV}" == "test" ]] && exit + +ipset flush cloudron_ldap_allowlist + +ldap_allowlist_json="/home/yellowtent/platformdata/firewall/ldap_allowlist.txt" + +if [[ -f "${ldap_allowlist_json}" ]]; then + # without the -n block, any last line without a new line won't be read it! + while read -r line || [[ -n "$line" ]]; do + [[ -z "${line}" ]] && continue # ignore empty lines + [[ "$line" =~ ^#.*$ ]] && continue # ignore lines starting with # + ipset add -! cloudron_ldap_allowlist "${line}" # the -! ignore duplicates + done < "${ldap_allowlist_json}" +fi diff --git a/src/settings.js b/src/settings.js index 2a334e868..0399281ca 100644 --- a/src/settings.js +++ b/src/settings.js @@ -146,8 +146,10 @@ const assert = require('assert'), externalLdap = require('./externalldap.js'), moment = require('moment-timezone'), mounts = require('./mounts.js'), + path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), + shell = require('./shell.js'), sysinfo = require('./sysinfo.js'), tokens = require('./tokens.js'), translation = require('./translation.js'), @@ -157,6 +159,7 @@ const assert = require('assert'), const SETTINGS_FIELDS = [ 'name', 'value' ].join(','); const SETTINGS_BLOB_FIELDS = [ 'name', 'valueBlob' ].join(','); +const SET_LDAP_ALLOWLIST_CMD = path.join(__dirname, 'scripts/setldapallowlist.sh'); const gDefaults = (function () { const result = { }; @@ -512,17 +515,30 @@ async function setExposedLdapConfig(exposedLdapConfig) { const config = { enabled: exposedLdapConfig.enabled, - allowlist: exposedLdapConfig.allowlistc || '' + // if list is empty, we allow all IPs + allowlist: exposedLdapConfig.allowlist || '0.0.0.0/0' }; - for (const line of exposedLdapConfig.allowlist.split('\n')) { - if (!line || line.startsWith('#')) continue; - const rangeOrIP = line.trim(); - if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`); + if (config.enabled) { + for (const line of exposedLdapConfig.allowlist.split('\n')) { + if (!line || line.startsWith('#')) continue; + const rangeOrIP = line.trim(); + if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`); + } } await set(exports.EXPOSED_LDAP_KEY, JSON.stringify(config)); + // this is done only because it's easier for the shell script and the firewall service to get the value + if (config.enabled) { + if (!safe.fs.writeFileSync(paths.LDAP_ALLOWLIST_FILE, exposedLdapConfig.allowlist + '\n', 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message); + } else { + safe.fs.unlinkSync(paths.LDAP_ALLOWLIST_FILE); + } + + const [error] = await safe(shell.promises.sudo('setLdapAllowlist', [ SET_LDAP_ALLOWLIST_CMD ], {})); + if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting ldap allowlist: ${error.message}`); + notifyChange(exports.EXPOSED_LDAP_KEY, config); }