From afc9f9686ae3f84664c30cc799ea9f6de26e9de6 Mon Sep 17 00:00:00 2001 From: "A. Schippers" Date: Fri, 29 May 2020 21:30:07 +0200 Subject: [PATCH 01/25] Publish MTA-STS policy for incoming mail (#1731) Co-authored-by: Daniel Mabbett --- CHANGELOG.md | 6 ++++++ conf/mta-sts.txt | 4 ++++ conf/nginx-alldomains.conf | 3 +++ management/dns_update.py | 39 +++++++++++++++++++++++++++++++++++++- management/web_update.py | 3 +++ security.md | 6 ++++++ setup/start.sh | 6 ++++++ setup/web.sh | 17 +++++++++++++++-- 8 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 conf/mta-sts.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd9e724..b792510b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ Web: * Add a new hidden feature to set nginx alias in www/custom.yaml. +MTA-STS: + +* Added support for client side MTA-STS when there is a valid SSL Certificate on the primary domain +* Automatically adds reporting when alias "tlsrpt@" is added. +* Starts default on 'testing', but changes will be kept between MiaB Upgrades. + Setup: * Improved error handling. diff --git a/conf/mta-sts.txt b/conf/mta-sts.txt new file mode 100644 index 00000000..376102bc --- /dev/null +++ b/conf/mta-sts.txt @@ -0,0 +1,4 @@ +version: STSv1 +mode: MODE +mx: PRIMARY_HOSTNAME +max_age: 86400 \ No newline at end of file diff --git a/conf/nginx-alldomains.conf b/conf/nginx-alldomains.conf index 1b3ad5a9..4c81e3f3 100644 --- a/conf/nginx-alldomains.conf +++ b/conf/nginx-alldomains.conf @@ -21,6 +21,9 @@ location = /mail/config-v1.1.xml { alias /var/lib/mailinabox/mozilla-autoconfig.xml; } + location = /.well-known/mta-sts.txt { + alias /var/lib/mailinabox/mta-sts.txt; + } # Roundcube Webmail configuration. rewrite ^/mail$ /mail/ redirect; diff --git a/management/dns_update.py b/management/dns_update.py index 7d053d5e..822af4d9 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -9,8 +9,9 @@ import ipaddress import rtyaml import dns.resolver -from mailconfig import get_mail_domains +from mailconfig import get_mail_domains, get_mail_aliases from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains +from ssl_certificates import get_ssl_certificates, check_certificate # From https://stackoverflow.com/questions/3026957/how-to-validate-a-domain-name-using-regex-php/16491074#16491074 # This regular expression matches domain names according to RFCs, it also accepts fqdn with an leading dot, @@ -303,6 +304,42 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en if not has_rec(qname, rtype): records.append((qname, rtype, value, explanation)) + # If this is a domain name that there are email addresses configured for, i.e. "something@" + # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) + # Policy Domain. + # + # A "_mta-sts" TXT record signals the presence of a MTA-STS policy, and an effectively random policy + # ID is used to signal that a new policy may (or may not) be deployed any time the DNS is + # updated. + # + # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. The + # TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX + # name (PRIMARY_HOSTNAME), so we do not set an MTA-STS policy if the certificate is not + # valid (e.g. because it is self-signed and a valid certificate has not yet been provisioned). + get_prim_cert = get_ssl_certificates(env)[env['PRIMARY_HOSTNAME']] + response = check_certificate(env['PRIMARY_HOSTNAME'], get_prim_cert['certificate'],get_prim_cert['private-key']) + + if response[0] == 'OK' and domain in get_mail_domains(env): + mta_sts_records = [ + ("mta-sts", "A", env["PUBLIC_IP"], "Provides MTA-STS support"), + ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Provides MTA-STS support"), + ("_mta-sts", "TXT", "v=STSv1; id=%sZ" % datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "Enables MTA-STS support") + ] + # Rules can be custom configured accoring to https://tools.ietf.org/html/rfc8460. + # Skip if the rules below if the user has set a custom _smtp._tls record. + if not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): + # if the alias 'tlsrpt@PRIMARY_HOSTNAME' is configured, automaticly, reporting will be enabled to this email address + tls_rpt_email = "tlsrpt@%s" % env['PRIMARY_HOSTNAME'] + tls_rpt_string = "" + for alias in get_mail_aliases(env): + if alias[0] == tls_rpt_email: + tls_rpt_string = " rua=mailto:%s" % tls_rpt_email + mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1;%s" % tls_rpt_string, "For reporting, add an email alias: 'tlsrpt@%s' or add a custom TXT record like 'v=TLSRPTv1; rua=mailto:[youremail]@%s' for reporting" % (env["PRIMARY_HOSTNAME"], env["PRIMARY_HOSTNAME"]))) + for qname, rtype, value, explanation in mta_sts_records: + if value is None or value.strip() == "": continue # skip IPV6 if not set + if not has_rec(qname, rtype): + records.append((qname, rtype, value, explanation)) + # Sort the records. The None records *must* go first in the nsd zone file. Otherwise it doesn't matter. records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else "")) diff --git a/management/web_update.py b/management/web_update.py index e2498e77..4a07dc9e 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -30,6 +30,9 @@ def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True) domains |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env)) domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env)) + # 'mta-sts.' for MTA-STS support. + domains |= set('mta-sts.' + maildomain for maildomain in get_mail_domains(env)) + if exclude_dns_elsewhere: # ...Unless the domain has an A/AAAA record that maps it to a different # IP address than this box. Remove those domains from our list. diff --git a/security.md b/security.md index 8c9d43e5..ae77f339 100644 --- a/security.md +++ b/security.md @@ -109,6 +109,12 @@ As discussed above, there is no way to require on-the-wire encryption of mail. W When DNSSEC is enabled at the box's domain name's registrar, [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) records are automatically published in DNS. Senders supporting DANE will enforce encryption on-the-wire between them and the box --- see the section on DANE for outgoing mail above. ([source](management/dns_update.py)) +### MTA-STS + +SMTP MTA Strict Transport Security ([SMTP MTA-STS for short](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security)). + +MTA-STS is a mechanism that instructs an SMTP server that the communication with the other SMTP server MUST be encrypted and that the domain name on the certificate should match the domain in the policy. It uses a combination of DNS and HTTPS to publish a policy that tells the sending party what to do when an encrypted channel can not be negotiated. + ### Filters Incoming mail is run through several filters. Email is bounced if the sender's IP address is listed in the [Spamhaus Zen blacklist](http://www.spamhaus.org/zen/) or if the sender's domain is listed in the [Spamhaus Domain Block List](http://www.spamhaus.org/dbl/). Greylisting (with [postgrey](http://postgrey.schweikert.ch/)) is also used to cut down on spam. ([source](setup/mail-postfix.sh)) diff --git a/setup/start.sh b/setup/start.sh index 0b145022..9e2db214 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -82,6 +82,11 @@ if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version fi +# Default policy (initial) for MTA_STS = testing in the current state of inclusion. +# it can be changed to "none", "testing" or "enforce". With this extention, this is preserved by +# future upgrades + +MTA_STS="${DEFAULT_MTA_STS:-testing}" # Save the global options in /etc/mailinabox.conf so that standalone # tools know where to look for data. @@ -93,6 +98,7 @@ PUBLIC_IP=$PUBLIC_IP PUBLIC_IPV6=$PUBLIC_IPV6 PRIVATE_IP=$PRIVATE_IP PRIVATE_IPV6=$PRIVATE_IPV6 +MTA_STS=$MTA_STS EOF # Start service configuration. diff --git a/setup/web.sh b/setup/web.sh index e6aac6ef..c384c00e 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -19,7 +19,7 @@ fi echo "Installing Nginx (web server)..." -apt_install nginx php-cli php-fpm +apt_install nginx php-cli php-fpm idn2 rm -f /etc/nginx/sites-enabled/default @@ -122,6 +122,20 @@ cat conf/mozilla-autoconfig.xml \ > /var/lib/mailinabox/mozilla-autoconfig.xml chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml +# Create a generic mta-sts.txt file which is exposed via the +# nginx configuration at /.well-known/mta-sts.txt +# more documentation is available on: +# https://www.uriports.com/blog/mta-sts-explained/ +# default mode is "testing", which means: "Messages will be delivered as +# though there was no failure but a report will be sent if TLS-RPT is configured" +# other valid modes are: "enforce" and "none". +PUNY_PRIMARY_HOSTNAME=$(echo "$PRIMARY_HOSTNAME" | idn2) +cat conf/mta-sts.txt \ + | sed "s/MODE/$MTA_STS/" \ + | sed "s/PRIMARY_HOSTNAME/$PUNY_PRIMARY_HOSTNAME/" \ + > /var/lib/mailinabox/mta-sts.txt +chmod a+r /var/lib/mailinabox/mta-sts.txt + # make a default homepage if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration #NODOC mkdir -p $STORAGE_ROOT/www/default @@ -137,4 +151,3 @@ restart_service php7.2-fpm # Open ports. ufw_allow http ufw_allow https - From 10bedad3a3f83a5e10551994e78a76da182df91f Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 17 May 2020 12:10:38 -0400 Subject: [PATCH 02/25] MTA-STS tweaks, add status check using postfix-mta-sts-resolver, change to enforce --- CHANGELOG.md | 14 +++++---- README.md | 19 +++++++----- management/dns_update.py | 58 ++++++++++++++++++++++--------------- management/status_checks.py | 20 +++++++++++++ security.md | 14 ++++----- setup/management.sh | 2 +- setup/start.sh | 12 +++----- setup/web.sh | 9 +++--- 8 files changed, 90 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b792510b..01f860fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +In Development +-------------- + +Mail: + +* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed. +* MTA-STS reporting is enabled with reports sent to administrator@ the primary hostname. + v0.45 (May 16, 2020) -------------------- @@ -24,12 +32,6 @@ Web: * Add a new hidden feature to set nginx alias in www/custom.yaml. -MTA-STS: - -* Added support for client side MTA-STS when there is a valid SSL Certificate on the primary domain -* Automatically adds reporting when alias "tlsrpt@" is added. -* Starts default on 'testing', but changes will be kept between MiaB Upgrades. - Setup: * Improved error handling. diff --git a/README.md b/README.md index e787c8d8..9a3f0ee8 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,20 @@ It is a one-click email appliance. There are no user-configurable setup options. The components installed are: -* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), Exchange ActiveSync ([z-push](http://z-push.org/)) -* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.org/)) -* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/)) -* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set -* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/)) +* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), and Exchange ActiveSync ([z-push](http://z-push.org/)) servers +* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (also using dovecot), email client autoconfig settings (served by [nginx](http://nginx.org/)) +* Spam filtering ([spamassassin](https://spamassassin.apache.org/)) and greylisting ([postgrey](http://postgrey.schweikert.ch/)) +* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), [MTA-STS](https://tools.ietf.org/html/rfc8461), and [SSHFP](https://tools.ietf.org/html/rfc4255) policy records automatically set +* TLS certificates automatically provisioned [Let's Encrypt](https://letsencrypt.org/) +* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), and basic system monitoring ([munin](http://munin-monitoring.org/)) -It also includes: +It also includes system management tools: -* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and detailed system monitoring. +* Comprehensive health monitoring that checks each day that services are running, ports are open, TLS certificates are valid, and DNS records are correct +* A control panel for adding/removing mail users, aliases, custom DNS records, configuring backups, etc. +* An API for all of the actions on the control panel + +It also supports static website hosting since the box is serving HTTPS anyway. For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md). diff --git a/management/dns_update.py b/management/dns_update.py index 822af4d9..98db700f 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -304,37 +304,47 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en if not has_rec(qname, rtype): records.append((qname, rtype, value, explanation)) - # If this is a domain name that there are email addresses configured for, i.e. "something@" - # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) - # Policy Domain. + # If this is a domain name that there are email addresses configured for, i.e. "something@" + # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) + # Policy Domain. # - # A "_mta-sts" TXT record signals the presence of a MTA-STS policy, and an effectively random policy - # ID is used to signal that a new policy may (or may not) be deployed any time the DNS is - # updated. - # - # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. The - # TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX - # name (PRIMARY_HOSTNAME), so we do not set an MTA-STS policy if the certificate is not - # valid (e.g. because it is self-signed and a valid certificate has not yet been provisioned). - get_prim_cert = get_ssl_certificates(env)[env['PRIMARY_HOSTNAME']] - response = check_certificate(env['PRIMARY_HOSTNAME'], get_prim_cert['certificate'],get_prim_cert['private-key']) - - if response[0] == 'OK' and domain in get_mail_domains(env): + # A "_mta-sts" TXT record signals the presence of a MTA-STS policy, and an effectively random policy + # ID is used to signal that a new policy may (or may not) be deployed any time the DNS is + # updated. + # + # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. Therefore + # the TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX + # domain name (PRIMARY_HOSTNAME) *and* the TLS certificate used by nginx for HTTPS on the mta-sts + # subdomain must be valid certificate for that domain. Do not set an MTA-STS policy if either + # certificate in use is not valid (e.g. because it is self-signed and a valid certificate has not + # yet been provisioned). + mta_sts_enabled = False + if domain in get_mail_domains(env): + # Check that PRIMARY_HOSTNAME and the mta_sts domain both have valid certificates. + for d in (env['PRIMARY_HOSTNAME'], "mta-sts." + domain): + cert = get_ssl_certificates(env).get(d) + if not cert: + break # no certificate provisioned for this domain + cert_status = check_certificate(d, cert['certificate'], cert['private-key']) + if cert_status[0] != 'OK': + break # certificate is not valid + else: + # 'break' was not encountered above, so both domains are good + mta_sts_enabled = True + if mta_sts_enabled: mta_sts_records = [ - ("mta-sts", "A", env["PUBLIC_IP"], "Provides MTA-STS support"), - ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Provides MTA-STS support"), - ("_mta-sts", "TXT", "v=STSv1; id=%sZ" % datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "Enables MTA-STS support") + ("mta-sts", "A", env["PUBLIC_IP"], "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), + ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), + ("_mta-sts", "TXT", "v=STSv1; id=%sZ" % datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.") ] # Rules can be custom configured accoring to https://tools.ietf.org/html/rfc8460. # Skip if the rules below if the user has set a custom _smtp._tls record. if not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): - # if the alias 'tlsrpt@PRIMARY_HOSTNAME' is configured, automaticly, reporting will be enabled to this email address - tls_rpt_email = "tlsrpt@%s" % env['PRIMARY_HOSTNAME'] tls_rpt_string = "" - for alias in get_mail_aliases(env): - if alias[0] == tls_rpt_email: - tls_rpt_string = " rua=mailto:%s" % tls_rpt_email - mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1;%s" % tls_rpt_string, "For reporting, add an email alias: 'tlsrpt@%s' or add a custom TXT record like 'v=TLSRPTv1; rua=mailto:[youremail]@%s' for reporting" % (env["PRIMARY_HOSTNAME"], env["PRIMARY_HOSTNAME"]))) + tls_rpt_email = env.get("MTA_STS_TLSRPT_EMAIL", "postmaster@%s" % env['PRIMARY_HOSTNAME']) + if tls_rpt_email: # if a reporting address is not cleared + tls_rpt_string = " rua=mailto:%s" % tls_rpt_email + mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1;%s" % tls_rpt_string, "Optional. Enables MTA-STS reporting.")) for qname, rtype, value, explanation in mta_sts_records: if value is None or value.strip() == "": continue # skip IPV6 if not set if not has_rec(qname, rtype): diff --git a/management/status_checks.py b/management/status_checks.py index a9d0595c..2cd56f74 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -5,11 +5,13 @@ # what to do next. import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool +import asyncio import dns.reversename, dns.resolver import dateutil.parser, dateutil.tz import idna import psutil +import postfix_mta_sts_resolver.resolver from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns, get_custom_dns_records from web_update import get_web_domains, get_domains_with_a_records @@ -327,6 +329,11 @@ def run_domain_checks(rounded_time, env, output, pool): def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records): output = BufferedOutput() + # When running inside Flask, the worker threads don't get a thread pool automatically. + # Also this method is called in a forked worker pool, so creating a new loop is probably + # a good idea. + asyncio.set_event_loop(asyncio.new_event_loop()) + # we'd move this up, but this returns non-pickleable values ssl_certificates = get_ssl_certificates(env) @@ -611,6 +618,19 @@ def check_mail_domain(domain, env, output): if mx != recommended_mx: good_news += " This configuration is non-standard. The recommended configuration is '%s'." % (recommended_mx,) output.print_ok(good_news) + + # Check MTA-STS policy. + loop = asyncio.get_event_loop() + sts_resolver = postfix_mta_sts_resolver.resolver.STSResolver(loop=loop) + valid, policy = loop.run_until_complete(sts_resolver.resolve(domain)) + if valid == postfix_mta_sts_resolver.resolver.STSFetchResult.VALID: + if policy[1].get("mx") == [env['PRIMARY_HOSTNAME']] and policy[1].get("mode") == "enforce": # policy[0] is the policyid + output.print_ok("MTA-STS policy is present.") + else: + output.print_error("MTA-STS policy is present but has unexpected settings. [{}]".format(policy[1])) + else: + output.print_error("MTA-STS policy is missing: {}".format(valid)) + else: output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '%s' but should be '%s'. Mail will not be delivered to this box. It may take several hours for public DNS to update after a change. This problem may result from diff --git a/security.md b/security.md index ae77f339..ba3e3847 100644 --- a/security.md +++ b/security.md @@ -101,20 +101,18 @@ The box restricts the envelope sender address (also called the return path or MA Incoming Mail ------------- -### Encryption +### Encryption Settings -As discussed above, there is no way to require on-the-wire encryption of mail. When the box receives an incoming email (SMTP on port 25), it offers encryption (STARTTLS) but cannot require that senders use it because some senders may not support STARTTLS at all and other senders may support STARTTLS but not with the latest protocols/ciphers. To give senders the best chance at making use of encryption, the box offers protocols back to TLSv1 and ciphers with key lengths as low as 112 bits. Modern clients (senders) will make use of the 256-bit ciphers and Diffie-Hellman ciphers with a 2048-bit key for perfect forward secrecy, however. ([source](setup/mail-postfix.sh)) +As with outbound email, there is no way to require on-the-wire encryption of incoming mail from all senders. When the box receives an incoming email (SMTP on port 25), it offers encryption (STARTTLS) but cannot require that senders use it because some senders may not support STARTTLS at all and other senders may support STARTTLS but not with the latest protocols/ciphers. To give senders the best chance at making use of encryption, the box offers protocols back to TLSv1 and ciphers with key lengths as low as 112 bits. Modern clients (senders) will make use of the 256-bit ciphers and Diffie-Hellman ciphers with a 2048-bit key for perfect forward secrecy, however. ([source](setup/mail-postfix.sh)) + +### MTA-STS + +The box publishes a SMTP MTA Strict Transport Security ([SMTP MTA-STS](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security)) policy (via DNS and HTTPS) in "enforce" mode. Senders that support MTA-STS will use a secure SMTP connection. (MTA-STS tells senders to connect and expect a signed TLS certificate for the "MX" domain without permitting a fallback to an unencrypted connection.) ### DANE When DNSSEC is enabled at the box's domain name's registrar, [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) records are automatically published in DNS. Senders supporting DANE will enforce encryption on-the-wire between them and the box --- see the section on DANE for outgoing mail above. ([source](management/dns_update.py)) -### MTA-STS - -SMTP MTA Strict Transport Security ([SMTP MTA-STS for short](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security)). - -MTA-STS is a mechanism that instructs an SMTP server that the communication with the other SMTP server MUST be encrypted and that the domain name on the certificate should match the domain in the policy. It uses a combination of DNS and HTTPS to publish a policy that tells the sending party what to do when an encrypted channel can not be negotiated. - ### Filters Incoming mail is run through several filters. Email is bounced if the sender's IP address is listed in the [Spamhaus Zen blacklist](http://www.spamhaus.org/zen/) or if the sender's domain is listed in the [Spamhaus Domain Block List](http://www.spamhaus.org/dbl/). Greylisting (with [postgrey](http://postgrey.schweikert.ch/)) is also used to cut down on spam. ([source](setup/mail-postfix.sh)) diff --git a/setup/management.sh b/setup/management.sh index 9d7c762c..4b398aa2 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -50,7 +50,7 @@ hide_output $venv/bin/pip install --upgrade pip hide_output $venv/bin/pip install --upgrade \ rtyaml "email_validator>=1.0.0" "exclusiveprocess" \ flask dnspython python-dateutil \ - "idna>=2.0.0" "cryptography==2.2.2" boto psutil + "idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver # CONFIGURATION diff --git a/setup/start.sh b/setup/start.sh index 9e2db214..cedc426d 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -82,14 +82,10 @@ if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version fi -# Default policy (initial) for MTA_STS = testing in the current state of inclusion. -# it can be changed to "none", "testing" or "enforce". With this extention, this is preserved by -# future upgrades - -MTA_STS="${DEFAULT_MTA_STS:-testing}" - # Save the global options in /etc/mailinabox.conf so that standalone -# tools know where to look for data. +# tools know where to look for data. The default MTA_STS_MODE setting +# is blank unless set by an environment variable, but see web.sh for +# how that is interpreted. cat > /etc/mailinabox.conf << EOF; STORAGE_USER=$STORAGE_USER STORAGE_ROOT=$STORAGE_ROOT @@ -98,7 +94,7 @@ PUBLIC_IP=$PUBLIC_IP PUBLIC_IPV6=$PUBLIC_IPV6 PRIVATE_IP=$PRIVATE_IP PRIVATE_IPV6=$PRIVATE_IPV6 -MTA_STS=$MTA_STS +MTA_STS_MODE=${MTA_STS_MODE-} EOF # Start service configuration. diff --git a/setup/web.sh b/setup/web.sh index c384c00e..42c301ec 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -126,12 +126,13 @@ chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml # nginx configuration at /.well-known/mta-sts.txt # more documentation is available on: # https://www.uriports.com/blog/mta-sts-explained/ -# default mode is "testing", which means: "Messages will be delivered as -# though there was no failure but a report will be sent if TLS-RPT is configured" -# other valid modes are: "enforce" and "none". +# default mode is "enforce". Change to "testing" which means +# "Messages will be delivered as though there was no failure +# but a report will be sent if TLS-RPT is configured" if you +# are not sure you want this yet. Or "none". PUNY_PRIMARY_HOSTNAME=$(echo "$PRIMARY_HOSTNAME" | idn2) cat conf/mta-sts.txt \ - | sed "s/MODE/$MTA_STS/" \ + | sed "s/MODE/${MTA_STS_MODE:-enforce}/" \ | sed "s/PRIMARY_HOSTNAME/$PUNY_PRIMARY_HOSTNAME/" \ > /var/lib/mailinabox/mta-sts.txt chmod a+r /var/lib/mailinabox/mta-sts.txt From b805f8695e971d9c4a5a5f3c2d77c36bf6106697 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 17 May 2020 12:43:58 -0400 Subject: [PATCH 03/25] Move status checks for www, autoconfig, autodiscover, and mta-sts to within the section for the parent domain Since we're checking the MTA-STS policy, there's no need to check that the domain resolves etc. directly. --- management/status_checks.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/management/status_checks.py b/management/status_checks.py index 2cd56f74..101a3537 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -311,6 +311,17 @@ def run_domain_checks(rounded_time, env, output, pool): domains_to_check = mail_domains | dns_domains | web_domains + # Remove "www", "autoconfig", "autodiscover", and "mta-sts" subdomains, which we group with their parent, + # if their parent is in the domains to check list. + domains_to_check = [ + d for d in domains_to_check + if not ( + d.split(".", 1)[0] in ("www", "autoconfig", "autodiscover", "mta-sts") + and len(d.split(".", 1)) == 2 + and d.split(".", 1)[1] in domains_to_check + ) + ] + # Get the list of domains that we don't serve web for because of a custom CNAME/A record. domains_with_a_records = get_domains_with_a_records(env) @@ -361,6 +372,26 @@ def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zone if domain in dns_domains: check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records) + # Check auto-configured subdomains. See run_domain_checks. + # Skip mta-sts because we check the policy directly. + for label in ("www", "autoconfig", "autodiscover"): + subdomain = label + "." + domain + if subdomain in web_domains or subdomain in mail_domains: + # Run checks. + subdomain_output = run_domain_checks_on_domain(subdomain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records) + + # Prepend the domain name to the start of each check line, and then add to the + # checks for this domain. + for attr, args, kwargs in subdomain_output[1].buf: + if attr == "add_heading": + # Drop the heading, but use its text as the subdomain name in + # each line since it is in Unicode form. + subdomain = args[0] + continue + if len(args) == 1 and isinstance(args[0], str): + args = [ subdomain + ": " + args[0] ] + getattr(output, attr)(*args, **kwargs) + return (domain, output) def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): From 37dad9d4bbe3f0e59815cad9b6e0e3b4460336bf Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 22 May 2020 07:05:24 -0400 Subject: [PATCH 04/25] Provision certificates from Let's Encrypt grouped by DNS zone Folks didn't want certificates exposing all of the domains hosted by the server (although this can already be found on the internet). Additionally, if one domain fails (usually because of a misconfiguration), it would be nice if not everything fails. So grouping them helps with that. Fixes #690. --- management/ssl_certificates.py | 44 +++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 76b0f8fa..1b1e9f83 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -180,7 +180,7 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True # for and subtract: # * domains not in limit_domains if limit_domains is not empty # * domains with custom "A" records, i.e. they are hosted elsewhere - # * domains with actual "A" records that point elsewhere + # * domains with actual "A" records that point elsewhere (misconfiguration) # * domains that already have certificates that will be valid for a while from web_update import get_web_domains @@ -256,15 +256,41 @@ def provision_certificates(env, limit_domains): "result": "skipped", }) + # Break into groups by DNS zone: Group every domain with its parent domain, if + # its parent domain is in the list of domains to request a certificate for. + # Start with the zones so that if the zone doesn't need a certificate itself, + # its children will still be grouped together. Sort the provision domains to + # put parents ahead of children. + # Since Let's Encrypt requests are limited to 100 domains at a time, + # we'll create a list of lists of domains where the inner lists have + # at most 100 items. By sorting we also get the DNS zone domain as the first + # entry in each list (unless we overflow beyond 100) which ends up as the + # primary domain listed in each certificate. + from dns_update import get_dns_zones + certs = { } + for zone, zonefile in get_dns_zones(env): + certs[zone] = [[]] + for domain in sort_domains(domains, env): + # Does the domain end with any domain we've seen so far. + for parent in certs.keys(): + if domain.endswith("." + parent): + # Add this to the parent's list of domains. + # Start a new group if the list already has + # 100 items. + if len(certs[parent][-1]) == 100: + certs[parent].append([]) + certs[parent][-1].append(domain) + break + else: + # This domain is not a child of any domain we've seen yet, so + # start a new group. This shouldn't happen since every zone + # was already added. + certs[domain] = [[domain]] - # Break into groups of up to 100 certificates at a time, which is Let's Encrypt's - # limit for a single certificate. We'll sort to put related domains together. - max_domains_per_group = 100 - domains = sort_domains(domains, env) - certs = [] - while len(domains) > 0: - certs.append( domains[:max_domains_per_group] ) - domains = domains[max_domains_per_group:] + # Flatten to a list of lists of domains (from a mapping). Remove empty + # lists (zones with no domains that need certs). + certs = sum(certs.values(), []) + certs = [_ for _ in certs if len(_) > 0] # Prepare to provision. From 3a4b8da8fdbf6c24895eb4959568663008654b83 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 30 May 2020 08:04:07 -0400 Subject: [PATCH 05/25] More for MTA-STS for incoming mail * Create the mta_sts A/AAAA records even if there is no valid TLS certificate because we can't get a TLS certificate if we don't set up the domains. * Make the policy id in the TXT record stable by using a hash of the policy file so that the DNS record doesn't change every day, which means no nightly notification and also it allows for longer caching by sending MTAs. --- management/dns_update.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 98db700f..5fdb3e0f 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -308,17 +308,23 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) # Policy Domain. # - # A "_mta-sts" TXT record signals the presence of a MTA-STS policy, and an effectively random policy - # ID is used to signal that a new policy may (or may not) be deployed any time the DNS is - # updated. + # A "_mta-sts" TXT record signals the presence of a MTA-STS policy. The id field helps clients + # cache the policy. It should be stable so we don't update DNS unnecessarily but change when + # the policy changes. It must be at most 32 letters and numbers, so we compute a hash of the + # policy file. # # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. Therefore # the TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX # domain name (PRIMARY_HOSTNAME) *and* the TLS certificate used by nginx for HTTPS on the mta-sts # subdomain must be valid certificate for that domain. Do not set an MTA-STS policy if either # certificate in use is not valid (e.g. because it is self-signed and a valid certificate has not - # yet been provisioned). + # yet been provisioned). Since we cannot provision a certificate without A/AAAA records, we + # always set them --- only the TXT records depend on there being valid certificates. mta_sts_enabled = False + mta_sts_records = [ + ("mta-sts", "A", env["PUBLIC_IP"], "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), + ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), + ] if domain in get_mail_domains(env): # Check that PRIMARY_HOSTNAME and the mta_sts domain both have valid certificates. for d in (env['PRIMARY_HOSTNAME'], "mta-sts." + domain): @@ -332,11 +338,15 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # 'break' was not encountered above, so both domains are good mta_sts_enabled = True if mta_sts_enabled: - mta_sts_records = [ - ("mta-sts", "A", env["PUBLIC_IP"], "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), - ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), - ("_mta-sts", "TXT", "v=STSv1; id=%sZ" % datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.") - ] + # Compute a up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy + # file (20 bytes) and encode it as base-64 (60 bytes) but then just take its first 20 bytes + # which should be sufficient to change whenever the policy file changes. + with open("/var/lib/mailinabox/mta-sts.txt", "rb") as f: + mta_sts_policy_id = base64.b64encode(hashlib.sha1(f.read()).digest()).decode("ascii")[0:20] + mta_sts_records.extend([ + ("_mta-sts", "TXT", "v=STSv1; id=" + mta_sts_policy_id, "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.") + ]) + # Rules can be custom configured accoring to https://tools.ietf.org/html/rfc8460. # Skip if the rules below if the user has set a custom _smtp._tls record. if not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): @@ -345,10 +355,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en if tls_rpt_email: # if a reporting address is not cleared tls_rpt_string = " rua=mailto:%s" % tls_rpt_email mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1;%s" % tls_rpt_string, "Optional. Enables MTA-STS reporting.")) - for qname, rtype, value, explanation in mta_sts_records: - if value is None or value.strip() == "": continue # skip IPV6 if not set - if not has_rec(qname, rtype): - records.append((qname, rtype, value, explanation)) + for qname, rtype, value, explanation in mta_sts_records: + if value is None or value.strip() == "": continue # skip IPV6 if not set + if not has_rec(qname, rtype): + records.append((qname, rtype, value, explanation)) # Sort the records. The None records *must* go first in the nsd zone file. Otherwise it doesn't matter. records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else "")) From bc1be9d70a19f37bf10f410e5768f0adf657503b Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 30 May 2020 08:15:31 -0400 Subject: [PATCH 06/25] readme fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a3f0ee8..630ce259 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ It is a one-click email appliance. There are no user-configurable setup options. The components installed are: * SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), and Exchange ActiveSync ([z-push](http://z-push.org/)) servers -* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (also using dovecot), email client autoconfig settings (served by [nginx](http://nginx.org/)) +* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (also using dovecot), and email client autoconfig settings (served by [nginx](http://nginx.org/)) * Spam filtering ([spamassassin](https://spamassassin.apache.org/)) and greylisting ([postgrey](http://postgrey.schweikert.ch/)) * DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), [MTA-STS](https://tools.ietf.org/html/rfc8461), and [SSHFP](https://tools.ietf.org/html/rfc4255) policy records automatically set -* TLS certificates automatically provisioned [Let's Encrypt](https://letsencrypt.org/) +* HTTPS TLS certificates are automatically provisioned using [Let's Encrypt](https://letsencrypt.org/) (needed for webmail, CardDAV/CalDAV, ActiveSync, MTA-STS policy, etc.). * Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), and basic system monitoring ([munin](http://munin-monitoring.org/)) It also includes system management tools: From cfc8fb484cfdb3ee581630a869fd93d4e1b3cb03 Mon Sep 17 00:00:00 2001 From: Marcus Bointon Date: Sun, 7 Jun 2020 15:47:51 +0200 Subject: [PATCH 07/25] Add rate limiting of SSH in the firewall (#1770) See #1767. --- setup/functions.sh | 9 ++++++++- setup/system.sh | 4 ++-- tools/readable_bash.py | 8 ++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/setup/functions.sh b/setup/functions.sh index b36d14bc..90c4c55d 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -136,7 +136,14 @@ function get_default_privateip { function ufw_allow { if [ -z "${DISABLE_FIREWALL:-}" ]; then # ufw has completely unhelpful output - ufw allow $1 > /dev/null; + ufw allow "$1" > /dev/null; + fi +} + +function ufw_limit { + if [ -z "${DISABLE_FIREWALL:-}" ]; then + # ufw has completely unhelpful output + ufw limit "$1" > /dev/null; fi } diff --git a/setup/system.sh b/setup/system.sh index 28043b16..4d33deb6 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -256,7 +256,7 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then apt_install ufw # Allow incoming connections to SSH. - ufw_allow ssh; + ufw_limit ssh; # ssh might be running on an alternate port. Use sshd -T to dump sshd's #NODOC # settings, find the port it is supposedly running on, and open that port #NODOC @@ -266,7 +266,7 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then if [ "$SSH_PORT" != "22" ]; then echo Opening alternate SSH port $SSH_PORT. #NODOC - ufw_allow $SSH_PORT #NODOC + ufw_limit $SSH_PORT #NODOC fi fi diff --git a/tools/readable_bash.py b/tools/readable_bash.py index 5207a78a..1fcdd5cd 100644 --- a/tools/readable_bash.py +++ b/tools/readable_bash.py @@ -58,7 +58,7 @@ def generate_documentation(): } .prose { - padding-top: 1em; + padding-top: 1em; padding-bottom: 1em; } .terminal { @@ -261,6 +261,10 @@ class UfwAllow(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("ufw_allow "), REST_OF_LINE, EOL) def value(self): return shell_line("ufw allow " + self[2].string) +class UfwLimit(Grammar): + grammar = (ZERO_OR_MORE(SPACE), L("ufw_limit "), REST_OF_LINE, EOL) + def value(self): + return shell_line("ufw limit " + self[2].string) class RestartService(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("restart_service "), REST_OF_LINE, EOL) def value(self): @@ -275,7 +279,7 @@ class OtherLine(Grammar): return "
" + recode_bash(self.string.strip()) + "
\n" class BashElement(Grammar): - grammar = Comment | CatEOF | EchoPipe | EchoLine | HideOutput | EditConf | SedReplace | AptGet | UfwAllow | RestartService | OtherLine + grammar = Comment | CatEOF | EchoPipe | EchoLine | HideOutput | EditConf | SedReplace | AptGet | UfwAllow | UfwLimit | RestartService | OtherLine def value(self): return self[0].value() From 339c330b4ff61e6bf116d98947e4a8e93e1b72f8 Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Sun, 7 Jun 2020 09:50:04 -0400 Subject: [PATCH 08/25] Fix roundcube error log file path in setup script (#1775) --- setup/webmail.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index 20d43c57..bd31e221 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -160,7 +160,7 @@ mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundc 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 +sudo -u www-data touch /var/log/roundcubemail/errors.log # Password changing plugin settings # The config comes empty by default, so we need the settings From df9bb263dc7983b71b5a1ecd400f5ae10ab16fbe Mon Sep 17 00:00:00 2001 From: Vasek Sraier Date: Sun, 7 Jun 2020 15:56:45 +0200 Subject: [PATCH 09/25] daily_tasks.sh: redirect stderr to stdout (#1768) When the management commands fail, they can print something to the standard error output. The administrator would never notice, because it wouldn't be send to him with the usual emails. Fixes #1763 --- management/daily_tasks.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index 2f723352..db496399 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -16,10 +16,10 @@ if [ `date "+%u"` -eq 1 ]; then fi # Take a backup. -management/backup.py | management/email_administrator.py "Backup Status" +management/backup.py 2>&1 | management/email_administrator.py "Backup Status" # Provision any new certificates for new domains or domains with expiring certificates. -management/ssl_certificates.py -q | management/email_administrator.py "TLS Certificate Provisioning Result" +management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result" # Run status checks and email the administrator if anything changed. -management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice" +management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice" From e03a6541ced593b6c19a875f3fe59139d193a41c Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 5 Jun 2020 13:45:50 -0400 Subject: [PATCH 10/25] Don't make autoconfig/autodiscover subdomains and SRV records when the parent domain has no user accounts These subdomains/records are for automatic configuration of mail clients, but if there are no user accounts on a domain, there is no need to publish a DNS record, provision a TLS certificate, or create an nginx server config block. --- CHANGELOG.md | 4 ++++ management/dns_update.py | 28 +++++++++++++++------------- management/mailconfig.py | 14 ++++++++------ management/web_update.py | 8 ++++---- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f860fe..04cfb753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Mail: * An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed. * MTA-STS reporting is enabled with reports sent to administrator@ the primary hostname. +DNS: + +* autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary. + v0.45 (May 16, 2020) -------------------- diff --git a/management/dns_update.py b/management/dns_update.py index 5fdb3e0f..80273a12 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -281,28 +281,30 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en if not has_rec(dmarc_qname, "TXT", prefix="v=DMARC1; "): records.append((dmarc_qname, "TXT", 'v=DMARC1; p=reject', "Recommended. Prevents use of this domain name for outbound mail by specifying that the SPF rule should be honoured for mail from @%s." % (qname + "." + domain))) - # Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname. + # Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname + # for autoconfiguration of mail clients (so only domains hosting user accounts need it). # The SRV record format is priority (0, whatever), weight (0, whatever), port, service provider hostname (w/ trailing dot). - if domain != env["PRIMARY_HOSTNAME"]: + if domain != env["PRIMARY_HOSTNAME"] and domain in get_mail_domains(env, users_only=True): for dav in ("card", "cal"): qname = "_" + dav + "davs._tcp" if not has_rec(qname, "SRV"): records.append((qname, "SRV", "0 0 443 " + env["PRIMARY_HOSTNAME"] + ".", "Recommended. Specifies the hostname of the server that handles CardDAV/CalDAV services for email addresses on this domain.")) - # Adds autoconfiguration A records for all domains. + # Adds autoconfiguration A records for all domains that there are user accounts at. # This allows the following clients to automatically configure email addresses in the respective applications. # autodiscover.* - Z-Push ActiveSync Autodiscover # autoconfig.* - Thunderbird Autoconfig - autodiscover_records = [ - ("autodiscover", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), - ("autodiscover", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), - ("autoconfig", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig."), - ("autoconfig", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig.") - ] - for qname, rtype, value, explanation in autodiscover_records: - if value is None or value.strip() == "": continue # skip IPV6 if not set - if not has_rec(qname, rtype): - records.append((qname, rtype, value, explanation)) + if domain in get_mail_domains(env, users_only=True): + autodiscover_records = [ + ("autodiscover", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), + ("autodiscover", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), + ("autoconfig", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig."), + ("autoconfig", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig.") + ] + for qname, rtype, value, explanation in autodiscover_records: + if value is None or value.strip() == "": continue # skip IPV6 if not set + if not has_rec(qname, rtype): + records.append((qname, rtype, value, explanation)) # If this is a domain name that there are email addresses configured for, i.e. "something@" # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) diff --git a/management/mailconfig.py b/management/mailconfig.py index 5f253c14..dd597cd6 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -258,13 +258,15 @@ def get_domain(emailaddr, as_unicode=True): pass return ret -def get_mail_domains(env, filter_aliases=lambda alias : True): +def get_mail_domains(env, filter_aliases=lambda alias : True, users_only=False): # Returns the domain names (IDNA-encoded) of all of the email addresses - # configured on the system. - return set( - [get_domain(login, as_unicode=False) for login in get_mail_users(env)] - + [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ] - ) + # configured on the system. If users_only is True, only return domains + # with email addresses that correspond to user accounts. + domains = [] + domains.extend([get_domain(login, as_unicode=False) for login in get_mail_users(env)]) + if not users_only: + domains.extend([get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ]) + return set(domains) def add_mail_user(email, pw, privs, env): # validate email diff --git a/management/web_update.py b/management/web_update.py index 4a07dc9e..78f86f4c 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -24,13 +24,13 @@ def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True) # the topmost of each domain we serve. domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) - # Add Autoconfiguration domains, allowing us to serve correct SSL certs. + # Add Autoconfiguration domains for domains that there are user accounts at: # 'autoconfig.' for Mozilla Thunderbird auto setup. # 'autodiscover.' for Activesync autodiscovery. - domains |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env)) - domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env)) + domains |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env, users_only=True)) + domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env, users_only=True)) - # 'mta-sts.' for MTA-STS support. + # 'mta-sts.' for MTA-STS support for all domains that have email addresses. domains |= set('mta-sts.' + maildomain for maildomain in get_mail_domains(env)) if exclude_dns_elsewhere: From 9db2fc7f0551b6ea9b7c73f447495fda722473fb Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 7 Jun 2020 09:45:04 -0400 Subject: [PATCH 11/25] In web proxies, add X-{Forwarded-{Host,Proto},Real-IP} and 'proxy_set_header Host' when there is a flag Merges #1432, more or less. --- management/web_update.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/management/web_update.py b/management/web_update.py index 78f86f4c..66340619 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -158,9 +158,23 @@ def make_domain_config(domain, templates, ssl_certificates, env): # any proxy or redirect here? for path, url in yaml.get("proxies", {}).items(): + # Parse some flags in the fragment of the URL. + pass_http_host_header = False + m = re.search("#(.*)$", url) + if m: + for flag in m.group(1).split(","): + if flag == "pass-http-host": + pass_http_host_header = True + url = re.sub("#(.*)$", "", url) + nginx_conf_extra += "\tlocation %s {" % path nginx_conf_extra += "\n\t\tproxy_pass %s;" % url + if pass_http_host_header: + nginx_conf_extra += "\n\t\tproxy_set_header Host $http_host;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;" + nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Host $http_host;" + nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Proto $scheme;" + nginx_conf_extra += "\n\t\tproxy_set_header X-Real-IP $remote_addr;" nginx_conf_extra += "\n\t}\n" for path, alias in yaml.get("aliases", {}).items(): nginx_conf_extra += "\tlocation %s {" % path From 6fd3195275fdfef3edd748a44b70dc830f320802 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 12 Jun 2020 13:09:11 -0400 Subject: [PATCH 12/25] Fix MTA-STS policy id so it does not have invalid characters, fixes #1779 --- management/dns_update.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 80273a12..2fb7b1b8 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -340,11 +340,13 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # 'break' was not encountered above, so both domains are good mta_sts_enabled = True if mta_sts_enabled: - # Compute a up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy - # file (20 bytes) and encode it as base-64 (60 bytes) but then just take its first 20 bytes - # which should be sufficient to change whenever the policy file changes. + # Compute an up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy + # file (20 bytes) and encode it as base-64 (28 bytes, using alphanumeric alternate characters + # instead of '+' and '/' which are not allowed in an MTA-STS policy id) but then just take its + # first 20 characters, which is more than sufficient to change whenever the policy file changes + # (and ensures any '=' padding at the end of the base64 encoding is dropped). with open("/var/lib/mailinabox/mta-sts.txt", "rb") as f: - mta_sts_policy_id = base64.b64encode(hashlib.sha1(f.read()).digest()).decode("ascii")[0:20] + mta_sts_policy_id = base64.b64encode(hashlib.sha1(f.read()).digest(), altchars=b"AA").decode("ascii")[0:20] mta_sts_records.extend([ ("_mta-sts", "TXT", "v=STSv1; id=" + mta_sts_policy_id, "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.") ]) From e6102eacfb1637d49eed62c6a2b9d499803b9cf8 Mon Sep 17 00:00:00 2001 From: David Duque Date: Wed, 8 Jul 2020 23:26:47 +0100 Subject: [PATCH 13/25] AXFR Transfers (for secondary DNS servers): Allow IPv6 addresses (#1787) --- management/dns_update.py | 9 +++++---- management/templates/custom-dns.html | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 2fb7b1b8..19830749 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -967,18 +967,19 @@ def set_secondary_dns(hostnames, env): try: response = resolver.query(item, "A") except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - raise ValueError("Could not resolve the IP address of %s." % item) + try: + response = resolver.query(item, "AAAA") + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + raise ValueError("Could not resolve the IP address of %s." % item) else: # Validate IP address. try: if "/" in item[4:]: v = ipaddress.ip_network(item[4:]) # raises a ValueError if there's a problem - if not isinstance(v, ipaddress.IPv4Network): raise ValueError("That's an IPv6 subnet.") else: v = ipaddress.ip_address(item[4:]) # raises a ValueError if there's a problem - if not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.") except ValueError: - raise ValueError("'%s' is not an IPv4 address or subnet." % item[4:]) + raise ValueError("'%s' is not an IPv4 or IPv6 address or subnet." % item[4:]) # Set. set_custom_dns_record("_secondary_nameserver", "A", " ".join(hostnames), "set", env) diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html index a2d5042d..6984b081 100644 --- a/management/templates/custom-dns.html +++ b/management/templates/custom-dns.html @@ -90,7 +90,7 @@

Multiple secondary servers can be separated with commas or spaces (i.e., ns2.hostingcompany.com ns3.hostingcompany.com). - To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using xfr:10.20.30.40 or xfr:10.20.30.40/24. + To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using xfr:10.20.30.40 or xfr:10.0.0.0/8.

").text(r));
+          show_modal_error("Remove Alias", $("
").text(r));
           show_aliases();
         });
     });

