From d703b0a2a1ab92e3a82dad9238d274da57dc1fe7 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 6 Nov 2015 07:47:40 -0500 Subject: [PATCH 01/11] change from /etc/cron.daily to /etc/cron.d --- setup/management.sh | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setup/management.sh b/setup/management.sh index 468ba91a..cadda813 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -31,13 +31,12 @@ ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox hide_output update-rc.d mailinabox defaults # Perform a daily backup. -cat > /etc/cron.daily/mailinabox-backup << EOF; -#!/bin/bash -# Mail-in-a-Box --- Do not edit / will be overwritten on update. -# Perform a backup. -$(pwd)/management/backup.py +cat > /etc/cron.d/mailinabox-backup << EOF; +# /etc/cron.d/mailinabox-backup: crontab fragment to run maininabox-backup +# This executes mailinabox-backup at 3am. + +0 3 * * * root $(pwd)/management/backup.py EOF -chmod +x /etc/cron.daily/mailinabox-backup # Perform daily status checks. Compare each day to the previous # for changes and mail the changes to the administrator. From 82f4f8b2eba5253cfcfe9a7e30373e2357ff339e Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 6 Nov 2015 07:55:48 -0500 Subject: [PATCH 02/11] delete /etc/cron.daily/mailinabox-backup --- setup/management.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup/management.sh b/setup/management.sh index cadda813..07171a0e 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -31,9 +31,12 @@ ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox hide_output update-rc.d mailinabox defaults # Perform a daily backup. +if [ -f /etc/cron.daily/mailinabox-backup ]; then + rm /etc/cron.daily/mailinabox-backup +fi cat > /etc/cron.d/mailinabox-backup << EOF; # /etc/cron.d/mailinabox-backup: crontab fragment to run maininabox-backup -# This executes mailinabox-backup at 3am. +# This executes $(pwd)/management/backup.py at 3am. 0 3 * * * root $(pwd)/management/backup.py EOF From 8a35905d2e9d967e7a3550e2620da2e341fbb3fa Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 23 Dec 2015 17:29:13 -0500 Subject: [PATCH 03/11] add timezone selection --- setup/management.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup/management.sh b/setup/management.sh index 07171a0e..4effb67b 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -30,6 +30,12 @@ rm -f /etc/init.d/mailinabox ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox hide_output update-rc.d mailinabox defaults +# Set time zone to something convenient for the user +# Backup will be set for 3am localtime so choice is important depending +# on user locations. + +dpkg-reconfigure tzdata + # Perform a daily backup. if [ -f /etc/cron.daily/mailinabox-backup ]; then rm /etc/cron.daily/mailinabox-backup From e4a4b47fac4bbdb5e8858c595dadda23412e030a Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 08:08:08 -0500 Subject: [PATCH 04/11] setup now asks for and sets the system timezone closes #294 see #328 maybe related to #235 --- CHANGELOG.md | 1 + setup/start.sh | 2 ++ setup/system.sh | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4651873d..5c98e9c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Control panel: System: +* Setup (first run only) now asks for your timezone to set the system time. * The Exchange/ActiveSync server is now taken offline during nightly backups (along with SMTP and IMAP). * The machine's random number generator (/dev/urandom) is now seeded with Ubuntu Pollinate and a blocking read on /dev/random. * DNSSEC key generation during install now uses /dev/urandom (instead of /dev/random), which is faster. diff --git a/setup/start.sh b/setup/start.sh index 96abd617..201fcee0 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -34,6 +34,8 @@ if [ -f /etc/mailinabox.conf ]; then cat /etc/mailinabox.conf | sed s/^/DEFAULT_/ > /tmp/mailinabox.prev.conf source /tmp/mailinabox.prev.conf rm -f /tmp/mailinabox.prev.conf +else + FIRST_TIME_SETUP=1 fi # Put a start script in a global location. We tell the user to run 'mailinabox' diff --git a/setup/system.sh b/setup/system.sh index 8f7b640b..1aeec458 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -55,6 +55,38 @@ apt_install python3 python3-dev python3-pip \ haveged pollinate \ unattended-upgrades cron ntp fail2ban +# ### Set the system timezone +# +# Some systems are missing /etc/timezone, which we cat into the configs for +# Z-Push and ownCloud, so we need to set it to something. Daily cron tasks +# like the system backup are run at a time tied to the system timezone, so +# letting the user choose will help us identify the right time to do those +# things (i.e. late at night in whatever timezone the user actually lives +# in). +# +# However, changing the timezone once it is set seems to confuse fail2ban +# and requires restarting fail2ban (done below in the fail2ban +# section) and syslog (see #328). There might be other issues, and it's +# not likely the user will want to change this, so we only ask on first +# setup. +if [ -z "$NONINTERACTIVE" ]; then + if [ ! -f /etc/timezone ] || [ ! -z $FIRST_TIME_SETUP ]; then + # If the file is missing or this is the user's first time running + # Mail-in-a-Box setup, run the interactive timezone configuration + # tool. + dpkg-reconfigure tzdata + restart_service rsyslog + fi +else + # This is a non-interactive setup so we can't ask the user. + # If /etc/timezone is missing, set it to UTC. + if [ ! -f /etc/timezone ]; then + echo "Setting timezone to UTC." + echo "Etc/UTC" > /etc/timezone + restart_service rsyslog + fi +fi + # ### Seed /dev/urandom # # /dev/urandom is used by various components for generating random bytes for From 3cb5e109a31a32f1364e094e5ab1c2a3d92be2da Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 08:25:47 -0500 Subject: [PATCH 05/11] update changelog entries --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c98e9c0..0d204e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Still In Development Mail: * Updated Roundcube to version 1.1.3. -* Auto-create RFC2142 aliases for abuse@. +* Auto-create aliases for abuse@, as required by RFC2142. Control panel: @@ -16,17 +16,20 @@ Control panel: * DNS checks now have a timeout in case a DNS server is not responding, so the checks don't stall indefinitely. * Better messages if external DNS is used and, weirdly, custom secondary nameservers are set. * Add POP to the mail client settings documentation. +* The box's IP address is added to the fail2ban whitelist so that the status checks don't trigger the machine banning itself, which results in the status checks showing services down even though they are running. System: +* fail2ban's recidive jail is now active, which guards against persistent brute force login attacks over long periods of time. * Setup (first run only) now asks for your timezone to set the system time. * The Exchange/ActiveSync server is now taken offline during nightly backups (along with SMTP and IMAP). * The machine's random number generator (/dev/urandom) is now seeded with Ubuntu Pollinate and a blocking read on /dev/random. * DNSSEC key generation during install now uses /dev/urandom (instead of /dev/random), which is faster. +* The $STORAGE_ROOT/ssl directory is flattened by a migration script and the system SSL certificate path is now a symlink to the actual certificate. * If ownCloud sends out email, it will use the box's administrative address now (admin@yourboxname). * Z-Push (Exchange/ActiveSync) logs now exclude warnings and are now rotated to save disk space. * Fix pip command that might have not installed all necessary Python packages. -* The control panel and backup would not work on Google Compute Engine because they install a conflicting boto package. +* The control panel and backup would not work on Google Compute Engine because GCE installs a conflicting boto package. * Added a new command `management/backup.py --restore` to restore files from a backup to a target directory (command line arguments are passed to `duplicity restore`). v0.14 (November 4, 2015) From a4d8e12fd79fa10c9c6e3399eda4f8fa1f117b89 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 08:39:22 -0500 Subject: [PATCH 06/11] clean up the backup time patch: dont choose timezone here, move status checks into the same 3am script --- CHANGELOG.md | 1 + management/daily_tasks.sh | 8 ++++++++ setup/management.sh | 31 ++++++++----------------------- 3 files changed, 17 insertions(+), 23 deletions(-) create mode 100755 management/daily_tasks.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index aea098d6..97f167f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Still In Development Mail: * Update Roundcube to version 1.1.3. +* Nightly backups and system status checks are now moved to 3am in the system's timezone. v0.14 (November 4, 2015) ------------------------ diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh new file mode 100755 index 00000000..6237e6c2 --- /dev/null +++ b/management/daily_tasks.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# This script is run daily (at 3am each night). + +# Take a backup. +management/backup.py + +# Run status checks and email the administrator if anything changed. +management/status_checks.py --show-changes --smtp diff --git a/setup/management.sh b/setup/management.sh index 4effb67b..096403b3 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -30,33 +30,18 @@ rm -f /etc/init.d/mailinabox ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox hide_output update-rc.d mailinabox defaults -# Set time zone to something convenient for the user -# Backup will be set for 3am localtime so choice is important depending -# on user locations. +# Remove old files we no longer use. +rm -f /etc/cron.daily/mailinabox-backup +rm -f /etc/cron.daily/mailinabox-statuschecks -dpkg-reconfigure tzdata +# Perform nightly tasks at 3am in system time: take a backup, run +# status checks and email the administrator any changes. -# Perform a daily backup. -if [ -f /etc/cron.daily/mailinabox-backup ]; then - rm /etc/cron.daily/mailinabox-backup -fi -cat > /etc/cron.d/mailinabox-backup << EOF; -# /etc/cron.d/mailinabox-backup: crontab fragment to run maininabox-backup -# This executes $(pwd)/management/backup.py at 3am. - -0 3 * * * root $(pwd)/management/backup.py -EOF - -# Perform daily status checks. Compare each day to the previous -# for changes and mail the changes to the administrator. -cat > /etc/cron.daily/mailinabox-statuschecks << EOF; -#!/bin/bash +cat > /etc/cron.d/mailinabox-nightly << EOF; # Mail-in-a-Box --- Do not edit / will be overwritten on update. -# Run status checks. -$(pwd)/management/status_checks.py --show-changes --smtp +# Run nightly tasks: backup, status checks. +0 3 * * * root (cd `pwd` && management/daily_tasks.sh) EOF -chmod +x /etc/cron.daily/mailinabox-statuschecks - # Start it. restart_service mailinabox From 392d33b902e1e2f09a036fbe3afc0a5a06206235 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 10:38:49 -0500 Subject: [PATCH 07/11] change DANE TLSA record to hash the subject public key rather than the whole certificate, which means it is good for any certificate tied to the same private key Better for short-lived certificates. This is especially in preparation to using certificates from Let's Encrypt. see #268 --- CHANGELOG.md | 1 + management/dns_update.py | 44 ++++++++++++++++++++++------------ management/ssl_certificates.py | 11 +++++---- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96325f36..b6b96f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Mail: * Updated Roundcube to version 1.1.3. * Auto-create aliases for abuse@, as required by RFC2142. +* The DANE TLSA record is changed to use the certificate subject public key rather than the whole certificate, which means the record remains valid after certificate changes (so long as the private key remains the same, which it does for us). Control panel: diff --git a/management/dns_update.py b/management/dns_update.py index 1ec88607..0aae94cf 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -283,26 +283,40 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en def build_tlsa_record(env): # A DANE TLSA record in DNS specifies that connections on a port - # must use TLS and the certificate must match a particular certificate. + # must use TLS and the certificate must match a particular criteria. # # Thanks to http://blog.huque.com/2012/10/dnssec-and-certificates.html - # for explaining all of this! + # and https://community.letsencrypt.org/t/please-avoid-3-0-1-and-3-0-2-dane-tlsa-records-with-le-certificates/7022 + # for explaining all of this! Also see https://tools.ietf.org/html/rfc6698#section-2.1 + # and https://github.com/mail-in-a-box/mailinabox/issues/268#issuecomment-167160243. + # + # There are several criteria. We used to use "3 0 1" criteria, which + # meant to pin a leaf (3) certificate (0) with SHA256 hash (1). But + # certificates change, and especially as we move to short-lived certs + # they change often. The TLSA record handily supports the criteria of + # a leaf certificate (3)'s subject public key (1) with SHA256 hash (1). + # The subject public key is the public key portion of the private key + # that generated the CSR that generated the certificate. Since we + # generate a private key once the first time Mail-in-a-Box is set up + # and reuse it for all subsequent certificates, the TLSA record will + # remain valid indefinitely. - # Get the hex SHA256 of the DER-encoded server certificate: - certder = shell("check_output", [ - "/usr/bin/openssl", - "x509", - "-in", os.path.join(env["STORAGE_ROOT"], "ssl", "ssl_certificate.pem"), - "-outform", "DER" - ], - return_bytes=True) - certhash = hashlib.sha256(certder).hexdigest() + from ssl_certificates import load_cert_chain, load_pem + from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + + fn = os.path.join(env["STORAGE_ROOT"], "ssl", "ssl_certificate.pem") + cert = load_pem(load_cert_chain(fn)[0]) + + subject_public_key = cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + # We could have also loaded ssl_private_key.pem and called priv_key.public_key().public_bytes(...) + + pk_hash = hashlib.sha256(subject_public_key).hexdigest() # Specify the TLSA parameters: - # 3: This is the certificate that the client should trust. No CA is needed. - # 0: The whole certificate is matched. - # 1: The certificate is SHA256'd here. - return "3 0 1 " + certhash + # 3: Match the (leaf) certificate. (No CA, no trust path needed.) + # 1: Match its subject public key. + # 1: Use SHA256. + return "3 1 1 " + pk_hash def build_sshfp_records(): # The SSHFP record is a way for us to embed this server's SSH public diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 0365251c..93275a13 100644 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -184,21 +184,22 @@ def install_cert(domain, ssl_cert, ssl_chain, env): # When updating the cert for PRIMARY_HOSTNAME, symlink it from the system # certificate path, which is hard-coded for various purposes, and then - # update DNS (because of the DANE TLSA record), postfix, and dovecot, - # which all use the file. + # restart postfix and dovecot. if domain == env['PRIMARY_HOSTNAME']: # Update symlink. system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) os.unlink(system_ssl_certificate) os.symlink(ssl_certificate, system_ssl_certificate) - # Update DNS & restart postfix and dovecot so they pick up the new file. - from dns_update import do_dns_update - ret.append( do_dns_update(env) ) + # Restart postfix and dovecot so they pick up the new file. shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) ret.append("mail services restarted") + # The DANE TLSA record will remain valid so long as the private key + # hasn't changed. We don't ever change the private key automatically. + # If the user does it, they must manually update DNS. + # Update the web configuration so nginx picks up the new certificate file. from web_update import do_web_update ret.append( do_web_update(env) ) From d53332b7cfabbbddd2b5d507cb723c73d80b9466 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 11:48:23 -0500 Subject: [PATCH 08/11] drop the CSR_COUNTRY setting and ask within the control panel --- CHANGELOG.md | 1 + Vagrantfile | 1 - {setup => management}/csr_country_codes.tsv | 36 ++++++++++----------- management/daemon.py | 13 +++++++- management/ssl_certificates.py | 4 +-- management/templates/ssl.html | 10 ++++++ setup/questions.sh | 29 ----------------- setup/ssl.sh | 2 +- setup/start.sh | 3 +- tools/readable_bash.py | 1 - 10 files changed, 45 insertions(+), 55 deletions(-) rename {setup => management}/csr_country_codes.tsv (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b96f50..0519c47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Control panel: * Better messages if external DNS is used and, weirdly, custom secondary nameservers are set. * Add POP to the mail client settings documentation. * The box's IP address is added to the fail2ban whitelist so that the status checks don't trigger the machine banning itself, which results in the status checks showing services down even though they are running. +* For SSL certificates, rather than asking you what country you are in during setup, ask at the time a CSR is generated. The default system self-signed certificate now omits a country in the subject (it was never needed). The CSR_COUNTRY Mail-in-a-Box setting is dropped entirely. System: diff --git a/Vagrantfile b/Vagrantfile index 2a21dd08..c6ef0ab9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -22,7 +22,6 @@ Vagrant.configure("2") do |config| export PUBLIC_IP=auto export PUBLIC_IPV6=auto export PRIMARY_HOSTNAME=auto-easy - export CSR_COUNTRY=US #export SKIP_NETWORK_CHECKS=1 # Start the setup script. diff --git a/setup/csr_country_codes.tsv b/management/csr_country_codes.tsv similarity index 98% rename from setup/csr_country_codes.tsv rename to management/csr_country_codes.tsv index 3f8f6586..21fde999 100644 --- a/setup/csr_country_codes.tsv +++ b/management/csr_country_codes.tsv @@ -1,27 +1,28 @@ # This list is derived from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2. # The columns are ISO_3166-1_alpha-2 code, display name, Wikipedia page name. -# The top 20 countries by number of Internet users are grouped first, see +# The top 21 countries by number of Internet users are grouped first, see # https://en.wikipedia.org/wiki/List_of_countries_by_number_of_Internet_users. -BR Brazil -CA Canada CN China -EG Egypt -FR France -DE Germany IN India -ID Indonesia -IT Italy -JP Japan -MX Mexico -NG Nigeria -PH Philippines -RU Russian Federation Russia -ES Spain -KR South Korea -TR Turkey -GB United Kingdom US United States +JP Japan +BR Brazil +RU Russian Federation Russia +DE Germany +NG Nigeria +GB United Kingdom +FR France +MX Mexico +EG Egypt +KR South Korea VN Vietnam +ID Indonesia +PH Philippines +TR Turkey +IT Italy +PK Pakistan +ES Spain +CA Canada AD Andorra AE United Arab Emirates AF Afghanistan @@ -183,7 +184,6 @@ PA Panama PE Peru PF French Polynesia PG Papua New Guinea -PK Pakistan PL Poland PM Saint Pierre and Miquelon PN Pitcairn Pitcairn Islands diff --git a/management/daemon.py b/management/daemon.py index 4f56a767..27e18e8f 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -28,6 +28,14 @@ try: except OSError: pass +# for generating CSRs we need a list of country codes +csr_country_codes = [] +with open(os.path.join(os.path.dirname(me), "csr_country_codes.tsv")) as f: + for line in f: + if line.strip() == "" or line.startswith("#"): continue + code, name = line.strip().split("\t")[0:2] + csr_country_codes.append((code, name)) + app = Flask(__name__, template_folder=os.path.abspath(os.path.join(os.path.dirname(me), "templates"))) # Decorator to protect views that require a user with 'admin' privileges. @@ -101,9 +109,12 @@ def index(): return render_template('index.html', hostname=env['PRIMARY_HOSTNAME'], storage_root=env['STORAGE_ROOT'], + no_users_exist=no_users_exist, no_admins_exist=no_admins_exist, + backup_s3_hosts=backup_s3_hosts, + csr_country_codes=csr_country_codes, ) @app.route('/me') @@ -321,7 +332,7 @@ def dns_get_dump(): def ssl_get_csr(domain): from ssl_certificates import create_csr ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) - return create_csr(domain, ssl_private_key, env) + return create_csr(domain, ssl_private_key, request.form.get('countrycode', ''), env) @app.route('/ssl/install', methods=['POST']) @authorized_personnel_only diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 93275a13..1e9a9ca8 100644 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -137,12 +137,12 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False return cert_info['private-key'], cert_info['certificate'], via -def create_csr(domain, ssl_key, env): +def create_csr(domain, ssl_key, country_code, env): return shell("check_output", [ "openssl", "req", "-new", "-key", ssl_key, "-sha256", - "-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)]) + "-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)]) def install_cert(domain, ssl_cert, ssl_chain, env): # Write the combined cert+chain to a temporary path and validate that it is OK. diff --git a/management/templates/ssl.html b/management/templates/ssl.html index 060bd447..d411098c 100644 --- a/management/templates/ssl.html +++ b/management/templates/ssl.html @@ -28,6 +28,15 @@

