diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c99020b..b4fa8198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,59 @@ CHANGELOG ========= +In Development +-------------- + +ownCloud: + +* Update ownCloud to 8.2.3 + +Mail: + +* Roundcube is updated to version 1.1.5 +* Fixed a long-standing issue with training the spam filter not working (because of a file permissions issue). + +Control panel: + +* Munin system monitoring graphs are now zoomable. +* When a reboot is required (due to Ubuntu security updates automatically installed), a Reboot Box button now appears. +* It is now possible to add SRV and secondary MX records in the Custom DNS page. +* Other minor fixes. + +System: + +* The fail2ban recidive jail, which blocks long-duration brute force attacks, now no longer sends the administrator emails (which were not helpful). + +Setup: + +* The system hostname is now set during setup. +* A swap file is now created if system memory is less than 2GB, 5GB of free disk space is available, and if no swap file yet exists. +* We now install Roundcube from the official GitHub repository instead of our own mirror, we have created to solve problems with SourceForge. + + +v0.17c (April 1, 2016) +---------------------- + +This update addresses some minor security concerns and some installation issues. + +ownCoud: + +* Block web access to the configuration parameters (config.php). There is no immediate impact (see [#776](https://github.com/mail-in-a-box/mailinabox/pull/776)), although advanced users may want to take note. + +Mail: + +* Roundcube html5_notifier plugin updated from version 0.6 to 0.6.2 to fix Roundcube getting stuck for some people. + +Control panel: + +* Prevent click-jacking of the management interface by adding HTTP headers. +* Failed login no longer reveals whether an account exists on the system. + +Setup: + +* Setup dialogs did not appear correctly when connecting to SSH using Putty on Windows. +* We now install Roundcube from our own mirror because Sourceforge's downloads experience frequent intermittant unavailability. + v0.17b (March 1, 2016) ---------------------- diff --git a/README.md b/README.md index d8329054..a12e48bb 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ by me: $ curl -s https://keybase.io/joshdata/key.asc | gpg --import gpg: key C10BDD81: public key "Joshua Tauberer " imported - $ git verify-tag v0.17b + $ git verify-tag v0.17c gpg: Signature made ..... using RSA key ID C10BDD81 gpg: Good signature from "Joshua Tauberer " gpg: WARNING: This key is not certified with a trusted signature! @@ -72,7 +72,7 @@ and on my [personal homepage](https://razor.occams.info/). (Of course, if this r Checkout the tag corresponding to the most recent release: - $ git checkout v0.17b + $ git checkout v0.17c Begin the installation. diff --git a/conf/fail2ban/jail.local b/conf/fail2ban/jail.local index b9340e52..cc741c80 100644 --- a/conf/fail2ban/jail.local +++ b/conf/fail2ban/jail.local @@ -27,3 +27,14 @@ maxretry = 20 [recidive] enabled = true maxretry = 10 +action = iptables-allports[name=recidive] +# In the recidive section of jail.conf the action contains: +# +# action = iptables-allports[name=recidive] +# sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log] +# +# The last line on the action will sent an email to the configured address. This mail will +# notify the administrator that someone has been repeatedly triggering one of the other jails. +# By default we don't configure this address and no action is required from the admin anyway. +# So the notification is ommited. This will prevent message appearing in the mail.log that mail +# can't be delivered to fail2ban@$HOSTNAME. diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf index 2fb9972e..55c80eba 100644 --- a/conf/nginx-primaryonly.conf +++ b/conf/nginx-primaryonly.conf @@ -6,6 +6,9 @@ location /admin/ { proxy_pass http://127.0.0.1:10222/; proxy_set_header X-Forwarded-For $remote_addr; + add_header X-Frame-Options "DENY"; + add_header X-Content-Type-Options nosniff; + add_header Content-Security-Policy "frame-ancestors 'none';"; } # ownCloud configuration. @@ -15,8 +18,11 @@ rewrite ^(/cloud/core/doc/[^\/]+/)$ $1/index.html; location /cloud/ { alias /usr/local/lib/owncloud/; - location ~ ^/(data|config|\.ht|db_structure\.xml|README) { - deny all; + location ~ ^/cloud/(build|tests|config|lib|3rdparty|templates|data|README)/ { + deny all; + } + location ~ ^/cloud/(?:\.|autotest|occ|issue|indie|db_|console) { + deny all; } } location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ { diff --git a/management/daemon.py b/management/daemon.py index bf3c9134..5400925f 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -1,10 +1,10 @@ #!/usr/bin/python3 import os, os.path, re, json - +import subprocess from functools import wraps -from flask import Flask, request, render_template, abort, Response, send_from_directory +from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response import auth, utils, multiprocessing.pool from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user @@ -43,7 +43,7 @@ def authorized_personnel_only(viewfunc): except ValueError as e: # Authentication failed. privs = [] - error = str(e) + error = "Incorrect username or password" # Authorized to access an API view? if "admin" in privs: @@ -119,7 +119,7 @@ def me(): except ValueError as e: return json_response({ "status": "invalid", - "reason": str(e), + "reason": "Incorrect username or password", }) resp = { @@ -453,6 +453,27 @@ def do_updates(): "DEBIAN_FRONTEND": "noninteractive" }) + +@app.route('/system/reboot', methods=["GET"]) +@authorized_personnel_only +def needs_reboot(): + from status_checks import is_reboot_needed_due_to_package_installation + if is_reboot_needed_due_to_package_installation(): + return json_response(True) + else: + return json_response(False) + +@app.route('/system/reboot', methods=["POST"]) +@authorized_personnel_only +def do_reboot(): + # To keep the attack surface low, we don't allow a remote reboot if one isn't necessary. + from status_checks import is_reboot_needed_due_to_package_installation + if is_reboot_needed_due_to_package_installation(): + return utils.shell("check_output", ["/sbin/shutdown", "-r", "now"], capture_stderr=True) + else: + return "No reboot is required, so it is not allowed." + + @app.route('/system/backup/status') @authorized_personnel_only def backup_status(): @@ -504,6 +525,64 @@ def munin(filename=""): if filename == "": filename = "index.html" return send_from_directory("/var/cache/munin/www", filename) +@app.route('/munin/cgi-graph/') +@authorized_personnel_only +def munin_cgi(filename): + """ Relay munin cgi dynazoom requests + /usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package + that is responsible for generating binary png images _and_ associated HTTP + headers based on parameters in the requesting URL. All output is written + to stdout which munin_cgi splits into response headers and binary response + data. + munin-cgi-graph reads environment variables as well as passed input to determine + what it should do. It expects a path to be in the env-var PATH_INFO, and a + querystring to be in the env-var QUERY_STRING as well as passed as input to the + command. + munin-cgi-graph has several failure modes. Some write HTTP Status headers and + others return nonzero exit codes. + Situating munin_cgi between the user-agent and munin-cgi-graph enables keeping + the cgi script behind mailinabox's auth mechanisms and avoids additional + support infrastructure like spawn-fcgi. + """ + + COMMAND = 'su - munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph "%s"' + # su changes user, we use the munin user here + # --preserve-environment retains the environment, which is where Popen's `env` data is + # --shell=/bin/bash ensures the shell used is bash + # -c "/usr/lib/munin/cgi/munin-cgi-graph" passes the command to run as munin + # "%s" is a placeholder for where the request's querystring will be added + + if filename == "": + return ("a path must be specified", 404) + + query_str = request.query_string.decode("utf-8", 'ignore') + + env = {'PATH_INFO': '/%s/' % filename, 'QUERY_STRING': query_str} + cmd = COMMAND % query_str + code, binout = utils.shell('check_output', + cmd.split(' ', 5), + # Using a maxsplit of 5 keeps the last 2 arguments together + input=query_str.encode('UTF-8'), + env=env, + return_bytes=True, + trap=True) + + if code != 0: + # nonzero returncode indicates error + app.logger.error("munin_cgi: munin-cgi-graph returned nonzero exit code, %s", process.returncode) + return ("error processing graph image", 500) + + # /usr/lib/munin/cgi/munin-cgi-graph returns both headers and binary png when successful. + # A double-Windows-style-newline always indicates the end of HTTP headers. + headers, image_bytes = binout.split(b'\r\n\r\n', 1) + response = make_response(image_bytes) + for line in headers.splitlines(): + name, value = line.decode("utf8").split(':', 1) + response.headers[name] = value + if 'Status' in response.headers and '404' in response.headers['Status']: + app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO']) + return response + # APP if __name__ == '__main__': diff --git a/management/dns_update.py b/management/dns_update.py index 6f4de318..836ad0d4 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -175,9 +175,6 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en for value in build_sshfp_records(): records.append((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh.")) - # The MX record says where email for the domain should be delivered: Here! - records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain)) - # Add DNS records for any subdomains of this domain. We should not have a zone for # both a domain and one of its subdomains. subdomains = [d for d in all_domains if d.endswith("." + domain)] @@ -244,6 +241,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # Don't pin the list of records that has_rec checks against anymore. has_rec_base = records + # The MX record says where email for the domain should be delivered: Here! + if not has_rec(None, "MX", prefix="10 "): + records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain)) + # SPF record: Permit the box ('mx', see above) to send mail on behalf of # the domain, and no one else. # Skip if the user has set a custom SPF record. diff --git a/management/status_checks.py b/management/status_checks.py index 1feea68a..36a87ea1 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -185,10 +185,13 @@ def check_ssh_password(env, output): else: output.print_ok("SSH disallows password-based login.") +def is_reboot_needed_due_to_package_installation(): + return os.path.exists("/var/run/reboot-required") + def check_software_updates(env, output): # Check for any software package updates. pkgs = list_apt_updates(apt_update=False) - if os.path.exists("/var/run/reboot-required"): + if is_reboot_needed_due_to_package_installation(): output.print_error("System updates have been installed and a reboot of the machine is required.") elif len(pkgs) == 0: output.print_ok("System software is up to date.") diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html index f1244810..bd5643c3 100644 --- a/management/templates/custom-dns.html +++ b/management/templates/custom-dns.html @@ -36,6 +36,7 @@ + diff --git a/management/templates/index.html b/management/templates/index.html index ef0821fd..3b7cd0c5 100644 --- a/management/templates/index.html +++ b/management/templates/index.html @@ -9,7 +9,7 @@ - + - + @@ -192,7 +192,7 @@ - + diff --git a/management/templates/web.html b/management/templates/web.html index 6f594eeb..6a09ef0e 100644 --- a/management/templates/web.html +++ b/management/templates/web.html @@ -82,7 +82,7 @@ function show_change_web_root(elem) { var root = $(elem).parents('tr').attr('data-custom-web-root'); show_modal_confirm( 'Change Root Directory for ' + domain, - $('

You can change the static directory for ' + domain + ' to:

' + root + '

First create this directory on the server. Then click Update to scan for the directory and update web settings.'), + $('

You can change the static directory for ' + domain + ' to:

' + root + '

First create this directory on the server. Then click Update to scan for the directory and update web settings.

'), 'Update', function() { do_web_update(); }); } diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 6ff21c50..3e793576 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -7,7 +7,7 @@ ######################################################### if [ -z "$TAG" ]; then - TAG=v0.17b + TAG=v0.17c fi # Are we running as root? diff --git a/setup/munin.sh b/setup/munin.sh index 0cee9bad..b1c5c8ba 100755 --- a/setup/munin.sh +++ b/setup/munin.sh @@ -7,7 +7,8 @@ source /etc/mailinabox.conf # load global vars # install Munin echo "Installing Munin (system monitoring)..." -apt_install munin munin-node +apt_install munin munin-node libcgi-fast-perl +# libcgi-fast-perl is needed by /usr/lib/munin/cgi/munin-cgi-graph # edit config cat > /etc/munin/munin.conf < /etc/hostname +hostname $PRIMARY_HOSTNAME + +# ### Add swap space to the system + +# If the physical memory of the system is below 2GB it is wise to create a +# swap file. This will make the system more resiliant to memory spikes and +# prevent for instance spam filtering from crashing + +# We will create a 1G file, this should be a good balance between disk usage +# and buffers for the system. We will only allocate this file if there is more +# than 5GB of disk space available + +# The following checks are performed: +# - Check if swap is currently mountend by looking at /proc/swaps +# - Check if the user intents to activate swap on next boot by checking fstab entries. +# - Check if a swapfile already exists +# - Check if the root file system is not btrfs, might be an incompatible version with +# swapfiles. User should hanle it them selves. +# - Check the memory requirements +# - Check available diskspace + +# See https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04 +# for reference + +SWAP_MOUNTED=$(cat /proc/swaps | tail -n+2) +SWAP_IN_FSTAB=$(grep "swap" /etc/fstab) +ROOT_IS_BTRFS=$(grep "\/ .*btrfs" /proc/mounts) +TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}') +AVAILABLE_DISK_SPACE=$(df / --output=avail | tail -n 1) +if + [ -z "$SWAP_MOUNTED" ] && + [ -z "$SWAP_IN_FSTAB" ] && + [ ! -e /swapfile ] && + [ -z "$ROOT_IS_BTRFS" ] && + [ $TOTAL_PHYSICAL_MEM -lt 1900000 ] && + [ $AVAILABLE_DISK_SPACE -gt 5242880 ] +then + echo "Adding a swap file to the system..." + + # Allocate and activate the swap file. Allocate in 1KB chuncks + # doing it in one go, could fail on low memory systems + dd if=/dev/zero of=/swapfile bs=1024 count=$[1024*1024] status=none + if [ -e /swapfile ]; then + chmod 600 /swapfile + hide_output mkswap /swapfile + swapon /swapfile + fi + + # Check if swap is mounted then activate on boot + if swapon -s | grep -q "\/swapfile"; then + echo "/swapfile none swap sw 0 0" >> /etc/fstab + else + echo "ERROR: Swap allocation failed" + fi +fi + # ### Add Mail-in-a-Box's PPA. # We've built several .deb packages on our own that we want to include. diff --git a/setup/webmail.sh b/setup/webmail.sh index 2bef9f98..e6434555 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -34,11 +34,11 @@ apt-get purge -qq -y roundcube* #NODOC # Install Roundcube from source if it is not already present or if it is out of date. # Combine the Roundcube version number with the commit hash of vacation_sieve to track # whether we have the latest version. -VERSION=1.1.4 -HASH=4883c8bb39fadf8af94ffb09ee426cba9f8ef2e3 +VERSION=1.1.5 +HASH=8A59D196EF0AA6D9C717B00699215135ABCB99CF VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5 PERSISTENT_LOGIN_VERSION=1e9d724476a370ce917a2fcd5b3217b0c306c24e -HTML5_NOTIFIER_VERSION=046eb388dd63b1ec77a3ee485757fc25ae9e684d +HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5 UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:a needs_update=0 #NODOC if [ ! -f /usr/local/lib/roundcubemail/version ]; then @@ -51,7 +51,7 @@ fi if [ $needs_update == 1 ]; then # install roundcube wget_verify \ - https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz \ + https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION.tar.gz \ $HASH \ /tmp/roundcube.tgz tar -C /usr/local/lib --no-same-owner -zxf /tmp/roundcube.tgz @@ -99,7 +99,7 @@ cat > /usr/local/lib/roundcubemail/config/config.inc.php <