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

Compare commits

...

95 Commits

Author SHA1 Message Date
Joshua Tauberer
34b7638342 v0.12b 2015-07-04 11:31:51 -04:00
Joshua Tauberer
acd91665b5 setting an alias to forward to two or more addresses was broken since aa33428311
fixes #482
2015-07-04 15:28:45 +00:00
Joshua Tauberer
b503ea1cf7 v0.12
--------------------

This is a minor update to v0.11, which was a major update. Please read v0.11's advisories.

* The administrator@ alias was incorrectly created starting with v0.11. If your first install was v0.11, check that the administrator@ alias forwards mail to you.
* Intrusion detection rules (fail2ban) are relaxed (i.e. less is blocked).
* SSL certificates could not be installed for the new automatic 'www.' redirect domains.
* PHP's default character encoding is changed from no default to UTF8. The effect of this change is unclear but should prevent possible future text conversion issues.
* User-installed SSL private keys in the BEGIN PRIVATE KEY format were not accepted.
* SSL certificates with SAN domains with IDNA encoding were broken in v0.11.
* Some IDNA functionality was using IDNA 2003 rather than IDNA 2008.
2015-07-03 10:34:33 -04:00
Joshua Tauberer
091c2e45bf always attempt to upgrade pip packages during setup 2015-07-03 14:25:41 +00:00
Joshua Tauberer
0a78d1d2fa update changelog 2015-07-03 14:15:14 +00:00
Joshua Tauberer
ff4780d5fb better error handling of invalid PEM files 2015-07-03 14:00:59 +00:00
Joshua Tauberer
0924f8ca7a allow for PEM private keys in the 'BEGIN PRIVATE KEY' format too
see https://discourse.mailinabox.email/t/another-upgrade-failure/630/5
2015-07-02 15:37:26 -04:00
Joshua Tauberer
6302aa6c12 Merge pull request #479 from hnk/patch-1
update docstring to clarify usage of -c option
2015-07-02 13:44:03 -04:00
Hnk Reno
da4d9ff607 update docstring to clarify usage of -c option 2015-07-02 19:27:05 +02:00
Joshua Tauberer
e57e08088a the control panel would not allow installing a certificate for a www redirect domain, fixes #475 2015-07-02 10:53:54 +00:00
Joshua Tauberer
5e43c394d5 Merge pull request #477 from anoma/master
cleanup and harden of fail2ban
2015-07-02 06:22:57 -04:00
anoma
b2eaaeca4b Revert to default 6 ssh/ddos login attempts
No legitimate admin will require 20 login attempts. The default 6 is a sane middle ground especially since in 10 minutes they can try again  or immediately from another IP anyway.
2015-07-02 10:23:48 +01:00
anoma
e2d9a523c3 Cleanup blank lines, comments and whitespace to make it easier to follow 2015-07-02 10:19:37 +01:00
anoma
11df1e4680 Unnecessary config item, inherited from default jail.conf 2015-07-02 10:10:50 +01:00
anoma
53d5542402 Revert to default 600 second ban time
A 60 second/1 minute ban time is not long enough to counter brute force attacks which is the main purpose of fail2ban for mail in a box. The default bantime of 10 minutes is still sane and I think we have proven fail2ban is reliable enough not to cause problems in general. It is not worth sacrificing security for the rare case where an admin locks themselves out for 10 minutes.
2015-07-02 10:08:50 +01:00
anoma
bfda3f40b9 Unnecessary config item, inherited from default jail.conf 2015-07-02 09:55:59 +01:00
Joshua Tauberer
c0ddceb2bd Merge pull request #471 from hnk/patch-1
Set PHPs default charset to UTF-8, since we use it. Closes #367.
2015-06-30 12:00:27 -04:00
Joshua Tauberer
42a506231b don't automatically create the administrator@ alias (e.g. on first user creation) because we dont know what it should be an alias to (leave this to be resolved manually), fixes #470
Was broken by 462a79cf47.
2015-06-30 09:16:22 -04:00
Joshua Tauberer
e3252f53da idna domains in certificate subject alternative names were not handled correctly after switching to cryptography package 2015-06-30 13:09:18 +00:00
Joshua Tauberer
aa33428311 some IDNA functionality was still using Python's built-in IDNA 2003 encoder rather than the idna package's IDNA 2008 encoder 2015-06-30 13:09:18 +00:00
Hnk Reno
ca5d228be6 Set PHPs default charset to UTF-8, since we use it. Closes #367. 2015-06-30 11:31:43 +02:00
Joshua Tauberer
f89a98c78a v0.11b to fix missing package for apt-add-repository 2015-06-29 21:52:47 -04:00
Joshua Tauberer
a3087d8815 must install software-properties-common to have add-apt-repository 2015-06-29 21:47:54 -04:00
Joshua Tauberer
23d2df7a93 v0.11
---------------------

Advisories:
* Users can no longer spoof arbitrary email addresses in outbound mail. When sending mail, the email address configured in your mail client must match the SMTP login username being used, or the email address must be an alias with the SMTP login username listed as one of the alias's targets.
* This update replaces your DKIM signing key with a stronger key. Because of DNS caching/propagation, mail sent within a few hours after this update could be marked as spam by recipients. If you use External DNS, you will need to update your DNS records.
* The box will now install software from a new Mail-in-a-Box PPA on Launchpad.net, where we are distributing two of our own packages: a patched postgrey and dovecot-lucene.

Mail:
* Greylisting will now let some reputable senders pass through immediately.
* Searching mail (via IMAP) will now be much faster using the dovecot lucene full text search plugin.
* Users can no longer spoof arbitrary email addresses in outbound mail (see above).
* Fix for deleting admin@ and postmaster@ addresses.
* Roundcube is updated to version 1.1.2, plugins updated.
* Exchange/ActiveSync autoconfiguration was not working on all devices (e.g. iPhone) because of a case-sensitive URL.
* The DKIM signing key has been increased to 2048 bits, from 1024, replacing the existing key.

