diff --git a/CHANGELOG.md b/CHANGELOG.md index c4033df1..9f4ae42d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ CHANGELOG ========= +In Development +-------------- + +System: + +* fail2ban didn't start after setup. + +Mail: + +* Disable Roundcube password plugin since it was corrupting the user database. + +Control panel: + +* Fix changing existing backup settings when the rsync type is used. +* Allow setting a custom port for rsync backups. +* Fixes to DNS lookups during status checks when there are timeouts, enforce timeouts better. +* A new check is added to ensure fail2ban is running. +* Fixed a color. +* Disable Roundcube password plugin since it was corrupting the user database +* Improve error messages in the management tools when external command-line tools are run. + Version 60.1 (October 30, 2022) ------------------------------- diff --git a/management/backup.py b/management/backup.py index 67deff83..2f63bcd0 100755 --- a/management/backup.py +++ b/management/backup.py @@ -208,9 +208,18 @@ def get_duplicity_additional_args(env): config = get_backup_config(env) if config["type"] == 'rsync': + # Extract a port number for the ssh transport. Duplicity accepts the + # optional port number syntax in the target, but it doesn't appear to act + # on it, so we set the ssh port explicitly via the duplicity options. + from urllib.parse import urlsplit + try: + port = urlsplit(config["target_url"]).port + except ValueError: + port = 22 + return [ - "--ssh-options= -i /root/.ssh/id_rsa_miab", - "--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"", + f"--ssh-options= -i /root/.ssh/id_rsa_miab -p {port}", + f"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p {port} -i /root/.ssh/id_rsa_miab\"", ] elif config["type"] == 's3': additional_args = ["--s3-endpoint-url", config["s3_endpoint_url"]] @@ -407,6 +416,14 @@ def list_target_files(config): rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)') rsync_target = '{host}:{path}' + # Strip off any trailing port specifier because it's not valid in rsync's + # DEST syntax. Explicitly set the port number for the ssh transport. + user_host, *_ = url.netloc.rsplit(':', 1) + try: + port = url.port + except ValueError: + port = 22 + url_path = url.path if not url_path.endswith('/'): url_path = url_path + '/' @@ -415,11 +432,11 @@ def list_target_files(config): rsync_command = [ 'rsync', '-e', - '/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes', + f'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes -p {port}', '--list-only', '-r', rsync_target.format( - host=url.netloc, + host=user_host, path=url_path) ] diff --git a/management/status_checks.py b/management/status_checks.py index 033834dd..b31a9818 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -95,6 +95,12 @@ def run_services_checks(env, output, pool): fatal = fatal or fatal2 output2.playback(output) + # Check fail2ban. + code, ret = shell('check_output', ["fail2ban-client", "status"], capture_stderr=True, trap=True) + if code != 0: + output.print_error("fail2ban is not running.") + all_running = False + if all_running: output.print_ok("All system services are running.") diff --git a/management/templates/index.html b/management/templates/index.html index f9c87f2c..323789ca 100644 --- a/management/templates/index.html +++ b/management/templates/index.html @@ -72,11 +72,6 @@ html { filter: invert(100%) hue-rotate(180deg); } - - /* Set explicit background color (necessary for Firefox) */ - html { - background-color: #111; - } /* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */ .form-control { diff --git a/management/templates/system-backup.html b/management/templates/system-backup.html index b6c5c5f0..62214579 100644 --- a/management/templates/system-backup.html +++ b/management/templates/system-backup.html @@ -45,6 +45,10 @@
+
+ The hostname at your rsync provider, e.g. da2327.rsync.net. Optionally includes a colon + and the provider's non-standard ssh port number, e.g. u215843.your-storagebox.de:23. +
@@ -266,12 +270,11 @@ function show_backup_configuration() { $("#min-age").val(r.min_age_in_days); } else if(r.type == "rsync") { $("#min-age").val(r.min_age_in_days); - $("#backup-target-type").val("rsync"); - var path = r.target_url.substring(8).split('//'); - var host_parts = path.shift().split('@'); - $("#backup-target-rsync-user").val(host_parts[0]); - $("#backup-target-rsync-host").val(host_parts[1]); - $("#backup-target-rsync-path").val('/'+path[0]); + const spec = url_split(r.target_url); + $("#backup-target-type").val(spec.scheme); + $("#backup-target-rsync-user").val(spec.user); + $("#backup-target-rsync-host").val(spec.host); + $("#backup-target-rsync-path").val(spec.path); } else if(r.type == "s3") { $("#backup-s3-access-key-id").val(r.s3_access_key_id); $("#backup-s3-secret-access-key").val(r.s3_secret_access_key); @@ -335,4 +338,31 @@ function set_backup_configuration() { }); return false; } + +// Return a two-element array of the substring preceding and the substring following +// the first occurence of separator in string. Return [undefined, string] if the +// separator does not appear in string. +const split1_rest = (string, separator) => { + const index = string.indexOf(separator); + return (index >= 0) ? [string.substring(0, index), string.substring(index + separator.length)] : [undefined, string]; +}; + +// Note: The manifest JS URL class does not work in some security-conscious +// settings, e.g. Brave browser, so we roll our own that handles only what we need. +// +// Use greedy separator parsing to get parts of a MIAB backup target url. +// Note: path will not include a leading forward slash '/' +const url_split = url => { + const [ scheme, scheme_rest ] = split1_rest(url, '://'); + const [ user, user_rest ] = split1_rest(scheme_rest, '@'); + const [ host, path ] = split1_rest(user_rest, '/'); + + return { + scheme, + user, + host, + path, + } +}; + diff --git a/management/utils.py b/management/utils.py index d10499f1..120c6c83 100644 --- a/management/utils.py +++ b/management/utils.py @@ -136,13 +136,16 @@ def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, tr if method == "check_output" and input is not None: kwargs['input'] = input - if not trap: + try: ret = getattr(subprocess, method)(cmd_args, **kwargs) - else: - try: - ret = getattr(subprocess, method)(cmd_args, **kwargs) - code = 0 - except subprocess.CalledProcessError as e: + code = 0 + except subprocess.CalledProcessError as e: + if not trap: + # Reformat exception. + msg = "Command failed with exit code {}: {}".format(e.returncode, subprocess.list2cmdline(cmd_args)) + if e.output: msg += "\n\nOutput:\n" + e.output + raise Exception(msg) + else: ret = e.output code = e.returncode if not return_bytes and isinstance(ret, bytes): ret = ret.decode("utf8") diff --git a/security.md b/security.md index ac508c93..8782343e 100644 --- a/security.md +++ b/security.md @@ -1,7 +1,7 @@ Mail-in-a-Box Security Guide ============================ -Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components. +Mail-in-a-Box turns a fresh Ubuntu 22.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components. This page documents the security posture of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.