From 1098e2b48e3cf542f114caed591337d3cddae762 Mon Sep 17 00:00:00 2001
From: Hilko 
Date: Wed, 29 Jul 2020 16:03:33 +0200
Subject: [PATCH 18/25] Add noindex to www_default meta tags (#1791)

---
 conf/www_default.html | 1 +
 1 file changed, 1 insertion(+)

diff --git a/conf/www_default.html b/conf/www_default.html
index edefc428..68d0366b 100644
--- a/conf/www_default.html
+++ b/conf/www_default.html
@@ -1,6 +1,7 @@
 
 	
 		this is a mail-in-a-box
+		
 	
 	
 		

this is a mail-in-a-box

From 4bbe4af37741b9dba3c766657ca36e198532b506 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Wed, 29 Jul 2020 10:11:47 -0400 Subject: [PATCH 19/25] Update CHANGELOG --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36656e53..be38130e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,29 @@ In Development Mail: -* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed. +* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced. * MTA-STS reporting is enabled with reports sent to administrator@ the primary hostname. +* The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT. DNS: * autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary. +* IPv6 addresses can now be specified for secondary DNS nameservers in the control panel. + +TLS: + +* TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains. + +Control Panel: + +* User passwords can now have spaces. +* Status checks for automatic subdomains have been moved into the section for the parent domain. +* Typo fixed. + +Web: + +* The default web page served on fresh installations now adds the `noindex` meta tag. +* The HSTS header is revised to also be sent on non-success responses. v0.46 (June 11, 2020) --------------------- From 94da7bb088d48ff5d4b87b9bc4a43c5585a51166 Mon Sep 17 00:00:00 2001 From: David Duque Date: Sun, 9 Aug 2020 16:42:39 +0100 Subject: [PATCH 20/25] status_checks.py: Properly terminate the process pools (#1795) * Only spawn a thread pool when strictly needed For --check-primary-hostname, the pool is not used. When exiting, the other processes are left alive and will hang. * Acquire pools with the 'with' statement --- management/daemon.py | 5 ++--- management/status_checks.py | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index 572b6b4a..b7bf2a66 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -437,9 +437,8 @@ def system_status(): self.items[-1]["extra"].append({ "text": message, "monospace": monospace }) output = WebOutput() # Create a temporary pool of processes for the status checks - pool = multiprocessing.pool.Pool(processes=5) - run_checks(False, env, output, pool) - pool.terminate() + with multiprocessing.pool.Pool(processes=5) as pool: + run_checks(False, env, output, pool) return json_response(output.items) @app.route('/system/updates') diff --git a/management/status_checks.py b/management/status_checks.py index 101a3537..36da034a 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -1021,13 +1021,14 @@ if __name__ == "__main__": from utils import load_environment env = load_environment() - pool = multiprocessing.pool.Pool(processes=10) if len(sys.argv) == 1: - run_checks(False, env, ConsoleOutput(), pool) + with multiprocessing.pool.Pool(processes=10) as pool: + run_checks(False, env, ConsoleOutput(), pool) elif sys.argv[1] == "--show-changes": - run_and_output_changes(env, pool) + with multiprocessing.pool.Pool(processes=10) as pool: + run_and_output_changes(env, pool) elif sys.argv[1] == "--check-primary-hostname": # See if the primary hostname appears resolvable and has a signed certificate. From 62b9b1f15f18745d10f1bb9ae1f25722f8057007 Mon Sep 17 00:00:00 2001 From: Richard Willis Date: Sat, 22 Aug 2020 20:44:19 +0100 Subject: [PATCH 21/25] Add OpenAPI HTTP spec (#1804) --- .gitignore | 1 + api/docs/generate-docs.sh | 23 + api/docs/template.hbs | 31 + api/mailinabox.yml | 2531 +++++++++++++++++++++++++++++++++++++ 4 files changed, 2586 insertions(+) create mode 100755 api/docs/generate-docs.sh create mode 100644 api/docs/template.hbs create mode 100644 api/mailinabox.yml diff --git a/.gitignore b/.gitignore index f3cdb1bc..14e6c4a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tools/__pycache__/ externals/ .env .vagrant +api/docs/api-docs.html \ No newline at end of file diff --git a/api/docs/generate-docs.sh b/api/docs/generate-docs.sh new file mode 100755 index 00000000..e7951d8a --- /dev/null +++ b/api/docs/generate-docs.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +# Requirements: +# - Node.js +# - redoc-cli (`npm install redoc-cli -g`) + +redoc-cli bundle ../mailinabox.yml \ + -t template.hbs \ + -o api-docs.html \ + --templateOptions.metaDescription="Mail-in-a-Box HTTP API" \ + --title="Mail-in-a-Box HTTP API" \ + --options.expandSingleSchemaField \ + --options.hideSingleRequestSampleTab \ + --options.jsonSampleExpandLevel=10 \ + --options.hideDownloadButton \ + --options.theme.logo.maxHeight=180px \ + --options.theme.logo.maxWidth=180px \ + --options.theme.colors.primary.main="#C52" \ + --options.theme.typography.fontSize=16px \ + --options.theme.typography.fontFamily="Raleway, sans-serif" \ + --options.theme.typography.headings.fontFamily="Ubuntu, Arial, sans-serif" \ + --options.theme.typography.code.fontSize=15px \ + --options.theme.typography.code.fontFamily='"Source Code Pro", monospace' \ No newline at end of file diff --git a/api/docs/template.hbs b/api/docs/template.hbs new file mode 100644 index 00000000..0de7d222 --- /dev/null +++ b/api/docs/template.hbs @@ -0,0 +1,31 @@ + + + + + + {{title}} + + + + + + + + + {{{redocHead}}} + + + + {{{redocHTML}}} + + + diff --git a/api/mailinabox.yml b/api/mailinabox.yml new file mode 100644 index 00000000..57ba5aa4 --- /dev/null +++ b/api/mailinabox.yml @@ -0,0 +1,2531 @@ +openapi: 3.0.3 +info: + title: Mail-in-a-Box + description: | + Mail-in-a-Box API HTTP specification. + + # Introduction + This API is documented in [**OpenAPI format**](http://spec.openapis.org/oas/v3.0.3). + ([View the full HTTP specification](https://raw.githubusercontent.com/mail-in-a-box/mailinabox/api-spec/api/mailinabox.yml).) + + All endpoints are relative to `https://{host}/admin` and are secured with [`Basic Access` authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). + contact: + name: Mail-in-a-Box support + url: https://mailinabox.email/ + license: + name: CC0 1.0 Universal + url: https://creativecommons.org/publicdomain/zero/1.0/legalcode + version: 0.47.0 + x-logo: + url: https://mailinabox.email/static/logo.png + altText: Mail-in-a-Box logo +externalDocs: + description: Find out more about Mail-in-a-box. + url: https://mailinabox.email/ +servers: + - url: https://{host}/admin + variables: + host: + default: box.example.com + description: The API hostname. +security: + - basicAuth: [] +tags: + - name: User + description: Endpoints related to user authentication. + - name: Mail + description: | + Mail operations, which include getting all users, getting all aliases, adding/updating/removing users and aliases and getting all mail domains. + - name: DNS + description: | + DNS operations, which include adding custom records, adding a secondary nameserver and viewing all DNS records. + - name: SSL + description: | + TLS (SSL) Certificates operations, which include checking certificate status + and installing custom certificates. + - name: Web + description: | + Static web hosting operations, which include getting domain information and updating domain root directories. + - name: System + description: | + System operations, which include system status checks, new version checks + and reboot status. +paths: + /me: + get: + tags: + - User + summary: Get user information + description: | + Returns user information. Used for user authentication. + + Authenticate a user by supplying the auth token as a base64 encoded string in + format `email:password` using basic authentication headers. + + If successful, a long-lived `api_key` is returned which can be used for subsequent + requests to the API. + operationId: getMe + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/me" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/MeResponse' + examples: + invalid: + value: + reason: Incorrect username or password + status: invalid + ok: + value: + api_key: 1a2b3c4d5e6f7g8h9i0j + email: user@example.com + privileges: + - admin + status: ok + /system/status: + post: + tags: + - System + summary: Get system status + description: | + Returns an array of statuses which can include headings. + operationId: getSystemStatus + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/status" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatusResponse' + example: + - type: heading + text: System + extra: [] + - type: warning + text: This domain's DNSSEC DS record is not set + extra: + - monospace: false + text: 'Digest Type: 2 / SHA-25' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/version: + get: + tags: + - System + summary: Get system version + description: Returns installed Mail-in-a-Box version. + operationId: getSystemVersion + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/version" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemVersionResponse' + example: v0.46 + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/latest-upstream-version: + post: + tags: + - System + summary: Get system upstream version + description: Returns Mail-in-a-Box upstream version. + operationId: getSystemUpstreamVersion + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/latest-upstream-version" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemVersionUpstreamResponse' + example: v0.47 + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/updates: + get: + tags: + - System + summary: Get system updates + description: Returns system (apt) updates. + operationId: getSystemUpdates + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/updates" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemUpdatesResponse' + example: | + libgnutls30 (3.5.18-1ubuntu1.4) + libxau6 (1:1.0.8-1ubuntu1) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/update-packages: + post: + tags: + - System + summary: Update system packages + description: Updates system (apt) packages. + operationId: updateSystemPackages + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/update-packages" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemUpdatePackagesResponse' + example: | + Calculating upgrade... + The following packages will be upgraded: + cloud-init grub-common + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/privacy: + get: + tags: + - System + summary: Get system privacy status + description: | + Returns system privacy (new-version check) status. + + Response: + + - `true`: Private, new-version checks will not be performed + - `false`: Not private, new-version checks will be performed + operationId: getSystemPrivacyStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/privacy" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemPrivacyStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Update system privacy + description: | + Updates system privacy (new-version checks). + + Request: + + - `value: private`: Disable new version checks + - `value: off`: Enable new version checks + operationId: updateSystemPrivacy + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SystemPrivacyUpdateRequest' + examples: + enable: + summary: Enable new version checks + value: + value: 'off' + disable: + summary: Disable new version checks + value: + value: private + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/privacy" \ + -d "value=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemPrivacyUpdateResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/reboot: + get: + tags: + - System + summary: Get system reboot status + description: | + Returns the system reboot status. + + Response: + + - `true`: A reboot is required + - `false`: A reboot is not required + operationId: getSystemRebootStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/reboot" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemRebootStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Reboot system + description: Reboots the system. + operationId: rebootSystem + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/reboot" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemRebootResponse' + example: No reboot is required, so it is not allowed. + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/backup/status: + get: + tags: + - System + summary: Get system backup status + description: | + Returns the system backup status. + + If the list of backups is empty, this implies no backups have been made yet. + operationId: getSystemBackupStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/backup/status" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemBackupStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/backup/config: + get: + tags: + - System + summary: Get system backup config + description: Returns the system backup config. + operationId: getSystemBackupConfig + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/backup/config" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemBackupConfigResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Update system backup config + description: Updates the system backup config. + operationId: updateSystemBackupConfig + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SystemBackupConfigUpdateRequest' + examples: + s3: + summary: S3 backup + value: + target: s3://s3.eu-central-1.amazonaws.com/box-example-com + target_user: ACCESS_KEY + target_pass: SECRET_ACCESS_KEY + minAge: 3 + local: + summary: Local backup + value: + target: local + target_user: '' + target_pass: '' + minAge: 3 + rsync: + summary: Rsync backup + value: + target: rsync://username@box.example.com//backups/box.example.com + target_user: '' + target_pass: '' + minAge: 3 + off: + summary: Disable backups + value: + target: 'off' + target_user: '' + target_pass: '' + minAge: 0 + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/backup/config" \ + -d "target=" \ + -d "target_user=" \ + -d "target_pass=" \ + -d "min_age=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemBackupConfigUpdateResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/status: + get: + tags: + - SSL + summary: Get SSL status + description: Returns the SSL status for all domains. + operationId: getSSLStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/ssl/status" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SSLStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/csr/{domain}: + post: + tags: + - SSL + summary: Generate SSL CSR + description: | + Generates a Certificate Signing Request (CSR) for a domain & country code. + operationId: generateSSLCSR + parameters: + - in: path + name: domain + schema: + $ref: '#/components/schemas/Hostname' + required: true + description: Domain to generate CSR for. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SSLCSRGenerateRequest' + example: + countrycode: 'GB' + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/ssl/csr/" \ + -d "countrycode=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SSLCSRGenerateResponse' + example: | + -----BEGIN CERTIFICATE REQUEST----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + ... + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE REQUEST----- + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/install: + post: + tags: + - SSL + summary: Install SSL certificate + description: | + Installs a custom certificate. The chain certificate is optional. + operationId: installSSLCertificate + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SSLCertificateInstallRequest' + example: + domain: example.com + cert: CERT_STRING + chain: CHAIN_STRING + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/ssl/install" \ + -d "domain=" \ + -d "cert=" \ + -d "chain=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SSLCertificateInstallResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/provision: + post: + tags: + - SSL + summary: Provision SSL certificates + description: | + Provisions certificates for all domains. + operationId: provisionSSLCertificates + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/ssl/provision" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SSLCertificatesProvisionResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/secondary-nameserver: + get: + tags: + - DNS + summary: Get DNS secondary nameserver + description: | + Returns a list of nameserver hostnames. + operationId: getDnsSecondaryNameserver + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/secondary-nameserver" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSSecondaryNameserverResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - DNS + summary: Add DNS secondary nameserver + description: | + Adds one or more secondary nameservers. + operationId: addDnsSecondaryNameserver + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/DNSSecondaryNameserverAddRequest' + example: + hostnames: ns2.hostingcompany.com, ns3.hostingcompany.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/secondary-nameserver" \ + -d "hostnames=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSSecondaryNameserverAddResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Could not resolve the IP address of badhostname + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/zones: + get: + tags: + - DNS + summary: Get DNS zones + description: Returns an array of all managed top-level domains. + operationId: getDnsZones + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/zones" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSZonesResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/update: + post: + tags: + - DNS + summary: Update DNS + description: Updates the DNS. Involves creating zone files and restarting `nsd`. + operationId: updateDns + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/DNSUpdateRequest' + example: + force: 1 + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/update" \ + -d "force=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSUpdateResponse' + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/custom: + get: + tags: + - DNS + summary: Get DNS custom records + description: Returns all custom DNS records. + operationId: getDnsCustomRecords + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/custom" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSCustomRecordsResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/custom/{qname}/{rtype}: + parameters: + - in: path + name: qname + schema: + $ref: '#/components/schemas/Hostname' + required: true + description: DNS record query name + - in: path + name: rtype + schema: + $ref: '#/components/schemas/DNSRecordType' + required: true + description: Record type + get: + tags: + - DNS + summary: Get DNS custom records + description: Returns all custom records for the specified query name and type. + operationId: getDnsCustomRecordsForQNameAndType + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/custom//" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSCustomRecordsResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - DNS + summary: Add DNS custom record + description: Adds a custom DNS record for the specified query name and type. + operationId: addDnsCustomRecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/custom//" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + put: + tags: + - DNS + summary: Update DNS custom record + description: Updates an existing DNS custom record value for the specified qname and type. + operationId: updateDnsCustomRecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -x PUT "https://{host}/admin/dns/custom//" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + delete: + tags: + - DNS + summary: Remove DNS custom record + description: Removes a DNS custom record for the specified domain, type & value. + operationId: removeDnsCustomRecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X DELETE "https://{host}/admin/dns/custom//" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordRemoveResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: badhostname is not a domain name or a subdomain of a domain name managed by this box + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/custom/{qname}: + parameters: + - in: path + name: qname + schema: + $ref: '#/components/schemas/Hostname' + required: true + description: DNS query name. + get: + tags: + - DNS + summary: Get DNS custom A records + description: Returns all custom A records for the specified query name. + operationId: getDnsCustomARecordsForQName + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/custom/" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSCustomRecordsResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - DNS + summary: Add DNS custom A record + description: Adds a custom DNS A record for the specified query name. + operationId: addDnsCustomARecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/custom/" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + put: + tags: + - DNS + summary: Update DNS custom A record + description: Updates an existing DNS custom A record value for the specified qname. + operationId: updateDnsCustomARecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -x PUT "https://{host}/admin/dns/custom/" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + delete: + tags: + - DNS + summary: Remove DNS custom A record + description: Removes a DNS custom A record for the specified domain & value. + operationId: removeDnsCustomARecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X DELETE "https://{host}/admin/dns/custom/" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordRemoveResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: badhostname is not a domain name or a subdomain of a domain name managed by this box + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/dump: + get: + tags: + - DNS + summary: Get DNS dump + description: Returns all DNS records. + operationId: getDnsDump + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/dump" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSDumpResponse' + example: + - - example1.com + - - explanation: Required. Specifies the hostname (and priority) of the machine that handles @example.com mail. + qname: example1.com + rtype: MX + value: 10 box.example1.com. + - - example2.com + - - explanation: Required. Specifies the hostname (and priority) of the machine that handles @example.com mail. + qname: example2.com + rtype: MX + value: 10 box.example2.com. + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users: + get: + tags: + - Mail + summary: Get mail users + description: Returns all mail users. + operationId: getMailUsers + parameters: + - in: query + name: format + schema: + $ref: '#/components/schemas/MailUsersResponseFormat' + description: The format of the response. + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/users?format=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/MailUsersResponse' + text/html: + schema: + $ref: '#/components/schemas/MailUsersSimpleResponse' + example: | + user1@example.com + user2@example.com + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/add: + post: + tags: + - Mail + summary: Add mail user + description: Adds a new mail user. + operationId: addMailUser + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserAddRequest' + examples: + normal: + summary: Normal user + value: + email: user@example.com + password: s3curE_pa5Sw0rD + privileges: '' + admin: + summary: Admin user + value: + email: user@example.com + password: s3curE_pa5Sw0rD + privileges: admin + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/add" \ + -d "email=" \ + -d "password=" \ + -d "privileges=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserAddResponse' + example: | + mail user added + updated DNS: OpenDKIM configuration + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Invalid email address + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/remove: + post: + tags: + - Mail + summary: Remove mail user + description: Removes an existing mail user. + operationId: removeMailUser + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserRemoveRequest' + example: + email: user@example.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/remove" \ + -d "email=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserRemoveResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not a user (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/privileges/add: + post: + tags: + - Mail + summary: Add mail user privilege + description: Adds a privilege to an existing mail user. + operationId: addMailUserPrivilege + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserAddPrivilegeRequest' + example: + email: user@example.com + privilege: admin + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/privileges/add" \ + -d "email=" \ + -d "privilege=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserAddPrivilegeResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not a user (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/privileges/remove: + post: + tags: + - Mail + summary: Remove mail user privilege + description: Removes a privilege from an existing mail user. + operationId: removeMailUserPrivilege + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserRemovePrivilegeRequest' + example: + email: user@example.com + privilege: admin + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/privileges/remove" \ + -d "email=" \ + -d "privilege=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserRemovePrivilegeResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not a user (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/password: + post: + tags: + - Mail + summary: Set mail user password + description: Sets a password for an existing mail user. + operationId: setMailUserPassword + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserSetPasswordRequest' + example: + email: user@example.com + password: s3curE_pa5Sw0rD + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/password" \ + -d "email=" \ + -d "password=" \ + -u ":" \ + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserSetPasswordResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Passwords must be at least eight characters + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/privileges: + get: + tags: + - Mail + summary: Get mail user privileges + description: Returns all privileges for an existing mail user. + operationId: getMailUserPrivileges + parameters: + - in: query + name: email + schema: + $ref: '#/components/schemas/Email' + description: The email you want to get privileges for. + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/users/privileges?email=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserPrivilegesResponse' + example: admin + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/domains: + get: + tags: + - Mail + summary: Get mail domains + description: Returns all mail domains. + operationId: getMailDomains + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/domains" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailDomainsResponse' + example: | + example1.com + example2.com + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/aliases: + get: + tags: + - Mail + summary: Get mail aliases + description: Returns all mail aliases. + operationId: getMailAliases + parameters: + - in: query + name: format + schema: + $ref: '#/components/schemas/MailAliasesResponseFormat' + description: The format of the response. + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/aliases?format=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MailAliasByDomain' + text/html: + schema: + $ref: '#/components/schemas/MailAliasesSimpleResponse' + example: | + abuse@example.com administrator@example.com + admin@example.com administrator@example.com + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/aliases/add: + post: + tags: + - Mail + summary: Upsert mail alias + description: | + Adds or updates a mail alias. If updating, you need to set `update_if_exists: 1`. + operationId: upsertMailAlias + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailAliasUpsertRequest' + examples: + regular: + summary: Regular alias + value: + update_if_exists: 0 + address: user@example.com + forwards_to: user2@example.com + permitted_senders: + catchall: + summary: Catch-all + value: + update_if_exists: 0 + address: '@example.com' + forwards_to: user@otherexample.com + permitted_senders: + domainalias: + summary: Domain alias + value: + update_if_exists: 0 + address: '@example.com' + forwards_to: '@otherexample.com' + permitted_senders: + update: + summary: Update existing alias + value: + update_if_exists: 1 + address: user@example.com + forwards_to: user2@example.com + permitted_senders: user3@example.com, user4@example.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/aliases/add" \ + -d "update_if_exists=" \ + -d "address=" \ + -d "forwards_to=" \ + -d "permitted_senders=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailAliasUpsertResponse' + example: alias updated + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Invalid email address (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/aliases/remove: + post: + tags: + - Mail + summary: Remove mail alias + description: Removes a mail alias. + operationId: removeMailAlias + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailAliasRemoveRequest' + example: + address: user@example.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/aliases/remove" \ + -d "address=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailAliasRemoveResponse' + example: alias removed + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not an alias (invalid@example) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /web/domains: + get: + tags: + - Web + summary: Get web domains + description: Returns all static web domains. + operationId: getWebDomains + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/web/domains" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WebDomain' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /web/update: + post: + tags: + - Web + summary: Update web + description: Updates static websites, used for updating domain root directories. + operationId: updateWeb + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/web/update" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/WebUpdateResponse' + example: web updated + 403: + description: Forbidden + content: + text/html: + schema: + type: string +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + description: | + Credentials can be supplied using the `Authorization` header in + format `Authorization: Basic {access-token}`. + + The `access-token` is comprised of the Base64 encoding of `username:password`. + The `username` is the mail user's email address, and `password` can either be the mail user's + password, or the `api_key` returned from the `getMe` operation. + + When using `curl`, you can supply user credentials using the `-u` or `--user` parameter. + requestBodies: + DNSCustomRecordRequest: + required: true + content: + text/plain: + schema: + type: string + example: 1.2.3.4 + description: The value of the DNS record. + example: '1.2.3.4' + schemas: + MailUsersResponseFormat: + type: string + enum: + - text + - json + example: json + description: Response format (`application/json` or `text/html`). + MailAliasesResponseFormat: + type: string + enum: + - text + - json + example: json + description: Response format (`application/json` or `text/html`). + MailUserSetPasswordResponse: + type: string + example: OK + description: Mail user set password response. + MailUserRemoveResponse: + type: string + example: OK + description: Mail user remove response. + MailUserAddResponse: + type: string + example: | + mail user added + updated DNS: OpenDKIM configuration + description: | + Mail user add response. + + Can include information about operations related to adding new users, like updating DNS. + MailUserAddPrivilegeResponse: + type: string + example: OK + description: Mail user add admin privilege response. + MailUserRemovePrivilegeResponse: + type: string + example: OK + description: Mail user remove admin privilege response. + MailUsersSimpleResponse: + type: string + example: | + user1@example.com + user2@example.com + description: Get mail users text format response. + MailUserPrivilegesResponse: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user privileges response. + example: admin + MailDomainsResponse: + type: string + example: | + example1.com + example2.com + description: Mail domains response. + MailUsersResponse: + type: array + items: + $ref: '#/components/schemas/MailUserByDomain' + description: Get mail aliases JSON format response. + MailUserByDomain: + type: object + required: + - domain + - users + properties: + domain: + $ref: '#/components/schemas/Hostname' + users: + type: array + items: + $ref: '#/components/schemas/MailUser' + description: Mail users by domain. + MailUser: + type: object + required: + - email + - privileges + - status + properties: + email: + $ref: '#/components/schemas/Email' + privileges: + type: array + items: + $ref: '#/components/schemas/MailUserPrivilege' + status: + $ref: '#/components/schemas/MailUserStatus' + mailbox: + type: string + example: /home/user-data/mail/mailboxes/example.com/user + description: Mail user details. + MailAliasesSimpleResponse: + type: string + example: | + abuse@example.com administrator@example.com + admin@example.com administrator@example.com + description: Get mail aliases text format response. + MailAliasByDomain: + type: object + required: + - domain + - aliases + properties: + domain: + $ref: '#/components/schemas/Hostname' + aliases: + type: array + items: + $ref: '#/components/schemas/MailAlias' + description: Mail aliases by domain. + MailAlias: + type: object + required: + - address + - address_display + - forwards_to + - permitted_senders + - required + properties: + address: + $ref: '#/components/schemas/Email' + address_display: + $ref: '#/components/schemas/Email' + forwards_to: + type: array + items: + $ref: '#/components/schemas/Email' + permitted_senders: + type: array + nullable: true + items: + $ref: '#/components/schemas/Email' + required: + type: boolean + example: true + description: Mail alias details. + MailAliasUpsertResponse: + type: string + example: alias updated + description: Mail alias add/update response. + MailAliasUpsertRequest: + type: object + required: + - update_if_exists + - address + - forwards_to + - permitted_senders + properties: + update_if_exists: + type: integer + format: int32 + minimum: 0 + maximum: 1 + example: 1 + description: Set to `1` when updating an alias. + address: + $ref: '#/components/schemas/Email' + forwards_to: + type: string + example: user1@example.com, user2@example.com + description: | + If adding a regular or catch-all alias, the format needs to be `user@example.com`. + Multiple address can be separated by newlines or commas. + + If adding a domain alias, the format needs to be `@example.com`. + permitted_senders: + type: string + nullable: true + example: user1@example.com, user2@example.com + description: | + Mail users that can send mail claiming to be from any address on the alias domain. + Multiple address can be separated by newlines or commas. + + Leave empty to allow any mail user listed in `forwards_to` to send mail claiming to be from any address on the alias domain. + description: Mail alias upsert request. + MailAliasRemoveResponse: + type: string + example: alias removed + description: Mail alias remove response. + MailAliasRemoveRequest: + type: object + required: + - address + properties: + address: + $ref: '#/components/schemas/Email' + description: Mail aliases remove request. + DNSRecordType: + enum: + - A + - AAAA + - CAA + - CNAME + - TXT + - MX + - SRV + - SSHFP + - NS + example: MX + description: DNS record type. + DNSDumpResponse: + type: array + items: + $ref: '#/components/schemas/DNSDumpDomains' + description: DNS dump response. + DNSDumpDomains: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Hostname' + - $ref: '#/components/schemas/DNSDumpDomainRecords' + description: | + A list of records per domain. + + The first item in the list is the domain and the second item is the list of records. + DNSDumpDomainRecords: + type: array + items: + $ref: '#/components/schemas/DNSDumpDomainRecord' + description: List of domain records. + DNSDumpDomainRecord: + type: object + required: + - explanation + - qname + - type + - value + properties: + explanation: + type: string + example: Required. Specifies the hostname (and priority) of the machine that handles @example.com mail + qname: + $ref: '#/components/schemas/Hostname' + rtype: + $ref: '#/components/schemas/DNSRecordType' + value: + type: string + example: 10 example.com. + description: Domain DNS record details. + DNSCustomRecord: + type: object + required: + - qname + - rtype + - value + properties: + qname: + $ref: '#/components/schemas/Hostname' + rtype: + $ref: '#/components/schemas/DNSRecordType' + value: + type: string + example: 10 example.com. + description: Custom DNS record detail detail. + DNSCustomRecordsResponse: + type: array + items: + $ref: '#/components/schemas/DNSCustomRecord' + description: Custom DNS records response. + DNSZonesResponse: + type: array + items: + $ref: '#/components/schemas/Hostname' + description: DNS zones response. + DNSSecondaryNameserverResponse: + type: object + required: + - hostnames + properties: + hostnames: + type: array + items: + type: string + example: ns1.example.com + description: Secondary nameserver/s response. + DNSCustomRecordRemoveResponse: + type: string + example: 'updated DNS: example.com' + description: Custom DNS record remove response. + DNSCustomRecordUpsertResponse: + type: string + example: 'updated DNS: example.com' + description: Custom DNS record add response. + DNSUpdateRequest: + type: object + required: + - force + properties: + force: + type: integer + format: int32 + minimum: 0 + maximum: 1 + example: 1 + description: Force an update even if mailinabox detects no changes are required. + description: DNS update request. + DNSUpdateResponse: + type: string + example: | + updated DNS: example1.com,example2.com + description: DNS update response. + DNSSecondaryNameserverAddRequest: + type: object + required: + - hostnames + properties: + hostnames: + type: string + description: Hostnames separated with commas or spaces. + example: ns2.hostingcompany.com, ns3.hostingcompany.com + description: Secondary nameserver/s add request. + DNSSecondaryNameserverAddResponse: + type: string + example: 'updated DNS: example.com' + description: Secondary nameserver/s add response. + SystemPrivacyUpdateRequest: + type: object + required: + - value + properties: + value: + $ref: '#/components/schemas/SystemPrivacyStatus' + description: Update system privacy request. + SystemPrivacyStatus: + type: string + enum: + - private + - 'off' + example: private + description: System privacy status. + MailUserSetPasswordRequest: + type: object + required: + - email + - password + properties: + email: + $ref: '#/components/schemas/Email' + password: + type: string + format: password + description: Mail user set password request. + MailUserAddRequest: + type: object + required: + - email + - password + - privileges + properties: + email: + $ref: '#/components/schemas/Email' + password: + type: string + format: password + privileges: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user add request. + MailUserRemoveRequest: + type: object + required: + - email + properties: + email: + $ref: '#/components/schemas/Email' + description: Mail user remove request. + MailUserStatus: + type: string + enum: + - active + - inactive + example: active + description: Mail user status. + MailUserPrivilege: + type: string + enum: + - admin + - '' + example: admin + description: Mail user privilege. + MailUserAddPrivilegeRequest: + type: object + required: + - email + - privilege + properties: + email: + $ref: '#/components/schemas/Email' + privilege: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user add privilege request. + MailUserRemovePrivilegeRequest: + type: object + required: + - email + - privilege + properties: + email: + $ref: '#/components/schemas/Email' + privilege: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user remove privilege request. + SSLCSRGenerateRequest: + type: object + required: + - countrycode + properties: + countrycode: + type: string + example: GB + description: Generate SSL CSR request. + SSLCSRGenerateResponse: + type: string + example: | + -----BEGIN CERTIFICATE REQUEST----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3K6dwLM2Nk8kVhIBaZmp + eY6y7O0T3jrexEKlW839TVYdcH+K35V1NxilbMFKMuHeowGwFyyiqOy/OUYNeq+T + Rz3s4b1qG2p01dwlsXHHYmXLYTAhvqvY+CU5ksieuZbyHRTwbHViQ0xtRXwoVCnj + CkN7kJVpkLfVN0/BG6NBFpv/JI8F+hwp+IHdkC1gUXRrLJNC79ERqFP8HoqdQWNw + OGGFaOe2aQhvj2zt8wFncyKVc40UKVbSzGGzdL2MPiAJHgZ2lmeY1xDyX1lOt12R + IFPwtxmbxaxYaVfe2hxl7m88xV3OjYcKgwVYDusk2XJ37cGew5g+NbBvzEeEUpF9 + 5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD7UPC3/Nkgpn53mT9puUonYdJg9 + SD8vvTK/N78CzoEgPNyq+bYbqlcvVPKIdItf9TMiqfOSvW3e3NvkRisYle8Qp+0C + 8pafXBvQ9eHt5CFeJn4sH9GnxeflOZT/P9Jnp71KtZQvOobirX4GgEWs79g+/NHb + Zyf8rbadt9HruNhKA5nlP8cn7Rdc/iuJU8MVSQszI1s1DEcXMPxr6iqb2g87/ifH + lWcK59kvRJkCcPhPzjpUy9NulucH4WFA/WqKeDNFS/oC+upV5w8EDEcfnenJFG+N + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE REQUEST----- + description: Generate SSL CSR response. + SSLCertificateInstallRequest: + type: object + required: + - domain + - cert + - chain + properties: + domain: + $ref: '#/components/schemas/Hostname' + cert: + type: string + description: TLS/SSL certificate. + example: | + -----BEGIN CERTIFICATE----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3K6dwLM2Nk8kVhIBaZmp + eY6y7O0T3jrexEKlW839TVYdcH+K35V1NxilbMFKMuHeowGwFyyiqOy/OUYNeq+T + Rz3s4b1qG2p01dwlsXHHYmXLYTAhvqvY+CU5ksieuZbyHRTwbHViQ0xtRXwoVCnj + CkN7kJVpkLfVN0/BG6NBFpv/JI8F+hwp+IHdkC1gUXRrLJNC79ERqFP8HoqdQWNw + OGGFaOe2aQhvj2zt8wFncyKVc40UKVbSzGGzdL2MPiAJHgZ2lmeY1xDyX1lOt12R + IFPwtxmbxaxYaVfe2hxl7m88xV3OjYcKgwVYDusk2XJ37cGew5g+NbBvzEeEUpF9 + 5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD7UPC3/Nkgpn53mT9puUonYdJg9 + SD8vvTK/N78CzoEgPNyq+bYbqlcvVPKIdItf9TMiqfOSvW3e3NvkRisYle8Qp+0C + 8pafXBvQ9eHt5CFeJn4sH9GnxeflOZT/P9Jnp71KtZQvOobirX4GgEWs79g+/NHb + Zyf8rbadt9HruNhKA5nlP8cn7Rdc/iuJU8MVSQszI1s1DEcXMPxr6iqb2g87/ifH + lWcK59kvRJkCcPhPzjpUy9NulucH4WFA/WqKeDNFS/oC+upV5w8EDEcfnenJFG+N + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE----- + chain: + type: string + description: TLS/SSL intermediate chain (if provided, else empty string). + example: | + -----BEGIN CERTIFICATE----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3K6dwLM2Nk8kVhIBaZmp + eY6y7O0T3jrexEKlW839TVYdcH+K35V1NxilbMFKMuHeowGwFyyiqOy/OUYNeq+T + Rz3s4b1qG2p01dwlsXHHYmXLYTAhvqvY+CU5ksieuZbyHRTwbHViQ0xtRXwoVCnj + CkN7kJVpkLfVN0/BG6NBFpv/JI8F+hwp+IHdkC1gUXRrLJNC79ERqFP8HoqdQWNw + OGGFaOe2aQhvj2zt8wFncyKVc40UKVbSzGGzdL2MPiAJHgZ2lmeY1xDyX1lOt12R + IFPwtxmbxaxYaVfe2hxl7m88xV3OjYcKgwVYDusk2XJ37cGew5g+NbBvzEeEUpF9 + 5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD7UPC3/Nkgpn53mT9puUonYdJg9 + SD8vvTK/N78CzoEgPNyq+bYbqlcvVPKIdItf9TMiqfOSvW3e3NvkRisYle8Qp+0C + 8pafXBvQ9eHt5CFeJn4sH9GnxeflOZT/P9Jnp71KtZQvOobirX4GgEWs79g+/NHb + Zyf8rbadt9HruNhKA5nlP8cn7Rdc/iuJU8MVSQszI1s1DEcXMPxr6iqb2g87/ifH + lWcK59kvRJkCcPhPzjpUy9NulucH4WFA/WqKeDNFS/oC+upV5w8EDEcfnenJFG+N + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE----- + description: Install certificate request. `chain` can be an empty string. + SSLCertificateInstallResponse: + type: string + example: OK + description: Install certificate response. + SSLCertificatesProvisionResponse: + type: object + required: + - requests + properties: + requests: + type: array + items: + type: object + required: + - log + - result + - domains + properties: + log: + type: array + items: + type: string + example: + - 'The domain name does not resolve to this machine: [Not Set] (A), [Not Set] (AAAA).' + result: + type: string + enum: + - installed + - error + - skipped + example: installed + domains: + type: array + items: + $ref: '#/components/schemas/Hostname' + description: SSL certificates provision response. + SystemPrivacyStatusResponse: + type: boolean + description: | + System privacy status response. + + - `true`: Private, new-version checks will not be performed + - `false`: Not private, new-version checks will be performed + example: false + SystemVersionResponse: + type: string + description: System version response. + example: v0.46 + SystemVersionUpstreamResponse: + type: string + description: System version upstream response. + example: v0.47 + SystemUpdatesResponse: + type: string + description: System updates response. + example: | + libgnutls30 (3.5.18-1ubuntu1.4) + libxau6 (1:1.0.8-1ubuntu1) + SystemUpdatePackagesResponse: + type: string + example: | + Reading package lists... + Building dependency tree... + Reading state information... + Calculating upgrade... + The following packages will be upgraded: + cloud-init grub-common grub-pc grub-pc-bin grub2-common libgnutls30 + libldap-2.4-2 libldap-common libxau6 linux-firmware python3-distupgrade + qemu-guest-agent sosreport ubuntu-release-upgrader-core + 14 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. + Need to get 79.9 MB of archives. + After this operation, 3893 kB of additional disk space will be used. + Get:1 http://archive.ubuntu.com/ubuntu bionic-updates/main amd64 libgnutls30 amd64 3.5.18-1ubuntu1.4 [645 kB] + Preconfiguring packages ... + Fetched 79.9 MB in 2s (52.4 MB/s) + (Reading database ... 48457 files and directories currently installed.) + description: System update packages response. + SystemPrivacyUpdateResponse: + type: string + example: OK + description: System privacy update response. + SystemRebootStatusResponse: + type: boolean + description: | + System reboot status response. + + - `true`: A reboot is required + - `false`: A reboot is not required + example: true + SystemRebootResponse: + type: string + example: No reboot is required, so it is not allowed. + description: System reboot response. + SystemStatusResponse: + type: array + items: + $ref: '#/components/schemas/StatusEntry' + description: System status response. + StatusEntry: + type: object + required: + - type + - text + - extra + properties: + type: + $ref: '#/components/schemas/StatusEntryType' + text: + type: string + example: This domain"s DNSSEC DS record is not set + extra: + type: array + items: + $ref: '#/components/schemas/StatusEntryExtra' + description: System status entry. + StatusEntryType: + type: string + enum: + - heading + - ok + - warning + - error + example: warning + description: System status entry type. + StatusEntryExtra: + type: object + required: + - monospace + - text + properties: + monospace: + type: boolean + example: false + text: + type: string + example: 'Digest Type: 2 / SHA-256' + description: System entry extra information. + SystemBackupConfigUpdateRequest: + type: object + required: + - target + - target_user + - target_pass + - min_age + properties: + target: + type: string + format: hostname + example: s3://s3.eu-central-1.amazonaws.com/box-example-com + target_user: + type: string + example: username + target_pass: + type: string + example: password + format: password + min_age: + type: integer + format: int32 + minimum: 1 + example: 3 + description: Backup config update request. + SystemBackupConfigUpdateResponse: + type: string + example: OK + description: Backup config update response. + SystemBackupConfigResponse: + type: object + required: + - enc_pw_file + - file_target_directory + - min_age_in_days + - ssh_pub_key + - target + properties: + enc_pw_file: + type: string + example: /home/user-data/backup/secret_key.txt + file_target_directory: + type: string + example: /home/user-data/backup/encrypted + min_age_in_days: + type: integer + format: int32 + minimum: 1 + example: 3 + ssh_pub_key: + type: string + example: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDb root@box.example.com\n + target: + type: string + format: hostname + example: s3://s3.eu-central-1.amazonaws.com/box-example-com + target_user: + type: string + target_pass: + type: string + description: Backup config response. + SystemBackupStatusResponse: + type: object + required: + - unmatched_file_size + properties: + backups: + type: array + items: + $ref: '#/components/schemas/SystemBackupStatus' + unmatched_file_size: + type: integer + format: int32 + example: 0 + error: + type: string + example: Something is wrong with the backup + description: Backup status response. Lists the status for all backups. + SystemBackupStatus: + type: object + required: + - date + - date_delta + - date_str + - full + - size + - volumes + properties: + date: + type: string + format: date-time + example: 20200801T023706Z + date_delta: + type: string + example: 15 hours, 40 minutes + date_str: + type: string + example: 2020-08-01 03:37:06 BST + deleted_in: + type: string + example: approx. 6 days + full: + type: boolean + example: false + size: + type: integer + format: int32 + example: 125332 + volumes: + type: integer + format: int32 + example: 1 + description: Backup status details. + SSLStatusResponse: + type: object + required: + - can_provision + - status + properties: + can_provision: + type: array + items: + type: string + status: + type: array + items: + $ref: '#/components/schemas/SSLStatus' + description: SSL status response for all relevant domains. + SSLStatus: + type: object + required: + - domain + - status + - text + properties: + domain: + $ref: '#/components/schemas/Hostname' + status: + $ref: '#/components/schemas/SSLStatusType' + text: + type: string + example: Signed & valid. The certificate expires in 87 days on 10/28/20. + description: SSL status details for domain. + SSLStatusType: + type: string + enum: + - success + - danger + - not-applicable + example: success + description: SSL status type. + Email: + type: string + format: email + example: user@example.com + description: Email format. + Hostname: + type: string + format: hostname + example: example.com + description: Hostname format. + MeResponse: + type: object + required: + - status + properties: + api_key: + type: string + example: 12345abcde + email: + $ref: '#/components/schemas/Email' + privileges: + type: array + items: + $ref: '#/components/schemas/MailUserPrivilege' + reason: + type: string + example: Incorrect username or password + status: + $ref: '#/components/schemas/MeAuthStatus' + description: Me (user) response. + MeAuthStatus: + type: string + enum: + - ok + - invalid + example: invalid + description: Me (user) authentication result. + WebDomain: + type: object + required: + - custom_root + - domain + - root + - ssl_certificate + - static_enabled + properties: + custom_root: + type: string + example: /home/user-data/www/example.com + domain: + $ref: '#/components/schemas/Hostname' + root: + type: string + example: /home/user-data/www/default + ssl_certificate: + type: array + minItems: 2 + maxItems: 2 + uniqueItems: true + items: + oneOf: + - type: string + example: No certificate installed. + - type: string + enum: + - danger + - success + example: danger + static_enabled: + type: boolean + example: true + description: Web domain details. + WebUpdateResponse: + type: string + example: web updated + description: Web update response. From 853008ddccf81a15e522fbd40707439a4d9de611 Mon Sep 17 00:00:00 2001 From: b-k Date: Mon, 21 Sep 2020 15:45:58 -0400 Subject: [PATCH 22/25] Be more forgiving of people who missed the train on upgrading NextCloud (#1813) Co-authored-by: B --- setup/nextcloud.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index 76c04800..90485c8b 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -134,11 +134,11 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc # Database migrations from ownCloud are no longer possible because ownCloud cannot be run under # PHP 7. if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^[89] ]]; then - echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 8 or 9) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup aborting." - exit 1 + echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 8 or 9) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup will continue, but skip the Nextcloud migration." + return 0 elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^1[012] ]]; then - echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 10, 11 or 12) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup aborting." - exit 1 + echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 10, 11 or 12) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup will continue, but skip the Nextcloud migration." + return 0 elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^13 ]]; then # If we are running Nextcloud 13, upgrade to Nextcloud 14 InstallNextcloud 14.0.6 4e43a57340f04c2da306c8eea98e30040399ae5a From 51aedcf6c36c3627ff71b1b39be1823672bafafb Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Mon, 21 Sep 2020 15:56:27 -0400 Subject: [PATCH 23/25] Drop the MTA-STS TLSRPT record unless set explicitly --- CHANGELOG.md | 1 - management/dns_update.py | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7fa49e1..1f8c6ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ In Development Mail: * An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced. -* MTA-STS reporting is enabled with reports sent to administrator@ the primary hostname. * The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT. DNS: diff --git a/management/dns_update.py b/management/dns_update.py index 19830749..748f87f1 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -351,14 +351,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en ("_mta-sts", "TXT", "v=STSv1; id=" + mta_sts_policy_id, "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.") ]) - # Rules can be custom configured accoring to https://tools.ietf.org/html/rfc8460. + # Enable SMTP TLS reporting (https://tools.ietf.org/html/rfc8460) if the user has set a config option. # Skip if the rules below if the user has set a custom _smtp._tls record. - if not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): - tls_rpt_string = "" - tls_rpt_email = env.get("MTA_STS_TLSRPT_EMAIL", "postmaster@%s" % env['PRIMARY_HOSTNAME']) - if tls_rpt_email: # if a reporting address is not cleared - tls_rpt_string = " rua=mailto:%s" % tls_rpt_email - mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1;%s" % tls_rpt_string, "Optional. Enables MTA-STS reporting.")) + if env.get("MTA_STS_TLSRPT_RUA") and not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): + mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1; rua=" + env["MTA_STS_TLSRPT_RUA"], "Optional. Enables MTA-STS reporting.")) for qname, rtype, value, explanation in mta_sts_records: if value is None or value.strip() == "": continue # skip IPV6 if not set if not has_rec(qname, rtype): From e891a9a3f372a89e6a2aa23d95d74d04952cc122 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Mon, 21 Sep 2020 15:59:38 -0400 Subject: [PATCH 24/25] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8c6ca9..fe1ba2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ CHANGELOG In Development -------------- +Setup: + +* When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation. + Mail: * An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced. @@ -20,6 +24,7 @@ TLS: Control Panel: +* The control panel API is now fully documented at https://mailinabox.email/api-docs.html. * User passwords can now have spaces. * Status checks for automatic subdomains have been moved into the section for the parent domain. * Typo fixed. From 03bff5292b5cf3b0947d04ac7b3f93e132bc0c31 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 25 Sep 2020 07:43:30 -0400 Subject: [PATCH 25/25] v0.50 v0.50 (September 25, 2020) -------------------------- Setup: * When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation. Mail: * An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced. * The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT. DNS: * autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary. * IPv6 addresses can now be specified for secondary DNS nameservers in the control panel. TLS: * TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains. Control Panel: * The control panel API is now fully documented at https://mailinabox.email/api-docs.html. * User passwords can now have spaces. * Status checks for automatic subdomains have been moved into the section for the parent domain. * Typo fixed. Web: * The default web page served on fresh installations now adds the `noindex` meta tag. * The HSTS header is revised to also be sent on non-success responses. --- CHANGELOG.md | 4 ++-- README.md | 24 +++++++++++++++--------- setup/bootstrap.sh | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1ba2a9..9c10d78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ CHANGELOG ========= -In Development --------------- +v0.50 (September 25, 2020) +-------------------------- Setup: diff --git a/README.md b/README.md index 2eea0573..151257ee 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,21 @@ Our goals are to: Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which supersedes the goals above. Please review it when joining our community. -The Box -------- + +In The Box +---------- Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components. -It is a one-click email appliance. There are no user-configurable setup options. It "just works". +It is a one-click email appliance. There are no user-configurable setup options. It "just works." The components installed are: -* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), and Exchange ActiveSync ([z-push](http://z-push.org/)) servers -* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (also using dovecot), and email client autoconfig settings (served by [nginx](http://nginx.org/)) +* SMTP ([postfix](http://www.postfix.org/)), IMAP ([Dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), and Exchange ActiveSync ([z-push](http://z-push.org/)) servers +* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (thanks to Roundcube and Dovecot), and email client autoconfig settings (served by [nginx](http://nginx.org/)) * Spam filtering ([spamassassin](https://spamassassin.apache.org/)) and greylisting ([postgrey](http://postgrey.schweikert.ch/)) * DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), [MTA-STS](https://tools.ietf.org/html/rfc8461), and [SSHFP](https://tools.ietf.org/html/rfc4255) policy records automatically set -* HTTPS TLS certificates are automatically provisioned using [Let's Encrypt](https://letsencrypt.org/) (needed for webmail, CardDAV/CalDAV, ActiveSync, MTA-STS policy, etc.). +* TLS certificates are automatically provisioned using [Let's Encrypt](https://letsencrypt.org/) for protecting https and all of the other services on the box * Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), and basic system monitoring ([munin](http://munin-monitoring.org/)) It also includes system management tools: @@ -41,10 +42,11 @@ It also includes system management tools: * A control panel for adding/removing mail users, aliases, custom DNS records, configuring backups, etc. * An API for all of the actions on the control panel -It also supports static website hosting since the box is serving HTTPS anyway. +It also supports static website hosting since the box is serving HTTPS anyway. (To serve a website for your domains elsewhere, just add a custom DNS "A" record in you Mail-in-a-Box's control panel to point domains to another server.) For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md). + Installation ------------ @@ -63,7 +65,7 @@ by him: $ curl -s https://keybase.io/joshdata/key.asc | gpg --import gpg: key C10BDD81: public key "Joshua Tauberer " imported - $ git verify-tag v0.48 + $ git verify-tag v0.50 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! @@ -76,7 +78,7 @@ and on his [personal homepage](https://razor.occams.info/). (Of course, if this Checkout the tag corresponding to the most recent release: - $ git checkout v0.48 + $ git checkout v0.50 Begin the installation. @@ -86,6 +88,9 @@ For help, DO NOT contact Josh directly --- I don't do tech support by email or t Post your question on the [discussion forum](https://discourse.mailinabox.email/) instead, where maintainers and Mail-in-a-Box users may be able to help you. +Note that while we want everything to "just work," we can't control the rest of the Internet. Other mail services might block or spam-filter email sent from your Mail-in-a-Box. +This is a challenge faced by everyone who runs their own mail server, with or without Mail-in-a-Box. See our discussion forum for tips about that. + Contributing and Development ---------------------------- @@ -99,6 +104,7 @@ This project was inspired in part by the ["NSA-proof your email in 2 hours"](htt Mail-in-a-Box is similar to [iRedMail](http://www.iredmail.org/) and [Modoboa](https://github.com/tonioo/modoboa). + The History ----------- diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index debe572b..aca68056 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -20,7 +20,7 @@ if [ -z "$TAG" ]; then # want to display in status checks. if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then # This machine is running Ubuntu 18.04. - TAG=v0.48 + TAG=v0.50 elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" == "Ubuntu 14.04 LTS" ]; then # This machine is running Ubuntu 14.04.