Web:
* 'www' subdomains now automatically redirect to their parent domain (but you'll need to install an SSL certificate).
* OCSP no longer uses Google Public DNS.
* The installed PHP version is no longer exposed through HTTP response headers, for better security.

DNS:
* Default IPv6 AAAA records were missing since version 0.09.

Control panel:
* Resetting a user's password now forces them to log in again everywhere.
* Status checks were not working if an ssh server was not installed.
* SSL certificate validation now uses the Python cryptography module in some places where openssl was used.
* There is a new tab to show the installed version of Mail-in-a-Box and to fetch the latest released version.

System:
* The munin system monitoring tool is now installed and accessible at /admin/munin.
* ownCloud updated to version 8.0.4. The ownCloud installation step now is reslient to download problems. The ownCloud configuration file is now stored in STORAGE_ROOT to fix loss of data when moving STORAGE_ROOT to a new machine.
* The setup scripts now run `apt-get update` prior to installing anything to ensure the apt database is in sync with the packages actually available.
2015-06-29 20:58:35 -04:00
Joshua Tauberer
1cd97d46a2 press hit 2015-06-27 10:10:33 -04:00
Joshua Tauberer
53f84a8092 set ssl_stapling_verify back to on, reverts part of 47de93961e
The sslmate guidance changed. See #458.
2015-06-27 07:14:16 -04:00
Joshua Tauberer
6441de63ba typo in security.md 2015-06-26 11:38:40 -04:00
Joshua Tauberer
b2553aea33 note the new sender email address spoofing restriction at the top of the changelog 2015-06-26 11:36:10 -04:00
Joshua Tauberer
5ef1cfbdc7 forgot new version.html template file 2015-06-25 17:43:50 +00:00
Joshua Tauberer
7527b4dc27 show the Mail-in-a-Box version in the control panel and a button to ping the MiaB website for the latest version
fixes #441
2015-06-25 13:43:11 +00:00
Joshua Tauberer
1367816b04 merge #451 - Increase DKIM key length to 2048 2015-06-25 13:07:29 +00:00
Joshua Tauberer
299a2315c1 dkim 2048 bits - migration and zone file generation changes
* Add a migration to delete any existing DKIM key so that existing machines get a fresh 2048-bit key. (Sadly we don't support key rotation so the change is immediate.)
* Because the DNS record for a 2048-bit key is so much longer, the way we read OpenDKIM's DNS record text file had to be modified to combine an arbitrary number of TXT record quoted ("...") strings.
* When writing out the TXT record value, the string must be split into quoted ("...") strings with a maximum length of 255 bytes each, per the DNS spec.
* Added a changelog entry.
2015-06-25 13:06:29 +00:00
Joshua Tauberer
9a6aea6940 changelog entry for z-push autodiscover case insensitivity 2015-06-25 12:32:07 +00:00
Joshua Tauberer
98cd04cccf Merge pull request #452 from m4rcs/master
Z-Push autoconfiguration fails due to URL case sensitivity
2015-06-25 08:28:44 -04:00
Marc Schiller
0cc20cbb97 Fixed a bug where autoconfiguration for Z-Push fails due to case of URL. 2015-06-25 11:56:33 +02:00
PortableTech
ef6a17d4a6 Increase DKIM key length to 2048
Currently MiaB creates 1024 bit keys which is seen as a minimum standard
by several providers such as Google who already uses a 2048 bit key.
Increasing the keysize beyond 2048 is an issue as it often goes beyond
supported DNS record sizes.
2015-06-24 18:49:19 -04:00
Joshua Tauberer
17a149947a other CHANGELOG updates 2015-06-24 18:16:25 -04:00
Joshua Tauberer
a2c50ae967 note the new SMTP mail from restriction in the changelog and security guide 2015-06-24 18:12:41 -04:00
Joshua Tauberer
13958ba4df Merge pull request #427 from pichak/add-sender-login-mismatch
Reject outgoing mail if MAIL FROM (envelope sender) does not match login name or is not an alias that directs mail (directly) to login name.
2015-06-24 18:03:03 -04:00
Joshua Tauberer
8eb71483f3 Merge pull request #450 from agriffaut/patch-1
ownCloud breaks if download fails (Issue #449)
2015-06-24 08:11:30 -04:00
aLeX
d8e30883fa Issue #449
If the downloaded file doesn't pass hash verification, the script exits and leaves a broken system
Just make hash verification before moving owncloud directory
2015-06-24 14:06:01 +02:00
Joshua Tauberer
47acbbf332 bump to latest version of my email_validator library 2015-06-23 16:43:35 -04:00
Joshua Tauberer
dece359c90 validate certificates using the cryptography python package as much as possible, shelling out to openssl just once instead of four times per certificate
* Use `cryptography` instead of parsing openssl's output.
* When checking if we can reuse the primary domain certificate or a www-parent-domain certificate for a domain, avoid shelling out to openssl entirely.
2015-06-21 14:53:37 +00:00
Joshua Tauberer
6a9eb4e367 improve inline documentation for the virtual-alias-maps query 2015-06-21 08:22:33 -04:00
Morteza Milani
fc03ce9b2f Fix login map. Now includes both emails and aliases 2015-06-20 03:27:18 -07:00
Toilal
ce17c12ca2 Use netcat to check if mailinabox webservice is available
[JT added installing netcat-openbsd in system.sh]
2015-06-18 08:04:46 -04:00
Joshua Tauberer
5edaeb8c7b add a new autoconfiguration option PRIMARY_HOSTNAME=auto to simply grab the hostname from reverse DNS
drawn from 5b23a06a74.
2015-06-18 07:46:09 -04:00
Joshua Tauberer
3a28d1b073 showing the Mail-in-a-Box version using git describe was broken since dd6a8d99 2015-06-18 07:45:55 -04:00
Joshua Tauberer
6f2226bfcd move more of start.sh into questions.sh to keep start.sh cleaner and encapsulate all of the variable setting in a single script
Based on 5b23a06a74.
2015-06-18 07:38:18 -04:00
Joshua Tauberer
97cd4c64ad don't expose PHP version in the X-Powered-By header, closes #439, fixes #433 2015-06-18 11:12:03 +00:00
Joshua Tauberer
43d50d0667 Merge pull request #445 from bizonix/patch-1
fix wrong redirect for automatic www subdomain redirects
2015-06-18 07:05:01 -04:00
Joshua Tauberer
6258a7f311 status checks were broken if sshd was not present, fixes #444 2015-06-18 11:01:11 +00:00
Joshua Tauberer
ab36cc8968 whitespace=>tabs 2015-06-18 10:54:51 +00:00
bizonix
33b71c6b3c fix wrong redirect
$ curl -I https://www.site.co.il/static/images/1.png?a=b | grep Location
Location: https://site.co.il?a=b
but should be something like 
Location: https://site.co.il/static/images/1.png?a=b
2015-06-18 01:48:15 +03:00
Joshua Tauberer
34e821c102 Roundcube 1.1.2 2015-06-17 11:00:15 +00:00
Joshua Tauberer
2af557139d default IPv6 AAAA records were missing
This was broken by the ability to have multiple TXT records in 9f1d633ae4.
2015-06-17 06:47:22 -04:00
Joshua Tauberer
9e0dcd8718 security.md: add a section on DNSSEC specifically 2015-06-15 10:24:16 -04:00
Joshua Tauberer
be2b5a62de ownCloud updated to version 8.0.4 2015-06-14 16:04:07 +00:00
Joshua Tauberer
0cbba71c72 merge #429 - Move OwnCloud's config to Storage Root 2015-06-14 15:48:09 +00:00
Joshua Tauberer
d28563fb45 tweak the ownCloud config location migration (no need for third ln) 2015-06-14 15:42:32 +00:00
Norman Stanke
38632f0f90 Move OwnCloud's config to Storage Root 2015-06-12 14:53:02 +02:00
Joshua Tauberer
0754ce01b1 questions.sh needs to apt-get update before it does an apt-get install, see #431, see #438 2015-06-10 09:43:22 -04:00
Joshua Tauberer
1ef455d37d bootstrap.sh needs to apt-get update before it does an apt-get install, fixes #431 2015-06-10 09:33:47 -04:00
Joshua Tauberer
d152603abd changelog entries and mention our forks of postgrey and dovecot in the README 2015-06-10 09:27:29 -04:00
Joshua Tauberer
9e125aec00 Merge pull request #436 from bizonix/patch-1
fix loop redirecting
2015-06-07 16:30:58 -04:00
bizonix
2c90c267bd fix loop redirecting
server is redirecting the request for this address in a way that will never complete
2015-06-07 21:50:41 +03:00
Joshua Tauberer
47de93961e OCSP improvements
* Set ssl_stapling_verify to off per https://sslmate.com/blog/post/ocsp_stapling_in_apache_and_nginx ('on' has no security benefits).
* Set resolver to 127.0.0.1, instead of Google Public DNS, because we might as well use our local nameserver anyway.
* Remove the commented line which per the link above would never be necessary anyway.

OCSP seems to work just fine after these changes.
2015-06-06 23:24:09 +00:00
Joshua Tauberer
1990f32ca4 typo, fixes #435 2015-06-06 13:22:50 +00:00
Joshua Tauberer
807939c0e4 make the +tag address tips clearer 2015-06-06 13:02:23 +00:00
Joshua Tauberer
a1c7bf0883 add munin to readme 2015-06-06 12:55:13 +00:00
Joshua Tauberer
5008cc603e merge - munin system monitoring 2015-06-06 12:52:22 +00:00
Joshua Tauberer
9857db96cd add a link to the /admin/munin page from the control panel nav bar 2015-06-06 12:52:16 +00:00
Joshua Tauberer
e9e6d94e3b the control panel auth hmac message should also include the user's password so that resetting a password in the database forces that user to log in to the control panel again; also use a sha256 hmac 2015-06-06 12:38:19 +00:00
Joshua Tauberer
462a79cf47 fix what counts as a required alias, fixes #434 2015-06-06 12:12:10 +00:00
Joshua Tauberer
f792deeebd when the undocumented custom web settings has a redirect or proxy at the root of a domain, use a minimal nginx config template (same as the new default www redirects) 2015-06-04 12:32:00 +00:00
Joshua Tauberer
95173bb327 provide redirects from www subdomains of zones to their parent domain
* Split the nginx templates again so we have just the part needed to make a domain do a redirect separate from the rest.
* Add server blocks to the nginx config for these domains.
* List these domains in the SSL certificate install admin panel.
* Generate default 'www' records just for domains we provide default redirects for.

Fixes #321.
2015-06-04 12:19:01 +00:00
Joshua Tauberer
1d09e2406b refactor how the nginx config file is assembled
This doesn't change anything. Just preparation for the next commit.
2015-06-04 12:19:01 +00:00
Joshua Tauberer
c9add7a8bf if a user sets a custom A record on PRIMARY_HOSTNAME, which is ignored anyway, don't let that cause PRIMARY_HOSTNAME from being dropped from nginx.conf
Could be related to https://discourse.mailinabox.email/t/nginx-lost-admin-record-after-install-ssl-cert-problem/528.
2015-06-04 12:19:01 +00:00
Joshua Tauberer
e4caed9277 add a note in the setup script about the use of our postgrey fork and dnswl's license terms 2015-06-03 16:28:20 -04:00
Joshua Tauberer
1760eaa601 merge #406 - dovecot-lucene & packaging 2015-06-03 15:51:16 -04:00
Joshua Tauberer
b25ce67fe1 bring the postgrey patches into this repository rather than maintaining them in a separate postgrey fork repository 2015-06-03 15:50:25 -04:00
Joshua Tauberer
b23ba6f75e simplify build/setup of dovecot-lucene package 2015-06-03 15:48:35 -04:00
Morteza Milani
cf904a05cc Reject outgoing mail if FROM does not match Login 2015-06-01 21:26:01 -07:00
Joshua Tauberer
cac6a251cc Merge pull request #411 from nstanke/munin
remove unnecessary source call
2015-05-26 07:12:54 -04:00
Norman Stanke
31d26a7bad remove unnecessary source call 2015-05-26 13:06:50 +02:00
Joshua Tauberer
a9ed9ae936 more work on munin
* install the munin-node package
* don't install munin-plugins-extra (if the user wants it they can add it)
* expose the munin www directory via the management daemon so that it can handle authorization, rather than manintaining a separate password file
2015-05-25 17:03:52 +00:00
Joshua Tauberer
a9892efe38 Merge branch 'master' of https://github.com/nstanke/mailinabox into munin 2015-05-25 16:03:45 +00:00
Brock Tice
f02e0a3ccb Fixed comment that still referenced solr. 2015-05-25 08:50:41 -06:00
Brock Tice
32f5632620 Switch to official PPA 2015-05-23 20:09:50 -04:00
Brock Tice
005cc08b40 Modified makefile to work with updated debian-lucene patch, use official locations.
Fixed conflicting edits to ppa/Makefile due to cherry-pick.
2015-05-23 20:09:06 -04:00
Brock Tice
6a659fe10d Fixed patches for pull request.
Resolved conflict between two patches.
2015-05-23 20:07:33 -04:00
Brock Tice
6941ca2f63 Added apt-get update before installing dovecot-lucene 2015-05-23 20:01:58 -04:00
Brock Tice
e4eba49c1b Added lucene.sh to start script. 2015-05-23 20:01:45 -04:00
Brock Tice
f289439d1d Adapted Jonty's original solr.sh script to instead set up lucene full-text search in dovecot. 2015-05-23 20:01:32 -04:00
Norman
f78cff225b Add Munin
removed testing source

fixed typo & dns

oh cat

more fixes

forgot root

more nginx stuff

nginx munin.conf fix

more fixes

set dns record
2015-01-28 21:42:16 +01:00
39 changed files with 1077 additions and 431 deletions

View File

@@ -1,6 +1,60 @@
CHANGELOG
=========
v0.12 (July 3, 2015)
--------------------
This is a minor update to v0.11, which was a major update. Please read v0.11's advisories.
* v0.12b was posted shortly after the initial posting of v0.12 correcting a minor regression in v0.12 related to creating aliases targetting multiple addresses.
* The administrator@ alias was incorrectly created starting with v0.11. If your first install was v0.11, check that the administrator@ alias forwards mail to you.
* Intrusion detection rules (fail2ban) are relaxed (i.e. less is blocked).
* SSL certificates could not be installed for the new automatic 'www.' redirect domains.
* PHP's default character encoding is changed from no default to UTF8. The effect of this change is unclear but should prevent possible future text conversion issues.
* User-installed SSL private keys in the BEGIN PRIVATE KEY format were not accepted.
* SSL certificates with SAN domains with IDNA encoding were broken in v0.11.
* Some IDNA functionality was using IDNA 2003 rather than IDNA 2008.
v0.11 (June 29, 2015)
---------------------
Advisories:
* Users can no longer spoof arbitrary email addresses in outbound mail. When sending mail, the email address configured in your mail client must match the SMTP login username being used, or the email address must be an alias with the SMTP login username listed as one of the alias's targets.
* This update replaces your DKIM signing key with a stronger key. Because of DNS caching/propagation, mail sent within a few hours after this update could be marked as spam by recipients. If you use External DNS, you will need to update your DNS records.
* The box will now install software from a new Mail-in-a-Box PPA on Launchpad.net, where we are distributing two of our own packages: a patched postgrey and dovecot-lucene.
* v0.11b was posted shortly after the initial posting of v0.11 correcting a missing dependency for the new PPA.
Mail:
* Greylisting will now let some reputable senders pass through immediately.
* Searching mail (via IMAP) will now be much faster using the dovecot lucene full text search plugin.
* Users can no longer spoof arbitrary email addresses in outbound mail (see above).
* Fix for deleting admin@ and postmaster@ addresses.
* Roundcube is updated to version 1.1.2, plugins updated.
* Exchange/ActiveSync autoconfiguration was not working on all devices (e.g. iPhone) because of a case-sensitive URL.
* The DKIM signing key has been increased to 2048 bits, from 1024, replacing the existing key.
Web:
* 'www' subdomains now automatically redirect to their parent domain (but you'll need to install an SSL certificate).
* OCSP no longer uses Google Public DNS.
* The installed PHP version is no longer exposed through HTTP response headers, for better security.
DNS:
* Default IPv6 AAAA records were missing since version 0.09.
Control panel:
* Resetting a user's password now forces them to log in again everywhere.
* Status checks were not working if an ssh server was not installed.
* SSL certificate validation now uses the Python cryptography module in some places where openssl was used.
* There is a new tab to show the installed version of Mail-in-a-Box and to fetch the latest released version.
System:
* The munin system monitoring tool is now installed and accessible at /admin/munin.
* ownCloud updated to version 8.0.4. The ownCloud installation step now is reslient to download problems. The ownCloud configuration file is now stored in STORAGE_ROOT to fix loss of data when moving STORAGE_ROOT to a new machine.
* The setup scripts now run `apt-get update` prior to installing anything to ensure the apt database is in sync with the packages actually available.
v0.10 (June 1, 2015)
--------------------

View File

@@ -32,8 +32,12 @@ The components installed are:
* 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](http://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
* Firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page))
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and system monitoring.
* 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/))
It also includes:
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and detailed system monitoring.
* Our own builds of postgrey and dovecot-lucene distributed via the [Mail-in-a-Box PPA](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa) on Launchpad.
For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md).
@@ -53,13 +57,16 @@ I sign the release tags on git. To verify that a tag is signed by me, you can pe
$ cd mailinabox
# Verify the tag.
$ git verify-tag v0.10
$ git verify-tag v0.12b
gpg: Signature made ..... using RSA key ID C10BDD81
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81
# Check out the tag.
$ git checkout v0.12b
The key ID and fingerprint above should match my [Keybase.io key](https://keybase.io/joshdata) and the fingerprint I publish on [my homepage](https://razor.occams.info/).
The Acknowledgements
@@ -76,3 +83,4 @@ The History
* In August 2013 I began Mail-in-a-Box by combining my own mail server configuration with the setup in ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) and making the setup steps reproducible with bash scripts.
* Mail-in-a-Box was a semifinalist in the 2014 [Knight News Challenge](https://www.newschallenge.org/challenge/2014/submissions/mail-in-a-box), but it was not selected as a winner.
* Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) 2014, [September](https://news.ycombinator.com/item?id=8276171) 2014, and [May](https://news.ycombinator.com/item?id=9624267) 2015.
* FastCompany mentioned Mail-in-a-Box a [roundup of privacy projects](http://www.fastcompany.com/3047645/your-own-private-cloud) on June 26, 2015.

View File

@@ -1,34 +1,19 @@
# Fail2Ban configuration file.
# For Mail-in-a-Box
[DEFAULT]
# Fail2Ban configuration file for Mail-in-a-Box
# bantime in seconds
bantime = 60
[DEFAULT]
# This should ban dumb brute-force attacks, not oblivious users.
findtime = 30
maxretry = 20
#
# JAILS
#
[ssh]
enabled = true
logpath = /var/log/auth.log
maxretry = 20
[ssh-ddos]
enabled = true
maxretry = 20
[sasl]
enabled = true
[dovecot]
enabled = true
filter = dovecotimap

View File

@@ -0,0 +1,79 @@
# Expose this directory as static files.
root $ROOT;
index index.html index.htm;
location = /robots.txt {
log_not_found off;
access_log off;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /mailinabox.mobileconfig {
alias /var/lib/mailinabox/mobileconfig.xml;
}
location = /.well-known/autoconfig/mail/config-v1.1.xml {
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
}
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
rewrite ^/mail/$ /mail/index.php;
location /mail/ {
index index.php;
alias /usr/local/lib/roundcubemail/;
}
location ~ /mail/config/.* {
# A ~-style location is needed to give this precedence over the next block.
return 403;
}
location ~ /mail/.*\.php {
# note: ~ has precendence over a regular location block
include fastcgi_params;
fastcgi_split_path_info ^/mail(/.*)()$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
fastcgi_pass php-fpm;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
# Z-Push (Microsoft Exchange ActiveSync)
location /Microsoft-Server-ActiveSync {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_read_timeout 630;
fastcgi_pass php-fpm;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
location ~* ^/autodiscover/autodiscover.xml$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_pass php-fpm;
}
# ADDITIONAL DIRECTIVES HERE
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
# This block is placed at the end. Nginx's precedence rules means this block
# takes precedence over all non-regex matches and only regex matches that
# come after it (i.e. none of those, since this is the last one.) That means
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
# handled locations for ownCloud (which serves user-uploaded files that might
# have this pattern, see #414) or some of the other services.
location ~ /\.(ht|svn|git|hg|bzr) {
log_not_found off;
access_log off;
deny all;
}

View File

@@ -2,6 +2,7 @@
# Proxy /admin to our Python based control panel daemon. It is
# listening on IPv4 only so use an IP address and not 'localhost'.
rewrite ^/admin$ /admin/;
rewrite ^/admin/munin$ /admin/munin/ redirect;
location /admin/ {
proxy_pass http://127.0.0.1:10222/;
proxy_set_header X-Forwarded-For $remote_addr;
@@ -29,8 +30,8 @@
fastcgi_param SCRIPT_FILENAME /usr/local/lib/owncloud/$2;
fastcgi_param SCRIPT_NAME $1$2;
fastcgi_param PATH_INFO $3;
# TODO: see the dispreferred "method 2" for xaccel at https://doc.owncloud.org/server/8.1/admin_manual/configuration_files/serving_static_files_configuration.html
fastcgi_param MOD_X_ACCEL_REDIRECT_ENABLED on;
fastcgi_param MOD_X_ACCEL_REDIRECT_PREFIX /owncloud-xaccel;
fastcgi_read_timeout 630;
fastcgi_pass php-fpm;
error_page 403 /cloud/core/templates/403.php;
@@ -38,15 +39,13 @@
client_max_body_size 1G;
fastcgi_buffers 64 4K;
}
location ^~ $STORAGE_ROOT/owncloud {
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. It works a little weird.
# The full path on disk of the file is passed as the URL path. ownCloud 8 totally
# busted the sane way this worked in ownCloud 7. There's a pending change using
# a new parameter to make this make more sense.
# We need to only allow 'internal' redirects within nginx so that the filesystem
location ^~ /owncloud-xaccel/ {
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. ownCloud sends the full file
# path on disk as a subdirectory under this virtual path.
# We must only allow 'internal' redirects within nginx so that the filesystem
# is not exposed to the world.
internal;
alias $STORAGE_ROOT/owncloud;
alias /;
}
location ~ ^/((caldav|carddav|webdav).*)$ {
# Z-Push doesn't like getting a redirect, and a plain rewrite didn't work either.
@@ -58,3 +57,4 @@
rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect;
rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect;
# ADDITIONAL DIRECTIVES HERE

View File

@@ -70,6 +70,5 @@ ssl_dhparam STORAGE_ROOT/ssl/dh2048.pem;
# nginx will use them to talk to the CA.
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=86400;
resolver 127.0.0.1 valid=86400;
resolver_timeout 10;
#ssl_trusted_certificate /path/to/all-certs-in-chain.crt;

View File

@@ -33,84 +33,5 @@ server {
ssl_certificate_key $SSL_KEY;
include /etc/nginx/nginx-ssl.conf;
# Expose this directory as static files.
root $ROOT;
index index.html index.htm;
location = /robots.txt {
log_not_found off;
access_log off;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /mailinabox.mobileconfig {
alias /var/lib/mailinabox/mobileconfig.xml;
}
location = /.well-known/autoconfig/mail/config-v1.1.xml {
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
}
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
rewrite ^/mail/$ /mail/index.php;
location /mail/ {
index index.php;
alias /usr/local/lib/roundcubemail/;
}
location ~ /mail/config/.* {
# A ~-style location is needed to give this precedence over the next block.
return 403;
}
location ~ /mail/.*\.php {
# note: ~ has precendence over a regular location block
include fastcgi_params;
fastcgi_split_path_info ^/mail(/.*)()$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
fastcgi_pass php-fpm;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
# Z-Push (Microsoft Exchange ActiveSync)
location /Microsoft-Server-ActiveSync {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_read_timeout 630;
fastcgi_pass php-fpm;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
location /autodiscover/autodiscover.xml {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_pass php-fpm;
}
# ADDITIONAL DIRECTIVES HERE
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
# This block is placed at the end. Nginx's precedence rules means this block
# takes precedence over all non-regex matches and only regex matches that
# come after it (i.e. none of those, since this is the last one.) That means
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
# handled locations for ownCloud (which serves user-uploaded files that might
# have this pattern, see #414) or some of the other services.
location ~ /\.(ht|svn|git|hg|bzr) {
log_not_found off;
access_log off;
deny all;
}
}

View File

@@ -88,8 +88,9 @@ class KeyAuthService:
if email == "" or pw == "":
raise ValueError("Enter an email address and password.")
# The password might be a user-specific API key.
if hmac.compare_digest(self.create_user_key(email), pw):
# The password might be a user-specific API key. create_user_key raises
# a ValueError if the user does not exist.
if hmac.compare_digest(self.create_user_key(email, env), pw):
# OK.
pass
else:
@@ -111,18 +112,26 @@ class KeyAuthService:
# Login failed.
raise ValueError("Invalid password.")
# Get privileges for authorization. This call should never fail on a valid user,
# but if the caller passed a user-specific API key then the user may no longer
# exist --- in that case, get_mail_user_privileges will return a tuple of an
# error message and an HTTP status code.
# Get privileges for authorization. This call should never fail because by this
# point we know the email address is a valid user. But on error the call will
# return a tuple of an error message and an HTTP status code.
privs = get_mail_user_privileges(email, env)
if isinstance(privs, tuple): raise ValueError(privs[0])
# Return a list of privileges.
return privs
def create_user_key(self, email):
return hmac.new(self.key.encode('ascii'), b"AUTH:" + email.encode("utf8"), digestmod="sha1").hexdigest()
def create_user_key(self, email, env):
# Store an HMAC with the client. The hashed message of the HMAC will be the user's
# email address & hashed password and the key will be the master API key. The user of
# course has their own email address and password. We assume they do not have the master
# API key (unless they are trusted anyway). The HMAC proves that they authenticated
# with us in some other way to get the HMAC. Including the password means that when
# a user's password is reset, the HMAC changes and they will correctly need to log
# in to the control panel again. This method raises a ValueError if the user does
# not exist, due to get_mail_password.
msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8")
return hmac.new(self.key.encode('ascii'), msg, digestmod="sha256").hexdigest()
def _generate_key(self):
raw_key = os.urandom(32)

View File

@@ -4,7 +4,7 @@ import os, os.path, re, json
from functools import wraps
from flask import Flask, request, render_template, abort, Response
from flask import Flask, request, render_template, abort, Response, send_from_directory
import auth, utils
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
@@ -118,7 +118,7 @@ def me():
# Is authorized as admin? Return an API key for future use.
if "admin" in privs:
resp["api_key"] = auth_service.create_user_key(email)
resp["api_key"] = auth_service.create_user_key(email, env)
# Return.
return json_response(resp)
@@ -340,6 +340,24 @@ def web_update():
# System
@app.route('/system/version', methods=["GET"])
@authorized_personnel_only
def system_version():
from status_checks import what_version_is_this
try:
return what_version_is_this(env)
except Exception as e:
return (str(e), 500)
@app.route('/system/latest-upstream-version', methods=["POST"])
@authorized_personnel_only
def system_latest_upstream_version():
from status_checks import get_latest_miab_version
try:
return get_latest_miab_version()
except Exception as e:
return (str(e), 500)
@app.route('/system/status', methods=["POST"])
@authorized_personnel_only
def system_status():
@@ -384,6 +402,17 @@ def backup_status():
from backup import backup_status
return json_response(backup_status(env))
# MUNIN
@app.route('/munin/')
@app.route('/munin/<path:filename>')
@authorized_personnel_only
def munin(filename=""):
# Checks administrative access (@authorized_personnel_only) and then just proxies
# the request to static files.
if filename == "": filename = "index.html"
return send_from_directory("/var/cache/munin/www", filename)
# APP
if __name__ == '__main__':

View File

@@ -57,13 +57,15 @@ def do_dns_update(env, force=False):
# Custom records to add to zones.
additional_records = list(get_custom_dns_config(env))
from web_update import get_default_www_redirects
www_redirect_domains = get_default_www_redirects(env)
# Write zone files.
os.makedirs('/etc/nsd/zones', exist_ok=True)
updated_domains = []
for i, (domain, zonefile) in enumerate(zonefiles):
# Build the records to put in the zone.
records = build_zone(domain, domains, additional_records, env)
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
# See if the zone has changed, and if so update the serial number
# and write the zone file.
@@ -126,7 +128,7 @@ def do_dns_update(env, force=False):
########################################################################
def build_zone(domain, all_domains, additional_records, env, is_zone=True):
def build_zone(domain, all_domains, additional_records, www_redirect_domains, env, is_zone=True):
records = []
# For top-level zones, define the authoritative name servers.
@@ -177,7 +179,7 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
subdomains = [d for d in all_domains if d.endswith("." + domain)]
for subdomain in subdomains:
subdomain_qname = subdomain[0:-len("." + domain)]
subzone = build_zone(subdomain, [], additional_records, env, is_zone=False)
subzone = build_zone(subdomain, [], additional_records, www_redirect_domains, env, is_zone=False)
for child_qname, child_rtype, child_value, child_explanation in subzone:
if child_qname == None:
child_qname = subdomain_qname
@@ -211,14 +213,20 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
records.append((qname, rtype, value, "(Set by user.)"))
# Add defaults if not overridden by the user's custom settings (and not otherwise configured).
# Any "CNAME" record on the qname overrides A and AAAA.
has_rec_base = records
# Any CNAME or A record on the qname overrides A and AAAA. But when we set the default A record,
# we should not cause the default AAAA record to be skipped because it thinks a custom A record
# was set. So set has_rec_base to a clone of the current set of DNS settings, and don't update
# during this process.
has_rec_base = list(records)
defaults = [
(None, "A", env["PUBLIC_IP"], "Required. May have a different value. Sets the IP address that %s resolves to for web hosting and other services besides mail. The A record must be present but its value does not affect mail delivery." % domain),
("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to, e.g. for web hosting." % domain),
(None, "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that %s resolves to, e.g. for web hosting. (It is not necessary for receiving mail on this domain.)" % domain),
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to, e.g. for web hosting." % domain),
]
if "www." + domain in www_redirect_domains:
defaults += [
("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to so that the box can provide a redirect to the parent domain." % domain),
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to so that the box can provide a redirect to the parent domain." % domain),
]
for qname, rtype, value, explanation in defaults:
if value is None or value.strip() == "": continue # skip IPV6 if not set
if not is_zone and qname == "www": continue # don't create any default 'www' subdomains on what are themselves subdomains
@@ -229,6 +237,9 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
if not has_rec(qname, rtype) and not has_rec(qname, "CNAME") and not has_rec(qname, "A"):
records.append((qname, rtype, value, explanation))
# Don't pin the list of records that has_rec checks against anymore.
has_rec_base = records
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
# the domain, and no one else.
# Skip if the user has set a custom SPF record.
@@ -239,8 +250,8 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
# Skip if the user has set a DKIM record already.
opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt')
with open(opendkim_record_file) as orf:
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( "([^"]+)"\s+"([^"]+)"\s*\)', orf.read(), re.S)
val = m.group(2) + m.group(3)
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( ((?:"[^"]+"\s+)+)\)', orf.read(), re.S)
val = "".join(re.findall(r'"([^"]+)"', m.group(2)))
if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "):
records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
@@ -362,9 +373,16 @@ $TTL 1800 ; default time to live
zone += subdomain
zone += "\tIN\t" + querytype + "\t"
if querytype == "TXT":
value = value.replace('\\', '\\\\') # escape backslashes
value = value.replace('"', '\\"') # escape quotes
value = '"' + value + '"' # wrap in quotes
# Divide into 255-byte max substrings.
v2 = ""
while len(value) > 0:
s = value[0:255]
value = value[255:]
s = s.replace('\\', '\\\\') # escape backslashes
s = s.replace('"', '\\"') # escape quotes
s = '"' + s + '"' # wrap in quotes
v2 += s + " "
value = v2
zone += value + "\n"
# DNSSEC requires re-signing a zone periodically. That requires
@@ -847,8 +865,10 @@ def build_recommended_dns(env):
domains = get_dns_domains(env)
zonefiles = get_dns_zones(env)
additional_records = list(get_custom_dns_config(env))
from web_update import get_default_www_redirects
www_redirect_domains = get_default_www_redirects(env)
for domain, zonefile in zonefiles:
records = build_zone(domain, domains, additional_records, env)
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
# remove records that we don't dislay
records = [r for r in records if r[3] is not False]

View File

@@ -3,6 +3,7 @@
import subprocess, shutil, os, sqlite3, re
import utils
from email_validator import validate_email as validate_email_, EmailNotValidError
import idna
def validate_email(email, mode=None):
# Checks that an email address is syntactically valid. Returns True/False.
@@ -52,11 +53,13 @@ def sanitize_idn_email_address(email):
# to the underlying protocols.
try:
localpart, domainpart = email.split("@")
domainpart = domainpart.encode("idna").decode('ascii')
domainpart = idna.encode(domainpart).decode('ascii')
return localpart + "@" + domainpart
except:
# Domain part is not IDNA-valid, so leave unchanged. If there
# are non-ASCII characters it will be filtered out by
except (ValueError, idna.IDNAError):
# ValueError: String does not have a single @-sign, so it is not
# a valid email address. IDNAError: Domain part is not IDNA-valid.
# Validation is not this function's job, so return value unchanged.
# If there are non-ASCII characters it will be filtered out by
# validate_email.
return email
@@ -65,10 +68,11 @@ def prettify_idn_email_address(email):
# names in IDNA in the database, but we want to show Unicode to the user.
try:
localpart, domainpart = email.split("@")
domainpart = domainpart.encode("ascii").decode('idna')
domainpart = idna.decode(domainpart.encode("ascii"))
return localpart + "@" + domainpart
except:
# Failed to decode IDNA. Should never happen.
except (ValueError, UnicodeError, idna.IDNAError):
# Failed to decode IDNA, or the email address does not have a
# single @-sign. Should never happen.
return email
def is_dcv_address(email):
@@ -211,7 +215,7 @@ def get_mail_aliases_ex(env):
for source, destination in get_mail_aliases(env):
# get alias info
domain = get_domain(source)
required = ((source in required_aliases) or (source == get_system_administrator(env)))
required = (source in required_aliases)
# add to list
if not domain in domains:
@@ -238,7 +242,7 @@ def get_domain(emailaddr, as_unicode=True):
# Gets the domain part of an email address. Turns IDNA
# back to Unicode for display.
ret = emailaddr.split('@', 1)[1]
if as_unicode: ret = ret.encode('ascii').decode('idna')
if as_unicode: ret = idna.decode(ret.encode('ascii'))
return ret
def get_mail_domains(env, filter_aliases=lambda alias : True):
@@ -493,15 +497,17 @@ def get_required_aliases(env):
# These are the aliases that must exist.
aliases = set()
# The system administrator alias is required.
aliases.add(get_system_administrator(env))
# The hostmaster alias is exposed in the DNS SOA for each zone.
aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME'])
# Get a list of domains we serve mail for, except ones for which the only
# email on that domain is a postmaster/admin alias to the administrator
# or a wildcard alias (since it will forward postmaster/admin).
# email on that domain are the required aliases or a catch-all/domain-forwarder.
real_mail_domains = get_mail_domains(env,
filter_aliases = lambda alias :
((not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")) or alias[1] != get_system_administrator(env))
not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")
and not alias[0].startswith("@")
)
@@ -538,12 +544,13 @@ def kick(env, mail_result=None):
for s, t in existing_aliases:
if s == source:
return
# Doesn't exist.
administrator = get_system_administrator(env)
if source == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually
add_mail_alias(source, administrator, env, do_kick=False)
results.append("added alias %s (=> %s)\n" % (source, administrator))
for alias in required_aliases:
ensure_admin_alias_exists(alias)

View File

@@ -10,9 +10,10 @@ import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
import dns.reversename, dns.resolver
import dateutil.parser, dateutil.tz
import idna
from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns
from web_update import get_web_domains, get_domain_ssl_files
from web_update import get_web_domains, get_default_www_redirects, get_domain_ssl_files
from mailconfig import get_mail_domains, get_mail_aliases
from utils import shell, sort_domains, load_env_vars_from_file
@@ -41,15 +42,22 @@ def run_checks(rounded_values, env, output, pool):
run_domain_checks(rounded_values, env, output, pool)
def get_ssh_port():
# Returns ssh port
output = shell('check_output', ['sshd', '-T'])
returnNext = False
# Returns ssh port
try:
output = shell('check_output', ['sshd', '-T'])
except FileNotFoundError:
# sshd is not installed. That's ok.
return None
for e in output.split():
if returnNext:
return int(e)
if e == "port":
returnNext = True
returnNext = False
for e in output.split():
if returnNext:
return int(e)
if e == "port":
returnNext = True
# Did not find port!
return None
def run_services_checks(env, output, pool):
# Check that system services are running.
@@ -81,6 +89,7 @@ def run_services_checks(env, output, pool):
fatal = False
ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(services)), chunksize=1)
for i, running, fatal2, output2 in sorted(ret):
if output2 is None: continue # skip check (e.g. no port was set, e.g. no sshd)
all_running = all_running and running
fatal = fatal or fatal2
output2.playback(output)
@@ -91,6 +100,10 @@ def run_services_checks(env, output, pool):
return not fatal
def check_service(i, service, env):
if not service["port"]:
# Skip check (no port, e.g. no sshd).
return (i, None, None, None)
import socket
output = BufferedOutput()
running = False
@@ -227,7 +240,7 @@ def run_domain_checks(rounded_time, env, output, pool):
dns_domains = set(dns_zonefiles)
# Get the list of domains we serve HTTPS for.
web_domains = set(get_web_domains(env))
web_domains = set(get_web_domains(env) + get_default_www_redirects(env))
domains_to_check = mail_domains | dns_domains | web_domains
@@ -247,7 +260,7 @@ def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zone
output = BufferedOutput()
# The domain is IDNA-encoded, but for display use Unicode.
output.add_heading(domain.encode('ascii').decode('idna'))
output.add_heading(idna.decode(domain.encode('ascii')))
if domain == env["PRIMARY_HOSTNAME"]:
check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles)
@@ -593,103 +606,115 @@ def check_ssl_cert(domain, rounded_time, env, output):
output.print_line(cert_status_details)
output.print_line("")
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False):
# Use openssl verify to check the status of a certificate.
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
# Check that the ssl_certificate & ssl_private_key files are good
# for the provided domain.
# First check that the certificate is for the right domain. The domain
# must be found in the Subject Common Name (CN) or be one of the
# Subject Alternative Names. A wildcard might also appear as the CN
# or in the SAN list, so check for that tool.
retcode, cert_dump = shell('check_output', [
"openssl", "x509",
"-in", ssl_certificate,
"-noout", "-text", "-nameopt", "rfc2253",
], trap=True)
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.x509 import Certificate, DNSName, ExtensionNotFound, OID_COMMON_NAME, OID_SUBJECT_ALTERNATIVE_NAME
import idna
# If the certificate is catastrophically bad, catch that now and report it.
# More information was probably written to stderr (which we aren't capturing),
# but it is probably not helpful to the user anyway.
if retcode != 0:
return ("The SSL certificate appears to be corrupted or not a PEM-formatted SSL certificate file. (%s)" % ssl_certificate, None)
# The ssl_certificate file may contain a chain of certificates. We'll
# need to split that up before we can pass anything to openssl or
# parse them in Python. Parse it with the cryptography library.
try:
ssl_cert_chain = load_cert_chain(ssl_certificate)
cert = load_pem(ssl_cert_chain[0])
if not isinstance(cert, Certificate): raise ValueError("This is not a certificate file.")
except ValueError as e:
return ("There is a problem with the certificate file: %s" % str(e), None)
cert_dump = cert_dump.split("\n")
certificate_names = set()
cert_expiration_date = None
while len(cert_dump) > 0:
line = cert_dump.pop(0)
# First check that the domain name is one of the names allowed by
# the certificate.
if domain is not None:
# The domain may be found in the Subject Common Name (CN). This comes back as an IDNA (ASCII)
# string, which is the format we store domains in - so good.
certificate_names = set()
try:
certificate_names.add(
cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value
)
except IndexError:
# No common name? Certificate is probably generated incorrectly.
# But we'll let it error-out when it doesn't find the domain.
pass
# Grab from the Subject Common Name. We include the indentation
# at the start of the line in case maybe the cert includes the
# common name of some other referenced entity (which would be
# indented, I hope).
m = re.match(" Subject: CN=([^,]+)", line)
if m:
certificate_names.add(m.group(1))
# ... or be one of the Subject Alternative Names. The cryptography library handily IDNA-decodes
# the names for us. We must encode back to ASCII, but wildcard certificates can't pass through
# IDNA encoding/decoding so we must special-case. See https://github.com/pyca/cryptography/pull/2071.
def idna_decode_dns_name(dns_name):
if dns_name.startswith("*."):
return "*." + idna.encode(dns_name[2:]).decode('ascii')
else:
return idna.encode(dns_name).decode('ascii')
# Grab from the Subject Alternative Name, which is a comma-delim
# list of names, like DNS:mydomain.com, DNS:otherdomain.com.
m = re.match(" X509v3 Subject Alternative Name:", line)
if m:
names = re.split(",\s*", cert_dump.pop(0).strip())
for n in names:
m = re.match("DNS:(.*)", n)
if m:
certificate_names.add(m.group(1))
try:
sans = cert.extensions.get_extension_for_oid(OID_SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(DNSName)
for san in sans:
certificate_names.add(idna_decode_dns_name(san))
except ExtensionNotFound:
pass
# Grab the expiration date for testing later.
m = re.match(" Not After : (.*)", line)
if m:
cert_expiration_date = dateutil.parser.parse(m.group(1))
# Check that the domain appears among the acceptable names, or a wildcard
# form of the domain name (which is a stricter check than the specs but
# should work in normal cases).
wildcard_domain = re.sub("^[^\.]+", "*", domain)
if domain not in certificate_names and wildcard_domain not in certificate_names:
return ("The certificate is for the wrong domain name. It is for %s."
% ", ".join(sorted(certificate_names)), None)
wildcard_domain = re.sub("^[^\.]+", "*", domain)
if domain is not None and domain not in certificate_names and wildcard_domain not in certificate_names:
return ("The certificate is for the wrong domain name. It is for %s."
% ", ".join(sorted(certificate_names)), None)
# Second, check that the certificate matches the private key. Get the modulus of the
# private key and of the public key in the certificate. They should match. The output
# of each command looks like "Modulus=XXXXX".
# Second, check that the certificate matches the private key.
if ssl_private_key is not None:
private_key_modulus = shell('check_output', [
"openssl", "rsa",
"-inform", "PEM",
"-noout", "-modulus",
"-in", ssl_private_key])
cert_key_modulus = shell('check_output', [
"openssl", "x509",
"-in", ssl_certificate,
"-noout", "-modulus"])
if private_key_modulus != cert_key_modulus:
return ("The certificate installed at %s does not correspond to the private key at %s." % (ssl_certificate, ssl_private_key), None)
try:
priv_key = load_pem(open(ssl_private_key, 'rb').read())
except ValueError as e:
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)
if not isinstance(priv_key, RSAPrivateKey):
return ("The private key file %s is not a private key file." % ssl_private_key, None)
if priv_key.public_key().public_numbers() != cert.public_key().public_numbers():
return ("The certificate does not correspond to the private key at %s." % ssl_private_key, None)
# We could also use the openssl command line tool to get the modulus
# listed in each file. The output of each command below looks like "Modulus=XXXXX".
# $ openssl rsa -inform PEM -noout -modulus -in ssl_private_key
# $ openssl x509 -in ssl_certificate -noout -modulus
# Third, check if the certificate is self-signed. Return a special flag string.
if cert.issuer == cert.subject:
return ("SELF-SIGNED", None)
# When selecting which certificate to use for non-primary domains, we check if the primary
# certificate or a www-parent-domain certificate is good for the domain. There's no need
# to run extra checks beyond this point.
if just_check_domain:
return ("OK", None)
# Check that the certificate hasn't expired. The datetimes returned by the
# certificate are 'naive' and in UTC. We need to get the current time in UTC.
now = datetime.datetime.utcnow()
if not(cert.not_valid_before <= now <= cert.not_valid_after):
return ("The certificate has expired or is not yet valid. It is valid from %s to %s." % (cert.not_valid_before, cert.not_valid_after), None)
# Next validate that the certificate is valid. This checks whether the certificate
# is self-signed, that the chain of trust makes sense, that it is signed by a CA
# that Ubuntu has installed on this machine's list of CAs, and I think that it hasn't
# expired.
# In order to verify with openssl, we need to split out any
# intermediary certificates in the chain (if any) from our
# certificate (at the top). They need to be passed separately.
cert = open(ssl_certificate).read()
m = re.match(r'(-*BEGIN CERTIFICATE-*.*?-*END CERTIFICATE-*)(.*)', cert, re.S)
if m == None:
return ("The certificate file is an invalid PEM certificate.", None)
mycert, chaincerts = m.groups()
# The certificate chain has to be passed separately and is given via STDIN.
# This command returns a non-zero exit status in most cases, so trap errors.
retcode, verifyoutput = shell('check_output', [
"openssl",
"verify", "-verbose",
"-purpose", "sslserver", "-policy_check",]
+ ([] if chaincerts.strip() == "" else ["-untrusted", "/dev/stdin"])
+ ([] if len(ssl_cert_chain) == 1 else ["-untrusted", "/dev/stdin"])
+ [ssl_certificate],
input=chaincerts.encode('ascii'),
input=b"\n\n".join(ssl_cert_chain[1:]),
trap=True)
if "self signed" in verifyoutput:
# Certificate is self-signed.
# Certificate is self-signed. Probably we detected this above.
return ("SELF-SIGNED", None)
elif retcode != 0:
@@ -704,7 +729,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# good.
# But is it expiring soon?
now = datetime.datetime.now(dateutil.tz.tzlocal())
cert_expiration_date = cert.not_valid_after
ndays = (cert_expiration_date-now).days
if not rounded_time or ndays < 7:
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
@@ -721,6 +746,33 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# Return the special OK code.
return ("OK", expiry_info)
def load_cert_chain(pemfile):
# A certificate .pem file may contain a chain of certificates.
# Load the file and split them apart.
re_pem = rb"(-+BEGIN (?:.+)-+[\r\n](?:[A-Za-z0-9+/=]{1,64}[\r\n])+-+END (?:.+)-+[\r\n])"
with open(pemfile, "rb") as f:
pem = f.read() + b"\n" # ensure trailing newline
pemblocks = re.findall(re_pem, pem)
if len(pemblocks) == 0:
raise ValueError("File does not contain valid PEM data.")
return pemblocks
def load_pem(pem):
# Parse a "---BEGIN .... END---" PEM string and return a Python object for it
# using classes from the cryptography package.
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
pem_type = re.match(b"-+BEGIN (.*?)-+\n", pem)
if pem_type is None:
raise ValueError("File is not a valid PEM-formatted file.")
pem_type = pem_type.group(1)
if pem_type in (b"RSA PRIVATE KEY", b"PRIVATE KEY"):
return serialization.load_pem_private_key(pem, password=None, backend=default_backend())
if pem_type == b"CERTIFICATE":
return load_pem_x509_certificate(pem, default_backend())
raise ValueError("Unsupported PEM object type: " + pem_type.decode("ascii", "replace"))
_apt_updates = None
def list_apt_updates(apt_update=True):
# See if we have this information cached recently.
@@ -755,6 +807,20 @@ def list_apt_updates(apt_update=True):
return pkgs
def what_version_is_this(env):
# This function runs `git describe` on the Mail-in-a-Box installation directory.
# Git may not be installed and Mail-in-a-Box may not have been cloned from github,
# so this function may raise all sorts of exceptions.
miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
tag = shell("check_output", ["/usr/bin/git", "describe"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip()
return tag
def get_latest_miab_version():
# This pings https://mailinabox.email/bootstrap.sh and extracts the tag named in
# the script to determine the current product version.
import urllib.request
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/bootstrap.sh?ping=1").read()).group(1).decode("utf8")
def run_and_output_changes(env, pool, send_via_email):
import json
from difflib import SequenceMatcher
@@ -935,3 +1001,6 @@ if __name__ == "__main__":
if cert_status != "OK":
sys.exit(1)
sys.exit(0)
elif sys.argv[1] == "--version":
print(what_version_is_this(env))

View File

@@ -98,9 +98,10 @@
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
<li class="divider"></li>
<li class="dropdown-header">Advanced Options</li>
<li class="dropdown-header">Advanced Pages</li>
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
<li><a href="/admin/munin">Munin Monitoring</a></li>
</ul>
</li>
<li class="dropdown">
@@ -113,6 +114,7 @@
</li>
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
<li><a href="#version" onclick="return show_panel(this);">Version</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
@@ -166,6 +168,10 @@
{% include "ssl.html" %}
</div>
<div id="panel_version" class="admin_panel">
{% include "version.html" %}
</div>
<hr>
<footer>

View File

@@ -60,7 +60,7 @@
<p>Your box using a technique called greylisting to cut down on spam. Greylisting works by delaying mail from people you haven&rsquo;t received mail from before for up to about 10 minutes. The vast majority of spam gets tricked by this. If you are waiting for an email from someone new, such as if you are registering on a new website and are waiting for an email confirmation, please give it up to 10-15 minutes to arrive.</p>
<h4>+tag addresses</h4>
<p>Every incoming email address also receives mail for <code>+tag</code> addresses. If your email address is <code>you@yourdomain.com</code>, you can also accept mail at <code>you+anythinghere@yourdomain.com</code>. Use this as a fast way to create aliases or to segment incoming mail for your own filtering rules.</p>
<p>Every incoming email address also receives mail for <code>+tag</code> addresses. If your email address is <code>you@yourdomain.com</code>, you&rsquo;ll also automatically get mail sent to <code>you+anythinghere@yourdomain.com</code>. Use this as a fast way to segment incoming mail for your own filtering rules without having to create aliases in this control panel.</p>
<h4>Use only this box to send as you</h4>
<p>Your box sets strict email sending policies for your domain names to make it harder for spam and other fraudulent mail to claim to be you. Only this machine is authorized to send email on behalf of your domain names. If you use any other service to send email as you, it will likely get spam filtered by recipients.</p>

View File

@@ -164,9 +164,14 @@ function do_add_user() {
function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
var yourpw = "";
if (api_credentials != null && email == api_credentials[0])
yourpw = "<p class='text-danger'>If you change your own password, you will be logged out of this control panel and will need to log in again.</p>";
show_modal_confirm(
"Archive User",
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small></p>"),
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small>" + yourpw + "</p>"),
"Set Password",
function() {
api(

View File

@@ -0,0 +1,36 @@
<style>
</style>
<h2>Mail-in-a-Box Version</h2>
<p>You are running Mail-in-a-Box version <span id="miab-version" style="font-weight: bold">...</span>.</p>
<p>The latest version of Mail-in-a-Box is <button id="miab-get-latest-upstream" onclick="check_latest_version()">Check</button>.</p>
<p>To find the latest version and for upgrade instructions, see <a href="https://mailinabox.email/">https://mailinabox.email/</a>, <a href="https://github.com/mail-in-a-box/mailinabox/blob/master/CHANGELOG.md">release notes</a>, and <a href="https://mailinabox.email/maintenance.html#updating-mail-in-a-box">upgrade instructions</a>.</p>
<script>
function show_version() {
$('#miab-version').text('loading...');
api(
"/system/version",
"GET",
{
},
function(version) {
$('#miab-version').text(version);
});
}
function check_latest_version() {
$('#miab-get-latest-upstream').text('loading...');
api(
"/system/latest-upstream-version",
"POST",
{
},
function(version) {
$('#miab-get-latest-upstream').text(version);
});
}
</script>

View File

@@ -5,7 +5,7 @@
import os, os.path, shutil, re, tempfile, rtyaml
from mailconfig import get_mail_domains
from dns_update import get_custom_dns_config, do_dns_update
from dns_update import get_custom_dns_config, do_dns_update, get_dns_zones
from utils import shell, safe_domain_name, sort_domains
def get_web_domains(env):
@@ -19,31 +19,71 @@ def get_web_domains(env):
# Also serve web for all mail domains so that we might at least
# provide auto-discover of email settings, and also a static website
# if the user wants to make one. These will require an SSL cert.
domains |= get_mail_domains(env)
# ...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.
dns = get_custom_dns_config(env)
for domain, rtype, value in dns:
if domain not in domains: continue
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
domains.remove(domain)
domains |= (get_mail_domains(env) - get_domains_with_a_records(env))
# Sort the list. Put PRIMARY_HOSTNAME first so it becomes the
# default server (nginx's default_server).
# Sort the list so the nginx conf gets written in a stable order.
domains = sort_domains(domains, env)
return domains
def get_domains_with_a_records(env):
domains = set()
dns = get_custom_dns_config(env)
for domain, rtype, value in dns:
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
domains.add(domain)
return domains
def get_web_domains_with_root_overrides(env):
# Load custom settings so we can tell what domains have a redirect or proxy set up on '/',
# which means static hosting is not happening.
root_overrides = { }
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
for domain, settings in custom_settings.items():
for type, value in [('redirect', settings.get('redirects', {}).get('/')),
('proxy', settings.get('proxies', {}).get('/'))]:
if value:
root_overrides[domain] = (type, value)
return root_overrides
def get_default_www_redirects(env):
# Returns a list of www subdomains that we want to provide default redirects
# for, i.e. any www's that aren't domains the user has actually configured
# to serve for real. Which would be unusual.
web_domains = set(get_web_domains(env))
www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env))
return sort_domains(www_domains - web_domains - get_domains_with_a_records(env), env)
def do_web_update(env):
# Build an nginx configuration file.
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
# Add configuration for each web domain.
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
# Load the templates.
template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read()
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], env)
# Add configuration all other web domains.
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
for domain in get_web_domains(env):
nginx_conf += make_domain_config(domain, template1, template2, env)
if domain == env['PRIMARY_HOSTNAME']: continue # handled above
if domain not in has_root_proxy_or_redirect:
nginx_conf += make_domain_config(domain, [template0, template1], env)
else:
nginx_conf += make_domain_config(domain, [template0], env)
# Add default www redirects.
for domain in get_default_www_redirects(env):
nginx_conf += make_domain_config(domain, [template0, template3], env)
# Did the file change? If not, don't bother writing & restarting nginx.
nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
@@ -64,11 +104,10 @@ def do_web_update(env):
return "web updated\n"
def make_domain_config(domain, template, template_for_primaryhost, env):
# How will we configure this domain.
def make_domain_config(domain, templates, env):
# GET SOME VARIABLES
# Where will its root directory be for static files?
root = get_web_root(domain, env)
# What private key and SSL certificate will we use for this domain?
@@ -78,18 +117,9 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
# available. Make a self-signed one now if one doesn't exist.
ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env)
# Put pieces together.
nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template)
nginx_conf = nginx_conf_parts[0] + "\n"
if domain == env['PRIMARY_HOSTNAME']:
nginx_conf += template_for_primaryhost + "\n"
# ADDITIONAL DIRECTIVES.
# Replace substitution strings in the template & return.
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
nginx_conf = nginx_conf.replace("$ROOT", root)
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
nginx_conf_extra = ""
# Because the certificate may change, we should recognize this so we
# can trigger an nginx update.
@@ -102,7 +132,7 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
finally:
f.close()
return sha1.hexdigest()
nginx_conf += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
# Add in any user customizations in YAML format.
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
@@ -111,17 +141,29 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
if domain in yaml:
yaml = yaml[domain]
for path, url in yaml.get("proxies", {}).items():
nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
for path, url in yaml.get("redirects", {}).items():
nginx_conf += "\trewrite %s %s permanent;\n" % (path, url)
nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url)
# Add in any user customizations in the includes/ folder.
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
if os.path.exists(nginx_conf_custom_include):
nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include)
nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
# PUT IT ALL TOGETHER
# Ending.
nginx_conf += nginx_conf_parts[1]
# Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder
# of the previous template.
nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n"
for t in templates + [nginx_conf_extra]:
nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf)
# Replace substitution strings in the template & return.
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
nginx_conf = nginx_conf.replace("$ROOT", root)
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
return nginx_conf
@@ -159,14 +201,14 @@ def get_domain_ssl_files(domain, env, allow_shared_cert=True):
# the user has uploaded a different private key for this domain.
if not ssl_key_is_alt and allow_shared_cert:
from status_checks import check_certificate
if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK":
if check_certificate(domain, ssl_certificate_primary, None, just_check_domain=True)[0] == "OK":
ssl_certificate = ssl_certificate_primary
ssl_via = "Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']
# For a 'www.' domain, see if we can reuse the cert of the parent.
elif domain.startswith('www.'):
ssl_certificate_parent = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain[4:]))
if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None)[0] == "OK":
if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None, just_check_domain=True)[0] == "OK":
ssl_certificate = ssl_certificate_parent
ssl_via = "Using multi/wildcard certificate of %s." % domain[4:]
@@ -212,7 +254,7 @@ def create_csr(domain, ssl_key, env):
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
def install_cert(domain, ssl_cert, ssl_chain, env):
if domain not in get_web_domains(env):
if domain not in get_web_domains(env) + get_default_www_redirects(env):
return "Invalid domain name."
# Write the combined cert+chain to a temporary path and validate that it is OK.
@@ -255,14 +297,7 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
return "\n".join(ret)
def get_web_domains_info(env):
# load custom settings so we can tell what domains have a redirect or proxy set up on '/',
# which means static hosting is not happening
custom_settings = { }
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
def has_root_proxy_or_redirect(domain):
return custom_settings.get(domain, {}).get('redirects', {}).get('/') or custom_settings.get(domain, {}).get('proxies', {}).get('/')
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
# for the SSL config panel, get cert status
def check_cert(domain):
@@ -288,7 +323,15 @@ def get_web_domains_info(env):
"root": get_web_root(domain, env),
"custom_root": get_web_root(domain, env, test_exists=False),
"ssl_certificate": check_cert(domain),
"static_enabled": not has_root_proxy_or_redirect(domain),
"static_enabled": domain not in has_root_proxy_or_redirect,
}
for domain in get_web_domains(env)
] + \
[
{
"domain": domain,
"ssl_certificate": check_cert(domain),
"static_enabled": False,
}
for domain in get_default_www_redirects(env)
]

