1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-12 17:07:23 +01:00

Compare commits

..

17 Commits
v60 ... v61

Author SHA1 Message Date
Joshua Tauberer
5e3e4a2161 v61 2023-01-21 08:20:48 -05:00
Joshua Tauberer
61d1ea1ea7 Changelog entries 2023-01-15 10:17:10 -05:00
Joshua Tauberer
b3743a31e9 Add a status checks check that fail2ban is running using fail2ban-client 2023-01-15 10:17:10 -05:00
Joshua Tauberer
26709a3c1d Improve error messages in the management tools when external command-line tools are run 2023-01-15 10:17:10 -05:00
jcm-shove-it
20ec6c2080 Updated security.md to reflect the support of ubuntu 22.04 (#2219) 2023-01-15 10:05:36 -05:00
Steven Conaway
7a79153afe Remove old darkmode background color (#2218)
Removing this old background color solves the problem of the bottom of short pages (like `/admin`'s login page) being white. The background was being set to black, which would be inverted, so it'd appear white. Since the `filter:` css has [~97% support](https://caniuse.com/?search=filter), I think that this change should be made. Tested on latest versions of Chrome (mac and iOS), Firefox, and Safari (mac and iOS).
2023-01-15 10:05:13 -05:00
Hugh Secker-Walker
a2565227f2 feat(rsync-port): Add support for non-standard ssh port for rsync backup (#2208) 2023-01-15 10:03:05 -05:00
Hugh Secker-Walker
02b34ce699 fix(backup-display): Fix parsing of rsync target in system-backup.html, fixes #2206 (#2207) 2023-01-15 10:01:07 -05:00
Hugh Secker-Walker
820a39b865 chore(python open): Refactor open and gzip.open to use context manager (#2203)
Co-authored-by: Hugh Secker-Walker <hsw+miac@hodain.net>
2023-01-15 08:28:43 -05:00
Hugh Secker-Walker
57047d96e9 chore(setup): Update obsolete chown group syntax (#2202)
Co-authored-by: Hugh Secker-Walker <hsw+miac@hodain.net>
2023-01-15 08:25:36 -05:00
KiekerJan
1587248762 Disable Roundcube password plugin since it was corrupting the user database (#2198) 2023-01-15 08:22:43 -05:00
KiekerJan
0fc5105da5 Fixes to DNS lookups during status checks when there are timeouts, enforce timeouts better (#2191)
* add dns query handling changes

* replace exception pass with error message

* simplify dns exception catching

* Add not set case to blacklist lookup result handling
2023-01-15 08:20:08 -05:00
KiekerJan
c29593b5ef explicitly enable fail2ban which didn't start (#2190) 2023-01-15 08:10:04 -05:00
Joshua Tauberer
3314c4f7de v60.1 2022-10-30 08:18:13 -04:00
Joshua Tauberer
1f60236985 Upgrade Nextcloud to 23.0.4 (contacts to 4.2.0, calendar to 3.5.0)
This fixes the monthly view calendar items being in random order.
2022-10-30 08:16:54 -04:00
alento-group
32c68874c5 Fix NSD not restarting (#2182)
A previous commit (0a970f4bb2) broke nsd restarting. This fixes that change by reverting it.

Josh added: Use nsd-control with reconfig and reload if they succeed and only fall back to restarting nsd if they fail

Co-authored-by: Joshua Tauberer <jt@occams.info>
2022-10-30 08:16:03 -04:00
Joshua Tauberer
286a4bd9e7 Remove stray quote in bootstrap.sh
Reported at https://discourse.mailinabox.email/t/version-60-for-ubuntu-22-04-is-released/9558/4.
2022-10-12 06:11:02 -04:00
24 changed files with 219 additions and 97 deletions

View File

@@ -1,6 +1,32 @@
CHANGELOG
=========
Version 61 (January 21, 2023)
-----------------------------
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.
* Improve error messages in the management tools when external command-line tools are run.
Version 60.1 (October 30, 2022)
-------------------------------
* A setup issue where the DNS server nsd isn't running at the end of setup is (hopefully) fixed.
* Nextcloud is updated to 23.0.10 (contacts to 4.2.2, calendar to 3.5.1).
Version 60 (October 11, 2022)
-----------------------------
@@ -17,7 +43,7 @@ No major features of Mail-in-a-Box have changed in this release, although some m
With the newer version of Ubuntu the following software packages we use are updated:
* dovecot is upgraded to 2.3.16, postfix to 3.6.4, opendmark to 1.4 (which adds ARC-Authentication-Results headers), and spampd to 2.53 (alleviating a mail delivery rate limiting bug).
* Nextcloud is upgraded to 23.0.4.
* Nextcloud is upgraded to 23.0.4 (contacts to 4.2.0, calendar to 3.5.0).
* Roundcube is upgraded to 1.6.0.
* certbot is upgraded to 1.21 (via the Ubuntu repository instead of a PPA).
* fail2ban is upgraded to 0.11.2.

View File

@@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele
$ git clone https://github.com/mail-in-a-box/mailinabox
$ cd mailinabox
$ git checkout v60
$ git checkout v61
Begin the installation.

View File

@@ -213,9 +213,18 @@ def get_duplicity_additional_args(env):
config = get_backup_config(env)
if get_target_type(config) == '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"]).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 get_target_type(config) == 's3':
# See note about hostname in get_duplicity_target_url.
@@ -408,6 +417,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, *_ = target.netloc.rsplit(':', 1)
try:
port = target.port
except ValueError:
port = 22
target_path = target.path
if not target_path.endswith('/'):
target_path = target_path + '/'
@@ -416,11 +433,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=target.netloc,
host=user_host,
path=target_path)
]
@@ -531,7 +548,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
# Merge in anything written to custom.yaml.
try:
custom_config = rtyaml.load(open(os.path.join(backup_root, 'custom.yaml')))
with open(os.path.join(backup_root, 'custom.yaml'), 'r') as f:
custom_config = rtyaml.load(f)
if not isinstance(custom_config, dict): raise ValueError() # caught below
config.update(custom_config)
except:
@@ -556,7 +574,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
config["target"] = "file://" + config["file_target_directory"]
ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub')
if os.path.exists(ssh_pub_key):
config["ssh_pub_key"] = open(ssh_pub_key, 'r').read()
with open(ssh_pub_key, 'r') as f:
config["ssh_pub_key"] = f.read()
return config

View File

@@ -47,7 +47,8 @@ def read_password():
return first
def setup_key_auth(mgmt_uri):
key = open('/var/lib/mailinabox/api.key').read().strip()
with open('/var/lib/mailinabox/api.key', 'r') as f:
key = f.read().strip()
auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(

View File

@@ -98,7 +98,16 @@ def do_dns_update(env, force=False):
# Tell nsd to reload changed zone files.
if len(updated_domains) > 0:
shell('check_call', ["/usr/sbin/nsd-control", "reload"])
# 'reconfig' is needed if there are added or removed zones, but
# it may not reload existing zones, so we call 'reload' too. If
# nsd isn't running, nsd-control fails, so in that case revert
# to restarting nsd to make sure it is running. Restarting nsd
# should also refresh everything.
try:
shell('check_call', ["/usr/sbin/nsd-control", "reconfig"])
shell('check_call', ["/usr/sbin/nsd-control", "reload"])
except:
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
# Write the OpenDKIM configuration tables for all of the mail domains.
from mailconfig import get_mail_domains
@@ -806,7 +815,8 @@ def write_opendkim_tables(domains, env):
def get_custom_dns_config(env, only_real_records=False):
try:
custom_dns = rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')))
with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), 'r') as f:
custom_dns = rtyaml.load(f)
if not isinstance(custom_dns, dict): raise ValueError() # caught below
except:
return [ ]
@@ -983,6 +993,7 @@ def set_custom_dns_record(qname, rtype, value, action, env):
def get_secondary_dns(custom_dns, mode=None):
resolver = dns.resolver.get_default_resolver()
resolver.timeout = 10
resolver.lifetime = 10
values = []
for qname, rtype, value in custom_dns:
@@ -1000,10 +1011,17 @@ def get_secondary_dns(custom_dns, mode=None):
# doesn't.
if not hostname.startswith("xfr:"):
if mode == "xfr":
response = dns.resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
values.extend(map(str, response))
response = dns.resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
values.extend(map(str, response))
try:
response = resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
values.extend(map(str, response))
except dns.exception.DNSException:
pass
try:
response = resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
values.extend(map(str, response))
except dns.exception.DNSException:
pass
continue
values.append(hostname)
@@ -1021,15 +1039,17 @@ def set_secondary_dns(hostnames, env):
# Validate that all hostnames are valid and that all zone-xfer IP addresses are valid.
resolver = dns.resolver.get_default_resolver()
resolver.timeout = 5
resolver.lifetime = 5
for item in hostnames:
if not item.startswith("xfr:"):
# Resolve hostname.
try:
response = resolver.resolve(item, "A")
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
try:
response = resolver.resolve(item, "AAAA")
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
raise ValueError("Could not resolve the IP address of %s." % item)
else:
# Validate IP address.
@@ -1062,7 +1082,7 @@ def get_custom_dns_records(custom_dns, qname, rtype):
def build_recommended_dns(env):
ret = []
for (domain, zonefile, records) in build_zones(env):
# remove records that we don't dislay
# remove records that we don't display
records = [r for r in records if r[3] is not False]
# put Required at the top, then Recommended, then everythiing else

View File

@@ -73,7 +73,8 @@ def scan_files(collector):
continue
elif fn[-3:] == '.gz':
tmp_file = tempfile.NamedTemporaryFile()
shutil.copyfileobj(gzip.open(fn), tmp_file)
with gzip.open(fn, 'rb') as f:
shutil.copyfileobj(f, tmp_file)
if VERBOSE:
print("Processing file", fn, "...")

View File

@@ -535,7 +535,8 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# Second, check that the certificate matches the private key.
if ssl_private_key is not None:
try:
priv_key = load_pem(open(ssl_private_key, 'rb').read())
with open(ssl_private_key, 'rb') as f:
priv_key = load_pem(f.read())
except ValueError as e:
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)

View File

@@ -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.")
@@ -207,7 +213,8 @@ def check_ssh_password(env, output):
# the configuration file.
if not os.path.exists("/etc/ssh/sshd_config"):
return
sshd = open("/etc/ssh/sshd_config").read()
with open("/etc/ssh/sshd_config", "r") as f:
sshd = f.read()
if re.search("\nPasswordAuthentication\s+yes", sshd) \
or not re.search("\nPasswordAuthentication\s+no", sshd):
output.print_error("""The SSH server on this machine permits password-based login. A more secure
@@ -308,6 +315,8 @@ def run_network_checks(env, output):
output.print_ok("IP address is not blacklisted by zen.spamhaus.org.")
elif zen == "[timeout]":
output.print_warning("Connection to zen.spamhaus.org timed out. We could not determine whether your server's IP address is blacklisted. Please try again later.")
elif zen == "[Not Set]":
output.print_warning("Could not connect to zen.spamhaus.org. We could not determine whether your server's IP address is blacklisted. Please try again later.")
else:
output.print_error("""The IP address of this machine %s is listed in the Spamhaus Block List (code %s),
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
@@ -541,7 +550,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
for ns in custom_secondary_ns:
# We must first resolve the nameserver to an IP address so we can query it.
ns_ips = query_dns(ns, "A")
if not ns_ips:
if not ns_ips or ns_ips in {'[Not Set]', '[timeout]'}:
output.print_error("Secondary nameserver %s is not valid (it doesn't resolve to an IP address)." % ns)
continue
# Choose the first IP if nameserver returns multiple
@@ -592,7 +601,8 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
# record that we suggest using is for the KSK (and that's how the DS records were generated).
# We'll also give the nice name for the key algorithm.
dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg]))
dnsssec_pubkey = open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')).read().split("\t")[3].split(" ")[3]
with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key'), 'r') as f:
dnsssec_pubkey = f.read().split("\t")[3].split(" ")[3]
expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = {
"record": rr_ds,
@@ -744,6 +754,8 @@ def check_mail_domain(domain, env, output):
output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.")
elif dbl == "[timeout]":
output.print_warning("Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
elif dbl == "[Not Set]":
output.print_warning("Could not connect to dbl.spamhaus.org. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
else:
output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s),
which may prevent recipients from receiving your mail.
@@ -788,12 +800,17 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None, as_list=False):
# running bind server), or if the 'at' argument is specified, use that host
# as the nameserver.
resolver = dns.resolver.get_default_resolver()
if at:
# Make sure at is not a string that cannot be used as a nameserver
if at and at not in {'[Not set]', '[timeout]'}:
resolver = dns.resolver.Resolver()
resolver.nameservers = [at]
# Set a timeout so that a non-responsive server doesn't hold us back.
resolver.timeout = 5
# The number of seconds to spend trying to get an answer to the question. If the
# lifetime expires a dns.exception.Timeout exception will be raised.
resolver.lifetime = 5
# Do the query.
try:
@@ -947,7 +964,8 @@ def run_and_output_changes(env, pool):
# Load previously saved status checks.
cache_fn = "/var/cache/mailinabox/status_checks.json"
if os.path.exists(cache_fn):
prev = json.load(open(cache_fn))
with open(cache_fn, 'r') as f:
prev = json.load(f)
# Group the serial output into categories by the headings.
def group_by_heading(lines):

View File

@@ -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 {

View File

@@ -45,6 +45,10 @@
<label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label>
<div class="col-sm-8">
<input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host">
<div class="small" style="margin-top: 2px">
The hostname at your rsync provider, e.g. <tt>da2327.rsync.net</tt>. Optionally includes a colon
and the provider's non-standard ssh port number, e.g. <tt>u215843.your-storagebox.de:23</tt>.
</div>
</div>
</div>
<div class="form-group backup-target-rsync">
@@ -259,12 +263,11 @@ function show_custom_backup() {
} else if (r.target == "off") {
$("#backup-target-type").val("off");
} else if (r.target.substring(0, 8) == "rsync://") {
$("#backup-target-type").val("rsync");
var path = r.target.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);
$("#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.target.substring(0, 5) == "s3://") {
$("#backup-target-type").val("s3");
var hostpath = r.target.substring(5).split('/');
@@ -344,4 +347,31 @@ function init_inputs(target_type) {
set_host($('#backup-target-s3-host-select').val());
}
}
// 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,
}
};
</script>

View File

@@ -14,7 +14,9 @@ def load_env_vars_from_file(fn):
# Load settings from a KEY=VALUE file.
import collections
env = collections.OrderedDict()
for line in open(fn): env.setdefault(*line.strip().split("=", 1))
with open(fn, 'r') as f:
for line in f:
env.setdefault(*line.strip().split("=", 1))
return env
def save_environment(env):
@@ -34,7 +36,8 @@ def load_settings(env):
import rtyaml
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
try:
config = rtyaml.load(open(fn, "r"))
with open(fn, "r") as f:
config = rtyaml.load(f)
if not isinstance(config, dict): raise ValueError() # caught below
return config
except:
@@ -119,13 +122,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")

View File

@@ -63,7 +63,8 @@ def get_web_domains_with_root_overrides(env):
root_overrides = { }
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
with open(nginx_conf_custom_fn, 'r') as f:
custom_settings = rtyaml.load(f)
for domain, settings in custom_settings.items():
for type, value in [('redirect', settings.get('redirects', {}).get('/')),
('proxy', settings.get('proxies', {}).get('/'))]:
@@ -75,13 +76,18 @@ def do_web_update(env):
# Pre-load what SSL certificates we will use for each domain.
ssl_certificates = get_ssl_certificates(env)
# Helper for reading config files and templates
def read_conf(conf_fn):
with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn), "r") as f:
return f.read()
# Build an nginx configuration file.
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
nginx_conf = read_conf("nginx-top.conf")
# Load the templates.
template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read()
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
template0 = read_conf("nginx.conf")
template1 = read_conf("nginx-alldomains.conf")
template2 = read_conf("nginx-primaryonly.conf")
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
@@ -141,11 +147,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
def hashfile(filepath):
import hashlib
sha1 = hashlib.sha1()
f = open(filepath, 'rb')
try:
with open(filepath, 'rb') as f:
sha1.update(f.read())
finally:
f.close()
return sha1.hexdigest()
nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
@@ -153,7 +156,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
hsts = "yes"
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
yaml = rtyaml.load(open(nginx_conf_custom_fn))
with open(nginx_conf_custom_fn, 'r') as f:
yaml = rtyaml.load(f)
if domain in yaml:
yaml = yaml[domain]

View File

@@ -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.

View File

@@ -19,11 +19,11 @@ if [ -z "$TAG" ]; then
# want to display in status checks.
#
# Allow point-release versions of the major releases, e.g. 22.04.1 is OK.
UBUNTU_VERSION=$( lsb_release -d | sed 's/.*:\s*//' | sed 's/\([0-9]*\.[0-9]*\)\.[0-9]/\1/' )"
UBUNTU_VERSION=$( lsb_release -d | sed 's/.*:\s*//' | sed 's/\([0-9]*\.[0-9]*\)\.[0-9]/\1/' )
if [ "$UBUNTU_VERSION" == "Ubuntu 22.04 LTS" ]; then
# This machine is running Ubuntu 22.04, which is supported by
# Mail-in-a-Box versions 60 and later.
TAG=v60
TAG=v61
elif [ "$UBUNTU_VERSION" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04, which is supported by
# Mail-in-a-Box versions 0.40 through 5x.

View File

@@ -202,13 +202,13 @@ chmod -R o-rwx /etc/dovecot
# Ensure mailbox files have a directory that exists and are owned by the mail user.
mkdir -p $STORAGE_ROOT/mail/mailboxes
chown -R mail.mail $STORAGE_ROOT/mail/mailboxes
chown -R mail:mail $STORAGE_ROOT/mail/mailboxes
# Same for the sieve scripts.
mkdir -p $STORAGE_ROOT/mail/sieve
mkdir -p $STORAGE_ROOT/mail/sieve/global_before
mkdir -p $STORAGE_ROOT/mail/sieve/global_after
chown -R mail.mail $STORAGE_ROOT/mail/sieve
chown -R mail:mail $STORAGE_ROOT/mail/sieve
# Allow the IMAP/POP ports in the firewall.
ufw_allow imaps

View File

@@ -34,8 +34,8 @@ contact.admin.always_send warning critical
EOF
# The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi
chown munin. /var/log/munin/munin-cgi-html.log
chown munin. /var/log/munin/munin-cgi-graph.log
chown munin /var/log/munin/munin-cgi-html.log
chown munin /var/log/munin/munin-cgi-graph.log
# ensure munin-node knows the name of this machine
# and reduce logging level to warning

View File

@@ -21,22 +21,22 @@ echo "Installing Nextcloud (contacts/calendar)..."
# we automatically install intermediate versions as needed.
# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and
# copying it from the error message when it doesn't match what is below.
nextcloud_ver=23.0.8
nextcloud_hash=9d63416a0697eeecf243d09461f5881f8997f50a
nextcloud_ver=23.0.10
nextcloud_hash=8831c7862e39460fbb789bacac8729fab0ba02dd
# Nextcloud apps
# --------------
# * Find the most recent tag that is compatible with the Nextcloud version above by
# consulting the <dependencies>...<nextcloud> node at:
# https://github.com/nextcloud-releases/contacts/blob/master/appinfo/info.xml
# https://github.com/nextcloud-releases/calendar/blob/master/appinfo/info.xml
# https://github.com/nextcloud-releases/contacts/blob/main/appinfo/info.xml
# https://github.com/nextcloud-releases/calendar/blob/main/appinfo/info.xml
# https://github.com/nextcloud/user_external/blob/master/appinfo/info.xml
# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and
# copying it from the error message when it doesn't match what is below.
contacts_ver=4.2.0
contacts_hash=79b506574834db5e1b6ab47aadd4041e12ad9a9c
calendar_ver=3.5.0
calendar_hash=941381536287a015081669513f8f79f6f262508a
contacts_ver=4.2.2
contacts_hash=ca13d608ed8955aa374cb4f31b6026b57ef88887
calendar_ver=3.5.1
calendar_hash=c8136a3deb872a3ef73ce1155b58f3ab27ec7110
user_external_ver=3.0.0
user_external_hash=0df781b261f55bbde73d8c92da3f99397000972f
@@ -110,7 +110,7 @@ InstallNextcloud() {
# Make sure permissions are correct or the upgrade step won't run.
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
# that error.
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud || /bin/true
chown -f -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud || /bin/true
# If this isn't a new installation, immediately run the upgrade script.
# Then check for success (0=ok and 3=no upgrade needed, both are success).
@@ -259,7 +259,7 @@ EOF
EOF
# Set permissions
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
chown -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
# Execute Nextcloud's setup step, which creates the Nextcloud sqlite database.
# It also wipes it if it exists. And it updates config.php with database
@@ -311,7 +311,7 @@ var_export(\$CONFIG);
echo ";";
?>
EOF
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
chown www-data:www-data $STORAGE_ROOT/owncloud/config.php
# Enable/disable apps. Note that this must be done after the Nextcloud setup.
# The firstrunwizard gave Josh all sorts of problems, so disabling that.

View File

@@ -85,7 +85,7 @@ f=$STORAGE_ROOT
while [[ $f != / ]]; do chmod a+rx "$f"; f=$(dirname "$f"); done;
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
setup/migrate.py --current > $STORAGE_ROOT/mailinabox.version
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
chown $STORAGE_USER:$STORAGE_USER $STORAGE_ROOT/mailinabox.version
fi
# Save the global options in /etc/mailinabox.conf so that standalone

View File

@@ -373,3 +373,5 @@ cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/
# scripts will ensure the files exist and then fail2ban is given another
# restart at the very end of setup.
restart_service fail2ban
systemctl enable fail2ban

View File

@@ -134,7 +134,7 @@ cat > $RCM_CONFIG <<EOF;
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
\$config['cipher_method'] = 'AES-256-CBC'; # persistent login cookie and potentially other things
\$config['des_key'] = '$SECRET_KEY'; # 37 characters -> ~256 bits for AES-256, see above
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
\$config['skin'] = 'elastic';
\$config['login_autocomplete'] = 2;
\$config['login_username_filter'] = 'email';
@@ -170,7 +170,7 @@ EOF
# Create writable directories.
mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
chown -R www-data:www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
sudo -u www-data touch /var/log/roundcubemail/errors.log
@@ -194,14 +194,14 @@ usermod -a -G dovecot www-data
# set permissions so that PHP can use users.sqlite
# could use dovecot instead of www-data, but not sure it matters
chown root.www-data $STORAGE_ROOT/mail
chown root:www-data $STORAGE_ROOT/mail
chmod 775 $STORAGE_ROOT/mail
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
chown root:www-data $STORAGE_ROOT/mail/users.sqlite
chmod 664 $STORAGE_ROOT/mail/users.sqlite
# Fix Carddav permissions:
chown -f -R root.www-data ${RCM_PLUGIN_DIR}/carddav
# root.www-data need all permissions, others only read
chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
# root:www-data need all permissions, others only read
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
# Run Roundcube database migration script (database is created if it does not exist)

View File

@@ -76,7 +76,8 @@ for setting in settings:
found = set()
buf = ""
input_lines = list(open(filename))
with open(filename, "r") as f:
input_lines = list(f)
while len(input_lines) > 0:
line = input_lines.pop(0)

View File

@@ -40,8 +40,8 @@ cp "$1/owncloud.db" $STORAGE_ROOT/owncloud/
cp "$1/config.php" $STORAGE_ROOT/owncloud/
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
chown -f -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
chown www-data:www-data $STORAGE_ROOT/owncloud/config.php
sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ maintenance:mode --off

View File

@@ -17,13 +17,8 @@ accesses = set()
# Scan the current and rotated access logs.
for fn in glob.glob("/var/log/nginx/access.log*"):
# Gunzip if necessary.
if fn.endswith(".gz"):
f = gzip.open(fn)
else:
f = open(fn, "rb")
# Loop through the lines in the access log.
with f:
with (gzip.open if fn.endswith(".gz") else open)(fn, "rb") as f:
for line in f:
# Find lines that are GETs on the bootstrap script by either curl or wget.
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
@@ -43,7 +38,8 @@ for date, ip in accesses:
# Since logs are rotated, store the statistics permanently in a JSON file.
# Load in the stats from an existing file.
if os.path.exists(outfn):
existing_data = json.load(open(outfn))
with open(outfn, "r") as f:
existing_data = json.load(f)
for date, count in existing_data:
if date not in by_date:
by_date[date] = count

View File

@@ -124,13 +124,14 @@ def generate_documentation():
""")
parser = Source.parser()
for line in open("setup/start.sh"):
try:
fn = parser.parse_string(line).filename()
except:
continue
if fn in ("setup/start.sh", "setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"):
continue
with open("setup/start.sh", "r") as start_file:
for line in start_file:
try:
fn = parser.parse_string(line).filename()
except:
continue
if fn in ("setup/start.sh", "setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"):
continue
import sys
print(fn, file=sys.stderr)
@@ -401,7 +402,8 @@ class BashScript(Grammar):
@staticmethod
def parse(fn):
if fn in ("setup/functions.sh", "/etc/mailinabox.conf"): return ""
string = open(fn).read()
with open(fn, "r") as f:
string = f.read()
# tokenize
string = re.sub(".* #NODOC\n", "", string)