+

What country are you in? This is required by some SSL certificate providers. You may leave this blank if you know your SSL certificate provider doesn't require it.

+ +

+ -
- -
- -
-
@@ -66,6 +60,13 @@
+
+ +
+ +
This is the minimum number of days backup data is kept for. The box makes an incremental backup, so backup data is often kept much longer. An incremental backup file that is less than this number of days old requires that all previous increments back to the most recent full backup, plus that full backup, remain available.
+
+
From c8fef45362b74350fa09a3054c9461b7b6142fee Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 14:00:44 -0500 Subject: [PATCH 10/11] v0.15-rc1 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0519c47c..5b7c416a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ CHANGELOG ========= -Still In Development --------------------- +v0.15 Release Candidate +----------------------- Mail: From 362bc060f6252280153f0f08d25c4ad1dd1319f1 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 26 Dec 2015 14:12:15 -0500 Subject: [PATCH 11/11] fix merge mistake (4305a71916c8c566234c575e1806b769a5960c2a) --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7c416a..156977da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,6 @@ System: * Fix pip command that might have not installed all necessary Python packages. * The control panel and backup would not work on Google Compute Engine because GCE installs a conflicting boto package. * Added a new command `management/backup.py --restore` to restore files from a backup to a target directory (command line arguments are passed to `duplicity restore`). -* Update Roundcube to version 1.1.3. v0.14 (November 4, 2015) ------------------------