View File

@@ -1,27 +1,35 @@
POSTGREY_VERSION=1.35-1+miab1
DOVECOT_VERSION=2.2.9-1ubuntu2.1+miab1
all: clean build_postgrey build_dovecot_lucene
clean:
# Clean.
rm -rf /tmp/build
# Prepare to build source packages.
mkdir -p /tmp/build
build_postgrey:
# Download our fork of the Debian postgrey package.
git clone https://github.com/mail-in-a-box/postgrey /tmp/build/postgrey
build_postgrey: clean
# Download the latest Debian postgrey package. It is ahead of Ubuntu,
# and we might as well jump ahead.
git clone git://git.debian.org/git/collab-maint/postgrey.git /tmp/build/postgrey
# Download the corresponding upstream package.
wget -O /tmp/build/postgrey_1.35.orig.tar.gz http://postgrey.schweikert.ch/pub/postgrey-1.35.tar.gz
# Add our source patch to the debian packaging listing.
cp postgrey_sources.diff /tmp/build/postgrey/debian/patches/mailinabox
# Patch the packaging to give it a new version.
patch -p1 -d /tmp/build/postgrey < postgrey.diff
# Build the source package.
(cd /tmp/build/postgrey; dpkg-buildpackage -S -us -uc -nc)
# Sign the packages.
debsign /tmp/build/postgrey_1.35-1miab1_source.changes
debsign /tmp/build/postgrey_$(POSTGREY_VERSION)_source.changes
# Upload to PPA.
dput ppa:mail-in-a-box/ppa /tmp/build/postgrey_1.35-1miab1_source.changes
dput ppa:mail-in-a-box/ppa /tmp/build/postgrey_$(POSTGREY_VERSION)_source.changes
# Clear the intermediate files.
rm -rf /tmp/build/postgrey
@@ -30,7 +38,7 @@ build_postgrey:
#sudo apt-get build-dep -y postgrey
#(cd /tmp/build/postgrey; dpkg-buildpackage -us -uc -nc)
build_dovecot_lucene:
build_dovecot_lucene: clean
# Get the upstream source.
(cd /tmp/build; apt-get source dovecot)
@@ -41,10 +49,10 @@ build_dovecot_lucene:
(cd /tmp/build/dovecot-2.2.9; dpkg-buildpackage -S -us -uc -nc)
# Sign the packages.
#debsign /tmp/build/dovecot_2.2.9-1ubuntu2.1_amd64.changes
debsign /tmp/build/dovecot_$(DOVECOT_VERSION)_source.changes
# Upload it.
#dput ppa:mail-in-a-box/ppa /tmp/build/dovecot_2.2.9-1ubuntu2.1_amd64.changes
dput ppa:mail-in-a-box/ppa /tmp/build/dovecot_$(DOVECOT_VERSION)_source.changes
# TESTING BINARY PACKAGE
# Install build dependencies and build dependencies we've added in our patch,

View File

@@ -6,7 +6,9 @@ Mail-in-a-Box maintains a Launchpad.net PPA ([Mail-in-a-Box PPA](https://launchp
Packages
--------
* [postgrey](https://github.com/mail-in-a-box/postgrey), with a modification to whitelist senders that are whitelisted by [dnswl.org](https://www.dnswl.org/) (i.e. don't greylist mail from them).
* postgrey, a fork of [postgrey](http://postgrey.schweikert.ch/) based on the [latest Debian package](http://git.debian.org/?p=collab-maint/postgrey.git), with a modification to whitelist senders that are whitelisted by [dnswl.org](https://www.dnswl.org/) (i.e. don't greylist mail from known good senders).
* dovecot-lucene, [dovecot's lucene full text search plugin](http://wiki2.dovecot.org/Plugins/FTS/Lucene), which isn't built by Ubuntu's dovecot package maintainer unfortunately.
Building
--------
@@ -30,9 +32,9 @@ To build:
# Build & upload to launchpad.
vagrant ssh -- "cd /vagrant && make"
To use on a Mail-in-a-Box box, add the PPA and then upgrade packages:
Mail-in-a-Box adds our PPA during setup, but if you need to do that yourself for testing:
apt-add-repository ppa:mail-in-a-box/ppa
apt-get update
apt-get upgrade
apt-get install postgrey dovecot-lucene

View File

@@ -1,6 +1,6 @@
--- a/debian/control
+++ b/debian/control
@@ -1,210 +1,22 @@
@@ -1,210 +1,23 @@
Source: dovecot
Section: mail
Priority: optional
@@ -13,11 +13,14 @@
+Build-Depends: debhelper (>= 7.2.3~), dpkg-dev (>= 1.16.1), pkg-config, libssl-dev, libpam0g-dev, libldap2-dev, libpq-dev, libmysqlclient-dev, libsqlite3-dev, libsasl2-dev, zlib1g-dev, libkrb5-dev, drac-dev (>= 1.12-5), libbz2-dev, libdb-dev, libcurl4-gnutls-dev, libexpat-dev, libwrap0-dev, dh-systemd, po-debconf, lsb-release, libclucene-dev (>= 2.3), liblzma-dev, libexttextcat-dev, libstemmer-dev, hardening-wrapper, dh-autoreconf, autotools-dev
Standards-Version: 3.9.4
Homepage: http://dovecot.org/
Vcs-Git: git://git.debian.org/git/collab-maint/dovecot.git
Vcs-Browser: http://git.debian.org/?p=collab-maint/dovecot.git
-Vcs-Git: git://git.debian.org/git/collab-maint/dovecot.git
-Vcs-Browser: http://git.debian.org/?p=collab-maint/dovecot.git
+Vcs-Git: https://github.com/mail-in-a-box/mailinabox
+Vcs-Browser: https://github.com/mail-in-a-box/mailinabox
-Package: dovecot-core
-Architecture: any
+Package: dovecot-lucene
Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-runtime (>= 0.76-13.1), openssl, adduser, ucf (>= 2.0020), ssl-cert (>= 1.0-11ubuntu1), lsb-base (>= 3.2-12ubuntu3)
-Suggests: ntp, dovecot-gssapi, dovecot-sieve, dovecot-pgsql, dovecot-mysql, dovecot-sqlite, dovecot-ldap, dovecot-imapd, dovecot-pop3d, dovecot-lmtpd, dovecot-managesieved, dovecot-solr, ufw
-Recommends: ntpdate
@@ -25,12 +28,14 @@
-Replaces: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
-Breaks: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
-Description: secure POP3/IMAP server - core files
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
+Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (>= 1:2.2.9-1ubuntu2.1)
+Description: secure POP3/IMAP server - Lucene support
Dovecot is a mail server whose major goals are security and extreme
reliability. It tries very hard to handle all error conditions and verify
that all data is valid, making it nearly impossible to crash. It supports
mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
fast, extensible, and portable.
.
- This package contains the Dovecot main server and its command line utility.
-
-Package: dovecot-dev
@@ -151,17 +156,15 @@
- This package provides LDAP support for Dovecot.
-
-Package: dovecot-gssapi
+Package: dovecot-lucene
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - GSSAPI support
+Description: secure POP3/IMAP server - Lucene support
Dovecot is a mail server whose major goals are security and extreme
reliability. It tries very hard to handle all error conditions and verify
that all data is valid, making it nearly impossible to crash. It supports
mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
fast, extensible, and portable.
.
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides GSSAPI authentication support for Dovecot.
-
-Package: dovecot-sieve
@@ -216,7 +219,9 @@
- This package contains configuration files for dovecot.
- .
- This package modifies postfix's configuration to integrate with dovecot
+ This package provides Lucene full text search support for Dovecot.
+ This package provides Lucene full text search support for Dovecot. It has been modified by Mail-in-a-Box
+ to supply a dovecot-lucene package compatible with the official ubuntu trusty dovecot-core.
diff --git a/debian/dovecot-lucene.links b/debian/dovecot-lucene.links
new file mode 100644
index 0000000..6ffcbeb
@@ -247,11 +252,9 @@ index 0000000..3d933a5
+++ b/debian/dovecot-lucene.triggers
@@ -0,0 +1 @@
+activate register-dovecot-plugin
diff --git a/debian/rules b/debian/rules
index dcee2f6..9533a4a 100755
--- a/debian/rules
+++ b/debian/rules
@@ -40,6 +40,7 @@ config-stamp: configure
@@ -40,6 +40,7 @@
--with-solr \
--with-ioloop=best \
--with-libwrap \
@@ -259,18 +262,18 @@ index dcee2f6..9533a4a 100755
--host=$(DEB_HOST_GNU_TYPE) \
--build=$(DEB_BUILD_GNU_TYPE) \
--prefix=/usr \
@@ -95,6 +96,10 @@ install: build
@@ -95,6 +96,10 @@
dh_testroot
dh_clean -k
dh_installdirs
+ mkdir -p $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules
+ mv $(CURDIR)/src/plugins/fts-lucene/.libs/* $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules/
+ mv $(CURDIR)/src/plugins/fts-lucene/.libs/* $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules/
+
+rest_disabled_by_miab:
$(MAKE) install DESTDIR=$(CURDIR)/debian/dovecot-core
$(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CURDIR)/debian/dovecot-core
rm `find $(CURDIR)/debian -name '*.la'`
@@ -209,7 +214,7 @@ binary-arch: build install
@@ -209,7 +214,7 @@
dh_installdocs -a
dh_installexamples -a
dh_installpam -a
@@ -279,7 +282,7 @@ index dcee2f6..9533a4a 100755
dh_systemd_enable
dh_installinit -pdovecot-core --name=dovecot
dh_systemd_start
@@ -220,10 +225,10 @@ binary-arch: build install
@@ -220,10 +225,10 @@
dh_lintian -a
dh_installchangelogs -a ChangeLog
dh_link -a
@@ -292,3 +295,25 @@ index dcee2f6..9533a4a 100755
dh_makeshlibs -a -n
dh_installdeb -a
dh_shlibdeps -a
--- a/debian/changelog
+++ a/debian/changelog
@@ -1,3 +1,9 @@
+dovecot (1:2.2.9-1ubuntu2.1+miab1) trusty; urgency=low
+
+ * Changed to just build dovecot-lucene for Mail-in-a-box PPA
+
+ -- Joshua Tauberer <jt@occams.info> Sat, 14 May 2015 16:13:00 -0400
+
dovecot (1:2.2.9-1ubuntu2.1) trusty-security; urgency=medium
* SECURITY UPDATE: denial of service via SSL connection exhaustion
--- a/debian/copyright 2014-03-07 07:26:37.000000000 -0500
+++ b/debian/copyright 2015-05-23 18:17:42.668005535 -0400
@@ -1,3 +1,7 @@
+This package is a fork by Mail-in-a-box (https://mailinabox.email). Original
+copyright statement follows:
+----------------------------------------------------------------------------
+
This package was debianized by Jaldhar H. Vyas <jaldhar@debian.org> on
Tue, 3 Dec 2002 01:10:07 -0500.

80
ppa/postgrey.diff Normal file
View File

@@ -0,0 +1,80 @@
diff --git a/debian/NEWS b/debian/NEWS
index dd09744..de7b640 100644
--- a/debian/NEWS
+++ b/debian/NEWS
@@ -1,3 +1,9 @@
+postgrey (1.35-1+miab1)
+
+ Added DNSWL.org whitelisting.
+
+ -- Joshua Tauberer <jt@occams.info> Mon May 18 18:58:40 EDT 2015
+
postgrey (1.32-1) unstable; urgency=low
Postgrey is now listening to port 10023 and not 60000. The latter was an
diff --git a/debian/changelog b/debian/changelog
index 1058e15..e5e3557 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+postgrey (1.35-1+miab1) trusty; urgency=low
+
+ * Added DNSWL.org whitelisting.
+
+ -- Joshua Tauberer <jt@occams.info> Mon, 18 May 2015 21:58:40 +0000
+
postgrey (1.35-1) unstable; urgency=low
* New upstream release (Closes: 756486)
diff --git a/debian/control b/debian/control
index ce12ba6..0a82855 100644
--- a/debian/control
+++ b/debian/control
@@ -1,14 +1,11 @@
Source: postgrey
Section: mail
Priority: optional
-Maintainer: Antonio Radici <antonio@debian.org>
-Uploaders: Jon Daley <jondaley-guest@alioth.debian.org>
+Maintainer: Joshua Tauberer <jt@occams.info>
Build-Depends: debhelper (>= 7), quilt
Build-Depends-Indep: po-debconf
Standards-Version: 3.9.6
Homepage: http://postgrey.schweikert.ch/
-Vcs-Browser: http://git.debian.org/?p=collab-maint/postgrey.git
-Vcs-Git: git://git.debian.org/git/collab-maint/postgrey.git
Package: postgrey
Architecture: all
@@ -25,3 +22,6 @@ Description: greylisting implementation for Postfix
.
While Postgrey is designed for use with Postfix, it can also be used
with Exim.
+ .
+ This version has been modified by Mail-in-a-Box to whitelist senders
+ in the DNSWL.org list. See https://mailinabox.email.
diff --git a/debian/copyright b/debian/copyright
index 3cbe377..bf09b89 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,6 +1,10 @@
+This package is a fork by Mail-in-a-Box (https://mailinabox.email). Original
+copyright statement follows:
+----------------------------------------------------------------------------
+
This Debian package was prepared by Adrian von Bidder <cmot@debian.org> in
July 2004, then the package was adopted by Antonio Radici <antonio@dyne.org>
-in Sept 2009
+in Sept 2009.
It was downloaded from http://postgrey.schweikert.ch/
diff --git a/debian/patches/series b/debian/patches/series
index f4c5e31..3cd62b8 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1,3 @@
imported-upstream-diff
disable-transaction-logic
-
+mailinabox

100
ppa/postgrey_sources.diff Normal file
View File

@@ -0,0 +1,100 @@
Description: whitelist whatever dnswl.org whitelists
.
postgrey (1.35-1+miab1) unstable; urgency=low
.
* Added DNSWL.org whitelisting.
Author: Joshua Tauberer <jt@occams.info>
--- postgrey-1.35.orig/README
+++ postgrey-1.35/README
@@ -13,7 +13,7 @@ Requirements
- BerkeleyDB (Perl Module)
- Berkeley DB >= 4.1 (Library)
- Digest::SHA (Perl Module, only for --privacy option)
-
+- Net::DNS (Perl Module)
Documentation
-------------
--- postgrey-1.35.orig/postgrey
+++ postgrey-1.35/postgrey
@@ -18,6 +18,7 @@ use Fcntl ':flock'; # import LOCK_* cons
use Sys::Hostname;
use Sys::Syslog; # used only to find out which version we use
use POSIX qw(strftime setlocale LC_ALL);
+use Net::DNS; # for DNSWL.org whitelisting
use vars qw(@ISA);
@ISA = qw(Net::Server::Multiplex);
@@ -26,6 +27,8 @@ my $VERSION = '1.35';
my $DEFAULT_DBDIR = '/var/lib/postgrey';
my $CONFIG_DIR = '/etc/postgrey';
+my $dns_resolver = Net::DNS::Resolver->new;
+
sub cidr_parse($)
{
defined $_[0] or return undef;
@@ -48,6 +51,36 @@ sub cidr_match($$$)
return ($addr & $mask) == $net;
}
+sub reverseDottedQuad {
+ # This is the sub _chkValidPublicIP from Net::DNSBL by PJ Goodwin
+ # at http://www.the42.net/net-dnsbl.
+ my ($quad) = @_;
+ if ($quad =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
+ my ($ip1,$ip2,$ip3,$ip4) = ($1, $2, $3, $4);
+ if (
+ $ip1 == 10 || #10.0.0.0/8 (10/8)
+ ($ip1 == 172 && $ip2 >= 16 && $ip2 <= 31) || #172.16.0.0/12 (172.16/12)
+ ($ip1 == 192 && $ip2 == 168) || #192.168.0.0/16 (192.168/16)
+ $quad eq '127.0.0.1' # localhost
+ ) {
+ # toss the RFC1918 specified privates
+ return undef;
+ } elsif (
+ ($ip1 <= 1 || $ip1 > 254) ||
+ ($ip2 < 0 || $ip2 > 255) ||
+ ($ip3 < 0 || $ip3 > 255) ||
+ ($ip4 < 0 || $ip4 > 255)
+ ) {
+ #invalid oct, toss it;
+ return undef;
+ }
+ my $revquad = $ip4 . "." . $ip3 . "." . $ip2 . "." . $ip1;
+ return $revquad;
+ } else { # invalid quad
+ return undef;
+ }
+}
+
sub read_clients_whitelists($)
{
my ($self) = @_;
@@ -361,6 +394,25 @@ sub smtpd_access_policy($$)
}
}
+ # whitelist clients in dnswl.org
+ my $revip = reverseDottedQuad($attr->{client_address});
+ if ($revip) { # valid IP / plausibly in DNSWL
+ my $answer = $dns_resolver->send($revip . '.list.dnswl.org');
+ if ($answer && scalar($answer->answer) > 0) {
+ my @rrs = $answer->answer;
+ if ($rrs[0]->type eq 'A' && $rrs[0]->address ne '127.0.0.255') {
+ # Address appears in DNSWL. (127.0.0.255 means we were rate-limited.)
+ my $code = $rrs[0]->address;
+ if ($code =~ /^127.0.(\d+)\.([0-3])$/) {
+ my %dnswltrust = (0 => 'legitimate', 1 => 'occasional spam', 2 => 'rare spam', 3 => 'highly unlikely to send spam');
+ $code = $2 . '/' . $dnswltrust{$2};
+ }
+ $self->mylog_action($attr, 'pass', 'client whitelisted by dnswl.org (' . $code . ')');
+ return 'DUNNO';
+ }
+ }
+ }
+
# auto whitelist clients (see below for explanation)
my ($cawl_db, $cawl_key, $cawl_count, $cawl_last);
if($self->{postgrey}{awl_clients}) {

View File

@@ -56,6 +56,8 @@ The cipher and protocol selection are chosen to support the following clients:
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py))
When using the web-based administrative control panel, after logging in an API key is placed in the browser's local storage (rather than, say, the user's actual password). The API key is an HMAC based on the user's email address and current password, and it is keyed by a secret known only to the control panel service. By resetting an administrator's password, any HMACs previously generated for that user will expire.
### Console access
Console access (e.g. via SSH) is configured by the system image used to create the box, typically from by a cloud virtual machine provider (e.g. Digital Ocean). Mail-in-a-Box does not set any console access settings, although it will warn the administrator in the System Status Checks if password-based login is turned on.
@@ -67,17 +69,27 @@ If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that
Outbound Mail
-------------
### Domain Policy Records
The basic protocols of email delivery did not plan for the presence of adversaries on the network. For a number of reasons it is not possible in most cases to guarantee that a connection to a recipient server is secure.
Domain policy records allow recipient MTAs to detect when the _domain_ part of incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py))
### DNSSEC
The first step in resolving the destination server for an email address is performing a DNS look-up for the MX record of the domain name. The box uses a locally-running [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware nameserver to perform the lookup. If the domain name has DNSSEC enabled, DNSSEC guards against DNS records being tampered with.
### Encryption
The basic protocols of email delivery did not plan for the need for encryption. For a number of reasons it is not possible in most cases to guarantee that a connection to a recipient server is secure. However, the box --- along with the vast majority of mail servers --- uses [opportunistic encryption](https://en.wikipedia.org/wiki/Opportunistic_encryption), meaning the mail is encrypted in transit and protected from passive eavesdropping, but it is not protected from an active man-in-the-middle attack. Modern encryption settings will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
The box (along with the vast majority of mail servers) uses [opportunistic encryption](https://en.wikipedia.org/wiki/Opportunistic_encryption), meaning the mail is encrypted in transit and protected from passive eavesdropping, but it is not protected from an active man-in-the-middle attack. Modern encryption settings will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
### DANE
The box is [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware (via a locally running DNSSEC-aware nameserver). When sending outbound mail, if the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, which contains a certificate fingerprint, the receiving MTA (server) must support TLS and its certificate must match the fingerprint. In other words, when a DANE TLSA record is published by the recipient, then on-the-wire encryption is forced between the box and the recipient MTA. ([source](setup/mail-postfix.sh))
If the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, then on-the-wire encryption is forced between the box and the recipient MTA and this encryption is not subject to a man-in-the-middle attack. The TLSA record contains a certificate fingerprint which the receiving MTA (server) must present to the box. ([source](setup/mail-postfix.sh))
### Domain Policy Records
Domain policy records allow recipient MTAs to detect when the _domain_ part of of the sender address in incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py))
### User Policy
While domain policy records prevent other servers from sending mail with a "From:" header that matches a domain hosted on the box (see above), those policy records do not guarnatee that the user portion of the sender email address matches the actual sender. In enterprise environments where the box may host the mail of untrusted users, it is important to guard against users impersonating other users. The box restricts the envelope sender address that users may put into outbound mail to either a) their own email address (their SMTP login username) or b) any alias that they are listed as a direct recipient of. Note that the envelope sender address is not the same as the "From:" header.
Incoming Mail
-------------

View File

@@ -7,7 +7,7 @@
#########################################################
if [ -z "$TAG" ]; then
TAG=v0.10
TAG=v0.12b
fi
# Are we running as root?
@@ -18,9 +18,12 @@ fi
# Clone the Mail-in-a-Box repository if it doesn't exist.
if [ ! -d $HOME/mailinabox ]; then
echo Installing git . . .
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
echo
if [ ! -f /usr/bin/git ]; then
echo Installing git . . .
apt-get -q -q update
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
echo
fi
echo Downloading Mail-in-a-Box $TAG. . .
git clone \

View File

@@ -35,13 +35,18 @@ RequireSafeKeys false
EOF
fi
# Create a new DKIM key. This creates
# mail.private and mail.txt in $STORAGE_ROOT/mail/dkim. The former
# is the actual private key and the latter is the suggested DNS TXT
# entry which we'll want to include in our DNS setup.
# Create a new DKIM key. This creates mail.private and mail.txt
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
# the latter is the suggested DNS TXT entry which we'll include
# in our DNS setup. Note tha the files are named after the
# 'selector' of the key, which we can change later on to support
# key rotation.
#
# A 1024-bit key is seen as a minimum standard by several providers
# such as Google. But they and others use a 2048 bit key, so we'll
# do the same. Keys beyond 2048 bits may exceed DNS record limits.
if [ ! -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then
# Should we specify -h rsa-sha256?
opendkim-genkey -r -s mail -D $STORAGE_ROOT/mail/dkim
opendkim-genkey -b 2048 -r -s mail -D $STORAGE_ROOT/mail/dkim
fi
# Ensure files are owned by the opendkim user and are private otherwise.

View File

@@ -18,11 +18,14 @@
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# Install packages...
# Install packages for dovecot. These are all core dovecot plugins,
# but dovecot-lucene is packaged by *us* in the Mail-in-a-Box PPA,
# not by Ubuntu.
apt_install \
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \
dovecot-sieve dovecot-managesieved
dovecot-sieve dovecot-managesieved dovecot-lucene
# The `dovecot-imapd`, `dovecot-pop3d`, and `dovecot-lmtpd` packages automatically
# enable IMAP, POP and LMTP protocols.
@@ -92,6 +95,17 @@ tools/editconf.py /etc/dovecot/conf.d/20-imap.conf \
tools/editconf.py /etc/dovecot/conf.d/20-pop3.conf \
pop3_uidl_format="%08Xu%08Xv"
# Full Text Search - Enable full text search of mail using dovecot's lucene plugin,
# which *we* package and distribute (dovecot-lucene package).
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
mail_plugins="\$mail_plugins fts fts_lucene"
cat > /etc/dovecot/conf.d/90-plugin-fts.conf << EOF;
plugin {
fts = lucene
fts_lucene = whitespace_chars=@.
}
EOF
# ### LDA (LMTP)
# Enable Dovecot's LDA service with the LMTP protocol. It will listen

View File

@@ -41,6 +41,13 @@ source /etc/mailinabox.conf # load global vars
# always will.
# * `ca-certificates`: A trust store used to squelch postfix warnings about
# untrusted opportunistically-encrypted connections.
#
# postgrey is going to come in via the Mail-in-a-Box PPA, which publishes
# a modified version of postgrey that lets senders whitelisted by dnswl.org
# pass through without being greylisted. So please note [dnswl's license terms](https://www.dnswl.org/?page_id=9):
# > Every user with more than 100000 queries per day on the public nameserver
# > infrastructure and every commercial vendor of dnswl.org data (eg through
# > anti-spam solutions) must register with dnswl.org and purchase a subscription.
apt_install postfix postfix-pcre postgrey ca-certificates
@@ -153,6 +160,7 @@ tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025
#
# * `reject_non_fqdn_sender`: Reject not-nice-looking return paths.
# * `reject_unknown_sender_domain`: Reject return paths with invalid domains.
# * `reject_authenticated_sender_login_mismatch`: Reject if mail FROM address does not match the client SASL login
# * `reject_rhsbl_sender`: Reject return paths that use blacklisted domains.
# * `permit_sasl_authenticated`: Authenticated users (i.e. on port 587) can skip further checks.
# * `permit_mynetworks`: Mail that originates locally can skip further checks.
@@ -166,7 +174,7 @@ tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025
# whitelisted) then postfix does a DEFER_IF_REJECT, which results in all "unknown user" sorts of messages turning into #NODOC
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC
tools/editconf.py /etc/postfix/main.cf \
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_rhsbl_sender dbl.spamhaus.org" \
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org" \
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023"
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that

View File

@@ -69,6 +69,22 @@ tools/editconf.py /etc/postfix/main.cf \
smtpd_sasl_path=private/auth \
smtpd_sasl_auth_enable=yes
# ### Sender Validation
# Use a Sqlite3 database to set login maps. This is used with
# reject_authenticated_sender_login_mismatch to see if user is
# allowed to send mail using FROM field specified in the request.
tools/editconf.py /etc/postfix/main.cf \
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
# SQL statement to set login map which includes the case when user is
# sending email using a valid alias.
# This is the same as virtual-alias-maps.cf, See below
cat > /etc/postfix/sender-login-maps.cf << EOF;
dbpath=$db_path
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
EOF
# ### Destination Validation
# Use a Sqlite3 database to check whether a destination email address exists,
@@ -92,13 +108,25 @@ query = SELECT 1 FROM users WHERE email='%s'
EOF
# SQL statement to rewrite an email address if an alias is present.
# Aliases have precedence over users, but that's counter-intuitive for
# catch-all aliases ("@domain.com") which should *not* catch mail users.
# To fix this, not only query the aliases table but also the users
# table, i.e. turn users into aliases from themselves to themselves.
#
# Postfix makes multiple queries for each incoming mail. It first
# queries the whole email address, then just the user part in certain
# locally-directed cases (but we don't use this), then just `@`+the
# domain part. The first query that returns something wins. See
# http://www.postfix.org/virtual.5.html.
#
# virtual-alias-maps has precedence over virtual-mailbox-maps, but
# we don't want catch-alls and domain aliases to catch mail for users
# that have been defined on those domains. To fix this, we not only
# query the aliases table but also the users table when resolving
# aliases, i.e. we turn users into aliases from themselves to
# themselves. That means users will match in postfix's first query
# before postfix gets to the third query for catch-alls/domain alises.
#
# If there is both an alias and a user for the same address either
# might be returned by the UNION, so the whole query is wrapped in
# another select that prioritizes the alias definition.
# another select that prioritizes the alias definition to preserve
# postfix's preference for aliases for whole email addresses.
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
dbpath=$db_path
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;

View File

@@ -2,8 +2,10 @@
source setup/functions.sh
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil
hide_output pip3 install rtyaml "email_validator==0.1.0-rc5"
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil \
build-essential libssl-dev libffi-dev python3-dev
hide_output pip3 install --upgrade rtyaml email_validator idna cryptography
# email_validator is repeated in setup/questions.sh
# Create a backup directory and a random key for encrypting backups.
@@ -42,8 +44,5 @@ EOF
chmod +x /etc/cron.daily/mailinabox-statuschecks
# Start it. Remove the api key file first so that start.sh
# can wait for it to be created to know that the management
# server is ready.
rm -f /var/lib/mailinabox/api.key
# Start it.
restart_service mailinabox

View File

@@ -95,6 +95,11 @@ def migration_7(env):
# Save.
conn.commit()
def migration_8(env):
# Delete DKIM keys. We had generated 1024-bit DKIM keys.
# By deleting the key file we'll automatically generate
# a new key, which will be 2048 bits.
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
def get_current_migration():
ver = 0

32
setup/munin.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Munin: resource monitoring tool
#################################################
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# install Munin
apt_install munin munin-node
# edit config
cat > /etc/munin/munin.conf <<EOF;
dbdir /var/lib/munin
htmldir /var/cache/munin/www
logdir /var/log/munin
rundir /var/run/munin
tmpldir /etc/munin/templates
includedir /etc/munin/munin-conf.d
# a simple host tree
[$PRIMARY_HOSTNAME]
address 127.0.0.1
# send alerts to the following address
contacts admin
contact.admin.command mail -s "Munin notification ${var:host}" administrator@$PRIMARY_HOSTNAME
contact.admin.always_send warning critical
EOF
# generate initial statistics so the directory isn't empty
sudo -u munin munin-cron

View File

@@ -15,25 +15,36 @@ apt_install \
apt-get purge -qq -y owncloud*
# Install ownCloud from source of this version:
owncloud_ver=8.0.3
owncloud_hash=3192f3d783f81247eaf2914df63afdd593def4e5
owncloud_ver=8.0.4
owncloud_hash=625b1c561ea51426047a3e79eda51ca05e9f978a
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
if [ ! -f $STORAGE_ROOT/owncloud/config.php ] \
&& [ -f /usr/local/lib/owncloud/config/config.php ]; then
# Move config.php and symlink back into previous location.
echo "Migrating owncloud/config.php to new location."
mv /usr/local/lib/owncloud/config/config.php $STORAGE_ROOT/owncloud/config.php \
&& \
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
fi
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
if [ ! -d /usr/local/lib/owncloud/ ] \
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
# Download and verify
echo "installing ownCloud..."
wget_verify https://download.owncloud.org/community/owncloud-$owncloud_ver.zip $owncloud_hash /tmp/owncloud.zip
# Clear out the existing ownCloud.
rm -f /tmp/owncloud-config.php
if [ ! -d /usr/local/lib/owncloud/ ]; then
echo installing ownCloud...
else
if [ -d /usr/local/lib/owncloud/ ]; then
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud directory to /tmp/owncloud-backup-$$)..."
cp /usr/local/lib/owncloud/config/config.php /tmp/owncloud-config.php
mv /usr/local/lib/owncloud /tmp/owncloud-backup-$$
fi
# Download and extract ownCloud.
wget_verify https://download.owncloud.org/community/owncloud-$owncloud_ver.zip $owncloud_hash /tmp/owncloud.zip
# Extract ownCloud
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
rm -f /tmp/owncloud.zip
@@ -46,10 +57,9 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
# Fix weird permissions.
chmod 750 /usr/local/lib/owncloud/{apps,config}
# Restore configuration file if we're doing an upgrade.
if [ -f /tmp/owncloud-config.php ]; then
mv /tmp/owncloud-config.php /usr/local/lib/owncloud/config/config.php
fi
# Create a symlink to the config.php in STORAGE_ROOT (for upgrades we're restoring the symlink we previously
# put in, and in new installs we're creating a symlink and will create the actual config later).
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
# Make sure permissions are correct or the upgrade step won't run.
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
@@ -65,10 +75,13 @@ fi
# Setup ownCloud if the ownCloud database does not yet exist. Running setup when
# the database does exist wipes the database and user data.
if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
# Create user data directory
mkdir -p $STORAGE_ROOT/owncloud
# Create a configuration file.
TIMEZONE=$(cat /etc/timezone)
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
cat > /usr/local/lib/owncloud/config/config.php <<EOF;
cat > $STORAGE_ROOT/owncloud/config.php <<EOF;
<?php
\$CONFIG = array (
'datadirectory' => '$STORAGE_ROOT/owncloud',
@@ -125,12 +138,12 @@ EOF
?>
EOF
# Create user data directory and set permissions
mkdir -p $STORAGE_ROOT/owncloud
# Set permissions
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
# Execute ownCloud's setup step, which creates the ownCloud sqlite database.
# It also wipes it if it exists. And it deletes the autoconfig.php file.
# It also wipes it if it exists. And it updates config.php with database
# settings and deletes the autoconfig.php file.
(cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;)
fi

View File

@@ -6,11 +6,14 @@ if [ -z "$NONINTERACTIVE" ]; then
# use a shell flag instead. Really supress any output from installing dialog.
#
# Also install depencies needed to validate the email address.
echo Installing packages needed for setup...
apt_get_quiet install dialog python3 python3-pip || exit 1
if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then
echo Installing packages needed for setup...
apt-get -q -q update
apt_get_quiet install dialog python3 python3-pip || exit 1
fi
# email_validator is repeated in setup/management.sh
hide_output pip3 install "email_validator==0.1.0-rc5" || exit 1
hide_output pip3 install email_validator || exit 1
message_box "Mail-in-a-Box Installation" \
"Hello and thanks for deploying a Mail-in-a-Box!
@@ -193,3 +196,48 @@ if [ -z "$CSR_COUNTRY" ]; then
exit
fi
fi
# Automatic configuration, e.g. as used in our Vagrant configuration.
if [ "$PUBLIC_IP" = "auto" ]; then
# Use a public API to get our public IP address, or fall back to local network configuration.
PUBLIC_IP=$(get_publicip_from_web_service 4 || get_default_privateip 4)
fi
if [ "$PUBLIC_IPV6" = "auto" ]; then
# Use a public API to get our public IPv6 address, or fall back to local network configuration.
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
fi
if [ "$PRIMARY_HOSTNAME" = "auto" ]; then
# Use reverse DNS to get this machine's hostname. Install bind9-host early.
hide_output apt-get -y install bind9-host
PRIMARY_HOSTNAME=$(get_default_hostname)
elif [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
# Generate a probably-unique subdomain under our justtesting.email domain.
PRIMARY_HOSTNAME=`echo $PUBLIC_IP | sha1sum | cut -c1-5`.justtesting.email
fi
# Set STORAGE_USER and STORAGE_ROOT to default values (user-data and /home/user-data), unless
# we've already got those values from a previous run.
if [ -z "$STORAGE_USER" ]; then
STORAGE_USER=$([[ -z "$DEFAULT_STORAGE_USER" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
fi
if [ -z "$STORAGE_ROOT" ]; then
STORAGE_ROOT=$([[ -z "$DEFAULT_STORAGE_ROOT" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
fi
# Show the configuration, since the user may have not entered it manually.
echo
echo "Primary Hostname: $PRIMARY_HOSTNAME"
echo "Public IP Address: $PUBLIC_IP"
if [ ! -z "$PUBLIC_IPV6" ]; then
echo "Public IPv6 Address: $PUBLIC_IPV6"
fi
if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then
echo "Private IP Address: $PRIVATE_IP"
fi
if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
echo "Private IPv6 Address: $PRIVATE_IPV6"
fi
if [ -f /usr/bin/git ] && [ -d .git ]; then
echo "Mail-in-a-Box Version: " $(git describe)
fi
echo

View File

@@ -47,71 +47,26 @@ chmod +x /usr/local/bin/mailinabox
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, PUBLIC_IPV6, and CSR_COUNTRY
# if values have not already been set in environment variables. When running
# non-interactively, be sure to set values for all!
# non-interactively, be sure to set values for all! Also sets STORAGE_USER and
# STORAGE_ROOT.
source setup/questions.sh
# Automatic configuration, e.g. as used in our Vagrant configuration.
if [ "$PUBLIC_IP" = "auto" ]; then
# Use a public API to get our public IP address, or fall back to local network configuration.
PUBLIC_IP=$(get_publicip_from_web_service 4 || get_default_privateip 4)
fi
if [ "$PUBLIC_IPV6" = "auto" ]; then
# Use a public API to get our public IPv6 address, or fall back to local network configuration.
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
fi
if [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
# Generate a probably-unique subdomain under our justtesting.email domain.
PRIMARY_HOSTNAME=`echo $PUBLIC_IP | sha1sum | cut -c1-5`.justtesting.email
fi
# Show the configuration, since the user may have not entered it manually.
echo
echo "Primary Hostname: $PRIMARY_HOSTNAME"
echo "Public IP Address: $PUBLIC_IP"
if [ ! -z "$PUBLIC_IPV6" ]; then
echo "Public IPv6 Address: $PUBLIC_IPV6"
fi
if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then
echo "Private IP Address: $PRIVATE_IP"
fi
if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
echo "Private IPv6 Address: $PRIVATE_IPV6"
fi
if [ -f .git ]; then
echo "Mail-in-a-Box Version: " $(git describe)
fi
echo
# Run some network checks to make sure setup on this machine makes sense.
if [ -z "$SKIP_NETWORK_CHECKS" ]; then
. setup/network-checks.sh
source setup/network-checks.sh
fi
# For the first time (if the config file (/etc/mailinabox.conf) not exists):
# Create the user named "user-data" and store all persistent user
# data (mailboxes, etc.) in that user's home directory.
#
# If the config file exists:
# Apply the existing configuration options for STORAGE_USER/ROOT
if [ -z "$STORAGE_USER" ]; then
STORAGE_USER=$([[ -z "$DEFAULT_STORAGE_USER" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
fi
if [ -z "$STORAGE_ROOT" ]; then
STORAGE_ROOT=$([[ -z "$DEFAULT_STORAGE_ROOT" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
fi
# Create the STORAGE_USER if it not exists
# Create the STORAGE_USER and STORAGE_ROOT directory if they don't already exist.
# If the STORAGE_ROOT is missing the mailinabox.version file that lists a
# migration (schema) number for the files stored there, assume this is a fresh
# installation to that directory and write the file to contain the current
# migration number for this version of Mail-in-a-Box.
if ! id -u $STORAGE_USER >/dev/null 2>&1; then
useradd -m $STORAGE_USER
fi
# Create the STORAGE_ROOT if it not exists
if [ ! -d $STORAGE_ROOT ]; then
mkdir -p $STORAGE_ROOT
fi
# Create mailinabox.version file if not exists
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
@@ -145,9 +100,11 @@ source setup/webmail.sh
source setup/owncloud.sh
source setup/zpush.sh
source setup/management.sh
source setup/munin.sh
# Ping the management daemon to write the DNS and nginx configuration files.
while [ ! -f /var/lib/mailinabox/api.key ]; do
until nc -z -w 4 localhost 10222
do
echo Waiting for the Mail-in-a-Box management daemon to start...
sleep 2
done

View File

@@ -3,7 +3,30 @@ source setup/functions.sh # load our functions
# Basic System Configuration
# -------------------------
# ### Install Packages
# ### Add Mail-in-a-Box's PPA.
# We've built several .deb packages on our own that we want to include.
# One is a replacement for Ubuntu's stock postgrey package that makes
# some enhancements. The other is dovecot-lucene, a Lucene-based full
# text search plugin for (and by) dovecot, which is not available in
# Ubuntu currently.
#
# Add that to the system's list of repositories using add-apt-repository.
# But add-apt-repository may not be installed. If it's not available,
# then install it. But we have to run apt-get update before we try to
# install anything so the package index is up to date. After adding the
# PPA, we have to run apt-get update *again* to load the PPA's index,
# so this must precede the apt-get update line below.
if [ ! -f /usr/bin/add-apt-repository ]; then
echo "Installing add-apt-repository..."
hide_output apt-get update
apt_install software-properties-common
fi
hide_output add-apt-repository -y ppa:mail-in-a-box/ppa
# ### Update Packages
# Update system packages to make sure we have the latest upstream versions of things from Ubuntu.
@@ -11,6 +34,8 @@ echo Updating system packages...
hide_output apt-get update
apt_get_quiet upgrade
# ### Install System Packages
# Install basic utilities.
#
# * haveged: Provides extra entropy to /dev/random so it doesn't stall
@@ -20,13 +45,14 @@ apt_get_quiet upgrade
# * cron: Runs background processes periodically.
# * ntp: keeps the system time correct
# * fail2ban: scans log files for repeated failed login attempts and blocks the remote IP at the firewall
# * netcat-openbsd: `nc` command line networking tool
# * git: we install some things directly from github
# * sudo: allows privileged users to execute commands as root without being root
# * coreutils: includes `nproc` tool to report number of processors
# * bc: allows us to do math to compute sane defaults
apt_install python3 python3-dev python3-pip \
wget curl git sudo coreutils bc \
netcat-openbsd wget curl git sudo coreutils bc \
haveged unattended-upgrades cron ntp fail2ban
# Allow apt to install system updates automatically every day.

View File

@@ -32,6 +32,14 @@ sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
tools/editconf.py /etc/nginx/nginx.conf -s \
server_names_hash_bucket_size="64;"
# Tell PHP not to expose its version number in the X-Powered-By header.
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
expose_php=Off
# Set PHPs default charset to UTF-8, since we use it. See #367.
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
default_charset="UTF-8"
# Bump up PHP's max_children to support more concurrent connections
tools/editconf.py /etc/php5/fpm/pool.d/www.conf -c ';' \
pm.max_children=8

View File

@@ -32,10 +32,10 @@ apt-get purge -qq -y roundcube* #NODOC
# Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
# whether we have the latest version.
VERSION=1.1.1
HASH=08222f382a8dd89bba7dbbad595f48443bec0aa2
VERSION=1.1.2
HASH=df88deae691da3ecf3e9f0aee674c1f3042ea1eb
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
PERSISTENT_LOGIN_VERSION=9a0bc59493beb573d515f82aec443e2098365d11
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION
needs_update=0 #NODOC
if [ ! -f /usr/local/lib/roundcubemail/version ]; then

View File

@@ -14,6 +14,8 @@
#
# NAME VALUE
#
# If the -c option is given, then the supplied character becomes the comment character
#
# If the -w option is given, then setting lines continue onto following
# lines while the lines start with whitespace, e.g.:
#
@@ -24,7 +26,7 @@ import sys, re
# sanity check
if len(sys.argv) < 3:
print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-t] NAME=VAL [NAME=VAL ...]")
print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c <CHARACTER>] [-t] NAME=VAL [NAME=VAL ...]")
sys.exit(1)
# parse command line arguments

View File

@@ -25,6 +25,7 @@ for fn in glob.glob("/var/log/nginx/access.log*"):
with f:
for line in f:
# Find lines that are GETs on /bootstrap.sh by either curl or wget.
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /bootstrap.sh HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
if m:
date, time = m.group("date").decode("ascii").split(":", 1)