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

Compare commits

...

253 Commits

Author SHA1 Message Date
Joshua Tauberer
e03b071e8b missed changelog header 2016-11-30 12:50:38 -05:00
Joshua Tauberer
df93d82d0f v0.21 released 2016-11-30 12:42:24 -05:00
Christian Koptein
59913a5e4c Added Hacker-News Reference Nov 2016 (#1014) 2016-11-28 07:24:57 -05:00
Michael Kroes
c3605f6211 Check if update-manager release-upgrades configuration file is present before editing (#996) 2016-11-13 17:36:33 -05:00
Joshua Tauberer
96b3a29800 rsync backup broke other things 2016-11-12 09:59:06 -05:00
Joshua Tauberer
abb6a1a070 changelog entries 2016-11-12 09:34:52 -05:00
guyzmo
041b5f883f Support for rsync+ssh backup target (#678)
* Added support for backup to a remote server using rsync

* updated web interface to get data from user
* added way to list files from server

It’s not using the “username” field of the yaml configuration
file to minimise the amount of patches needed. So the username
is actually sorted within the rsync URL.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Added ssh key generation upon installation for root user.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Removed stale blank lines, and fixed typo

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* fix backup-location lines, by switching it from id to class

* Various web UI fixes

- fixed user field being shadowed ;
- fixed settings reading comparaison ;
- fixed forgotten min-age field.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Added SSH Public Key shown on the web interface UI

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* trailing spaces.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* fixed the extraneous environment

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Updated key setup

- made key lower in bits, but stronger (using -a option),
- made ssh-keygen run in background using nohup,
- added independent key file, as id_rsa_miab,
- added ssh-options to all duplicity calls to use the id_rsa_miab keyfile,
- changed path to the public key display

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* added rsync options for ssh identity support

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* removed strict host checking for all backup operations

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Remove nohup from ssh-keygen so errors aren't hidden. Also only generate a key if none exists yet

* Add trailing slash when checking a remote backup. Also check if we actually can read the remote size

* Factorisation of the repeated rsync/ssh options

cf https://github.com/mail-in-a-box/mailinabox/pull/678#discussion_r81478919

* Updated message SSH key creation

https://github.com/mail-in-a-box/mailinabox/pull/678#discussion_r81478886
2016-11-12 09:28:55 -05:00
yodax
3b78a8d9d6 If ufw isn't installed on the machine the status checks shouldn't fail 2016-11-12 09:25:34 -05:00
Scott Bronson
6ea1a06a12 suppress Ubuntu's upgrade prompts (#992)
On every login we're notified:

  New release '16.04.1 LTS' available.
  Run 'do-release-upgrade' to upgrade to it.

Disable this so that an eager yet inattentive admin
doesn't accidentally follow these instructions.
2016-11-08 21:41:02 -05:00
Michael Kroes
2b00478b8b Check if apc is disabled during ownCloud setup, if so enable it (#983) 2016-10-24 07:59:34 -04:00
Michael Kroes
155bcfc654 Add ownCloud 9.1.1 to the changelog (#984) 2016-10-21 10:12:46 -04:00
Tristan Hill
4b07a6aa8f disable nested checker checks (#972)
fixes #967
2016-10-18 14:15:33 -04:00
Michael Kroes
2151d81453 update to ownCloud 9.1.1 (with intermediate upgrades) (#894)
[this is a squashed merge from-]

* Install owncoud 9.1 and provide an upgrade path from 8.2. This also disables memcached and goes with apc. The upgrade fails with memcached.

* Remove php apc setting

* Add dav migrations for each user

* Add some comments to the code

* When upgrading owncloud from 8.2.3 to 9.1.0 the backup of 8.2.3 was overwritten when going from 9.0 to 9.1

* Add upgrade path from 8.1.1. Only do an upgrade check if owncloud was previously installed.

* Stop php5-fpm before owncloud upgrade to prevent database locks

* Fix fail2ban tests for owncloud 9

* When upgrading owncloud copy the database to the user-data/owncloud-backup directory

* Remove not need unzip directives during owncloud extraction. Directory is removed beforehand so a normal extraction is fine

* Improve backup of owncloud installation and provide a post installation restore script. Update the owncloud version number to 9.1.1. Update the calendar and contacts apps to the latest versions

* Separate the ownCloud upgrades visually in the console output.
2016-10-18 06:04:13 -04:00
Michael Kroes
fd6226187a lower memory requirements to 512MB, display a warning if system memory is below 768MB. (#952) 2016-10-15 15:41:25 -04:00
rxcomm
bbe27df413 SSHFP record creation should scan nonstandard SSH port if necessary (#974)
* sshfp records from nonstandard ports

If port 22 is not open, dns_update.py will not create SSHFP records
because it only scans port 22 for keys. This commit modifies
dns_update.py to parse the sshd_config file for open ports, and
then obtains keys from one of them (even if port 22 is not open).

* modified test of s per JoshData request

* edit CHANGELOG per JoshData

* fix typo
2016-10-15 15:36:13 -04:00
Michael Kroes
a658abc95f Fix status checks for ufw when the system doesn't support iptables (#961) 2016-10-08 14:35:19 -04:00
Joshua Tauberer
9331dbc519 merge z-push-from-name #940 2016-10-08 14:32:57 -04:00
Steve Gregg
8b5eba21c0 Correct typo of "PRIORITY" in the template (#965) 2016-10-05 18:43:50 -04:00
yodax
da5497cd1c Update changelog entries 2016-09-28 08:37:24 +02:00
Michael Kroes
a27ec68467 Merge pull request #951 from MariusBluem/remove-certificate-providers
Remove Certificate Providers / Fix #950
2016-09-28 08:33:11 +02:00
Marius Blüm
3ac4b8aca8 Remove Certificate Providers / Fix #950
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-09-27 15:06:50 +02:00
Michael Kroes
02feeafe6a change bayes_file_mode to world writable (merges #931)
fixes #534, again, hopefully
2016-09-23 15:14:21 -04:00
Marius Blüm
5f0376bfbf Fix typo in alias-page, fixes #943 (merges #949)
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-09-23 15:11:37 -04:00
Joshua Tauberer
4e4fe90fc7 v0.20 2016-09-23 07:49:13 -04:00
Joshua Tauberer
3cd5a6eee7 changelog entries 2016-09-23 07:46:01 -04:00
Joshua Tauberer
c26bc841a2 more for dnspython exception with IPv6 addresses
fixes #945, corrects prev commit (#947) in case of multiple AAAA records, adds changelog
2016-09-23 07:41:24 -04:00
Mathis Hoffmann
163daea41c dnspython exception with IPv6 addresses
see #945, merges #947
2016-09-23 07:35:53 -04:00
Corey Hinshaw
d8316119eb Use Roundcube identities to populate Z-Push From name 2016-09-19 11:10:44 -04:00
Scott Bronson
102b2d46ab typo fix: seconday -> secondary (#939) 2016-09-18 08:10:49 -04:00
Joshua Tauberer
58541c467f merge #936 - fix wonky free disk space messages - from cmsirbu/master
fix status_checks.py free disk space reporting, fixes #932
2016-09-16 07:31:57 -04:00
cs@twoflower
00bd23eb04 fix status_checks.py free disk space reporting #932 2016-09-15 17:01:21 +01:00
Joshua Tauberer
d73d1c6900 changelog typos 2016-08-24 07:47:55 -04:00
Joshua Tauberer
fc0abd5b4d confirm that fail2ban is protecting pop3s, closes #629 2016-08-22 19:18:23 -04:00
Joshua Tauberer
27b4edfc76 merge v0.19b hot fix release 2016-08-20 11:50:26 -04:00
Joshua Tauberer
ba75ff7820 v0.19b 2016-08-20 11:48:08 -04:00
Joshua Tauberer
a14b17794b simplify how munin-cgi-graph is called to reduce the attack surface area
Seems like if REQUEST_METHOD is set to GET, then we can drop two redundant ways the query string is given. munin-cgi-graph itself reads the environment variables only, but its calls to Perl's CGI::param will look at the command line if REQUEST_METHOD is not used, otherwise it uses environment variables like CGI used to work.

Since this is all behind admin auth anyway, there isn't a public vulnerability. #914 was opened without comment which lead me to notice the redundancy and worry about a vulnerability, before I realized this is admin-only anyway.

The vulnerability was created by 6d6f3ea391.

See #914.

This is the v0.19b hotfix commit.
2016-08-20 11:47:44 -04:00
Joshua Tauberer
35a360ef0b simplify how munin-cgi-graph is called to reduce the attack surface area
Seems like if REQUEST_METHOD is set to GET, then we can drop two redundant ways the query string is given. munin-cgi-graph itself reads the environment variables only, but its calls to Perl's CGI::param will look at the command line if REQUEST_METHOD is not used, otherwise it uses environment variables like CGI used to work.

Since this is all behind admin auth anyway, there isn't a public vulnerability. #914 was opened without comment which lead me to notice the redundancy and worry about a vulnerability, before I realized this is admin-only anyway.
2016-08-19 12:42:43 -04:00
Joshua Tauberer
86457e5bc4 merge: fail2ban broke, released v0.19a 2016-08-18 08:39:31 -04:00
Joshua Tauberer
7c9f3e0b23 v0.19a 2016-08-18 08:36:28 -04:00
Joshua Tauberer
83d8dbca3e fail2ban won't start until the roundcube log file is created
fixes #911
2016-08-18 08:32:14 -04:00
Joshua Tauberer
8cf2e468bd [merge #900] Adding a Code of Conduct
Merge pull request #900 from mail-in-a-box/code_of_conduct
2016-08-15 20:10:37 -04:00
Joshua Tauberer
440a545010 some improvements suggested by the community 2016-08-15 20:09:05 -04:00
Marius Blüm
942bcfc7c5 Update Bootstrap to 3.3.7 (#909)
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-08-15 18:06:12 -04:00
ReadmeCritic
4f2d16a31d Update README URLs based on HTTP redirects (#908) 2016-08-15 11:07:09 -04:00
Joshua Tauberer
e9368de462 [merge #902] Upgrade ownCloud from 8.2.3 to 8.2.7
Merge https://github.com/mar1u5/mailinabox

fixes #901
2016-08-13 17:36:08 -04:00
Joshua Tauberer
cdd0a821eb v0.19
closes #898
2016-08-13 17:27:10 -04:00
Marius Blüm
6f165d0aeb Update Changelog
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-08-09 00:58:10 +02:00
Marius Blüm
6c22c0533e Upgrade ownCloud from 8.2.3 to 8.2.7
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-08-09 00:53:15 +02:00
Joshua Tauberer
d38b732b0a add a Code of Conduct 2016-08-08 08:19:42 -04:00
Joshua Tauberer
81b5af6b64 document fail2ban filters in security.md 2016-08-08 07:55:46 -04:00
Joshua Tauberer
fc5cc9753b roundcube 1.2.1 2016-08-08 07:32:02 -04:00
Joshua Tauberer
1aca6fe08f some minor tweaks to the new users/aliases API documentation 2016-08-08 07:28:10 -04:00
Joshua Tauberer
cf3e1cd595 add SRV records for CardDAV/CalDAV
DavDroid's latest version's account configuration no longer just asked for a hostname. Its email address & password configuration mode did not work without a SRV record.
2016-07-31 20:53:57 -04:00
Joshua Tauberer
b044dda28f put the ufw status checks in the network section, add a punctuation mark, add changelog entry 2016-07-29 09:23:36 -04:00
Joshua Tauberer
f66f39b61d Merge branch 'ufw_status_check' of https://github.com/yodax/mailinabox 2016-07-29 09:16:22 -04:00
Joshua Tauberer
6de7d59f14 changelog entries 2016-07-29 09:12:01 -04:00
Michael Kroes
9c8f2e75fc allow i686 as a supported architecture
This is checked during preflight. See https://github.com/mail-in-a-box/mailinabox/issues/885 (#889)
2016-07-29 09:07:16 -04:00
Joshua Tauberer
cbc4bf553d Merge pull request #880 from schlypel/master
Added information about API endpoints
2016-07-29 09:04:27 -04:00
Michael Kroes
4e3cfead46 Add HSTS to the control panel headers (#879) 2016-07-29 09:01:40 -04:00
Joshua Tauberer
8844a9185f Merge pull request #798 from mail-in-a-box/fail2banjails
add fail2ban jails for ownCloud, postfix submission, roundcube, and the Mail-in-a-Box management daemon
2016-07-29 08:52:44 -04:00
schlypel
3249a55f3a added API info to users page template 2016-06-29 13:35:42 +02:00
schlypel
b58fb54725 added API info to aliases page template 2016-06-29 13:34:54 +02:00
Joshua Tauberer
82903cd09e Merge pull request #857 from biermeester/master
Small extension to mail log management script
2016-06-27 06:17:16 -04:00
Michael Kroes
fb14e30feb Remove owncloud log configuration from initial setup and only apply it during the configuration updates. This applies to both the timezone and the log format 2016-06-27 06:03:24 -04:00
Michael Kroes
d9ac321f25 Owncloud needs more time to detect blocks. It doesn't respond as fast as the other services. Also owncloud logs UTC (since latest update) even though the timezone is not UTC. Also to detect a block, we get a timeout instead of a refused) 2016-06-27 06:03:19 -04:00
Michael Kroes
bf5e9200f8 Update owncloud url to use webdav and increase http timeout 2016-06-27 06:03:14 -04:00
Joshua Tauberer
5f5f00af4a for DANE, the smtp_tls_mandatory_protocols setting seems like it also needs to be set (unlike the cipher settings, this isn't documented to be in addition to the non-mandatory setting) 2016-06-12 09:11:55 -04:00
Joshua Tauberer
6b73bb5d80 outbound SMTP connections should use the same TLS settings as inbound: drop SSLv2, SSLv3, anonymous ciphers, RC4 2016-06-12 09:11:54 -04:00
Joshua Tauberer
3055f9a79c drop SSLv3, RC4 ciphers from SMTP port 25
Per http://googleappsupdates.blogspot.ro/2016/05/disabling-support-for-sslv3-and-rc4-for.html, Google is about to do the same.

fixes #611
2016-06-12 09:11:50 -04:00
Rinze
1c84e0aeb6 Added received mail count to hourly activity overview in mail log management script 2016-06-10 13:08:57 +02:00
Rinze
ae1b56d23f Added POP3 support to mail log management script 2016-06-10 11:19:03 +02:00
Rinze
946cd63e8e Mail log management script cleanup 2016-06-10 10:32:32 +02:00
Michael Kroes
01fa8cf72c add fail2ban jails for ownCloud, postfix submission, roundcube, and the Mail-in-a-Box management daemon
(tests squashed into this commit by josh)
2016-06-06 09:13:10 -04:00
Chris Blankenship
fac8477ba1 Configured Dovecot to log into its own logfile 2016-06-06 08:21:44 -04:00
aspdye
61744095a8 Update Roundcube to 1.2.0
closes #840
2016-06-06 07:32:54 -04:00
Joshua Tauberer
d5b38a27e6 run roundcube's database migration script on every update
There hasn't been a sqlite migration yet, since Mail-in-a-Box's creation, but with Roundcube 1.2 there will be.
2016-06-06 07:28:12 -04:00
Joshua Tauberer
6666d28c44 v0.18c 2016-06-02 15:47:45 -04:00
Joshua Tauberer
66675ff2e9 Dovecot LMTP accepted all mail regardless of whether destination was a user, broken by ae8cd4ef, fixes #852
In the earlier commit, I added a Dovecot userdb lookup. Without a userdb lookup, Dovecot would use the password db for user lookups. With a userdb lookup we can support iterating over users.

But I forgot the WHERE clause in the query, resulting in every incoming message being accepted if the user database contained any users at all. Since the mailbox path template is the same for all users, mail was delivered correctly except that mail that should have been rejected was delivered too.
2016-06-02 08:05:34 -04:00
Joshua Tauberer
9ee2d946b7 Merge pull request #821 from m4rcs/before-backup
Added a pre-backup script to complement post-backup script.
2016-05-17 19:48:14 -04:00
Arnaud
ff7d4196a6 target to blank for munin link in tempalte (#822)
adding :
target="_blank"
to 
<li><a href="/admin/munin">Munin Monitoring</a></li> on line 96
Why ?
Because when you click on munin link, and follow links, you lose your index, or click back many times...
So i propose my pull request.
Et voilà ^^
2016-05-17 19:46:45 -04:00
aspdye
490b36d86c Fix #819 (#823) 2016-05-17 19:46:10 -04:00
Joshua Tauberer
867d9c4669 v0.18b 2016-05-16 07:17:20 -04:00
Joshua Tauberer
1ad5892acd can't change roundcube's default_host setting, partially reverts 6d259a6e12
The default_host setting is a part of the internal username key. We can't change that without causing Roundcube to create new internal user accounts.
2016-05-16 07:14:45 -04:00
Joshua Tauberer
94b7c80792 v0.18 2016-05-15 20:41:31 -04:00
Marc Schiller
69bd137b4e Added a pre-backup script to complement post-backup script. 2016-05-11 10:11:16 +02:00
Joshua Tauberer
ae8cd4efdf support 'dovecot -A' iteration of all users 2016-05-06 09:16:48 -04:00
Joshua Tauberer
6d259a6e12 use "127.0.0.1" throughout rather than mixing use of an IP address and "localhost"
On some machines localhost is defined as something other than 127.0.0.1, and if we mix "127.0.0.1" and "localhost" then some connections won't be to to the address a service is actually running on.

This was the case with DKIM: It was running on "localhost" but Postfix was connecting to it at 127.0.0.1. (https://discourse.mailinabox.email/t/opendkim-is-not-running-port-8891/1188/12.)

I suppose "localhost" could be an alias to an IPv6 address? We don't really want local services binding on IPv6, so use "127.0.0.1" to be explicit and don't use "localhost" to be sure we get an IPv4 address.

Fixes #797
2016-05-06 09:10:38 -04:00
Joshua Tauberer
e7fffc66c7 changelog tweaks, fixes #805 2016-05-06 08:51:53 -04:00
aspdye
8548ede638 Merge pull #806 - Update Roundcube to 1.1.5 2016-04-24 06:31:28 -04:00
Joshua Tauberer
6eeb107ee3 Merge #795 - Upgrade Bootstrap 3.3.5 to 3.3.6 2016-04-24 06:27:50 -04:00
Joshua Tauberer
31eefa18da Merge #793 - Hostname as Roundcube Name 2016-04-23 13:45:09 -04:00
Joshua Tauberer
20adbb51cb Merge #804 - Make clear that Let's Encrypt is reccomended! 2016-04-23 09:51:44 -04:00
aspdye
79a39d86f9 reseller -> provider 2016-04-23 15:18:21 +02:00
aspdye
0ebf33e9df Make clear that Let's Encrypt is reccomended! 2016-04-23 11:35:02 +02:00
Joshua Tauberer
d3818d1db6 changelog entries 2016-04-13 18:42:53 -04:00
aspdye
f65d9d3196 Upgrade Bootstrap 3.3.5 to 3.3.6 2016-04-09 13:27:27 +02:00
aspdye
74fea6b93e Hostname as Roundcube Name 2016-04-09 10:23:20 +02:00
Joshua Tauberer
7a935d8385 Merge #791 - Add ownCloud 8.2.3 update to changelog 2016-04-08 08:20:06 -04:00
aspdye
7e0f534aea Add ownCloud update to changelog 2016-04-08 14:04:15 +02:00
Michael Kroes
736b3de221 Improve matching of ufw output. Reuse network service list. Improve messages 2016-04-07 16:03:28 +02:00
Joshua Tauberer
5628f8eecb Merge #773 - Set the hostname of the box during the setup 2016-04-07 09:44:39 -04:00
Joshua Tauberer
9cc5160c38 Merge #789 - Update to ownCloud v8.2.3 2016-04-07 09:32:24 -04:00
Michael Kroes
42f2e983e5 Merge branch 'master' into ufw_status_check 2016-04-07 15:13:59 +02:00
Michael Kroes
bc40134b7b Remove comment about loopback interface 2016-04-07 10:55:20 +02:00
Michael Kroes
3649ba1ce9 Merge branch 'master' into hostname 2016-04-07 10:54:53 +02:00
kurt89523
22395bdb8b Update to ownCloud v8.2.3 2016-04-06 17:31:59 -07:00
Joshua Tauberer
30c89be982 merge #771 - stop fail2ban recidive emails
The emails were not deliverable anyway.
2016-04-06 19:03:44 -04:00
Joshua Tauberer
853b641d1b Merge #787 - Add SRV record to the Custom DNS page 2016-04-05 07:17:12 -04:00
msgerbs
703a963ae5 Add SRV record to the Custom DNS page
Add SRV to the drop-down to add a custom DNS zone. I made this change on my up-to-date install and it worked without any issues.
2016-04-05 00:54:26 -05:00
Michael Kroes
c9f30e8059 Add status checks for ufw 2016-04-02 13:41:16 +02:00
Joshua Tauberer
1a1d125b31 merge hotfix release tag 'v0.17c' into master
The hotfixes were all already applied to master in original PRs. This merge merely brings over the CHANGELOG and the updated install instructions (v0.17b=>v0.17c), including to bootstrap.sh which is what triggers v0.17c being the latest release.
2016-04-01 08:00:10 -04:00
Joshua Tauberer
86881c0107 v0.17c 2016-04-01 07:58:28 -04:00
Joshua Tauberer
e65c77588e hotfix merge #776 - some owncloud paths were improperly exposed over http 2016-04-01 07:58:24 -04:00
Joshua Tauberer
3843f63416 hotfix merge #772 - yodax/generic-login-message
Make control panel login failed messages generic - don't reveal if an email address has an account on the system.
2016-03-31 10:46:38 -04:00
Joshua Tauberer
703e6795e8 hotfix merge #769 - update the Roundcube html5_notifier plugin from version 0.6 to 0.6.2
fixes Roundcube getting stuck for some people, hopefully fixes #693
2016-03-31 10:46:34 -04:00
Joshua Tauberer
b3223136f4 hotfix - install roundcube from our own mirror, hosted in Josh's AWS S3 account, because sourceforge is down all the time
fixes #750, see #701, see #370

was df92a10eba
2016-03-31 10:35:48 -04:00
Joshua Tauberer
aa1fdaddaf hotfix merge #755 - Prevent click jacking of the management interface 2016-03-31 10:34:52 -04:00
Joshua Tauberer
7fa9baf308 hotfix merge #744 - Fix for putty Line Drawing issues 2016-03-31 10:33:42 -04:00
Joshua Tauberer
36d51bbde0 merge #776 - some owncloud paths were improperly exposed over http 2016-03-31 10:20:21 -04:00
Joshua Tauberer
eb8cfaab75 changelog entry for html5_notifier bump 2016-03-31 10:20:13 -04:00
Tibor Blaho
c5e8a975cd Fix denied ownCloud nginx locations 2016-03-31 00:07:48 +02:00
Michael Kroes
3210ccdcac Don't set the hostname on the loopback 127.0.1.1 2016-03-26 15:41:20 +01:00
Joshua Tauberer
252c35c66e Merge pull request #772 from yodax/generic-login-message
Make control panel login failed messages generic - don't reveal if an email address has an account on the system.
2016-03-26 09:22:02 -04:00
Michael Kroes
c910a58f07 Set the hostname of the box during the setup 2016-03-26 14:15:28 +01:00
Michael Kroes
f292e8fc5b Add generic login failed message 2016-03-26 14:06:43 +01:00
Michael Kroes
4d7229ccb0 Add documentation on why the notification was removed from the recidive jail 2016-03-26 13:37:33 +01:00
Joshua Tauberer
1e1c3cbd00 Merge pull request #768 from yodax/web
Static web hosting, instructions weren't rendered
2016-03-26 07:46:49 -04:00
Joshua Tauberer
611e9cc84d merges #769 - update the Roundcube html5_notifier plugin from version 0.6 to 0.6.2
fixes Roundcube getting stuck for some people, hopefully fixes #693
2016-03-26 07:43:22 -04:00
Michael Kroes
454a2b167b Stop fail2ban recidive from sending emails, like all other jails 2016-03-26 09:04:51 +01:00
david
f6e0af124f updated html5_notifier version to 0.6.2 in setup 2016-03-25 20:16:51 +01:00
Michael Kroes
d7d8bda0a4 Instructions on how to create a web site for a domain weren't rendered. Users would miss the step about manually creating the directory to put files in there and wouldn't see anything happen 2016-03-25 13:37:55 +01:00
Joshua Tauberer
df92a10eba install roundcube from our own mirror, hosted in Josh's AWS S3 account, because sourceforge is down all the time
fixes #750, see #701, see #370
2016-03-23 17:31:24 -04:00
Joshua Tauberer
74a0359cec Merge pull request #763 from Neopallium/master
Fix creation of custom MX records.
2016-03-23 17:22:42 -04:00
Joshua Tauberer
336b95b3d5 Merge pull request #756 from yodax/preflight_arm
Add a preflight check for supported architecture
2016-03-23 17:19:21 -04:00
Joshua Tauberer
56591abbc2 merge #766 - Configure bayes_file_mode in spamassassin/local.cf 2016-03-23 17:17:30 -04:00
Joshua Tauberer
313a86d0fa add changelog entry for bayes file permissions 2016-03-23 17:16:50 -04:00
Joshua Tauberer
083e3cf755 merge #757 (squashed) - add swap space to low-memory systems 2016-03-23 17:07:40 -04:00
Michael Kroes
696bbe4e82 Add a swap file to the system if system memory is less than 2GB, 5GB of free disk space is available, and if no swap file yet exists 2016-03-23 17:07:04 -04:00
Joshua Tauberer
3d4cabbcd5 merge #755 - Prevent click jacking of the management interface 2016-03-23 16:53:48 -04:00
Joshua Tauberer
cdedaed3b0 merge #744 - Fix for putty Line Drawing issues 2016-03-23 16:51:01 -04:00
Joshua Tauberer
c01f903413 edit NCURSES_NO_UTF8_ACS's comment, add changelog entry 2016-03-23 16:50:27 -04:00
Joshua Tauberer
5edefbec27 merge #735 - Allow a server to be rebooted when a reboot is required 2016-03-23 16:39:40 -04:00
Joshua Tauberer
67555679bd move the reboot button, fix grammar, refactor check for DRY, add changelog entry 2016-03-23 16:37:15 -04:00
Joshua Tauberer
546d6f0026 merge #674 - Support munin's cgi dynazoom 2016-03-23 16:10:30 -04:00
Joshua Tauberer
bd86d44c8b simplify the munin_cgi wrapper / add changelog entry 2016-03-23 16:09:19 -04:00
Robert G. Jakabosky
72fcb005b2 Check MX priority. 2016-03-22 03:07:14 +08:00
Robert G. Jakabosky
84638ab11e Fix creation of custom MX records. 2016-03-21 21:12:08 +08:00
yodax
84f4509b48 Configure bayes_file_mode in spamassassin/local.cf 2016-03-20 05:55:58 +01:00
Michael Kroes
35a593af13 Improve preflight message 2016-03-14 07:14:09 +01:00
Michael Kroes
f69d6e9015 Add a preflight check for supported architecture 2016-03-14 07:00:33 +01:00
Michael Kroes
44705a32b7 Never allow admin panel to be inside a frame, use both modern and old headers. Also set no content sniffing 2016-03-13 18:40:02 +01:00
Michael Kroes
e343061cf4 Prevent clickjacking of management interface 2016-03-13 18:23:10 +01:00
Joshua Tauberer
65add24e2a Merge pull request #751 from yodax/wgetrc
Add a preflight check for ~/.wgetrc
2016-03-11 10:29:24 -05:00
Michael Kroes
33a9fb6aa2 Add a better message 2016-03-11 15:14:37 +01:00
Michael Kroes
0bc5d20e8f Add check for user overrides to wgetrc 2016-03-11 15:10:31 +01:00
Joshua Tauberer
49ea9cddd1 ssl_certificates: also forgot to catch free_tls_certificates.client.RateLimited 2016-03-06 14:39:34 -05:00
c0h1b4
6a48cdcdf3 Fix for putty Line Drawing issues
Fix for putty (Windows) Line Drawing characters to be shown correctly.
2016-03-01 10:40:39 -03:00
Joshua Tauberer
f78f039776 merge point release v0.17b
ownCloud moved their source code to a new location, breaking our installation script.
2016-03-01 07:24:06 -05:00
Joshua Tauberer
d881487d68 v0.17b 2016-03-01 07:23:20 -05:00
Joshua Tauberer
33d07b2b54 ownCloud moved their source code to a new location, breaking our installation script.
Fixes #741.
2016-03-01 07:23:16 -05:00
Joshua Tauberer
3bbec18ac6 Merge pull request #734 from yodax/dynamicpool
Create a temporary multiprocessing pool
2016-02-28 12:39:11 -05:00
Joshua Tauberer
fc5c198646 Merge pull request #728 from yodax/noexec
Add check to preflight for exec on tmp
2016-02-28 12:38:43 -05:00
Joshua Tauberer
2be373fd06 Merge pull request #727 from yodax/userlist
Allow files in /home/user-data/mail/mailboxes
2016-02-28 12:33:38 -05:00
Michael Kroes
b71ad85e9f Restore an empty line 2016-02-26 09:51:22 +01:00
Michael Kroes
86d3e9da86 Merge branch 'master' into reboot 2016-02-26 09:49:03 +01:00
yodax
f53d3bc390 Merge branch 'master' into dynamicpool 2016-02-26 09:20:23 +01:00
Joshua Tauberer
f9ca440ce8 v0.17 2016-02-25 18:36:14 -05:00
Michael Kroes
8ea2f5a766 Allow a server to be rebooted when a reboot is required 2016-02-25 21:56:27 +01:00
yodax
6c1357e16c Merge branch 'master' into dynamicpool 2016-02-23 17:01:13 +01:00
Joshua Tauberer
d880f088be fix changelog description of a bug, see #725 2016-02-23 10:24:26 -05:00
Joshua Tauberer
5cabfd591b (re-fix) mail sent from an address on a subdomain of a domain hosted by the box (a non-zone domain) would never be DKIM-signed because only zones were included in the openDKIM configuration, mistakenly
This was originally fixed in 143bbf37f4 (February 16, 2015). Then I broke it in 7a93d219ef (November 2015) while doing some refactoring ahead of v0.15.
2016-02-23 10:16:04 -05:00
yodax
721730f0e8 Create a temporary multiprocessing pool 2016-02-23 06:32:01 +01:00
Joshua Tauberer
af80849857 Merge pull request #732 from yodax/memory
Reduce percentages for required free memory checks
2016-02-22 15:02:50 -05:00
yodax
7a191e67b8 Add a changelog entry 2016-02-22 21:01:33 +01:00
Joshua Tauberer
4b2e48f2c0 Merge pull request #726 from yodax/login
When previous panel was login, move to system_status
2016-02-22 14:44:23 -05:00
Joshua Tauberer
eb545d7941 Merge pull request #733 from yodax/daemons
Reduce number of processes in the pool to 5
2016-02-22 14:42:20 -05:00
yodax
a2e6e81697 Add a changelog entry 2016-02-22 19:14:46 +01:00
yodax
1b24e2cbaf Reduce percentages for required memory checks 2016-02-22 17:49:19 +01:00
yodax
0843159fb4 Reduce number of processes in the pool to 5 2016-02-22 17:38:30 +01:00
Michael Kroes
a7e60af93f Update comments 2016-02-21 12:47:09 -05:00
Michael Kroes
42f879687f Add check to preflight for exec on tmp 2016-02-21 12:43:04 -05:00
yodax
057903a303 Allow files in /home/user-data/mail/mailboxes 2016-02-21 13:49:07 +01:00
yodax
b8e99c30a2 When previous panel was login, move to system_status 2016-02-20 18:42:28 +01:00
Joshua Tauberer
3d933c16d0 Merge pull request #718 from shakaran/patch-1
Fix small typo in comments
2016-02-18 17:49:35 -05:00
Ángel Guzmán Maeso
e785886447 Fix small typo in comments 2016-02-18 15:38:33 +01:00
Joshua Tauberer
23ecff04b8 the logic in 4ed23f44e6 for taking backups more often was partly backward 2016-02-18 07:50:59 -05:00
Joshua Tauberer
a0bae5db5c update changelog 2016-02-18 07:18:51 -05:00
Joshua Tauberer
86368ed165 clean up apt_install lines and comments in setup/management.sh 2016-02-18 06:59:38 -05:00
Joshua Tauberer
5e4c0ed825 Revert "install boto (py2) via the package manager, not pip (used by duplicity)"
This reverts commit b32cb6229b.

Fixes #627. Fixes #653. Closes #714.
2016-02-18 06:54:23 -05:00
Joshua Tauberer
ffa9dc5d67 Merge pull request #716 from pra85/patch-1
Fix a typo in Readme
2016-02-18 06:44:25 -05:00
Prayag Verma
43cb6c4995 Fix a typo in Readme
`matchs` → `matches`
2016-02-18 09:47:47 +05:30
Joshua Tauberer
36cb2ef41d missing elif 2016-02-16 09:11:54 -05:00
Joshua Tauberer
098e250cc4 bump free_tls_certificates, fixes #695, if a challenge fails dont cache it permanently (or at all) 2016-02-16 09:08:58 -05:00
Joshua Tauberer
3d5a35b184 typo 2016-02-15 18:47:19 -05:00
Joshua Tauberer
87d3f2641d merge #685 - tweak postfix mail queue/warn/bounce times 2016-02-15 18:44:56 -05:00
Joshua Tauberer
c6c75c5a17 document the default values for delay_warning_time, maximal_queue_lifetime, bounce_queue_lifetime 2016-02-15 18:38:55 -05:00
Joshua Tauberer
1ba44b02d4 forgot to catch free_tls_certificates.client.ChallengeFailed
Provisioning could crash if, e.g., the DNS we see is different from the DNS Let's Encrypt sees.

see #695, probably fixes it
2016-02-15 18:22:16 -05:00
Joshua Tauberer
6fd4cd85ca Merge pull request #712 from s4wny/patch-2
Added a warning to the installation / setup script
2016-02-14 14:29:13 -05:00
Sony?
6182347641 spelling box->Box 2016-02-14 20:24:00 +01:00
Sony?
401b0526a3 Added a warning to the installation / setup script
See pull request #638 and issue #635 for more information.
2016-02-14 19:40:43 +01:00
Joshua Tauberer
2f24328608 before the user agrees to Let's Encrypt's ToS the admin could get a nightly email with weird interactive text
Made a mistake refactoring the headless variable earlier.

fixes #696
2016-02-13 12:38:16 -05:00
Joshua Tauberer
8ea42847da nightly status checks could fail if any domains had non-ASCII characters
https://discourse.mailinabox.email/t/status-check-emails-empty-after-upgrading-to-v0-16/1082/3

A user on that thread suggests an alternate solution, adding `PYTHONIOENCODING=utf-8` to `/etc/environment`. Python docs say that affects stdin/out/err. But we also use these environment variables elsewhere to ensure that config files we read/write are opened with UTF8 too. Maybe all that can be simplified too.
2016-02-13 11:51:06 -05:00
Joshua Tauberer
4ed23f44e6 take a full backup more often so we don't keep backups around for so long 2016-02-05 11:08:33 -05:00
Joshua Tauberer
178527dab1 convert the backup increment time to the local timezone, fixes #700
Duplicity gives times in UTC. We were assuming times were in local time.
2016-02-05 08:58:07 -05:00
Joshua Tauberer
f5c376dca8 Merge pull request #699 from BastianPoe/patch-1
Fix: Correct IP is reported when using custom DNS
2016-02-04 15:42:10 -05:00
Wolf-Bastian Pöttner
239eac662c Fix: Correct IP is reported when using custom DNS
Fix bug that reports wrong ip, when custom DNS is enabled
2016-02-04 21:32:11 +01:00
Joshua Tauberer
4e18f66db6 tls control panel: only show integral seconds while waiting the requested time from Lets Encrypt, in case we got back a non-integral number of seconds to wait 2016-02-03 08:21:22 -05:00
Joshua Tauberer
77937df955 bind postfix to the right network interface when sending outbound mail so that SPF checks on the receiving end will pass
fixes #3 (again)
2016-02-01 12:36:52 -05:00
Joshua Tauberer
4db8efa0df bump Roundcube to 1.1.4 2016-02-01 12:31:42 -05:00
Joshua Tauberer
66c80bd16a Merge pull request #688 from OmgImAlexis/patch-1
fixed typo
2016-01-30 19:31:47 -05:00
X O
5895aeecd7 fixed typo 2016-01-31 11:01:00 +10:30
Joshua Tauberer
83ffc99b9c change the public URL of bootstrap.sh to setup.sh 2016-01-30 11:19:51 -05:00
Joshua Tauberer
3615772b2d v0.16 2016-01-30 11:15:14 -05:00
Joshua Tauberer
78729bd277 update CHANGELOG 2016-01-27 20:23:41 -05:00
dofl
85a9a1608c Update mail-postfix.sh 2016-01-21 16:05:43 +01:00
dofl
2e693f7011 Update mail-postfix.sh
Updated according to Josh's latest reaction. Sounds good.
2016-01-21 08:38:39 +01:00
dofl
6f0220da4b Update mail-postfix.sh
Same result as maximal_queue_lifetime and bounce_queue_lifetime, but complies with rfc2821.
2016-01-20 15:34:22 +01:00
dofl
09a45b4397 Update mail-postfix.sh
The default timeout for Postfix's maximal_queue_lifetime and bounce_queue_lifetime is 5 days. This is way too long if you expect someone to have an answer and after 5 days you'll get the message that it's not delivered. This disrupts communication. It would be more responsive if the user got the 'can't deliver' error after 24 hours.
2016-01-20 13:25:41 +01:00
mike
6b408ef824 Use utils.shell instead of subprocess.Popen 2016-01-14 10:24:04 -05:00
Joshua Tauberer
62b6117638 Merge pull request #675 from jeroenj/patch-1
Removes border and rounded corners from navbar
2016-01-14 09:53:36 -05:00
Jeroen Jacobs
70111dafbc Removes border and rounded corners from navbar 2016-01-14 15:48:39 +01:00
Joshua Tauberer
faaa74c3a7 tls: hide extra reasons why domains aren't getting a new certificate during setup 2016-01-14 07:21:08 -05:00
mike
8932aaf4ef needed libcgi-fast-perl and chown log files 2016-01-13 23:55:45 -05:00
mike
6d6f3ea391 Added ability to use munin's dynazoom 2016-01-13 22:20:33 -05:00
Joshua Tauberer
2ad7d0830e add exception handling for what_version_is_this, fixes #659 2016-01-09 09:23:07 -05:00
Joshua Tauberer
5045e206c2 roundcube file ownership should not preserve uid/gid from the release tarball, tar (when run as root) should always extract using --no-same-owner, fixes #667 2016-01-09 09:17:45 -05:00
Joshua Tauberer
07f9228694 Merge branch 'letsencrypt' for automatic provisioning of TLS certificates from Let's Encrypt 2016-01-09 08:58:35 -05:00
Joshua Tauberer
50b5b91216 Merge release branch v0.15a
v0.15a

Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14. This is now fixed.
2016-01-09 08:48:37 -05:00
Joshua Tauberer
413af1fe67 Merge pull request #668 from guyzmo/z-push-smtp-fix
Fixing issue making it impossible to send mail from Z-Push
2016-01-09 08:41:59 -05:00
Bernard `Guyzmo` Pratz
b09cbb0ca4 Fixing issue making it impossible to send mail from Z-Push
* added IMAP_SMTP_METHOD to z_push/backend_imap
 * reverting that line accidentally deleted in commit 5055ef
 * cf pull request GH-580 that commit is part of

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>
2016-01-08 16:43:09 +00:00
Joshua Tauberer
5b135738b4 Merge pull request #661 from baltoche/master
Gandi tells me that .be TLD only supports algorithms 8 and 10 for DNSSEC
2016-01-05 11:10:21 -05:00
baltoche
36e5772a8e Update dns_update.py 2016-01-05 16:56:16 +01:00
Joshua Tauberer
2b9fb9643d changelog entries for lets encrypt 2016-01-04 18:43:17 -05:00
Joshua Tauberer
2882e63dd8 second part of provisioning tls certificates from the control panel 2016-01-04 18:43:17 -05:00
Joshua Tauberer
812ef024ef status checks: check that the non-primary domains also resolve over IPv6, if configured 2016-01-04 18:43:17 -05:00
Joshua Tauberer
40cdc5aa30 status checks: if a domain's DNS isnt working dont check the TLS certificate because we cant automatically provision one now anyway 2016-01-04 18:43:17 -05:00
Joshua Tauberer
b8d6226a9a when provisioning tls certs from the command line, specify domain names as command line arguments to force getting certs for those domains 2016-01-04 18:43:17 -05:00
Joshua Tauberer
bac15d3919 provision tls certificates from the control panel 2016-01-04 18:43:16 -05:00
Joshua Tauberer
4b4f670adf s/SSL/TLS/ in user-visible text throughout the project 2016-01-04 18:43:16 -05:00
Joshua Tauberer
b1b57f9bfd don't try to get certs for IDNA domains and report all reasons for not fetching a certificate
fixes #646
2016-01-04 18:43:16 -05:00
Joshua Tauberer
b6933a73fa provision and install free SSL certificates from Let's Encrypt 2016-01-04 18:43:16 -05:00
Joshua Tauberer
5033042b8c backups: email the administrator when there's a problem
Refactor by moving the email-the-admin code out of the status checks and into a new separate tool.

This is why I suppressed non-error output of the backups last commit - so it doesn't send a daily email.
2016-01-04 18:43:02 -05:00
Joshua Tauberer
89a46089ee backups: suppress all output except errors 2016-01-04 18:43:02 -05:00
Joshua Tauberer
e288d7730b backups: trap an error that occurs as early as getting the current backup status 2016-01-04 18:43:02 -05:00
Joshua Tauberer
5ae75e723c Merge pull request #654 from ChlorideCull/cull-fix-typo
Fix miscellaneous typos in comments
2016-01-03 10:53:25 -05:00
Chloride Cull
d6d5009d23 Fix typos in questions.sh
sed s/supress/suppress/g
sed s/depencies/dependencies/g
2016-01-03 16:48:23 +01:00
Joshua Tauberer
06a0e7f3fe merge #584 - Add checks to the management interface to report memory usage 2016-01-01 18:13:21 -05:00
Joshua Tauberer
a9cd72bbf9 tighten the status text strings for free memory, add changelog entry 2016-01-01 18:12:36 -05:00
Joshua Tauberer
f184a74fa0 merge #647 - open the port for Sieve 2016-01-01 17:53:40 -05:00
Joshua Tauberer
682b1dea5e changelog/status checks updated for opening the sieve port 2016-01-01 17:53:05 -05:00
Ralph J.Mayer
2835d9b468 Merge pull request #1 from TabTwo/TabTwo-patch-1
Allow remote client for Sieve
2015-12-31 18:23:30 +01:00
Ralph J.Mayer
afd401c3d4 Allow remote client for Sieve 2015-12-31 18:22:31 +01:00
Michael Kroes
59f8aa1c31 Add checks to the management interface to report memory usage 2015-11-20 01:48:59 -05:00
57 changed files with 2673 additions and 618 deletions

View File

@@ -1,12 +1,210 @@
CHANGELOG
=========
v0.21 (November 30, 2016)
-------------------------
This version updates ownCloud, which may include security fixes, and makes some other smaller improvements.
Mail:
* Header privacy filters were improperly running on the contents of forwarded email --- that's fixed.
* We have another go at fixing a long-standing issue with training the spam filter (because of a file permissions issue).
* Exchange/ActiveSync will now use your display name set in Roundcube in the From: line of outgoing email.
ownCloud:
* Updated ownCloud to version 9.1.1.
Control panel:
* Backups can now be made using rsync-over-ssh!
* Status checks failed if the system doesn't support iptables or doesn't have ufw installed.
* Added support for SSHFP records when sshd listens on non-standard ports.
* Recommendations for TLS certificate providers were removed now that everyone mostly uses Let's Encrypt.
System:
* Ubuntu's "Upgrade to 16.04" notice is suppressed since you should not do that.
* Lowered memory requirements to 512MB, display a warning if system memory is below 768MB.
v0.20 (September 23, 2016)
--------------------------
ownCloud:
* Updated to ownCloud to 8.2.7.
Control Panel:
* Fixed a crash that occurs when there are IPv6 DNS records due to a bug in dnspython 1.14.0.
* Improved the wonky low disk space check.
v0.19b (August 20, 2016)
------------------------
This update corrects a security issue introduced in v0.18.
* A remote code execution vulnerability is corrected in how the munin system monitoring graphs are generated for the control panel. The vulnerability involves an administrative user visiting a carefully crafted URL.
v0.19a (August 18, 2016)
------------------------
This update corrects a security issue in v0.19.
* fail2ban won't start if Roundcube had not yet been used - new installations probably do not have fail2ban running.
v0.19 (August 13, 2016)
-----------------------
Mail:
* Roundcube is updated to version 1.2.1.
* SSLv3 and RC4 are now no longer supported in incoming and outgoing mail (SMTP port 25).
Control panel:
* The users and aliases APIs are now documented on their control panel pages.
* The HSTS header was missing.
* New status checks were added for the ufw firewall.
DNS:
* Add SRV records for CardDAV/CalDAV to facilitate autoconfiguration (e.g. in DavDroid, whose latest version didn't seem to work to configure with entering just a hostname).
System:
* fail2ban jails added for SMTP submission, Roundcube, ownCloud, the control panel, and munin.
* Mail-in-a-Box can now be installed on the i686 architecture.
v0.18c (June 2, 2016)
---------------------
* Domain aliases (and misconfigured aliases/catch-alls with non-existent local targets) would accept mail and deliver it to new mailbox folders on disk even if the target address didn't correspond with an existing mail user, instead of rejecting the mail. This issue was introduced in v0.18.
* The Munin Monitoring link in the control panel now opens a new window.
* Added an undocumented before-backup script.
v0.18b (May 16, 2016)
---------------------
* Fixed a Roundcube user accounts issue introduced in v0.18.
v0.18 (May 15, 2016)
--------------------
ownCloud:
* Updated to ownCloud to 8.2.3
Mail:
* Roundcube is updated to version 1.1.5 and the Roundcube login screen now says "[hostname] Webmail" instead of "Mail-in-a-Box/Roundcube webmail".
* Fixed a long-standing issue with training the spam filter not working (because of a file permissions issue).
Control panel:
* Munin system monitoring graphs are now zoomable.
* When a reboot is required (due to Ubuntu security updates automatically installed), a Reboot Box button now appears on the System Status Checks page of the control panel.
* It is now possible to add SRV and secondary MX records in the Custom DNS page.
* Other minor fixes.
System:
* The fail2ban recidive jail, which blocks long-duration brute force attacks, now no longer sends the administrator emails (which were not helpful).
Setup:
* The system hostname is now set during setup.
* A swap file is now created if system memory is less than 2GB, 5GB of free disk space is available, and if no swap file yet exists.
* We now install Roundcube from the official GitHub repository instead of our own mirror, which we had previously created to solve problems with SourceForge.
* DKIM was incorrectly set up on machines where "localhost" was defined as something other than "127.0.0.1".
v0.17c (April 1, 2016)
----------------------
This update addresses some minor security concerns and some installation issues.
ownCoud:
* Block web access to the configuration parameters (config.php). There is no immediate impact (see [#776](https://github.com/mail-in-a-box/mailinabox/pull/776)), although advanced users may want to take note.
Mail:
* Roundcube html5_notifier plugin updated from version 0.6 to 0.6.2 to fix Roundcube getting stuck for some people.
Control panel:
* Prevent click-jacking of the management interface by adding HTTP headers.
* Failed login no longer reveals whether an account exists on the system.
Setup:
* Setup dialogs did not appear correctly when connecting to SSH using Putty on Windows.
* We now install Roundcube from our own mirror because Sourceforge's downloads experience frequent intermittant unavailability.
v0.17b (March 1, 2016)
----------------------
ownCloud moved their source code to a new location, breaking our installation script.
v0.17 (February 25, 2016)
-------------------------
Mail:
* Roundcube updated to version 1.1.4.
* When there's a problem delivering an outgoing message, a new 'warning' bounce will come after 3 hours and the box will stop trying after 2 days (instead of 5).
* On multi-homed machines, Postfix now binds to the right network interface when sending outbound mail so that SPF checks on the receiving end will pass.
* Mail sent from addresses on subdomains of other domains hosted by this box would not be DKIM-signed and so would fail DMARC checks by recipients, since version v0.15.
Control panel:
* TLS certificate provisioning would crash if DNS propagation was in progress and a challenge failed; might have shown the wrong error when provisioning fails.
* Backup times were displayed with the wrong time zone.
* Thresholds for displaying messages when the system is running low on memory have been reduced from 30% to 20% for a warning and from 15% to 10% for an error.
* Other minor fixes.
System:
* Backups to some AWS S3 regions broke in version 0.15 because we reverted the version of boto. That's now fixed.
* On low-usage systems, don't hold backups for quite so long by taking a full backup more often.
* Nightly status checks might fail on systems not configured with a default Unicode locale.
* If domains need a TLS certificate and the user hasn't installed one yet using Let's Encrypt, the administrator would get a nightly email with weird interactive text asking them to agree to Let's Encrypt's ToS. Now just say that the provisioning can't be done automatically.
* Reduce the number of background processes used by the management daemon to lower memory consumption.
Setup:
* The first screen now warns users not to install on a machine used for other things.
v0.16 (January 30, 2016)
------------------------
This update primarily adds automatic SSL (now "TLS") certificate provisioning from Let's Encrypt (https://letsencrypt.org/).
Control Panel:
* The SSL certificates (now referred to as "TLS ccertificates") page now supports provisioning free certificates from Let's Encrypt.
* Report free memory usage.
* Fix a crash when the git directory is not checked out to a tag.
* When IPv6 is enabled, check that all domains (besides the system hostname) resolve over IPv6.
* When a domain doesn't resolve to the box, don't bother checking if the TLS certificate is valid.
* Remove rounded border on the menu bar.
Other:
* The Sieve port is now open so tools like the Thunderbird Sieve extension can be used to edit mail filters.
* .be domains now offer DNSSEC options supported by the TLD
* The daily backup will now email the administrator if there is a problem.
* Expiring TLS certificates are now automatically renewed via Let's Encrypt.
* File ownership for installed Roundcube files is fixed.
* Typos fixed.
v0.15a (January 9, 2016)
------------------------
Mail:
* Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14. This is now fixed.
* Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14 in some setups. This is now fixed.
v0.15 (January 1, 2016)
-----------------------
@@ -420,4 +618,4 @@ v0.02 (September 21, 2014)
v0.01 (August 19, 2014)
-----------------------
First release.
First versioned release after a year of unversioned development.

48
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,48 @@
# Mail-in-a-Box Code of Conduct
Mail-in-a-Box is an open source community project about working, as a group, to empower ourselves and others to have control over our own digital communications. Just as we hope to increase technological diversity on the Internet through decentralization, we also believe that diverse viewpoints and voices among our community members foster innovation and creative solutions to the challenges we face.
We are committed to providing a safe, welcoming, and harrassment-free space for collaboration, for everyone, without regard to age, disability, economic situation, ethnicity, gender identity and expression, language fluency, level of knowledge or experience, nationality, personal appearance, race, religion, sexual identity and orientation, or any other attribute. Community comes first. This policy supersedes all other project goals.
The maintainers of Mail-in-a-Box share the dual responsibility of leading by example and enforcing these policies as necessary to maintain an open and welcoming environment. All community members should be excellent to each other.
## Scope
This Code of Conduct applies to all places where Mail-in-a-Box community activity is ocurring, including on GitHub, in discussion forums, on Slack, on social media, and in real life. The Code of Conduct applies not only on websites/at events run by the Mail-in-a-Box community (e.g. our GitHub organization, our Slack team) but also at any other location where the Mail-in-a-Box community is present (e.g. in issues of other GitHub organizations where Mail-in-a-Box community members are discussing problems related to Mail-in-a-Box, or real-life professional conferences), or whenever a Mail-in-a-Box community member is representing Mail-in-a-Box to the public at large or acting on behalf of Mail-in-a-Box.
This code does not apply to activity on a server running Mail-in-a-Box software, unless your server is hosting a service for the Mail-in-a-Box community at large.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Showing empathy towards other community members
* Making room for new and quieter voices
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory/unwelcome comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Aggressive and micro-aggressive behavior, such as unconstructive criticism, providing corrections that do not improve the conversation (sometimes referred to as "well actually"s), repeatedly interrupting or talking over someone else, feigning surprise at someone's lack of knowledge or awareness about a topic, or subtle prejudice (for example, comments like "That's so easy my grandmother could do it.", which is prejudicial toward grandmothers).
* Other conduct which could reasonably be considered inappropriate in a professional setting
* Retaliating against anyone who reports a violation of this code.
We will not tolerate harassment. Harassment is any unwelcome or hostile behavior towards another person for any reason. This includes, but is not limited to, offensive verbal comments related to personal characteristics or choices, sexual images or comments, deliberate intimidation, bullying, stalking, following, harassing photography or recording, sustained disruption of discussion or events, nonconsensual publication of private comments, inappropriate physical contact, or unwelcome sexual attention. Conduct need not be intentional to be harassment.
## Enforcement
We will remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not consistent with this Code of Conduct. We may ban, temporarily or permanently, any contributor for violating this code, when appropriate.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project lead, [Joshua Tauberer](https://razor.occams.info/). All reports will be treated confidentially, impartially, consistently, and swiftly.
Because the need for confidentiality for all parties involved in an enforcement action outweighs the goals of openness, limited information will be shared with the Mail-in-a-Box community regarding enforcement actions that have taken place.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant, version 1.4](http://contributor-covenant.org/version/1/4) and the code of conduct of [Code for DC](http://codefordc.org/resources/codeofconduct.html).

View File

@@ -5,3 +5,7 @@ This project is in the public domain. Copyright and related rights in the work w
All contributions to this project must be released under the same CC0 wavier. By submitting a pull request or patch, you are agreeing to comply with this waiver of copyright interest.
[CC0]: http://creativecommons.org/publicdomain/zero/1.0/
## Code of Conduct
This project has a [Code of Conduct](CODE_OF_CONDUCT.md). Please review it when joining our community.

View File

@@ -9,15 +9,15 @@ Mail-in-a-Box helps individuals take back control of their email by defining a o
* * *
I am trying to:
Our goals are to:
* Make deploying a good mail server easy.
* Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web.
* Have automated, auditable, and [idempotent](http://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
* Have automated, auditable, and [idempotent](https://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
* **Not** make a totally unhackable, NSA-proof server.
* **Not** make something customizable by power users.
This setup is what has been powering my own personal email since September 2013.
Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which supersedes the goals above. Please review it when joining our community.
The Box
-------
@@ -28,10 +28,10 @@ It is a one-click email appliance. There are no user-configurable setup options.
The components installed are:
* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([ownCloud](http://owncloud.org/)), Exchange ActiveSync ([z-push](https://github.com/fmbiete/Z-Push-contrib))
* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([ownCloud](https://owncloud.org/)), Exchange ActiveSync ([z-push](https://github.com/fmbiete/Z-Push-contrib))
* 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
* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set
* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/))
It also includes:
@@ -59,20 +59,20 @@ by me:
$ curl -s https://keybase.io/joshdata/key.asc | gpg --import
gpg: key C10BDD81: public key "Joshua Tauberer <jt@occams.info>" imported
$ git verify-tag v0.15a
$ git verify-tag v0.21
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
You'll get a lot of warnings, but that's OK. Check that the primary key fingerprint matchs the
You'll get a lot of warnings, but that's OK. Check that the primary key fingerprint matches the
fingerprint in the key details at [https://keybase.io/joshdata](https://keybase.io/joshdata)
and on my [personal homepage](https://razor.occams.info/). (Of course, if this repository has been compromised you can't trust these instructions.)
Checkout the tag corresponding to the most recent release:
$ git checkout v0.15a
$ git checkout v0.21
Begin the installation.
@@ -85,7 +85,7 @@ Post your question on the [discussion forum](https://discourse.mailinabox.email/
The Acknowledgements
--------------------
This project was inspired in part by the ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) blog post by Drew Crawford, [Sovereign](https://github.com/al3x/sovereign) by Alex Payne, and conversations with <a href="http://twitter.com/shevski" target="_blank">@shevski</a>, <a href="https://github.com/konklone" target="_blank">@konklone</a>, and <a href="https://github.com/gregelin" target="_blank">@GregElin</a>.
This project was inspired in part by the ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) blog post by Drew Crawford, [Sovereign](https://github.com/sovereign/sovereign) by Alex Payne, and conversations with <a href="https://twitter.com/shevski" target="_blank">@shevski</a>, <a href="https://github.com/konklone" target="_blank">@konklone</a>, and <a href="https://github.com/gregelin" target="_blank">@GregElin</a>.
Mail-in-a-Box is similar to [iRedMail](http://www.iredmail.org/) and [Modoboa](https://github.com/tonioo/modoboa).
@@ -95,5 +95,5 @@ The History
* In 2007 I wrote a relatively popular Mozilla Thunderbird extension that added client-side SPF and DKIM checks to mail to warn users about possible phishing: [add-on page](https://addons.mozilla.org/en-us/thunderbird/addon/sender-verification-anti-phish/), [source](https://github.com/JoshData/thunderbird-spf).
* 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.
* 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, [May](https://news.ycombinator.com/item?id=9624267) 2015, and [November](https://news.ycombinator.com/item?id=13050500) 2016.
* 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

@@ -0,0 +1,12 @@
# Fail2Ban filter Mail-in-a-Box management daemon
[INCLUDES]
before = common.conf
[Definition]
_daemon = mailinabox
failregex = Mail-in-a-Box Management Daemon: Failed login attempt from ip <HOST> - timestamp .*
ignoreregex =

View File

@@ -0,0 +1,7 @@
[INCLUDES]
before = common.conf
[Definition]
failregex=<HOST> - .*GET /admin/munin/.* HTTP/1.1\" 401.*
ignoreregex =

View File

@@ -0,0 +1,7 @@
[INCLUDES]
before = common.conf
[Definition]
failregex=Login failed: .*Remote IP: '<HOST>[\)']
ignoreregex =

View File

@@ -0,0 +1,7 @@
[INCLUDES]
before = common.conf
[Definition]
failregex=postfix/submission/smtpd.*warning.*\[<HOST>\]: .* authentication (failed|aborted)
ignoreregex =

View File

@@ -0,0 +1,9 @@
[INCLUDES]
before = common.conf
[Definition]
failregex = IMAP Error: Login failed for .*? from <HOST>\. AUTHENTICATE.*
ignoreregex =

View File

@@ -1,29 +0,0 @@
# Fail2Ban configuration file for Mail-in-a-Box
[DEFAULT]
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
# ping services over the public interface so we should whitelist that address of
# ours too. The string is substituted during installation.
ignoreip = 127.0.0.1/8 PUBLIC_IP
# JAILS
[ssh]
maxretry = 7
bantime = 3600
[ssh-ddos]
enabled = true
[sasl]
enabled = true
[dovecot]
enabled = true
filter = dovecotimap
findtime = 30
maxretry = 20
[recidive]
enabled = true
maxretry = 10

80
conf/fail2ban/jails.conf Normal file
View File

@@ -0,0 +1,80 @@
# Fail2Ban configuration file for Mail-in-a-Box. Do not edit.
# This file is re-generated on updates.
[DEFAULT]
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
# ping services over the public interface so we should whitelist that address of
# ours too. The string is substituted during installation.
ignoreip = 127.0.0.1/8 PUBLIC_IP
[dovecot]
enabled = true
filter = dovecotimap
logpath = /var/log/mail.log
findtime = 30
maxretry = 20
[miab-management]
enabled = true
filter = miab-management-daemon
port = http,https
logpath = /var/log/syslog
maxretry = 20
findtime = 30
[miab-munin]
enabled = true
port = http,https
filter = miab-munin
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 30
[miab-owncloud]
enabled = true
port = http,https
filter = miab-owncloud
logpath = STORAGE_ROOT/owncloud/owncloud.log
maxretry = 20
findtime = 120
[miab-postfix587]
enabled = true
port = 587
filter = miab-postfix-submission
logpath = /var/log/mail.log
maxretry = 20
findtime = 30
[miab-roundcube]
enabled = true
port = http,https
filter = miab-roundcube
logpath = /var/log/roundcubemail/errors
maxretry = 20
findtime = 30
[recidive]
enabled = true
maxretry = 10
action = iptables-allports[name=recidive]
# In the recidive section of jail.conf the action contains:
#
# action = iptables-allports[name=recidive]
# sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log]
#
# The last line on the action will sent an email to the configured address. This mail will
# notify the administrator that someone has been repeatedly triggering one of the other jails.
# By default we don't configure this address and no action is required from the admin anyway.
# So the notification is ommited. This will prevent message appearing in the mail.log that mail
# can't be delivered to fail2ban@$HOSTNAME.
[sasl]
enabled = true
[ssh]
maxretry = 7
bantime = 3600
[ssh-ddos]
enabled = true

View File

@@ -27,9 +27,9 @@ EXEC_AS_USER=root
# Ensure Python reads/writes files in UTF-8. If the machine
# triggers some other locale in Python, like ASCII encoding,
# Python may not be able to read/write files. Here and in
# Python may not be able to read/write files. Set also
# setup/start.sh (where the locale is also installed if not
# already present).
# already present) and management/daily_tasks.sh.
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8

View File

@@ -6,6 +6,10 @@
location /admin/ {
proxy_pass http://127.0.0.1:10222/;
proxy_set_header X-Forwarded-For $remote_addr;
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "frame-ancestors 'none';";
add_header Strict-Transport-Security max-age=31536000;
}
# ownCloud configuration.
@@ -15,7 +19,10 @@
rewrite ^(/cloud/core/doc/[^\/]+/)$ $1/index.html;
location /cloud/ {
alias /usr/local/lib/owncloud/;
location ~ ^/(data|config|\.ht|db_structure\.xml|README) {
location ~ ^/cloud/(build|tests|config|lib|3rdparty|templates|data|README)/ {
deny all;
}
location ~ ^/cloud/(?:\.|autotest|occ|issue|indie|db_|console) {
deny all;
}
}

View File

@@ -1,6 +1,8 @@
## $HOSTNAME
# Redirect all HTTP to HTTPS.
# Redirect all HTTP to HTTPS *except* the ACME challenges (Let's Encrypt TLS certificate
# domain validation challenges) path, which must be served over HTTP per the ACME spec
# (due to some Apache vulnerability).
server {
listen 80;
listen [::]:80;
@@ -12,10 +14,19 @@ server {
# error pages and in the "Server" HTTP-Header.
server_tokens off;
location / {
# Redirect using the 'return' directive and the built-in
# variable '$request_uri' to avoid any capturing, matching
# or evaluation of regular expressions.
return 301 https://$HOSTNAME$request_uri;
}
location /.well-known/acme-challenge/ {
# This path must be served over HTTP for ACME domain validation.
# We map this to a special path where our TLS cert provisioning
# tool knows to store challenge response files.
alias $STORAGE_ROOT/ssl/lets_encrypt/acme_challenges/;
}
}
# The secure HTTPS server.

View File

@@ -6,7 +6,7 @@
************************************************/
define('CALDAV_PROTOCOL', 'https');
define('CALDAV_SERVER', 'localhost');
define('CALDAV_SERVER', '127.0.0.1');
define('CALDAV_PORT', '443');
define('CALDAV_PATH', '/caldav/calendars/%u/');
define('CALDAV_PERSONAL', 'PRINCIPAL');

View File

@@ -7,7 +7,7 @@
define('CARDDAV_PROTOCOL', 'https'); /* http or https */
define('CARDDAV_SERVER', 'localhost');
define('CARDDAV_SERVER', '127.0.0.1');
define('CARDDAV_PORT', '443');
define('CARDDAV_PATH', '/carddav/addressbooks/%u/');
define('CARDDAV_DEFAULT_PATH', '/carddav/addressbooks/%u/contacts/'); /* subdirectory of the main path */

View File

@@ -5,10 +5,10 @@
* Descr : IMAP backend configuration file
************************************************/
define('IMAP_SERVER', 'localhost');
define('IMAP_SERVER', '127.0.0.1');
define('IMAP_PORT', 993);
define('IMAP_OPTIONS', '/ssl/norsh/novalidate-cert');
define('IMAP_DEFAULTFROM', '');
define('IMAP_DEFAULTFROM', 'sql');
define('SYSTEM_MIME_TYPES_MAPPING', '/etc/mime.types');
define('IMAP_AUTOSEEN_ON_DELETE', false);
@@ -23,15 +23,16 @@ define('IMAP_FOLDER_TRASH', 'TRASH');
define('IMAP_FOLDER_SPAM', 'SPAM');
define('IMAP_FOLDER_ARCHIVE', 'ARCHIVE');
// not used
define('IMAP_FROM_SQL_DSN', '');
define('IMAP_FROM_SQL_DSN', 'sqlite:STORAGE_ROOT/mail/roundcube/roundcube.sqlite');
define('IMAP_FROM_SQL_USER', '');
define('IMAP_FROM_SQL_PASSWORD', '');
define('IMAP_FROM_SQL_OPTIONS', serialize(array(PDO::ATTR_PERSISTENT => true)));
define('IMAP_FROM_SQL_QUERY', "select first_name, last_name, mail_address from users where mail_address = '#username@#domain'");
define('IMAP_FROM_SQL_FIELDS', serialize(array('first_name', 'last_name', 'mail_address')));
define('IMAP_FROM_SQL_FROM', '#first_name #last_name <#mail_address>');
define('IMAP_FROM_SQL_QUERY', "SELECT name, email FROM identities i INNER JOIN users u ON i.user_id = u.user_id WHERE u.username = '#username' AND i.standard = 1 AND i.del = 0 AND i.name <> ''");
define('IMAP_FROM_SQL_FIELDS', serialize(array('name', 'email')));
define('IMAP_FROM_SQL_FROM', '#name <#email>');
define('IMAP_FROM_SQL_FULLNAME', '#name');
// not used
define('IMAP_FROM_LDAP_SERVER', '');
define('IMAP_FROM_LDAP_SERVER_PORT', '389');
define('IMAP_FROM_LDAP_USER', 'cn=zpush,ou=servers,dc=zpush,dc=org');
@@ -40,11 +41,12 @@ define('IMAP_FROM_LDAP_BASE', 'dc=zpush,dc=org');
define('IMAP_FROM_LDAP_QUERY', '(mail=#username@#domain)');
define('IMAP_FROM_LDAP_FIELDS', serialize(array('givenname', 'sn', 'mail')));
define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>');
define('IMAP_FROM_LDAP_FULLNAME', '#givenname #sn');
define('IMAP_SMTP_METHOD', 'sendmail');
global $imap_smtp_params;
$imap_smtp_params = array('host' => 'ssl://localhost', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
$imap_smtp_params = array('host' => 'ssl://127.0.0.1', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
define('MAIL_MIMEPART_CRLF', "\r\n");

View File

@@ -1,19 +1,23 @@
#!/usr/bin/python3
# This script performs a backup of all user data:
# 1) System services are stopped while a copy of user data is made.
# 2) An incremental encrypted backup is made using duplicity into the
# directory STORAGE_ROOT/backup/encrypted. The password used for
# encryption is stored in backup/secret_key.txt.
# 3) The stopped services are restarted.
# 5) STORAGE_ROOT/backup/after-backup is executd if it exists.
# 1) System services are stopped.
# 2) STORAGE_ROOT/backup/before-backup is executed if it exists.
# 3) An incremental encrypted backup is made using duplicity.
# 4) The stopped services are restarted.
# 5) STORAGE_ROOT/backup/after-backup is executed if it exists.
import os, os.path, shutil, glob, re, datetime
import os, os.path, shutil, glob, re, datetime, sys
import dateutil.parser, dateutil.relativedelta, dateutil.tz
import rtyaml
from utils import exclusive_process, load_environment, shell, wait_for_service, fix_boto
rsync_ssh_options = [
"--ssh-options='-i /root/.ssh/id_rsa_miab'",
"--rsync-options=-e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
]
def backup_status(env):
# Root folder
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
@@ -44,15 +48,16 @@ def backup_status(env):
# Get duplicity collection status and parse for a list of backups.
def parse_line(line):
keys = line.strip().split()
date = dateutil.parser.parse(keys[1])
date = dateutil.parser.parse(keys[1]).astimezone(dateutil.tz.tzlocal())
return {
"date": keys[1],
"date_str": date.strftime("%x %X"),
"date_str": date.strftime("%x %X") + " " + now.tzname(),
"date_delta": reldate(date, now, "the future?"),
"full": keys[0] == "full",
"size": 0, # collection-status doesn't give us the size
"volumes": keys[2], # number of archive volumes for this backup (not really helpful)
}
code, collection_status = shell('check_output', [
"/usr/bin/duplicity",
"collection-status",
@@ -60,13 +65,13 @@ def backup_status(env):
"--gpg-options", "--cipher-algo=AES256",
"--log-fd", "1",
config["target"],
],
] + rsync_ssh_options,
get_env(env),
trap=True)
if code != 0:
# Command failed. This is likely due to an improperly configured remote
# destination for the backups.
return { }
# destination for the backups or the last backup job terminated unexpectedly.
raise Exception("Something is wrong with the backup: " + collection_status)
for line in collection_status.split('\n'):
if line.startswith(" full") or line.startswith(" inc"):
backup = parse_line(line)
@@ -83,50 +88,66 @@ def backup_status(env):
# This is relied on by should_force_full() and the next step.
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
# Get the average size of incremental backups and the size of the
# most recent full backup.
# Get the average size of incremental backups, the size of the
# most recent full backup, and the date of the most recent
# backup and the most recent full backup.
incremental_count = 0
incremental_size = 0
first_date = None
first_full_size = None
first_full_date = None
for bak in backups:
if first_date is None:
first_date = dateutil.parser.parse(bak["date"])
if bak["full"]:
first_full_size = bak["size"]
first_full_date = dateutil.parser.parse(bak["date"])
break
incremental_count += 1
incremental_size += bak["size"]
# Predict how many more increments until the next full backup,
# and add to that the time we hold onto backups, to predict
# how long the most recent full backup+increments will be held
# onto. Round up since the backup occurs on the night following
# when the threshold is met.
# When will the most recent backup be deleted? It won't be deleted if the next
# backup is incremental, because the increments rely on all past increments.
# So first guess how many more incremental backups will occur until the next
# full backup. That full backup frees up this one to be deleted. But, the backup
# must also be at least min_age_in_days old too.
deleted_in = None
if incremental_count > 0 and first_full_size is not None:
deleted_in = "approx. %d days" % round(config["min_age_in_days"] + (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count) + .5)
# How many days until the next incremental backup? First, the part of
# the algorithm based on increment sizes:
est_days_to_next_full = (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count)
est_time_of_next_full = first_date + datetime.timedelta(days=est_days_to_next_full)
# When will a backup be deleted?
# ...And then the part of the algorithm based on full backup age:
est_time_of_next_full = min(est_time_of_next_full, first_full_date + datetime.timedelta(days=config["min_age_in_days"]*10+1))
# It still can't be deleted until it's old enough.
est_deleted_on = max(est_time_of_next_full, first_date + datetime.timedelta(days=config["min_age_in_days"]))
deleted_in = "approx. %d days" % round((est_deleted_on-now).total_seconds()/60/60/24 + .5)
# When will a backup be deleted? Set the deleted_in field of each backup.
saw_full = False
days_ago = now - datetime.timedelta(days=config["min_age_in_days"])
for bak in backups:
if deleted_in:
# Subsequent backups are deleted when the most recent increment
# in the chain would be deleted.
# The most recent increment in a chain and all of the previous backups
# it relies on are deleted at the same time.
bak["deleted_in"] = deleted_in
if bak["full"]:
# Reset when we get to a full backup. A new chain start next.
# Reset when we get to a full backup. A new chain start *next*.
saw_full = True
deleted_in = None
elif saw_full and not deleted_in:
# Mark deleted_in only on the first increment after a full backup.
deleted_in = reldate(days_ago, dateutil.parser.parse(bak["date"]), "on next daily backup")
# We're now on backups prior to the most recent full backup. These are
# free to be deleted as soon as they are min_age_in_days old.
deleted_in = reldate(now, dateutil.parser.parse(bak["date"]) + datetime.timedelta(days=config["min_age_in_days"]), "on next daily backup")
bak["deleted_in"] = deleted_in
return {
"tz": now.tzname(),
"backups": backups,
}
def should_force_full(env):
def should_force_full(config, env):
# Force a full backup when the total size of the increments
# since the last full backup is greater than half the size
# of that full backup.
@@ -138,8 +159,14 @@ def should_force_full(env):
inc_size += bak["size"]
else:
# ...until we reach the most recent full backup.
# Return if we should to a full backup.
return inc_size > .5*bak["size"]
# Return if we should to a full backup, which is based
# on the size of the increments relative to the full
# backup, as well as the age of the full backup.
if inc_size > .5*bak["size"]:
return True
if dateutil.parser.parse(bak["date"]) + datetime.timedelta(days=config["min_age_in_days"]*10+1) < datetime.datetime.now(dateutil.tz.tzlocal()):
return True
return False
else:
# If we got here there are no (full) backups, so make one.
# (I love for/else blocks. Here it's just to show off.)
@@ -183,7 +210,7 @@ def perform_backup(full_backup):
backup_cache_dir = os.path.join(backup_root, 'cache')
backup_dir = os.path.join(backup_root, 'encrypted')
# Are backups dissbled?
# Are backups disabled?
if config["target"] == "off":
return
@@ -217,12 +244,35 @@ def perform_backup(full_backup):
# will fail. Otherwise do a full backup when the size of
# the increments since the most recent full backup are
# large.
full_backup = full_backup or should_force_full(env)
try:
full_backup = full_backup or should_force_full(config, env)
except Exception as e:
# This was the first call to duplicity, and there might
# be an error already.
print(e)
sys.exit(1)
# Stop services.
shell('check_call', ["/usr/sbin/service", "php5-fpm", "stop"])
shell('check_call', ["/usr/sbin/service", "postfix", "stop"])
shell('check_call', ["/usr/sbin/service", "dovecot", "stop"])
def service_command(service, command, quit=None):
# Execute silently, but if there is an error then display the output & exit.
code, ret = shell('check_output', ["/usr/sbin/service", service, command], capture_stderr=True, trap=True)
if code != 0:
print(ret)
if quit:
sys.exit(code)
service_command("php5-fpm", "stop", quit=True)
service_command("postfix", "stop", quit=True)
service_command("dovecot", "stop", quit=True)
# Execute a pre-backup script that copies files outside the homedir.
# Run as the STORAGE_USER user, not as root. Pass our settings in
# environment variables so the script has access to STORAGE_ROOT.
pre_script = os.path.join(backup_root, 'before-backup')
if os.path.exists(pre_script):
shell('check_call',
['su', env['STORAGE_USER'], '-c', pre_script, config["target"]],
env=env)
# Run a backup of STORAGE_ROOT (but excluding the backups themselves!).
# --allow-source-mismatch is needed in case the box's hostname is changed
@@ -231,6 +281,7 @@ def perform_backup(full_backup):
shell('check_call', [
"/usr/bin/duplicity",
"full" if full_backup else "incr",
"--verbosity", "warning", "--no-print-statistics",
"--archive-dir", backup_cache_dir,
"--exclude", backup_root,
"--volsize", "250",
@@ -238,13 +289,13 @@ def perform_backup(full_backup):
env["STORAGE_ROOT"],
config["target"],
"--allow-source-mismatch"
],
] + rsync_ssh_options,
get_env(env))
finally:
# Start services again.
shell('check_call', ["/usr/sbin/service", "dovecot", "start"])
shell('check_call', ["/usr/sbin/service", "postfix", "start"])
shell('check_call', ["/usr/sbin/service", "php5-fpm", "start"])
service_command("dovecot", "start", quit=False)
service_command("postfix", "start", quit=False)
service_command("php5-fpm", "start", quit=False)
# Once the migrated backup is included in a new backup, it can be deleted.
if os.path.isdir(migrated_unencrypted_backup_dir):
@@ -256,10 +307,11 @@ def perform_backup(full_backup):
"/usr/bin/duplicity",
"remove-older-than",
"%dD" % config["min_age_in_days"],
"--verbosity", "error",
"--archive-dir", backup_cache_dir,
"--force",
config["target"]
],
] + rsync_ssh_options,
get_env(env))
# From duplicity's manual:
@@ -270,10 +322,11 @@ def perform_backup(full_backup):
shell('check_call', [
"/usr/bin/duplicity",
"cleanup",
"--verbosity", "error",
"--archive-dir", backup_cache_dir,
"--force",
config["target"]
],
] + rsync_ssh_options,
get_env(env))
# Change ownership of backups to the user-data user, so that the after-bcakup
@@ -312,7 +365,7 @@ def run_duplicity_verification():
"--exclude", backup_root,
config["target"],
env["STORAGE_ROOT"],
], get_env(env))
] + rsync_ssh_options, get_env(env))
def run_duplicity_restore(args):
env = load_environment()
@@ -323,7 +376,7 @@ def run_duplicity_restore(args):
"restore",
"--archive-dir", backup_cache_dir,
config["target"],
] + args,
] + rsync_ssh_options + args,
get_env(env))
def list_target_files(config):
@@ -336,6 +389,36 @@ def list_target_files(config):
if p.scheme == "file":
return [(fn, os.path.getsize(os.path.join(p.path, fn))) for fn in os.listdir(p.path)]
elif p.scheme == "rsync":
rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)')
rsync_target = '{host}:{path}'
_, target_host, target_path = config['target'].split('//')
target_path = '/' + target_path
if not target_path.endswith('/'):
target_path += '/'
rsync_command = [ 'rsync',
'-e',
'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes',
'--list-only',
'-r',
rsync_target.format(
host=target_host,
path=target_path)
]
code, listing = shell('check_output', rsync_command, trap=True)
if code == 0:
ret = []
for l in listing.split('\n'):
match = rsync_fn_size_re.match(l)
if match:
ret.append( (match.groups()[1], int(match.groups()[0].replace(',',''))) )
return ret
else:
raise ValueError("Connection to rsync host failed")
elif p.scheme == "s3":
# match to a Region
fix_boto() # must call prior to importing boto
@@ -435,6 +518,9 @@ def get_backup_config(env, for_save=False, for_ui=False):
if config["target"] == "local":
# Expand to the full URL.
config["target"] = "file://" + config["file_target_directory"]
ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub')
if os.path.exists(ssh_pub_key):
config["ssh_pub_key"] = open(ssh_pub_key, 'r').read()
return config

View File

@@ -1,22 +1,17 @@
#!/usr/bin/python3
import os, os.path, re, json
import os, os.path, re, json, time
import subprocess
from functools import wraps
from flask import Flask, request, render_template, abort, Response, send_from_directory
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
import auth, utils
import auth, utils, multiprocessing.pool
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
# Create a worker pool for the status checks. The pool should
# live across http requests so we don't baloon the system with
# processes.
import multiprocessing.pool
pool = multiprocessing.pool.Pool(processes=10)
env = utils.load_environment()
auth_service = auth.KeyAuthService()
@@ -49,7 +44,10 @@ def authorized_personnel_only(viewfunc):
except ValueError as e:
# Authentication failed.
privs = []
error = str(e)
error = "Incorrect username or password"
# Write a line in the log recording the failed login
log_failed_login(request)
# Authorized to access an API view?
if "admin" in privs:
@@ -123,9 +121,12 @@ def me():
try:
email, privs = auth_service.authenticate(request, env)
except ValueError as e:
# Log the failed login
log_failed_login(request)
return json_response({
"status": "invalid",
"reason": str(e),
"reason": "Incorrect username or password",
})
resp = {
@@ -327,6 +328,33 @@ def dns_get_dump():
# SSL
@app.route('/ssl/status')
@authorized_personnel_only
def ssl_get_status():
from ssl_certificates import get_certificates_to_provision
from web_update import get_web_domains_info, get_web_domains
# What domains can we provision certificates for? What unexpected problems do we have?
provision, cant_provision = get_certificates_to_provision(env, show_extended_problems=False)
# What's the current status of TLS certificates on all of the domain?
domains_status = get_web_domains_info(env)
domains_status = [{ "domain": d["domain"], "status": d["ssl_certificate"][0], "text": d["ssl_certificate"][1] } for d in domains_status ]
# Warn the user about domain names not hosted here because of other settings.
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
domains_status.append({
"domain": domain,
"status": "not-applicable",
"text": "The domain's website is hosted elsewhere.",
})
return json_response({
"can_provision": utils.sort_domains(provision, env),
"cant_provision": [{ "domain": domain, "problem": cant_provision[domain] } for domain in utils.sort_domains(cant_provision, env) ],
"status": domains_status,
})
@app.route('/ssl/csr/<domain>', methods=['POST'])
@authorized_personnel_only
def ssl_get_csr(domain):
@@ -346,6 +374,17 @@ def ssl_install_cert():
return "Invalid domain name."
return install_cert(domain, ssl_cert, ssl_chain, env)
@app.route('/ssl/provision', methods=['POST'])
@authorized_personnel_only
def ssl_provision_certs():
from ssl_certificates import provision_certificates
agree_to_tos_url = request.form.get('agree_to_tos_url')
status = provision_certificates(env,
agree_to_tos_url=agree_to_tos_url,
jsonable=True)
return json_response(status)
# WEB
@app.route('/web/domains')
@@ -398,7 +437,10 @@ def system_status():
def print_line(self, message, monospace=False):
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
output = WebOutput()
# Create a temporary pool of processes for the status checks
pool = multiprocessing.pool.Pool(processes=5)
run_checks(False, env, output, pool)
pool.terminate()
return json_response(output.items)
@app.route('/system/updates')
@@ -418,11 +460,35 @@ def do_updates():
"DEBIAN_FRONTEND": "noninteractive"
})
@app.route('/system/reboot', methods=["GET"])
@authorized_personnel_only
def needs_reboot():
from status_checks import is_reboot_needed_due_to_package_installation
if is_reboot_needed_due_to_package_installation():
return json_response(True)
else:
return json_response(False)
@app.route('/system/reboot', methods=["POST"])
@authorized_personnel_only
def do_reboot():
# To keep the attack surface low, we don't allow a remote reboot if one isn't necessary.
from status_checks import is_reboot_needed_due_to_package_installation
if is_reboot_needed_due_to_package_installation():
return utils.shell("check_output", ["/sbin/shutdown", "-r", "now"], capture_stderr=True)
else:
return "No reboot is required, so it is not allowed."
@app.route('/system/backup/status')
@authorized_personnel_only
def backup_status():
from backup import backup_status
try:
return json_response(backup_status(env))
except Exception as e:
return json_response({ "error": str(e) })
@app.route('/system/backup/config', methods=["GET"])
@authorized_personnel_only
@@ -466,6 +532,77 @@ def munin(filename=""):
if filename == "": filename = "index.html"
return send_from_directory("/var/cache/munin/www", filename)
@app.route('/munin/cgi-graph/<path:filename>')
@authorized_personnel_only
def munin_cgi(filename):
""" Relay munin cgi dynazoom requests
/usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package
that is responsible for generating binary png images _and_ associated HTTP
headers based on parameters in the requesting URL. All output is written
to stdout which munin_cgi splits into response headers and binary response
data.
munin-cgi-graph reads environment variables to determine
what it should do. It expects a path to be in the env-var PATH_INFO, and a
querystring to be in the env-var QUERY_STRING.
munin-cgi-graph has several failure modes. Some write HTTP Status headers and
others return nonzero exit codes.
Situating munin_cgi between the user-agent and munin-cgi-graph enables keeping
the cgi script behind mailinabox's auth mechanisms and avoids additional
support infrastructure like spawn-fcgi.
"""
COMMAND = 'su - munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph'
# su changes user, we use the munin user here
# --preserve-environment retains the environment, which is where Popen's `env` data is
# --shell=/bin/bash ensures the shell used is bash
# -c "/usr/lib/munin/cgi/munin-cgi-graph" passes the command to run as munin
# "%s" is a placeholder for where the request's querystring will be added
if filename == "":
return ("a path must be specified", 404)
query_str = request.query_string.decode("utf-8", 'ignore')
env = {'PATH_INFO': '/%s/' % filename, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': query_str}
code, binout = utils.shell('check_output',
COMMAND.split(" ", 5),
# Using a maxsplit of 5 keeps the last arguments together
env=env,
return_bytes=True,
trap=True)
if code != 0:
# nonzero returncode indicates error
app.logger.error("munin_cgi: munin-cgi-graph returned nonzero exit code, %s", process.returncode)
return ("error processing graph image", 500)
# /usr/lib/munin/cgi/munin-cgi-graph returns both headers and binary png when successful.
# A double-Windows-style-newline always indicates the end of HTTP headers.
headers, image_bytes = binout.split(b'\r\n\r\n', 1)
response = make_response(image_bytes)
for line in headers.splitlines():
name, value = line.decode("utf8").split(':', 1)
response.headers[name] = value
if 'Status' in response.headers and '404' in response.headers['Status']:
app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO'])
return response
def log_failed_login(request):
# We need to figure out the ip to list in the message, all our calls are routed
# through nginx who will put the original ip in X-Forwarded-For.
# During setup we call the management interface directly to determine the user
# status. So we can't always use X-Forwarded-For because during setup that header
# will not be present.
if request.headers.getlist("X-Forwarded-For"):
ip = request.headers.getlist("X-Forwarded-For")[0]
else:
ip = request.remote_addr
# We need to add a timestamp to the log message, otherwise /dev/log will eat the "duplicate"
# message.
app.logger.warning( "Mail-in-a-Box Management Daemon: Failed login attempt from ip %s - timestamp %s" % (ip, time.time()))
# APP
if __name__ == '__main__':

View File

@@ -1,8 +1,19 @@
#!/bin/bash
# This script is run daily (at 3am each night).
# Set character encoding flags to ensure that any non-ASCII
# characters don't cause problems. See setup/start.sh and
# the management daemon startup script.
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8
# Take a backup.
management/backup.py
management/backup.py | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py --headless | management/email_administrator.py "Error Provisioning TLS Certificate"
# Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes --smtp
management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice"

View File

@@ -91,7 +91,7 @@ def do_dns_update(env, force=False):
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
# Write the OpenDKIM configuration tables for all of the domains.
if write_opendkim_tables([domain for domain, zonefile in zonefiles], env):
if write_opendkim_tables(get_mail_domains(env), env):
# Settings changed. Kick opendkim.
shell('check_call', ["/usr/sbin/service", "opendkim", "restart"])
if len(updated_domains) == 0:
@@ -175,9 +175,6 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
for value in build_sshfp_records():
records.append((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh."))
# The MX record says where email for the domain should be delivered: Here!
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
# Add DNS records for any subdomains of this domain. We should not have a zone for
# both a domain and one of its subdomains.
subdomains = [d for d in all_domains if d.endswith("." + domain)]
@@ -244,6 +241,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
# Don't pin the list of records that has_rec checks against anymore.
has_rec_base = records
# The MX record says where email for the domain should be delivered: Here!
if not has_rec(None, "MX", prefix="10 "):
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
# the domain, and no one else.
# Skip if the user has set a custom SPF record.
@@ -273,6 +274,13 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
if not has_rec(dmarc_qname, "TXT", prefix="v=DMARC1; "):
records.append((dmarc_qname, "TXT", 'v=DMARC1; p=reject', "Recommended. Prevents use of this domain name for outbound mail by specifying that the SPF rule should be honoured for mail from @%s." % (qname + "." + domain)))
# Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname.
# The SRV record format is priority (0, whatever), weight (0, whatever), port, service provider hostname (w/ trailing dot).
if domain != env["PRIMARY_HOSTNAME"]:
for dav in ("card", "cal"):
qname = "_" + dav + "davs._tcp"
if not has_rec(qname, "SRV"):
records.append((qname, "SRV", "0 0 443 " + env["PRIMARY_HOSTNAME"] + ".", "Recommended. Specifies the hostname of the server that handles CardDAV/CalDAV services for email addresses on this domain."))
# Sort the records. The None records *must* go first in the nsd zone file. Otherwise it doesn't matter.
records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else ""))
@@ -340,7 +348,18 @@ def build_sshfp_records():
# like the known_hosts file: hostname, keytype, fingerprint. The order
# of the output is arbitrary, so sort it to prevent spurrious updates
# to the zone file (that trigger bumping the serial number).
keys = shell("check_output", ["ssh-keyscan", "localhost"])
# scan the sshd_config and find the ssh ports (port 22 may be closed)
with open('/etc/ssh/sshd_config', 'r') as f:
ports = []
t = f.readlines()
for line in t:
s = line.split()
if len(s) == 2 and s[0] == 'Port':
ports = ports + [s[1]]
# the keys are the same at each port, so we only need to get
# them at the first port found (may not be port 22)
keys = shell("check_output", ["ssh-keyscan", "-p", ports[0], "localhost"])
for key in sorted(keys.split("\n")):
if key.strip() == "" or key[0] == "#": continue
try:
@@ -502,11 +521,12 @@ zone:
def dnssec_choose_algo(domain, env):
if '.' in domain and domain.rsplit('.')[-1] in \
("email", "guide", "fund"):
("email", "guide", "fund", "be"):
# At GoDaddy, RSASHA256 is the only algorithm supported
# for .email and .guide.
# A variety of algorithms are supported for .fund. This
# is preferred.
# Gandi tells me that .be does not support RSASHA1-NSEC3-SHA1
return "RSASHA256"
# For any domain we were able to sign before, don't change the algorithm

View File

@@ -0,0 +1,42 @@
#!/usr/bin/python3
# Reads in STDIN. If the stream is not empty, mail it to the system administrator.
import sys
import smtplib
from email.message import Message
from utils import load_environment
# Load system environment info.
env = load_environment()
# Process command line args.
subject = sys.argv[1]
# Administrator's email address.
admin_addr = "administrator@" + env['PRIMARY_HOSTNAME']
# Read in STDIN.
content = sys.stdin.read().strip()
# If there's nothing coming in, just exit.
if content == "":
sys.exit(0)
# create MIME message
msg = Message()
msg['From'] = "\"%s\" <%s>" % (env['PRIMARY_HOSTNAME'], admin_addr)
msg['To'] = admin_addr
msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject)
msg.set_payload(content, "UTF-8")
# send
smtpclient = smtplib.SMTP('127.0.0.1', 25)
smtpclient.ehlo()
smtpclient.sendmail(
admin_addr, # MAIL FROM
admin_addr, # RCPT TO
msg.as_string())
smtpclient.quit()

View File

@@ -1,25 +1,45 @@
#!/usr/bin/python3
import os.path
import re
from collections import defaultdict
import re, os.path
import dateutil.parser
import mailconfig
import utils
def scan_mail_log(logger, env):
""" Scan the system's mail log files and collect interesting data
This function scans the 2 most recent mail log files in /var/log/.
Args:
logger (ConsoleOutput): Object used for writing messages to the console
env (dict): Dictionary containing MiaB settings
"""
collector = {
"other-services": set(),
"imap-logins": { },
"postgrey": { },
"rejected-mail": { },
"activity-by-hour": { "imap-logins": defaultdict(int), "smtp-sends": defaultdict(int) },
"imap-logins": {},
"pop3-logins": {},
"postgrey": {},
"rejected-mail": {},
"activity-by-hour": {
"imap-logins": defaultdict(int),
"pop3-logins": defaultdict(int),
"smtp-sends": defaultdict(int),
"smtp-receives": defaultdict(int),
},
"real_mail_addresses": (
set(mailconfig.get_mail_users(env)) | set(alias[0] for alias in mailconfig.get_mail_aliases(env))
)
}
collector["real_mail_addresses"] = set(mailconfig.get_mail_users(env)) | set(alias[0] for alias in mailconfig.get_mail_aliases(env))
for fn in ('/var/log/mail.log.1', '/var/log/mail.log'):
if not os.path.exists(fn): continue
if not os.path.exists(fn):
continue
with open(fn, 'rb') as log:
for line in log:
line = line.decode("utf8", errors='replace')
@@ -27,18 +47,30 @@ def scan_mail_log(logger, env):
if collector["imap-logins"]:
logger.add_heading("Recent IMAP Logins")
logger.print_block("The most recent login from each remote IP adddress is show.")
logger.print_block("The most recent login from each remote IP adddress is shown.")
for k in utils.sort_email_addresses(collector["imap-logins"], env):
for ip, date in sorted(collector["imap-logins"][k].items(), key = lambda kv : kv[1]):
for ip, date in sorted(collector["imap-logins"][k].items(), key=lambda kv: kv[1]):
logger.print_line(k + "\t" + str(date) + "\t" + ip)
if collector["pop3-logins"]:
logger.add_heading("Recent POP3 Logins")
logger.print_block("The most recent login from each remote IP adddress is shown.")
for k in utils.sort_email_addresses(collector["pop3-logins"], env):
for ip, date in sorted(collector["pop3-logins"][k].items(), key=lambda kv: kv[1]):
logger.print_line(k + "\t" + str(date) + "\t" + ip)
if collector["postgrey"]:
logger.add_heading("Greylisted Mail")
logger.print_block("The following mail was greylisted, meaning the emails were temporarily rejected. Legitimate senders will try again within ten minutes.")
logger.print_line("recipient" + "\t" + "received" + "\t" + "sender" + "\t" + "delivered")
logger.print_block("The following mail was greylisted, meaning the emails were temporarily rejected. "
"Legitimate senders will try again within ten minutes.")
logger.print_line("recipient" + "\t" + "received" + 3 * "\t" + "sender" + 6 * "\t" + "delivered")
for recipient in utils.sort_email_addresses(collector["postgrey"], env):
for (client_address, sender), (first_date, delivered_date) in sorted(collector["postgrey"][recipient].items(), key = lambda kv : kv[1][0]):
logger.print_line(recipient + "\t" + str(first_date) + "\t" + sender + "\t" + (("delivered " + str(delivered_date)) if delivered_date else "no retry yet"))
sorted_recipients = sorted(collector["postgrey"][recipient].items(), key=lambda kv: kv[1][0])
for (client_address, sender), (first_date, delivered_date) in sorted_recipients:
logger.print_line(
recipient + "\t" + str(first_date) + "\t" + sender + "\t" +
(("delivered " + str(delivered_date)) if delivered_date else "no retry yet")
)
if collector["rejected-mail"]:
logger.add_heading("Rejected Mail")
@@ -48,51 +80,73 @@ def scan_mail_log(logger, env):
logger.print_line(k + "\t" + str(date) + "\t" + sender + "\t" + message)
logger.add_heading("Activity by Hour")
logger.print_block("Dovecot logins and Postfix mail traffic per hour.")
logger.print_block("Hour\tIMAP\tPOP3\tSent\tReceived")
for h in range(24):
logger.print_line("%d\t%d\t%d" % (h, collector["activity-by-hour"]["imap-logins"][h], collector["activity-by-hour"]["smtp-sends"][h] ))
logger.print_line(
"%d\t%d\t\t%d\t\t%d\t\t%d" % (
h,
collector["activity-by-hour"]["imap-logins"][h],
collector["activity-by-hour"]["pop3-logins"][h],
collector["activity-by-hour"]["smtp-sends"][h],
collector["activity-by-hour"]["smtp-receives"][h],
)
)
if len(collector["other-services"]) > 0:
logger.add_heading("Other")
logger.print_block("Unrecognized services in the log: " + ", ".join(collector["other-services"]))
def scan_mail_log_line(line, collector):
""" Scan a log line and extract interesting data """
m = re.match(r"(\S+ \d+ \d+:\d+:\d+) (\S+) (\S+?)(\[\d+\])?: (.*)", line)
if not m: return
if not m:
return
date, system, service, pid, log = m.groups()
date = dateutil.parser.parse(date)
if service == "dovecot":
scan_dovecot_line(date, log, collector)
elif service == "postgrey":
scan_postgrey_line(date, log, collector)
elif service == "postfix/smtpd":
scan_postfix_smtpd_line(date, log, collector)
elif service == "postfix/cleanup":
scan_postfix_cleanup_line(date, log, collector)
elif service == "postfix/submission/smtpd":
scan_postfix_submission_line(date, log, collector)
elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup",
"postfix/scache", "spampd", "postfix/anvil", "postfix/master",
"opendkim", "postfix/lmtp", "postfix/tlsmgr"):
elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup", "postfix/scache", "spampd", "postfix/anvil",
"postfix/master", "opendkim", "postfix/lmtp", "postfix/tlsmgr"):
# nothing to look at
pass
else:
collector["other-services"].add(service)
def scan_dovecot_line(date, log, collector):
m = re.match("imap-login: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", log)
def scan_dovecot_line(date, line, collector):
""" Scan a dovecot log line and extract interesting data """
m = re.match("(imap|pop3)-login: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", line)
if m:
login, ip = m.group(1), m.group(2)
prot, login, ip = m.group(1), m.group(2), m.group(3)
logins_key = "%s-logins" % prot
if ip != "127.0.0.1": # local login from webmail/zpush
collector["imap-logins"].setdefault(login, {})[ip] = date
collector["activity-by-hour"]["imap-logins"][date.hour] += 1
collector[logins_key].setdefault(login, {})[ip] = date
collector["activity-by-hour"][logins_key][date.hour] += 1
def scan_postgrey_line(date, log, collector):
m = re.match("action=(greylist|pass), reason=(.*?), (?:delay=\d+, )?client_name=(.*), client_address=(.*), sender=(.*), recipient=(.*)", log)
""" Scan a postgrey log line and extract interesting data """
m = re.match("action=(greylist|pass), reason=(.*?), (?:delay=\d+, )?client_name=(.*), client_address=(.*), "
"sender=(.*), recipient=(.*)",
log)
if m:
action, reason, client_name, client_address, sender, recipient = m.groups()
key = (client_address, sender)
@@ -101,14 +155,20 @@ def scan_postgrey_line(date, log, collector):
elif action == "pass" and reason == "triplet found" and key in collector["postgrey"].get(recipient, {}):
collector["postgrey"][recipient][key] = (collector["postgrey"][recipient][key][0], date)
def scan_postfix_smtpd_line(date, log, collector):
""" Scan a postfix smtpd log line and extract interesting data """
# Check if the incomming mail was rejected
m = re.match("NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
if m:
message, sender, recipient = m.groups()
if recipient in collector["real_mail_addresses"]:
# only log mail to real recipients
# skip this, is reported in the greylisting report
# skip this, if reported in the greylisting report
if "Recipient address rejected: Greylisted" in message:
return
@@ -122,15 +182,30 @@ def scan_postfix_smtpd_line(date, log, collector):
if m:
message = "domain blocked: " + m.group(2)
collector["rejected-mail"].setdefault(recipient, []).append( (date, sender, message) )
collector["rejected-mail"].setdefault(recipient, []).append((date, sender, message))
def scan_postfix_cleanup_line(date, _, collector):
""" Scan a postfix cleanup log line and extract interesting data
It is assumed that every log of postfix/cleanup indicates an email that was successfulfy received by Postfix.
"""
collector["activity-by-hour"]["smtp-receives"][date.hour] += 1
def scan_postfix_submission_line(date, log, collector):
""" Scan a postfix submission log line and extract interesting data """
m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=PLAIN, sasl_username=(\S+)", log)
if m:
procid, client, user = m.groups()
# procid, client, user = m.groups()
collector["activity-by-hour"]["smtp-sends"][date.hour] += 1
if __name__ == "__main__":
from status_checks import ConsoleOutput
env = utils.load_environment()
scan_mail_log(ConsoleOutput(), env)
env_vars = utils.load_environment()
scan_mail_log(ConsoleOutput(), env_vars)

View File

@@ -137,6 +137,7 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
if with_archived:
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
for domain in os.listdir(root):
if os.path.isdir(os.path.join(root, domain)):
for user in os.listdir(os.path.join(root, domain)):
email = user + "@" + domain
mbox = os.path.join(root, domain, user)

484
management/ssl_certificates.py Normal file → Executable file
View File

@@ -1,8 +1,13 @@
#!/usr/bin/python3
# Utilities for installing and selecting SSL certificates.
import os, os.path, re, shutil
from utils import shell, safe_domain_name
from utils import shell, safe_domain_name, sort_domains
import idna
# SELECTING SSL CERTIFICATES FOR USE IN WEB
def get_ssl_certificates(env):
# Scan all of the installed SSL certificates and map every domain
@@ -17,6 +22,8 @@ def get_ssl_certificates(env):
# List all of the files in the SSL directory and one level deep.
def get_file_list():
if not os.path.exists(ssl_root):
return
for fn in os.listdir(ssl_root):
fn = os.path.join(ssl_root, fn)
if os.path.isfile(fn):
@@ -82,10 +89,27 @@ def get_ssl_certificates(env):
# prefer one that is not self-signed
cert.issuer != cert.subject,
###########################################################
# The above lines ensure that valid certificates are chosen
# over invalid certificates. The lines below choose between
# multiple valid certificates available for this domain.
###########################################################
# prefer one with the expiration furthest into the future so
# that we can easily rotate to new certs as we get them
cert.not_valid_after,
###########################################################
# We always choose the certificate that is good for the
# longest period of time. This is important for how we
# provision certificates for Let's Encrypt. To ensure that
# we don't re-provision every night, we have to ensure that
# if we choose to provison a certificate that it will
# *actually* be used so the provisioning logic knows it
# doesn't still need to provision a certificate for the
# domain.
###########################################################
# in case a certificate is installed in multiple paths,
# prefer the... lexicographically last one?
cert._filename,
@@ -96,46 +120,440 @@ def get_ssl_certificates(env):
"private-key": cert._private_key._filename,
"certificate": cert._filename,
"primary-domain": cert._primary_domain,
"certificate_object": cert,
}
return ret
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False):
# Get the default paths.
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False, raw=False):
# Get the system certificate info.
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
system_certificate = {
"private-key": ssl_private_key,
"certificate": ssl_certificate,
"primary-domain": env['PRIMARY_HOSTNAME'],
"certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]),
}
if domain == env['PRIMARY_HOSTNAME']:
# The primary domain must use the server certificate because
# it is hard-coded in some service configuration files.
return ssl_private_key, ssl_certificate, None
return system_certificate
wildcard_domain = re.sub("^[^\.]+", "*", domain)
if domain in ssl_certificates:
cert_info = ssl_certificates[domain]
cert_type = "multi-domain"
return ssl_certificates[domain]
elif wildcard_domain in ssl_certificates:
cert_info = ssl_certificates[wildcard_domain]
cert_type = "wildcard"
return ssl_certificates[wildcard_domain]
elif not allow_missing_cert:
# No certificate is available for this domain! Return default files.
ssl_via = "Using certificate for %s." % env['PRIMARY_HOSTNAME']
return ssl_private_key, ssl_certificate, ssl_via
# No valid certificate is available for this domain! Return default files.
return system_certificate
else:
# No certificate is available - and warn appropriately.
# No valid certificate is available for this domain.
return None
# 'via' is a hint to the user about which certificate is in use for the domain
if cert_info['certificate'] == os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'):
# Using the server certificate.
via = "Using same %s certificate as for %s." % (cert_type, env['PRIMARY_HOSTNAME'])
elif cert_info['primary-domain'] != domain and cert_info['primary-domain'] in ssl_certificates and cert_info == ssl_certificates[cert_info['primary-domain']]:
via = "Using same %s certificate as for %s." % (cert_type, cert_info['primary-domain'])
else:
via = None # don't show a hint - show expiration info instead
return cert_info['private-key'], cert_info['certificate'], via
# PROVISIONING CERTIFICATES FROM LETSENCRYPT
def get_certificates_to_provision(env, show_extended_problems=True, force_domains=None):
# Get a set of domain names that we should now provision certificates
# for. Provision if a domain name has no valid certificate or if any
# certificate is expiring in 14 days. If provisioning anything, also
# provision certificates expiring within 30 days. The period between
# 14 and 30 days allows us to consolidate domains into multi-domain
# certificates for domains expiring around the same time.
from web_update import get_web_domains
import datetime
now = datetime.datetime.utcnow()
# Get domains with missing & expiring certificates.
certs = get_ssl_certificates(env)
domains = set()
domains_if_any = set()
problems = { }
for domain in get_web_domains(env):
# If the user really wants a cert for certain domains, include it.
if force_domains:
if force_domains == "ALL" or (isinstance(force_domains, list) and domain in force_domains):
domains.add(domain)
continue
# Include this domain if its certificate is missing, self-signed, or expiring soon.
try:
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
except FileNotFoundError as e:
# system certificate is not present
problems[domain] = "Error: " + str(e)
continue
if cert is None:
# No valid certificate available.
domains.add(domain)
else:
cert = cert["certificate_object"]
if cert.issuer == cert.subject:
# This is self-signed. Get a real one.
domains.add(domain)
# Valid certificate today, but is it expiring soon?
elif cert.not_valid_after-now < datetime.timedelta(days=14):
domains.add(domain)
elif cert.not_valid_after-now < datetime.timedelta(days=30):
domains_if_any.add(domain)
# It's valid. Should we report its validness?
elif show_extended_problems:
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
# Warn the user about domains hosted elsewhere.
if not force_domains and show_extended_problems:
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
problems[domain] = "The domain's DNS is pointed elsewhere, so there is no point to installing a TLS certificate here and we could not automatically provision one anyway because provisioning requires access to the website (which isn't here)."
# Filter out domains that we can't provision a certificate for.
def can_provision_for_domain(domain):
# Let's Encrypt doesn't yet support IDNA domains.
# We store domains in IDNA (ASCII). To see if this domain is IDNA,
# we'll see if its IDNA-decoded form is different.
if idna.decode(domain.encode("ascii")) != domain:
problems[domain] = "Let's Encrypt does not yet support provisioning certificates for internationalized domains."
return False
# Does the domain resolve to this machine in public DNS? If not,
# we can't do domain control validation. For IPv6 is configured,
# make sure both IPv4 and IPv6 are correct because we don't know
# how Let's Encrypt will connect.
import dns.resolver
for rtype, value in [("A", env["PUBLIC_IP"]), ("AAAA", env.get("PUBLIC_IPV6"))]:
if not value: continue # IPv6 is not configured
try:
# Must make the qname absolute to prevent a fall-back lookup with a
# search domain appended, by adding a period to the end.
response = dns.resolver.query(domain + ".", rtype)
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
problems[domain] = "DNS isn't configured properly for this domain: DNS resolution failed (%s: %s)." % (rtype, str(e) or repr(e)) # NoAnswer's str is empty
return False
except Exception as e:
problems[domain] = "DNS isn't configured properly for this domain: DNS lookup had an error: %s." % str(e)
return False
# Unfortunately, the response.__str__ returns bytes
# instead of string, if it resulted from an AAAA-query.
# We need to convert manually, until this is fixed:
# https://github.com/rthalley/dnspython/issues/204
#
# BEGIN HOTFIX
def rdata__str__(r):
s = r.to_text()
if isinstance(s, bytes):
s = s.decode('utf-8')
return s
# END HOTFIX
if len(response) != 1 or rdata__str__(response[0]) != value:
problems[domain] = "Domain control validation cannot be performed for this domain because DNS points the domain to another machine (%s %s)." % (rtype, ", ".join(rdata__str__(r) for r in response))
return False
return True
domains = set(filter(can_provision_for_domain, domains))
# If there are any domains we definitely will provision for, add in
# additional domains to do at this time.
if len(domains) > 0:
domains |= set(filter(can_provision_for_domain, domains_if_any))
return (domains, problems)
def provision_certificates(env, agree_to_tos_url=None, logger=None, show_extended_problems=True, force_domains=None, jsonable=False):
import requests.exceptions
import acme.messages
from free_tls_certificates import client
# What domains should we provision certificates for? And what
# errors prevent provisioning for other domains.
domains, problems = get_certificates_to_provision(env, force_domains=force_domains, show_extended_problems=show_extended_problems)
# Exit fast if there is nothing to do.
if len(domains) == 0:
return {
"requests": [],
"problems": problems,
}
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
# limit for a single certificate. We'll sort to put related domains together.
domains = sort_domains(domains, env)
certs = []
while len(domains) > 0:
certs.append( domains[0:100] )
domains = domains[100:]
# Prepare to provision.
# Where should we put our Let's Encrypt account info and state cache.
account_path = os.path.join(env['STORAGE_ROOT'], 'ssl/lets_encrypt')
if not os.path.exists(account_path):
os.mkdir(account_path)
# Where should we put ACME challenge files. This is mapped to /.well-known/acme_challenge
# by the nginx configuration.
challenges_path = os.path.join(account_path, 'acme_challenges')
if not os.path.exists(challenges_path):
os.mkdir(challenges_path)
# Read in the private key that we use for all TLS certificates. We'll need that
# to generate a CSR (done by free_tls_certificates).
with open(os.path.join(env['STORAGE_ROOT'], 'ssl/ssl_private_key.pem'), 'rb') as f:
private_key = f.read()
# Provision certificates.
ret = []
for domain_list in certs:
# For return.
ret_item = {
"domains": domain_list,
"log": [],
}
ret.append(ret_item)
# Logging for free_tls_certificates.
def my_logger(message):
if logger: logger(message)
ret_item["log"].append(message)
# Attempt to provision a certificate.
try:
try:
cert = client.issue_certificate(
domain_list,
account_path,
agree_to_tos_url=agree_to_tos_url,
private_key=private_key,
logger=my_logger)
except client.NeedToTakeAction as e:
# Write out the ACME challenge files.
for action in e.actions:
if isinstance(action, client.NeedToInstallFile):
fn = os.path.join(challenges_path, action.file_name)
with open(fn, 'w') as f:
f.write(action.contents)
else:
raise ValueError(str(action))
# Try to provision now that the challenge files are installed.
cert = client.issue_certificate(
domain_list,
account_path,
private_key=private_key,
logger=my_logger)
except client.NeedToAgreeToTOS as e:
# The user must agree to the Let's Encrypt terms of service agreement
# before any further action can be taken.
ret_item.update({
"result": "agree-to-tos",
"url": e.url,
})
except client.WaitABit as e:
# We need to hold on for a bit before querying again to see if we can
# acquire a provisioned certificate.
import time, datetime
ret_item.update({
"result": "wait",
"until": e.until_when if not jsonable else e.until_when.isoformat(),
"seconds": (e.until_when - datetime.datetime.now()).total_seconds()
})
except client.AccountDataIsCorrupt as e:
# This is an extremely rare condition.
ret_item.update({
"result": "error",
"message": "Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file " + e.account_file_path + ".",
})
except (client.InvalidDomainName, client.NeedToTakeAction, client.ChallengeFailed, client.RateLimited, acme.messages.Error, requests.exceptions.RequestException) as e:
ret_item.update({
"result": "error",
"message": "Something unexpected went wrong: " + str(e),
})
else:
# A certificate was issued.
install_status = install_cert(domain_list[0], cert['cert'].decode("ascii"), b"\n".join(cert['chain']).decode("ascii"), env, raw=True)
# str indicates the certificate was not installed.
if isinstance(install_status, str):
ret_item.update({
"result": "error",
"message": "Something unexpected was wrong with the provisioned certificate: " + install_status,
})
else:
# A list indicates success and what happened next.
ret_item["log"].extend(install_status)
ret_item.update({
"result": "installed",
})
# Return what happened with each certificate request.
return {
"requests": ret,
"problems": problems,
}
def provision_certificates_cmdline():
import sys
from utils import load_environment, exclusive_process
exclusive_process("update_tls_certificates")
env = load_environment()
verbose = False
headless = False
force_domains = None
show_extended_problems = True
args = list(sys.argv)
args.pop(0) # program name
if args and args[0] == "-v":
verbose = True
args.pop(0)
if args and args[0] == "q":
show_extended_problems = False
args.pop(0)
if args and args[0] == "--headless":
headless = True
args.pop(0)
if args and args[0] == "--force":
force_domains = "ALL"
args.pop(0)
else:
force_domains = args
agree_to_tos_url = None
while True:
# Run the provisioning script. This installs certificates. If there are
# a very large number of domains on this box, it issues separate
# certificates for groups of domains. We have to check the result for
# each group.
def my_logger(message):
if verbose:
print(">", message)
status = provision_certificates(env, agree_to_tos_url=agree_to_tos_url, logger=my_logger, force_domains=force_domains, show_extended_problems=show_extended_problems)
agree_to_tos_url = None # reset to prevent infinite looping
if not status["requests"]:
# No domains need certificates.
if not headless or verbose:
if len(status["problems"]) == 0:
print("No domains hosted on this box need a new TLS certificate at this time.")
elif len(status["problems"]) > 0:
print("No TLS certificates could be provisoned at this time:")
print()
for domain in sort_domains(status["problems"], env):
print("%s: %s" % (domain, status["problems"][domain]))
sys.exit(0)
# What happened?
wait_until = None
wait_domains = []
for request in status["requests"]:
if request["result"] == "agree-to-tos":
# We may have asked already in a previous iteration.
if agree_to_tos_url is not None:
continue
# Can't ask the user a question in this mode. Warn the user that something
# needs to be done.
if headless:
print(", ".join(request["domains"]) + " need a new or renewed TLS certificate.")
print()
print("This box can't do that automatically for you until you agree to Let's Encrypt's")
print("Terms of Service agreement. Use the Mail-in-a-Box control panel to provision")
print("certificates for these domains.")
sys.exit(1)
print("""
I'm going to provision a TLS certificate (formerly called a SSL certificate)
for you from Let's Encrypt (letsencrypt.org).
TLS certificates are cryptographic keys that ensure communication between
you and this box are secure when getting and sending mail and visiting
websites hosted on this box. Let's Encrypt is a free provider of TLS
certificates.
Please open this document in your web browser:
%s
It is Let's Encrypt's terms of service agreement. If you agree, I can
provision that TLS certificate. If you don't agree, you will have an
opportunity to install your own TLS certificate from the Mail-in-a-Box
control panel.
Do you agree to the agreement? Type Y or N and press <ENTER>: """
% request["url"], end='', flush=True)
if sys.stdin.readline().strip().upper() != "Y":
print("\nYou didn't agree. Quitting.")
sys.exit(1)
# Okay, indicate agreement on next iteration.
agree_to_tos_url = request["url"]
if request["result"] == "wait":
# Must wait. We'll record until when. The wait occurs below.
if wait_until is None:
wait_until = request["until"]
else:
wait_until = max(wait_until, request["until"])
wait_domains += request["domains"]
if request["result"] == "error":
print(", ".join(request["domains"]) + ":")
print(request["message"])
if request["result"] == "installed":
print("A TLS certificate was successfully installed for " + ", ".join(request["domains"]) + ".")
if wait_until:
# Wait, then loop.
import time, datetime
print()
print("A TLS certificate was requested for: " + ", ".join(wait_domains) + ".")
first = True
while wait_until > datetime.datetime.now():
if not headless or first:
print ("We have to wait", int(round((wait_until - datetime.datetime.now()).total_seconds())), "seconds for the certificate to be issued...")
time.sleep(10)
first = False
continue # Loop!
if agree_to_tos_url:
# The user agrees to the TOS. Loop to try again by agreeing.
continue # Loop!
# Unless we were instructed to wait, or we just agreed to the TOS,
# we're done for now.
break
# And finally show the domains with problems.
if len(status["problems"]) > 0:
print("TLS certificates could not be provisoned for:")
for domain in sort_domains(status["problems"], env):
print("%s: %s" % (domain, status["problems"][domain]))
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
def create_csr(domain, ssl_key, country_code, env):
return shell("check_output", [
@@ -144,7 +562,7 @@ def create_csr(domain, ssl_key, country_code, env):
"-sha256",
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)])
def install_cert(domain, ssl_cert, ssl_chain, env):
def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
# Write the combined cert+chain to a temporary path and validate that it is OK.
# The certificate always goes above the chain.
import tempfile
@@ -203,8 +621,10 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
# Update the web configuration so nginx picks up the new certificate file.
from web_update import do_web_update
ret.append( do_web_update(env) )
if raw: return ret
return "\n".join(ret)
# VALIDATION OF CERTIFICATES
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
@@ -296,7 +716,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
# There is some unknown problem. Return the `openssl verify` raw output.
return ("There is a problem with the SSL certificate.", verifyoutput.strip())
return ("There is a problem with the certificate.", verifyoutput.strip())
else:
# `openssl verify` returned a zero exit status so the cert is currently
@@ -305,16 +725,16 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# But is it expiring soon?
cert_expiration_date = cert.not_valid_after
ndays = (cert_expiration_date-now).days
if not rounded_time or ndays < 7:
if not rounded_time or ndays <= 10:
# Yikes better renew soon!
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
elif ndays <= 14:
expiry_info = "The certificate expires in less than two weeks, on %s." % cert_expiration_date.strftime("%x")
elif ndays <= 31:
expiry_info = "The certificate expires in less than a month, on %s." % cert_expiration_date.strftime("%x")
else:
# We'll renew it with Lets Encrypt.
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
if ndays <= 31 and warn_if_expiring_soon:
if ndays <= 10 and warn_if_expiring_soon:
# Warn on day 10 to give 4 days for us to automatically renew the
# certificate, which occurs on day 14.
return ("The certificate is expiring soon: " + expiry_info, None)
# Return the special OK code.
@@ -381,3 +801,7 @@ def get_certificate_domains(cert):
pass
return names, cn
if __name__ == "__main__":
# Provision certificates.
provision_certificates_cmdline()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python3
#
# Checks that the upstream DNS has been set correctly and that
# SSL certificates have been signed, etc., and if not tells the user
# TLS certificates have been signed, etc., and if not tells the user
# what to do next.
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
@@ -9,6 +9,7 @@ import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
import dns.reversename, dns.resolver
import dateutil.parser, dateutil.tz
import idna
import psutil
from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns, get_custom_dns_record
from web_update import get_web_domains, get_domains_with_a_records
@@ -17,6 +18,29 @@ from mailconfig import get_mail_domains, get_mail_aliases
from utils import shell, sort_domains, load_env_vars_from_file, load_settings
def get_services():
return [
{ "name": "Local DNS (bind9)", "port": 53, "public": False, },
#{ "name": "NSD Control", "port": 8952, "public": False, },
{ "name": "Local DNS Control (bind9/rndc)", "port": 953, "public": False, },
{ "name": "Dovecot LMTP LDA", "port": 10026, "public": False, },
{ "name": "Postgrey", "port": 10023, "public": False, },
{ "name": "Spamassassin", "port": 10025, "public": False, },
{ "name": "OpenDKIM", "port": 8891, "public": False, },
{ "name": "OpenDMARC", "port": 8893, "public": False, },
{ "name": "Memcached", "port": 11211, "public": False, },
{ "name": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, },
{ "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
{ "name": "Public DNS (nsd4)", "port": 53, "public": True, },
{ "name": "Incoming Mail (SMTP/postfix)", "port": 25, "public": True, },
{ "name": "Outgoing Mail (SMTP 587/postfix)", "port": 587, "public": True, },
#{ "name": "Postfix/master", "port": 10587, "public": True, },
{ "name": "IMAPS (dovecot)", "port": 993, "public": True, },
{ "name": "Mail Filters (Sieve/dovecot)", "port": 4190, "public": True, },
{ "name": "HTTP Web (nginx)", "port": 80, "public": True, },
{ "name": "HTTPS Web (nginx)", "port": 443, "public": True, },
]
def run_checks(rounded_values, env, output, pool):
# run systems checks
output.add_heading("System")
@@ -60,33 +84,9 @@ def get_ssh_port():
def run_services_checks(env, output, pool):
# Check that system services are running.
services = [
{ "name": "Local DNS (bind9)", "port": 53, "public": False, },
#{ "name": "NSD Control", "port": 8952, "public": False, },
{ "name": "Local DNS Control (bind9/rndc)", "port": 953, "public": False, },
{ "name": "Dovecot LMTP LDA", "port": 10026, "public": False, },
{ "name": "Postgrey", "port": 10023, "public": False, },
{ "name": "Spamassassin", "port": 10025, "public": False, },
{ "name": "OpenDKIM", "port": 8891, "public": False, },
{ "name": "OpenDMARC", "port": 8893, "public": False, },
{ "name": "Memcached", "port": 11211, "public": False, },
{ "name": "Sieve (dovecot)", "port": 4190, "public": False, },
{ "name": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, },
{ "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
{ "name": "Public DNS (nsd4)", "port": 53, "public": True, },
{ "name": "Incoming Mail (SMTP/postfix)", "port": 25, "public": True, },
{ "name": "Outgoing Mail (SMTP 587/postfix)", "port": 587, "public": True, },
#{ "name": "Postfix/master", "port": 10587, "public": True, },
{ "name": "IMAPS (dovecot)", "port": 993, "public": True, },
{ "name": "HTTP Web (nginx)", "port": 80, "public": True, },
{ "name": "HTTPS Web (nginx)", "port": 443, "public": True, },
]
all_running = True
fatal = False
ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(services)), chunksize=1)
ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(get_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
@@ -166,6 +166,38 @@ def run_system_checks(rounded_values, env, output):
check_miab_version(env, output)
check_system_aliases(env, output)
check_free_disk_space(rounded_values, env, output)
check_free_memory(rounded_values, env, output)
def check_ufw(env, output):
if not os.path.isfile('/usr/sbin/ufw'):
output.print_warning("""The ufw program was not installed. If your system is able to run iptables, rerun the setup.""")
return
code, ufw = shell('check_output', ['ufw', 'status'], trap=True)
if code != 0:
# The command failed, it's safe to say the firewall is disabled
output.print_warning("""The firewall is not working on this machine. An error was received
while trying to check the firewall. To investigate run 'sudo ufw status'.""")
return
ufw = ufw.splitlines()
if ufw[0] == "Status: active":
not_allowed_ports = 0
for service in get_services():
if service["public"] and not is_port_allowed(ufw, service["port"]):
not_allowed_ports += 1
output.print_error("Port %s (%s) should be allowed in the firewall, please re-run the setup." % (service["port"], service["name"]))
if not_allowed_ports == 0:
output.print_ok("Firewall is active.")
else:
output.print_warning("""The firewall is disabled on this machine. This might be because the system
is protected by an external firewall. We can't protect the system against bruteforce attacks
without the local firewall active. Connect to the system via ssh and try to run: ufw enable.""")
def is_port_allowed(ufw, port):
return any(re.match(str(port) +"[/ \t].*", item) for item in ufw)
def check_ssh_password(env, output):
# Check that SSH login with password is disabled. The openssh-server
@@ -183,10 +215,13 @@ def check_ssh_password(env, output):
else:
output.print_ok("SSH disallows password-based login.")
def is_reboot_needed_due_to_package_installation():
return os.path.exists("/var/run/reboot-required")
def check_software_updates(env, output):
# Check for any software package updates.
pkgs = list_apt_updates(apt_update=False)
if os.path.exists("/var/run/reboot-required"):
if is_reboot_needed_due_to_package_installation():
output.print_error("System updates have been installed and a reboot of the machine is required.")
elif len(pkgs) == 0:
output.print_ok("System software is up to date.")
@@ -205,22 +240,38 @@ def check_free_disk_space(rounded_values, env, output):
st = os.statvfs(env['STORAGE_ROOT'])
bytes_total = st.f_blocks * st.f_frsize
bytes_free = st.f_bavail * st.f_frsize
if not rounded_values:
disk_msg = "The disk has %s GB space remaining." % str(round(bytes_free/1024.0/1024.0/1024.0*10.0)/10)
else:
disk_msg = "The disk has less than %s%% space left." % str(round(bytes_free/bytes_total/10 + .5)*10)
disk_msg = "The disk has %.2f GB space remaining." % (bytes_free/1024.0/1024.0/1024.0)
if bytes_free > .3 * bytes_total:
if rounded_values: disk_msg = "The disk has more than 30% free space."
output.print_ok(disk_msg)
elif bytes_free > .15 * bytes_total:
if rounded_values: disk_msg = "The disk has less than 30% free space."
output.print_warning(disk_msg)
else:
if rounded_values: disk_msg = "The disk has less than 15% free space."
output.print_error(disk_msg)
def check_free_memory(rounded_values, env, output):
# Check free memory.
percent_free = 100 - psutil.virtual_memory().percent
memory_msg = "System memory is %s%% free." % str(round(percent_free))
if percent_free >= 20:
if rounded_values: memory_msg = "System free memory is at least 20%."
output.print_ok(memory_msg)
elif percent_free >= 10:
if rounded_values: memory_msg = "System free memory is below 20%."
output.print_warning(memory_msg)
else:
if rounded_values: memory_msg = "System free memory is below 10%."
output.print_error(memory_msg)
def run_network_checks(env, output):
# Also see setup/network-checks.sh.
output.add_heading("Network")
check_ufw(env, output)
# Stop if we cannot make an outbound connection on port 25. Many residential
# networks block outbound port 25 to prevent their network from sending spam.
# See if we can reach one of Google's MTAs with a 5-second timeout.
@@ -262,23 +313,24 @@ def run_domain_checks(rounded_time, env, output, pool):
# Get the list of domains that we don't serve web for because of a custom CNAME/A record.
domains_with_a_records = get_domains_with_a_records(env)
ssl_certificates = get_ssl_certificates(env)
# Serial version:
#for domain in sort_domains(domains_to_check, env):
# run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
# Parallelize the checks across a worker pool.
args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records, ssl_certificates)
args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records)
for domain in domains_to_check)
ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1)
ret = dict(ret) # (domain, output) => { domain: output }
for domain in sort_domains(ret, env):
ret[domain].playback(output)
def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records, ssl_certificates):
def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records):
output = BufferedOutput()
# we'd move this up, but this returns non-pickleable values
ssl_certificates = get_ssl_certificates(env)
# The domain is IDNA-encoded in the database, but for display use Unicode.
try:
domain_display = idna.decode(domain.encode('ascii'))
@@ -447,7 +499,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
elif ip is None:
output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns)
else:
output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, env['PUBLIC_IP']))
output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, correct_ip))
def check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records):
# Warn if a custom DNS record is preventing this or the automatic www redirect from
@@ -584,15 +636,23 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output):
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
# other domains, it is required to access its website.
if domain != env['PRIMARY_HOSTNAME']:
ip = query_dns(domain, "A")
if ip == env['PUBLIC_IP']:
output.print_ok("Domain resolves to this box's IP address. [%s%s]" % (domain, env['PUBLIC_IP']))
ok_values = []
for (rtype, expected) in (("A", env['PUBLIC_IP']), ("AAAA", env.get('PUBLIC_IPV6'))):
if not expected: continue # IPv6 is not configured
value = query_dns(domain, rtype)
if value == expected:
ok_values.append(value)
else:
output.print_error("""This domain should resolve to your box's IP address (%s) if you would like the box to serve
output.print_error("""This domain should resolve to your box's IP address (%s %s) if you would like the box to serve
webmail or a website on this domain. The domain currently resolves to %s in public DNS. It may take several hours for
public DNS to update after a change. This problem may result from other issues listed here.""" % (env['PUBLIC_IP'], ip))
public DNS to update after a change. This problem may result from other issues listed here.""" % (rtype, expected, value))
return
# We need a SSL certificate for PRIMARY_HOSTNAME because that's where the
# If both A and AAAA are correct...
output.print_ok("Domain resolves to this box's IP address. [%s%s]" % (domain, '; '.join(ok_values)))
# We need a TLS certificate for PRIMARY_HOSTNAME because that's where the
# user will log in with IMAP or webmail. Any other domain we serve a
# website for also needs a signed certificate.
check_ssl_cert(domain, rounded_time, ssl_certificates, env, output)
@@ -631,59 +691,58 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None):
# periods from responses since that's how qnames are encoded in DNS but is
# confusing for us. The order of the answers doesn't matter, so sort so we
# can compare to a well known order.
# Unfortunately, the response.__str__ returns bytes
# instead of string, if it resulted from an AAAA-query.
# We need to convert manually, until this is fixed:
# https://github.com/rthalley/dnspython/issues/204
#
# BEGIN HOTFIX
response_new = []
for r in response:
if isinstance(r.to_text(), bytes):
response_new.append(r.to_text().decode('utf-8'))
else:
response_new.append(r)
response = response_new
# END HOTFIX
return "; ".join(sorted(str(r).rstrip('.') for r in response))
def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
# Check that SSL certificate is signed.
# Check that TLS certificate is signed.
# Skip the check if the A record is not pointed here.
if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return
# Where is the SSL stored?
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
if x is None:
output.print_warning("""No SSL certificate is installed for this domain. Visitors to a website on
# Where is the certificate file stored?
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
if tls_cert is None:
output.print_warning("""No TLS (SSL) certificate is installed for this domain. Visitors to a website on
this domain will get a security warning. If you are not serving a website on this domain, you do
not need to take any action. Use the SSL Certificates page in the control panel to install a
SSL certificate.""")
not need to take any action. Use the TLS Certificates page in the control panel to install a
TLS certificate.""")
return
ssl_key, ssl_certificate, ssl_via = x
# Check that the certificate is good.
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, rounded_time=rounded_time)
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"], rounded_time=rounded_time)
if cert_status == "OK":
# The certificate is ok. The details has expiry info.
output.print_ok("SSL certificate is signed & valid. %s %s" % (ssl_via if ssl_via else "", cert_status_details))
output.print_ok("TLS (SSL) certificate is signed & valid. " + cert_status_details)
elif cert_status == "SELF-SIGNED":
# Offer instructions for purchasing a signed certificate.
fingerprint = shell('check_output', [
"openssl",
"x509",
"-in", ssl_certificate,
"-noout",
"-fingerprint"
])
fingerprint = re.sub(".*Fingerprint=", "", fingerprint).strip()
if domain == env['PRIMARY_HOSTNAME']:
output.print_error("""The SSL certificate for this domain is currently self-signed. You will get a security
output.print_error("""The TLS (SSL) certificate for this domain is currently self-signed. You will get a security
warning when you check or send email and when visiting this domain in a web browser (for webmail or
static site hosting). Use the SSL Certificates page in the control panel to install a signed SSL certificate.
You may choose to leave the self-signed certificate in place and confirm the security exception, but check that
the certificate fingerprint matches the following:""")
output.print_line("")
output.print_line(" " + fingerprint, monospace=True)
static site hosting).""")
else:
output.print_error("""The SSL certificate for this domain is self-signed.""")
output.print_error("""The TLS (SSL) certificate for this domain is self-signed.""")
else:
output.print_error("The SSL certificate has a problem: " + cert_status)
output.print_error("The TLS (SSL) certificate has a problem: " + cert_status)
if cert_status_details:
output.print_line("")
output.print_line(cert_status_details)
@@ -732,10 +791,10 @@ def what_version_is_this(env):
return tag
def get_latest_miab_version():
# This pings https://mailinabox.email/bootstrap.sh and extracts the tag named in
# This pings https://mailinabox.email/setup.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")
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/setup.sh?ping=1").read()).group(1).decode("utf8")
def check_miab_version(env, output):
config = load_settings(env)
@@ -743,23 +802,24 @@ def check_miab_version(env, output):
if config.get("privacy", True):
output.print_warning("Mail-in-a-Box version check disabled by privacy setting.")
else:
try:
this_ver = what_version_is_this(env)
except:
this_ver = "Unknown"
latest_ver = get_latest_miab_version()
if this_ver == latest_ver:
output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver)
else:
output.print_error("A new version of Mail-in-a-Box is available. You are running version %s. The latest version is %s. For upgrade instructions, see https://mailinabox.email. "
% (this_ver, latest_ver))
def run_and_output_changes(env, pool, send_via_email):
def run_and_output_changes(env, pool):
import json
from difflib import SequenceMatcher
if not send_via_email:
out = ConsoleOutput()
else:
import io
out = FileOutput(io.StringIO(""), 70)
# Run status checks.
cur = BufferedOutput()
@@ -818,28 +878,6 @@ def run_and_output_changes(env, pool, send_via_email):
out.add_heading(category)
out.print_warning("This section was removed.")
if send_via_email:
# If there were changes, send off an email.
buf = out.buf.getvalue()
if len(buf) > 0:
# create MIME message
from email.message import Message
msg = Message()
msg['From'] = "\"%s\" <administrator@%s>" % (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'])
msg['To'] = "administrator@%s" % env['PRIMARY_HOSTNAME']
msg['Subject'] = "[%s] Status Checks Change Notice" % env['PRIMARY_HOSTNAME']
msg.set_payload(buf, "UTF-8")
# send to administrator@
import smtplib
mailserver = smtplib.SMTP('localhost', 25)
mailserver.ehlo()
mailserver.sendmail(
"administrator@%s" % env['PRIMARY_HOSTNAME'], # MAIL FROM
"administrator@%s" % env['PRIMARY_HOSTNAME'], # RCPT TO
msg.as_string())
mailserver.quit()
# Store the current status checks output for next time.
os.makedirs(os.path.dirname(cache_fn), exist_ok=True)
with open(cache_fn, "w") as f:
@@ -870,7 +908,7 @@ class FileOutput:
words = re.split("(\s+)", message)
linelen = 0
for w in words:
if linelen + len(w) > self.width-1-len(first_line):
if self.width and (linelen + len(w) > self.width-1-len(first_line)):
print(file=self.buf)
print(" ", end="", file=self.buf)
linelen = 0
@@ -886,11 +924,23 @@ class FileOutput:
class ConsoleOutput(FileOutput):
def __init__(self):
self.buf = sys.stdout
# Do nice line-wrapping according to the size of the terminal.
# The 'stty' program queries standard input for terminal information.
if sys.stdin.isatty():
try:
self.width = int(shell('check_output', ['stty', 'size']).split()[1])
except:
self.width = 76
else:
# However if standard input is not a terminal, we would get
# "stty: standard input: Inappropriate ioctl for device". So
# we test with sys.stdin.isatty first, and if it is not a
# terminal don't do any line wrapping. When this script is
# run from cron, or if stdin has been redirected, this happens.
self.width = None
class BufferedOutput:
# Record all of the instance method calls so we can play them back later.
def __init__(self, with_lines=None):
@@ -917,7 +967,7 @@ if __name__ == "__main__":
run_checks(False, env, ConsoleOutput(), pool)
elif sys.argv[1] == "--show-changes":
run_and_output_changes(env, pool, sys.argv[-1] == "--smtp")
run_and_output_changes(env, pool)
elif sys.argv[1] == "--check-primary-hostname":
# See if the primary hostname appears resolvable and has a signed certificate.
@@ -925,10 +975,10 @@ if __name__ == "__main__":
if query_dns(domain, "A") != env['PUBLIC_IP']:
sys.exit(1)
ssl_certificates = get_ssl_certificates(env)
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, ssl_certificates, env)
if not os.path.exists(ssl_certificate):
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
if not os.path.exists(tls_cert["certificate"]):
sys.exit(1)
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, warn_if_expiring_soon=False)
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"], warn_if_expiring_soon=False)
if cert_status != "OK":
sys.exit(1)
sys.exit(0)

View File

@@ -106,6 +106,41 @@
</table>
</div>
<h3>Mail aliases API (advanced)</h3>
<p>Use your box&rsquo;s mail aliases API to add and remove mail aliases from the command-line or custom services you build.</p>
<p>Usage:</p>
<pre>curl -X <b>VERB</b> [-d "<b>parameters</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/aliases[<b>action</b>]</pre>
<p>Brackets denote an optional argument. Please note that the POST body <code>parameters</code> must be URL-encoded.</p>
<p>The email and password given to the <code>--user</code> option must be an administrative user on this system.</p>
<h4 style="margin-bottom: 0">Verbs</h4>
<table class="table" style="margin-top: .5em">
<thead><th>Verb</th> <th>Action</th><th></th></thead>
<tr><td>GET</td><td><i>(none)</i></td> <td>Returns a list of existing mail aliases. Adding <code>?format=json</code> to the URL will give JSON-encoded results.</td></tr>
<tr><td>POST</td><td>/add</td> <td>Adds a new mail alias. Required POST-body parameters are <code>address</code> and <code>forwards_to</code>.</td></tr>
<tr><td>POST</td><td>/remove</td> <td>Removes a mail alias. Required POST-body parameter is <code>address</code>.</td></tr>
</table>
<h4>Examples:</h4>
<p>Try these examples. For simplicity the examples omit the <code>--user me@mydomain.com:yourpassword</code> command line argument which you must fill in with your email address and password.</p>
<pre># Gives a JSON-encoded list of all mail aliases
curl -X GET https://{{hostname}}/admin/mail/aliases?format=json
# Adds a new alias
curl -X POST -d "address=new_alias@mydomail.com" -d "forwards_to=my_email@mydomain.com" https://{{hostname}}/admin/mail/aliases/add
# Removes an alias
curl -X POST -d "address=new_alias@mydomail.com" https://{{hostname}}/admin/mail/aliases/remove
</pre>
<script>
function show_aliases() {

View File

@@ -10,7 +10,7 @@
<p>It is possible to set custom DNS records on domains hosted here.</p>
<h3>Set Custom DNS Records</h3>
<h3>Set custom DNS records</h3>
<p>You can set additional DNS records, such as if you have a website running on another server, to add DKIM records for external mail providers, or for various confirmation-of-ownership tests.</p>
@@ -35,7 +35,8 @@
<option value="AAAA" data-hint="Enter an IPv6 address.">AAAA (IPv6 address)</option>
<option value="CNAME" data-hint="Enter another domain name followed by a period at the end (e.g. mypage.github.io.).">CNAME (DNS forwarding)</option>
<option value="TXT" data-hint="Enter arbitrary text.">TXT (text record)</option>
<option value="MX" data-hint="Enter record in the form of PRIORIY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">MX (mail exchanger)</option>
<option value="MX" data-hint="Enter record in the form of PRIORITY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">MX (mail exchanger)</option>
<option value="SRV" data-hint="Enter record in the form of PRIORITY WEIGHT PORT TARGET., including trailing period (e.g. 10 10 5060 sip.example.com.).">SRV (service record)</option>
</select>
</div>
</div>
@@ -65,10 +66,10 @@
</tbody>
</table>
<h3>Using a Secondary Nameserver</h3>
<h3>Using a secondary nameserver</h3>
<p>If your TLD requires you to have two separate nameservers, you can either set up <a href="#" onclick="return show_panel('external_dns')">external DNS</a> and ignore the DNS server on this box entirely, or use the DNS server on this box but add a secondary (aka &ldquo;slave&rdquo;) nameserver.</p>
<p>If you choose to use a seconday nameserver, you must find a seconday nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday nameserver service, enter the hostname (not the IP address) of <em>their</em> secondary nameserver in the box below.</p>
<p>If you choose to use a secondary nameserver, you must find a secondary nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the secondary nameserver service, enter the hostname (not the IP address) of <em>their</em> secondary nameserver in the box below.</p>
<form class="form-horizontal" role="form" onsubmit="do_set_secondary_dns(); return false;">
<div class="form-group">

View File

@@ -9,7 +9,7 @@
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc=" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<style>
body {
overflow-y: scroll;
@@ -63,14 +63,14 @@
margin-bottom: 1em;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css" integrity="sha256-bHQiqcFbnJb1Qhh61RY9cMh6kR0gTuQY6iFOBj1yj00=" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
</head>
<body>
<!--[if lt IE 8]><p>Internet Explorer version 8 or any modern web browser is required to use this website, sorry.<![endif]-->
<!--[if gt IE 7]><!-->
<div class="navbar navbar-inverse" role="navigation">
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
@@ -87,13 +87,13 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
<li><a href="#tls" onclick="return show_panel(this);">TLS (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 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>
<li><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
</ul>
</li>
<li class="dropdown">
@@ -155,7 +155,7 @@
{% include "web.html" %}
</div>
<div id="panel_ssl" class="admin_panel">
<div id="panel_tls" class="admin_panel">
{% include "ssl.html" %}
</div>
@@ -192,7 +192,7 @@
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js" integrity="sha256-rsPUGdUPBXgalvIj4YKJrrUlmLXbOb6Cp7cdxn1qeUc=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha256-Sk3nkD6mLTMOF0EOpNtsIry+s1CsaqQC1rVLTAy+0yc=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script>
var global_modal_state = null;

View File

@@ -117,7 +117,7 @@ function do_login() {
// Open the next panel the user wants to go to. Do this after the XHR response
// is over so that we don't start a new XHR request while this one is finishing,
// which confuses the loading indicator.
setTimeout(function() { show_panel(!switch_back_to_panel ? 'system_status' : switch_back_to_panel) }, 300);
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
}
})
}

View File

@@ -1,12 +1,46 @@
<style>
</style>
<h2>SSL Certificates</h2>
<h2>TLS (SSL) Certificates</h2>
<h3>Certificate Status</h3>
<p>A TLS (formerly called SSL) certificate is a cryptographic file that proves to anyone connecting to a web address that the connection is secure between you and the owner of that address.</p>
<p>You need a TLS certificate for this box&rsquo;s hostname ({{hostname}}) and every other domain name and subdomain that this box is hosting a website for (see the list below).</p>
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;">
<div id="ssl_provision">
<h3>Provision a certificate</h3>
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
<button onclick='return provision_tls_cert();' class='btn btn-primary' style="float: left; margin: 0 1.5em 1em 0;">Provision</button>
<p>A TLS certificate can be automatically provisioned from <a href="https://letsencrypt.org/" target="_blank">Let&rsquo;s Encrypt</a>, a free TLS certificate provider, for:<br>
<span class="text-primary"></span></p>
</div>
<div class="clearfix"> </div>
<div id="ssl_provision_result"></div>
<div id="ssl_provision_problems_div" style="display: none;">
<p style="margin-bottom: .5em;">Certificates cannot be automatically provisioned for:</p>
<table id="ssl_provision_problems" style="margin-top: 0;" class="table">
<thead>
<tr>
<th>Domain</th>
<th>Problem</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p>Use the <em>Install Certificate</em> button below for these domains.</p>
</div>
</div>
<h3>Certificate status</h3>
<p style="margin-top: 1.5em">Certificates expire after a period of time. All certificates will be automatically renewed through <a href="https://letsencrypt.org/" target="_blank">Let&rsquo;s Encrypt</a> 14 days prior to expiration.</p>
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto; display: none">
<thead>
<tr>
<th>Domain</th>
@@ -18,17 +52,18 @@
</tbody>
</table>
<p>A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for.</p>
<h3 id="ssl_install_header">Install SSL Certificate</h3>
<h3 id="ssl_install_header">Install certificate</h3>
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap&rsquo;s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL&rsquo;s free express lane</a> or <a href="https://buy.wosign.com/free/">WoSign&rsquo;s free SSL</a></a>.</p>
<p>If you don't want to use our automatic Let's Encrypt integration, you can give any other certificate provider a try. You can generate the needed CSR below.</p>
<p>Which domain are you getting an SSL certificate for?</p>
<p>Which domain are you getting a certificate for?</p>
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
<p>What country are you in? This is required by some SSL certificate providers. You may leave this blank if you know your SSL certificate provider doesn't require it.</p>
<p>(A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for besides the one you choose above.)</p>
<p>What country are you in? This is required by some TLS certificate providers. You may leave this blank if you know your TLS certificate provider doesn't require it.</p>
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
<option value="">(Select)</option>
@@ -38,18 +73,18 @@
</select></p>
<div id="csr_info" style="display: none">
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
<p>You will need to provide the certificate provider this Certificate Signing Request (CSR):</p>
<pre id="ssl_csr"></pre>
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
<p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
<p>The certificate provider will then provide you with a TLS/SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
<p style="margin-bottom: .5em">SSL certificate:</p>
<p style="margin-bottom: .5em">TLS/SSL certificate:</p>
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p>
<p style="margin-bottom: .5em">TLS/SSL intermediate chain (if provided):</p>
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----&#xA;-----BEGIN CERTIFICATE-----&#xA;more stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p>After you paste in the information, click the install button.</p>
@@ -58,26 +93,53 @@
</div>
<script>
function show_ssl() {
function show_tls(keep_provisioning_shown) {
api(
"/web/domains",
"/ssl/status",
"GET",
{
},
function(domains) {
function(res) {
// provisioning status
if (!keep_provisioning_shown)
$('#ssl_provision').toggle(res.can_provision.length + res.cant_provision.length > 0)
$('#ssl_provision_p').toggle(res.can_provision.length > 0);
if (res.can_provision.length > 0)
$('#ssl_provision_p span').text(res.can_provision.join(", "));
$('#ssl_provision_problems_div').toggle(res.cant_provision.length > 0);
$('#ssl_provision_problems tbody').text("");
for (var i = 0; i < res.cant_provision.length; i++) {
var domain = res.cant_provision[i];
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td></tr>");
$('#ssl_provision_problems tbody').append(row);
row.attr('data-domain', domain.domain);
row.find('.domain a').text(domain.domain);
row.find('.domain a').attr('href', 'https://' + domain.domain);
row.find('.status').text(domain.problem);
}
// certificate status
var domains = res.status;
var tb = $('#ssl_domains tbody');
tb.text('');
$('#ssldomain').html('<option value="">(select)</option>');
$('#ssl_domains').show();
for (var i = 0; i < domains.length; i++) {
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
tb.append(row);
row.attr('data-domain', domains[i].domain);
row.find('.domain a').text(domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.addClass("text-" + domains[i].ssl_certificate[0]);
row.find('.status').text(domains[i].ssl_certificate[1]);
if (domains[i].ssl_certificate[0] == "success") {
if (domains[i].status == "not-applicable") {
domains[i].status = "muted"; // text-muted css class
row.find('.actions a').remove(); // no actions applicable
}
row.addClass("text-" + domains[i].status);
row.find('.status').text(domains[i].text);
if (domains[i].status == "success") {
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
} else {
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
@@ -91,14 +153,15 @@ function show_ssl() {
function ssl_install(elem) {
var domain = $(elem).parents('tr').attr('data-domain');
$('#ssldomain').val(domain);
$('#csr_info').slideDown();
$('#ssl_csr').text('Loading...');
show_csr();
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
return false;
}
function show_csr() {
if ($('#ssldomain').val() == "") return;
$('#csr_info').slideDown();
$('#ssl_csr').text('Loading...');
api(
"/ssl/csr/" + $('#ssldomain').val(),
"POST",
@@ -122,10 +185,100 @@ function install_cert() {
function(status) {
if (/^OK($|\n)/.test(status)) {
console.log(status)
show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
show_modal_error("TLS Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
} else {
show_modal_error("SSL Certificate Installation", status);
show_modal_error("TLS Certificate Installation", status);
}
});
}
var agree_to_tos_url_prompt = null;
var agree_to_tos_url = null;
function provision_tls_cert() {
// Automatically provision any certs.
$('#ssl_provision_p .btn').attr('disabled', '1'); // prevent double-clicks
api(
"/ssl/provision",
"POST",
{
agree_to_tos_url: agree_to_tos_url
},
function(status) {
// Clear last attempt.
agree_to_tos_url = null;
$('#ssl_provision_result').text("");
may_reenable_provision_button = true;
// Nothing was done. There might also be problem domains, but we've already displayed those.
if (status.requests.length == 0) {
show_modal_error("TLS Certificate Provisioning", "There were no domain names to provision certificates for.");
// don't return - haven't re-enabled the provision button
}
// Each provisioning API call returns zero or more "requests" which represent
// a request to Let's Encrypt for a single certificate. Normally there is just
// one request (for a single multi-domain certificate).
for (var i = 0; i < status.requests.length; i++) {
var r = status.requests[i];
// create an HTML block to display the results of this request
var n = $("<div><h4/><p/></div>");
$('#ssl_provision_result').append(n);
// show a header only to disambiguate request blocks
if (status.requests.length > 0)
n.find("h4").text(r.domains.join(", "));
if (r.result == "agree-to-tos") {
// user needs to agree to Let's Encrypt's TOS
agree_to_tos_url_prompt = r.url;
$('#ssl_provision_p .btn').attr('disabled', '1');
n.find("p").html("Please open and review <a href='" + r.url + "' target='_blank'>Let's Encrypt's terms of service agreement</a>. You must agree to their terms for a certificate to be automatically provisioned from them.");
n.append($('<button onclick="agree_to_tos_url = agree_to_tos_url_prompt; return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Agree &amp; Try Again</button>'));
// don't re-enable the Provision button -- user must use the Agree button
may_reenable_provision_button = false;
} else if (r.result == "error") {
n.find("p").addClass("text-danger").text(r.message);
} else if (r.result == "wait") {
// Show a button that counts down to zero, at which point it becomes enabled.
n.find("p").text("A certificate is now in the process of being provisioned, but it takes some time. Please wait until the Finish button is enabled, and then click it to acquire the certificate.");
var b = $('<button onclick="return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Finish</button>');
b.attr("disabled", "1");
var now = new Date();
n.append(b);
function ready_to_finish() {
var remaining = Math.round(r.seconds - (new Date() - now)/1000);
if (remaining > 0) {
setTimeout(ready_to_finish, 1000);
b.text("Finish (" + remaining + "...)")
} else {
b.text("Finish (ready)")
b.removeAttr("disabled");
}
}
ready_to_finish();
// don't re-enable the Provision button -- user must use the Retry button when it becomes enabled
may_reenable_provision_button = false;
} else if (r.result == "installed") {
n.find("p").addClass("text-success").text("The TLS certificate was provisioned and installed.");
setTimeout("show_tls(true)", 1); // update main table of certificate statuses, call with arg keep_provisioning_shown true so that we don't clear what we just outputted
}
// display the detailed log info in case of problems
var trace = $("<div class='small text-muted' style='margin-top: 1.5em'>Log:</div>");
n.append(trace);
for (var j = 0; j < r.log.length; j++)
trace.append($("<div/>").text(r.log[j]));
}
if (may_reenable_provision_button)
$('#ssl_provision_p .btn').removeAttr("disabled");
});
}
</script>

View File

@@ -16,16 +16,60 @@
<select class="form-control" rows="1" id="backup-target-type" onchange="toggle_form()">
<option value="off">Nowhere (Disable Backups)</option>
<option value="local">{{hostname}}</option>
<option value="rsync">rsync</option>
<option value="s3">Amazon S3</option>
</select>
</div>
</div>
<!-- LOCAL BACKUP -->
<div class="form-group backup-target-local">
<div class="col-sm-10 col-sm-offset-2">
<p>Backups are stored on this machine&rsquo;s own hard disk. You are responsible for periodically using SFTP (FTP over SSH) to copy the backup files from <tt id="backup-location"></tt> to a safe location. These files are encrypted, so they are safe to store anywhere.</p>
<p>Backups are stored on this machine&rsquo;s own hard disk. You are responsible for periodically using SFTP (FTP over SSH) to copy the backup files from <tt class="backup-location"></tt> to a safe location. These files are encrypted, so they are safe to store anywhere.</p>
<p>Separately copy the encryption password from <tt class="backup-encpassword-file"></tt> to a safe and secure location. You will need this file to decrypt backup files.</p>
</div>
</div>
<!-- RSYNC BACKUP -->
<div class="form-group backup-target-rsync">
<div class="col-sm-10 col-sm-offset-2">
<p>Backups synced to a remote machine using rsync over SSH, with local
copies in <tt class="backup-location"></tt>. These files are encrypted, so
they are safe to store anywhere.</p> <p>Separately copy the encryption
password from <tt class="backup-encpassword-file"></tt> to a safe and
secure location. You will need this file to decrypt backup files.</p>
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label>
<div class="col-sm-8">
<input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host">
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="backup-target-rsync-path" class="col-sm-2 control-label">Path</label>
<div class="col-sm-8">
<input type="text" placeholder="/backups/{{hostname}}" class="form-control" rows="1" id="backup-target-rsync-path">
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="backup-target-rsync-user" class="col-sm-2 control-label">Username</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-rsync-user">
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="ssh-pub-key" class="col-sm-2 control-label">Public SSH Key</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="ssh-pub-key" readonly>
<div class="small" style="margin-top: 2px">
Copy the Public SSH Key above, and paste it within the <tt>~/.ssh/authorized_keys</tt>
of target user on the backup server specified above. That way you'll enable secure and
passwordless authentication from your mail-in-a-box server and your backup server.
</div>
</div>
</div>
<!-- S3 BACKUP -->
<div class="form-group backup-target-s3">
<div class="col-sm-10 col-sm-offset-2">
<p>Backups are stored in an Amazon Web Services S3 bucket. You must have an AWS account already.</p>
@@ -60,7 +104,8 @@
<input type="text" class="form-control" rows="1" id="backup-target-pass">
</div>
</div>
<div class="form-group backup-target-local backup-target-s3">
<!-- Common -->
<div class="form-group backup-target-local backup-target-rsync backup-target-s3">
<label for="min-age" class="col-sm-2 control-label">Days:</label>
<div class="col-sm-8">
<input type="number" class="form-control" rows="1" id="min-age">
@@ -74,7 +119,7 @@
</div>
</form>
<h3>Available Backups</h3>
<h3>Available backups</h3>
<p>The backup location currently contains the backups listed below. The total size of the backups is currently <span id="backup-total-size"></span>.</p>
@@ -92,7 +137,7 @@
function toggle_form() {
var target_type = $("#backup-target-type").val();
$(".backup-target-local, .backup-target-s3").hide();
$(".backup-target-local, .backup-target-rsync, .backup-target-s3").hide();
$(".backup-target-" + target_type).show();
}
@@ -121,6 +166,11 @@ function show_system_backup() {
"GET",
{ },
function(r) {
if (r.error) {
show_modal_error("Backup Error", $("<pre/>").text(r.error));
return;
}
$('#backup-status tbody').html("");
var total_disk_size = 0;
@@ -137,7 +187,7 @@ function show_system_backup() {
var b = r.backups[i];
var tr = $('<tr/>');
if (b.full) tr.addClass("full-backup");
tr.append( $('<td/>').text(b.date_str + " " + r.tz) );
tr.append( $('<td/>').text(b.date_str) );
tr.append( $('<td/>').text(b.date_delta + " ago") );
tr.append( $('<td/>').text(b.full ? "full" : "increment") );
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.size)) );
@@ -155,16 +205,30 @@ function show_system_backup() {
}
function show_custom_backup() {
$(".backup-target-local, .backup-target-s3").hide();
$(".backup-target-local, .backup-target-rsync, .backup-target-s3").hide();
api(
"/system/backup/config",
"GET",
{ },
function(r) {
$("#backup-target-user").val(r.target_user);
$("#backup-target-pass").val(r.target_pass);
$("#min-age").val(r.min_age_in_days);
$(".backup-location").text(r.file_target_directory);
$(".backup-encpassword-file").text(r.enc_pw_file);
$("#ssh-pub-key").val(r.ssh_pub_key);
if (r.target == "file://" + r.file_target_directory) {
$("#backup-target-type").val("local");
} else if (r.target == "off") {
$("#backup-target-type").val("off");
} else if (r.target.substring(0, 8) == "rsync://") {
$("#backup-target-type").val("rsync");
var path = r.target.substring(8).split('//');
var host_parts = path.shift().split('@');
$("#backup-target-rsync-user").val(host_parts[0]);
$("#backup-target-rsync-host").val(host_parts[1]);
$("#backup-target-rsync-path").val('/'+path[0]);
} else if (r.target.substring(0, 5) == "s3://") {
$("#backup-target-type").val("s3");
var hostpath = r.target.substring(5).split('/');
@@ -172,11 +236,6 @@ function show_custom_backup() {
$("#backup-target-s3-host").val(host);
$("#backup-target-s3-path").val(hostpath.join('/'));
}
$("#backup-target-user").val(r.target_user);
$("#backup-target-pass").val(r.target_pass);
$("#min-age").val(r.min_age_in_days);
$('#backup-location').text(r.file_target_directory);
$('.backup-encpassword-file').text(r.enc_pw_file);
toggle_form()
})
}
@@ -191,6 +250,12 @@ function set_custom_backup() {
target = target_type;
else if (target_type == "s3")
target = "s3://" + $("#backup-target-s3-host").val() + "/" + $("#backup-target-s3-path").val();
else if (target_type == "rsync") {
target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val()
+ "/" + $("#backup-target-rsync-path").val();
target_user = '';
}
var min_age = $("#min-age").val();
api(

View File

@@ -34,19 +34,23 @@
font-family: monospace;
white-space: pre-wrap;
}
#system-privacy-setting {
float: right;
max-width: 20em;
margin-bottom: 1em;
}
</style>
<div class="row">
<div class="col-md-push-9 col-md-3">
<div id="system-reboot-required" style="display: none; margin-bottom: 1em;">
<button type="button" class="btn btn-danger" onclick="confirm_reboot(); return false;">Reboot Box</button>
<div>No reboot is necessary.</div>
</div>
<div id="system-privacy-setting" style="display: none">
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span> New-Version Check</a></div>
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of Mail-in-a-Box.)</small></p>
</div>
</div> <!-- /col -->
<div class="col-md-pull-3 col-md-8">
<table id="system-checks" class="table" style="max-width: 60em">
<thead>
@@ -55,6 +59,9 @@
</tbody>
</table>
</div> <!-- /col -->
</div> <!-- /row -->
<script>
function show_system_status() {
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
@@ -70,6 +77,16 @@ function show_system_status() {
$('#system-privacy-setting p').toggle(r);
});
api(
"/system/reboot",
"GET",
{ },
function(r) {
$('#system-reboot-required').show(); // show when r becomes available
$('#system-reboot-required').find('button').toggle(r);
$('#system-reboot-required').find('div').toggle(!r);
});
api(
"/system/status",
"POST",
@@ -122,4 +139,22 @@ function enable_privacy(status) {
});
return false; // disable link
}
function confirm_reboot() {
show_modal_confirm(
"Reboot",
$("<p>This will reboot your Mail-in-a-Box <code>{{hostname}}</code>.</p> <p>Until the machine is fully restarted, your users will not be able to send and receive email, and you will not be able to connect to this control panel or with SSH. The reboot cannot be cancelled.</p>"),
"Reboot Now",
function() {
api(
"/system/reboot",
"POST",
{ },
function(r) {
var msg = "<p>Please reload this page after a minute or so.</p>";
if (r) msg = "<p>The reboot command said:</p> <pre>" + $("<pre/>").text(r).html() + "</pre>"; // successful reboots don't produce any output; the output must be HTML-escaped
show_modal_error("Reboot", msg);
});
});
}
</script>

View File

@@ -84,6 +84,48 @@
</table>
</div>
<h3>Mail user API (advanced)</h3>
<p>Use your box&rsquo;s mail user API to add/change/remove users from the command-line or custom services you build.</p>
<p>Usage:</p>
<pre>curl -X <b>VERB</b> [-d "<b>parameters</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/users[<b>action</b>]</pre>
<p>Brackets denote an optional argument. Please note that the POST body <code>parameters</code> must be URL-encoded.</p>
<p>The email and password given to the <code>--user</code> option must be an administrative user on this system.</p>
<h4 style="margin-bottom: 0">Verbs</h4>
<table class="table" style="margin-top: .5em">
<thead><th>Verb</th> <th>Action</th><th></th></thead>
<tr><td>GET</td><td><i>(none)</i></td> <td>Returns a list of existing mail users. Adding <code>?format=json</code> to the URL will give JSON-encoded results.</td></tr>
<tr><td>POST</td><td>/add</td> <td>Adds a new mail user. Required POST-body parameters are <code>email</code> and <code>password</code>.</td></tr>
<tr><td>POST</td><td>/remove</td> <td>Removes a mail user. Required POST-by parameter is <code>email</code>.</td></tr>
<tr><td>POST</td><td>/privileges/add</td> <td>Used to make a mail user an admin. Required POST-body parameters are <code>email</code> and <code>privilege=admin</code>.</td></tr>
<tr><td>POST</td><td>/privileges/remove</td> <td>Used to remove the admin privilege from a mail user. Required POST-body parameter is <code>email</code>.</td></tr>
</table>
<h4>Examples:</h4>
<p>Try these examples. For simplicity the examples omit the <code>--user me@mydomain.com:yourpassword</code> command line argument which you must fill in with your administrative email address and password.</p>
<pre># Gives a JSON-encoded list of all mail users
curl -X GET https://{{hostname}}/admin/mail/users?format=json
# Adds a new email user
curl -X POST -d "email=new_user@mydomail.com" -d "password=s3curE_pa5Sw0rD" https://{{hostname}}/admin/mail/users/add
# Removes a email user
curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/users/remove
# Adds admin privilege to an email user
curl -X POST -d "email=new_user@mydomail.com" -d "privilege=admin" https://{{hostname}}/admin/mail/users/privileges/add
# Removes admin privilege from an email user
curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/users/privileges/remove
</pre>
<script>
function show_users() {
@@ -170,7 +212,7 @@ function users_set_password(elem) {
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",
"Set Password",
$("<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() {

View File

@@ -82,7 +82,7 @@ function show_change_web_root(elem) {
var root = $(elem).parents('tr').attr('data-custom-web-root');
show_modal_confirm(
'Change Root Directory for ' + domain,
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.'),
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.</p>'),
'Update',
function() { do_web_update(); });
}

View File

@@ -9,7 +9,7 @@ from dns_update import get_custom_dns_config, get_dns_zones
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
from utils import shell, safe_domain_name, sort_domains
def get_web_domains(env, include_www_redirects=True):
def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True):
# What domains should we serve HTTP(S) for?
domains = set()
@@ -24,6 +24,7 @@ def get_web_domains(env, include_www_redirects=True):
# the topmost of each domain we serve.
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
if exclude_dns_elsewhere:
# ...Unless the domain has an A/AAAA record that maps it to a different
# IP address than this box. Remove those domains from our list.
domains -= get_domains_with_a_records(env)
@@ -119,7 +120,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
root = get_web_root(domain, env)
# What private key and SSL certificate will we use for this domain?
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, ssl_certificates, env)
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
# ADDITIONAL DIRECTIVES.
@@ -136,7 +137,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
finally:
f.close()
return sha1.hexdigest()
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
# Add in any user customizations in YAML format.
hsts = "yes"
@@ -177,8 +178,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
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("$SSL_KEY", tls_cert["private-key"])
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"])
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
return nginx_conf
@@ -193,20 +194,15 @@ def get_web_root(domain, env, test_exists=True):
def get_web_domains_info(env):
www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
ssl_certificates = get_ssl_certificates(env)
# for the SSL config panel, get cert status
def check_cert(domain):
ssl_certificates = get_ssl_certificates(env)
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
if x is None: return ("danger", "No Certificate Installed")
ssl_key, ssl_certificate, ssl_via = x
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
if tls_cert is None: return ("danger", "No Certificate Installed")
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"])
if cert_status == "OK":
if not ssl_via:
return ("success", "Signed & valid. " + cert_status_details)
else:
# This is an alternate domain but using the same cert as the primary domain.
return ("success", "Signed & valid. " + ssl_via)
elif cert_status == "SELF-SIGNED":
return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
else:

View File

@@ -38,7 +38,7 @@ These services are protected by [TLS](https://en.wikipedia.org/wiki/Transport_La
The services all follow these rules:
* SSL certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
* TLS certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
* Only TLSv1, TLSv1.1 and TLSv1.2 are offered (the older SSL protocols are not offered).
* Export-grade ciphers, the anonymous DH/ECDH algorithms (aNULL), and clear-text ciphers (eNULL) are not offered.
* The minimum cipher key length offered is 112 bits. The maximum is 256 bits. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy.
@@ -69,6 +69,16 @@ The [setup guide video](https://mailinabox.email/) explains how to verify the ho
If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that the box automatically puts into DNS can also be used to verify the host key fingerprint by setting `VerifyHostKeyDNS yes` in your `ssh/.config` file or by logging in with `ssh -o VerifyHostKeyDNS=yes`. ([source](management/dns_update.py))
### Brute-force attack mitigation
`fail2ban` provides some protection from brute-force login attacks (repeated logins that guess account passwords) by blocking offending IP addresses at the network level.
The following services are protected: SSH, IMAP (dovecot), SMTP submission (postfix), webmail (roundcube), ownCloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel & munin (over HTTP).
Some other services running on the box may be missing fail2ban filters.
`fail2ban` only blocks IPv4 addresses, however. If the box has a public IPv6 address, it is not protected from these attacks.
Outbound Mail
-------------
@@ -80,7 +90,7 @@ The first step in resolving the destination server for an email address is perfo
### Encryption
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 (TLSv1 and later, no RC4) will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
### DANE
@@ -101,7 +111,7 @@ Incoming Mail
### Encryption
As discussed above, there is no way to require on-the-wire encryption of mail. When the box receives an incoming email (SMTP on port 25), it offers encryption (STARTTLS) but cannot require that senders use it because some senders may not support STARTTLS at all and other senders may support STARTTLS but not with the latest protocols/ciphers. To give senders the best chance at making use of encryption, the box offers protocols back to SSLv3 and ciphers with key lengths as low as 112 bits. Modern clients (senders) will make use of the 256-bit ciphers and Diffie-Hellman ciphers with a 2048-bit key for forward secrecy, however. ([source](setup/mail-postfix.sh))
As discussed above, there is no way to require on-the-wire encryption of mail. When the box receives an incoming email (SMTP on port 25), it offers encryption (STARTTLS) but cannot require that senders use it because some senders may not support STARTTLS at all and other senders may support STARTTLS but not with the latest protocols/ciphers. To give senders the best chance at making use of encryption, the box offers protocols back to TLSv1 and ciphers with key lengths as low as 112 bits. Modern clients (senders) will make use of the 256-bit ciphers and Diffie-Hellman ciphers with a 2048-bit key for perfect forward secrecy, however. ([source](setup/mail-postfix.sh))
### DANE

View File

@@ -2,12 +2,12 @@
#########################################################
# This script is intended to be run like this:
#
# curl https://.../bootstrap.sh | sudo bash
# curl https://mailinabox.email/setup.sh | sudo bash
#
#########################################################
if [ -z "$TAG" ]; then
TAG=v0.15a
TAG=v0.21
fi
# Are we running as root?

View File

@@ -31,7 +31,7 @@ ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Socket inet:8891@localhost
Socket inet:8891@127.0.0.1
RequireSafeKeys false
EOF
fi
@@ -39,7 +39,7 @@ fi
# 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
# in our DNS setup. Note that the files are named after the
# 'selector' of the key, which we can change later on to support
# key rotation.
#

View File

@@ -38,7 +38,8 @@ apt_install \
# would be 20 users). Set it to 250 times the number of cores this
# machine has, so on a two-core machine that's 500 processes/100 users).
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
default_process_limit=$(echo "`nproc` * 250" | bc)
default_process_limit=$(echo "`nproc` * 250" | bc) \
log_path=/var/log/mail.log
# The inotify `max_user_instances` default is 128, which constrains
# the total number of watched (IMAP IDLE push) folders by open connections.
@@ -203,5 +204,8 @@ chown -R mail.mail $STORAGE_ROOT/mail/sieve
ufw_allow imaps
ufw_allow pop3s
# Allow the Sieve port in the firewall.
ufw_allow sieve
# Restart services.
restart_service dovecot

View File

@@ -57,15 +57,26 @@ apt_install postfix postfix-pcre postgrey ca-certificates
# Set some basic settings...
#
# * Have postfix listen on all network interfaces.
# * Make outgoing connections on a particular interface (if multihomed) so that SPF passes on the receiving side.
# * Set our name (the Debian default seems to be "localhost" but make it our hostname).
# * Set the name of the local machine to localhost, which means xxx@localhost is delivered locally, although we don't use it.
# * Set the SMTP banner (which must have the hostname first, then anything).
tools/editconf.py /etc/postfix/main.cf \
inet_interfaces=all \
smtp_bind_address=$PRIVATE_IP \
smtp_bind_address6=$PRIVATE_IPV6 \
myhostname=$PRIMARY_HOSTNAME\
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
mydestination=localhost
# Tweak some queue settings:
# * Inform users when their e-mail delivery is delayed more than 3 hours (default is not to warn).
# * Stop trying to send an undeliverable e-mail after 2 days (instead of 5), and for bounce messages just try for 1 day.
tools/editconf.py /etc/postfix/main.cf \
delay_warning_time=3h \
maximal_queue_lifetime=2d \
bounce_queue_lifetime=1d
# ### Outgoing Mail
# Enable the 'submission' port 587 smtpd server and tweak its settings.
@@ -80,7 +91,8 @@ tools/editconf.py /etc/postfix/main.cf \
# * Give it a different name in syslog to distinguish it from the port 25 smtpd server.
# * Add a new cleanup service specific to the submission service ('authclean')
# that filters out privacy-sensitive headers on mail being sent out by
# authenticated users.
# authenticated users. By default Postfix also applies this to attached
# emails but we turn this off by setting nested_header_checks empty.
tools/editconf.py /etc/postfix/master.cf -s -w \
"submission=inet n - - - - smtpd
-o syslog_name=postfix/submission
@@ -89,7 +101,8 @@ tools/editconf.py /etc/postfix/master.cf -s -w \
-o smtpd_tls_ciphers=high -o smtpd_tls_exclude_ciphers=aNULL,DES,3DES,MD5,DES+MD5,RC4 -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3
-o cleanup_service_name=authclean" \
"authclean=unix n - - - 0 cleanup
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters"
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters
-o nested_header_checks="
# Install the `outgoing_mail_header_filters` file required by the new 'authclean' service.
cp conf/postfix_outgoing_mail_header_filters /etc/postfix/outgoing_mail_header_filters
@@ -111,8 +124,9 @@ tools/editconf.py /etc/postfix/main.cf \
smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \
smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \
smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \
smtpd_tls_protocols=\!SSLv2,\!SSLv3 \
smtpd_tls_ciphers=medium \
smtpd_tls_exclude_ciphers=aNULL \
smtpd_tls_exclude_ciphers=aNULL,RC4 \
smtpd_tls_received_header=yes
# Prevent non-authenticated users from sending mail that requires being
@@ -147,6 +161,10 @@ tools/editconf.py /etc/postfix/main.cf \
# even if we don't know if it's to the right party, than to not encrypt at all. Instead we'll
# now see notices about trusted certs. The CA file is provided by the package `ca-certificates`.
tools/editconf.py /etc/postfix/main.cf \
smtp_tls_protocols=\!SSLv2,\!SSLv3 \
smtp_tls_mandatory_protocols=\!SSLv2,\!SSLv3 \
smtp_tls_ciphers=medium \
smtp_tls_exclude_ciphers=aNULL,RC4 \
smtp_tls_security_level=dane \
smtp_dns_support_level=dnssec \
smtp_tls_CAfile=/etc/ssl/certs/ca-certificates.crt \

View File

@@ -38,17 +38,19 @@ passdb {
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=mail gid=mail home=$STORAGE_ROOT/mail/mailboxes/%d/%n
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
EOF
# Configure the SQL to query for a user's password.
# Configure the SQL to query for a user's metadata and password.
cat > /etc/dovecot/dovecot-sql.conf.ext << EOF;
driver = sqlite
connect = $db_path
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM users WHERE email='%u';
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users WHERE email='%u';
iterate_query = SELECT email AS user FROM users;
EOF
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions

View File

@@ -4,16 +4,25 @@ source setup/functions.sh
echo "Installing Mail-in-a-Box system management daemon..."
# Switching python 2 boto to package manager's, not pypi's.
if [ -f /usr/local/lib/python2.7/dist-packages/boto/__init__.py ]; then hide_output pip uninstall -y boto; fi
# Install packages.
# flask, yaml, dnspython, and dateutil are all for our Python 3 management daemon itself.
# duplicity does backups. python-pip is so we can 'pip install boto' for Python 2, for duplicity, so it can do backups to AWS S3.
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil python-pip
# duplicity uses python 2 so we need to use the python 2 package of boto
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
apt_install python3-flask links duplicity python-boto libyaml-dev python3-dnspython python3-dateutil \
build-essential libssl-dev libffi-dev python3-dev python-pip
hide_output pip3 install --upgrade rtyaml "email_validator>=1.0.0" "idna>=2.0.0" "cryptography>=1.0.2" boto
# These are required to pip install cryptography.
apt_install build-essential libssl-dev libffi-dev python3-dev
# email_validator is repeated in setup/questions.sh
# Install other Python 3 packages used by the management daemon.
# The first line is the packages that Josh maintains himself!
# NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced.
hide_output pip3 install --upgrade \
rtyaml "email_validator>=1.0.0" "free_tls_certificates>=0.1.3" \
"idna>=2.0.0" "cryptography>=1.0.2" boto psutil
# duplicity uses python 2 so we need to get the python 2 package of boto to have backups to S3.
# boto from the Ubuntu package manager is too out-of-date -- it doesn't support the newer
# S3 api used in some regions, which breaks backups to those regions. See #627, #653.
hide_output pip install --upgrade boto
# Create a backup directory and a random key for encrypting backups.
mkdir -p $STORAGE_ROOT/backup
@@ -44,5 +53,5 @@ cat > /etc/cron.d/mailinabox-nightly << EOF;
0 3 * * * root (cd `pwd` && management/daily_tasks.sh)
EOF
# Start it.
# Start the management server.
restart_service mailinabox

View File

@@ -7,7 +7,8 @@ source /etc/mailinabox.conf # load global vars
# install Munin
echo "Installing Munin (system monitoring)..."
apt_install munin munin-node
apt_install munin munin-node libcgi-fast-perl
# libcgi-fast-perl is needed by /usr/lib/munin/cgi/munin-cgi-graph
# edit config
cat > /etc/munin/munin.conf <<EOF;
@@ -19,6 +20,9 @@ tmpldir /etc/munin/templates
includedir /etc/munin/munin-conf.d
# path dynazoom uses for requests
cgiurl_graph /admin/munin/cgi-graph
# a simple host tree
[$PRIMARY_HOSTNAME]
address 127.0.0.1
@@ -29,6 +33,10 @@ contact.admin.command mail -s "Munin notification ${var:host}" administrator@$PR
contact.admin.always_send warning critical
EOF
# The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi
chown munin. /var/log/munin/munin-cgi-html.log
chown munin. /var/log/munin/munin-cgi-graph.log
# ensure munin-node knows the name of this machine
tools/editconf.py /etc/munin/munin-node.conf -s \
host_name=$PRIMARY_HOSTNAME

View File

@@ -16,10 +16,6 @@ apt_install \
apt-get purge -qq -y owncloud*
# Install ownCloud from source of this version:
owncloud_ver=8.1.1
owncloud_hash=34077e78575a3e689825a00964ee37fbf83fbdda
# 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 ] \
@@ -32,28 +28,34 @@ if [ ! -f $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
InstallOwncloud() {
echo
echo "Upgrading to ownCloud version $1"
echo
version=$1
hash=$2
# Remove the current owncloud
rm -rf /usr/local/lib/owncloud
# Download and verify
wget_verify https://download.owncloud.org/community/owncloud-$owncloud_ver.zip $owncloud_hash /tmp/owncloud.zip
# Clear out the existing ownCloud.
if [ -d /usr/local/lib/owncloud/ ]; then
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud directory to /tmp/owncloud-backup-$$)..."
mv /usr/local/lib/owncloud /tmp/owncloud-backup-$$
fi
wget_verify https://download.owncloud.org/community/owncloud-$version.zip $hash /tmp/owncloud.zip
# Extract ownCloud
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
unzip -q /tmp/owncloud.zip -d /usr/local/lib
rm -f /tmp/owncloud.zip
# The two apps we actually want are not in ownCloud core. Clone them from
# The two apps we actually want are not in ownCloud core. Download the releases from
# their github repositories.
mkdir -p /usr/local/lib/owncloud/apps
git_clone https://github.com/owncloud/contacts 4ff855e7c2075309041bead09fbb9eb7df678244 '' /usr/local/lib/owncloud/apps/contacts
git_clone https://github.com/owncloud/calendar ec53139b144c0f842c33813305612e8006c42ea5 '' /usr/local/lib/owncloud/apps/calendar
wget_verify https://github.com/owncloud/contacts/releases/download/v1.4.0.0/contacts.tar.gz c1c22d29699456a45db447281682e8bc3f10e3e7 /tmp/contacts.tgz
tar xf /tmp/contacts.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/contacts.tgz
wget_verify https://github.com/nextcloud/calendar/releases/download/v1.4.0/calendar.tar.gz c84f3170efca2a99ea6254de34b0af3cb0b3a821 /tmp/calendar.tgz
tar xf /tmp/calendar.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/calendar.tgz
# Fix weird permissions.
chmod 750 /usr/local/lib/owncloud/{apps,config}
@@ -69,7 +71,7 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
# If this isn't a new installation, immediately run the upgrade script.
# Then check for success (0=ok and 3=no upgrade needed, both are success).
if [ -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then
# ownCloud 8.1.1 broke upgrades. It may fail on the first attempt, but
# that can be OK.
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade
@@ -81,6 +83,76 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
echo "...which seemed to work."
fi
fi
}
owncloud_ver=9.1.1
# 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
# Stop php-fpm
hide_output service php5-fpm stop
# Backup the existing ownCloud.
# Create a backup directory to store the current installation and database to
BACKUP_DIRECTORY=$STORAGE_ROOT/owncloud-backup/`date +"%Y-%m-%d-%T"`
mkdir -p "$BACKUP_DIRECTORY"
if [ -d /usr/local/lib/owncloud/ ]; then
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud installation, configuration and database to directory to $BACKUP_DIRECTORY..."
cp -r /usr/local/lib/owncloud "$BACKUP_DIRECTORY/owncloud-install"
fi
if [ -e /home/user-data/owncloud/owncloud.db ]; then
cp /home/user-data/owncloud/owncloud.db $BACKUP_DIRECTORY
fi
if [ -e /home/user-data/owncloud/config.php ]; then
cp /home/user-data/owncloud/config.php $BACKUP_DIRECTORY
fi
# We only need to check if we do upgrades when owncloud was previously installed
if [ -e /usr/local/lib/owncloud/version.php ]; then
if grep -q "8.1.[0-9]" /usr/local/lib/owncloud/version.php; then
echo "We are running 8.1.x, upgrading to 8.2.3 first"
InstallOwncloud 8.2.3 bfdf6166fbf6fc5438dc358600e7239d1c970613
fi
# If we are upgrading from 8.2.x we should go to 9.0 first. Owncloud doesn't support skipping minor versions
if grep -q "8.2.[0-9]" /usr/local/lib/owncloud/version.php; then
echo "We are running version 8.2.x, upgrading to 9.0.2 first"
# We need to disable memcached. The upgrade and install fails
# with memcached
CONFIG_TEMP=$(/bin/mktemp)
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
<?php
include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['memcache.local'] = '\OC\Memcache\APC';
echo "<?php\n\\\$CONFIG = ";
var_export(\$CONFIG);
echo ";";
?>
EOF
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
# We can now install owncloud 9.0.2
InstallOwncloud 9.0.2 72a3d15d09f58c06fa8bee48b9e60c9cd356f9c5
# The owncloud 9 migration doesn't migrate calendars and contacts
# The option to migrate these are removed in 9.1
# So the migrations should be done when we have 9.0 installed
sudo -u www-data php /usr/local/lib/owncloud/occ dav:migrate-addressbooks
# The following migration has to be done for each owncloud user
for directory in $STORAGE_ROOT/owncloud/*@*/ ; do
username=$(basename "${directory}")
sudo -u www-data php /usr/local/lib/owncloud/occ dav:migrate-calendar $username
done
sudo -u www-data php /usr/local/lib/owncloud/occ dav:sync-birthday-calendar
fi
fi
InstallOwncloud $owncloud_ver 72ed9812432f01b3a459c4afc33f5c76b71eec09
fi
# ### Configuring ownCloud
@@ -92,7 +164,6 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
mkdir -p $STORAGE_ROOT/owncloud
# Create an initial configuration file.
TIMEZONE=$(cat /etc/timezone)
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
cat > $STORAGE_ROOT/owncloud/config.php <<EOF;
<?php
@@ -108,13 +179,10 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
'user_backends' => array(
array(
'class'=>'OC_User_IMAP',
'arguments'=>array('{localhost:993/imap/ssl/novalidate-cert}')
'arguments'=>array('{127.0.0.1:993/imap/ssl/novalidate-cert}')
)
),
'memcache.local' => '\\OC\\Memcache\\Memcached',
"memcached_servers" => array (
array('localhost', 11211),
),
'memcache.local' => '\OC\Memcache\APC',
'mail_smtpmode' => 'sendmail',
'mail_smtpsecure' => '',
'mail_smtpauthtype' => 'LOGIN',
@@ -125,7 +193,6 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
'mail_smtppassword' => '',
'mail_from_address' => 'owncloud',
'mail_domain' => '$PRIMARY_HOSTNAME',
'logtimezone' => '$TIMEZONE',
);
?>
EOF
@@ -163,7 +230,11 @@ fi
# so set it here. It also can change if the box's PRIMARY_HOSTNAME changes, so
# this will make sure it has the right value.
# * Some settings weren't included in previous versions of Mail-in-a-Box.
# * We need to set the timezone to the system timezone to allow fail2ban to ban
# users within the proper timeframe
# * We need to set the logdateformat to something that will work correctly with fail2ban
# Use PHP to read the settings file, modify it, and write out the new settings array.
TIMEZONE=$(cat /etc/timezone)
CONFIG_TEMP=$(/bin/mktemp)
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
<?php
@@ -171,10 +242,13 @@ include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME');
\$CONFIG['memcache.local'] = '\\OC\\Memcache\\Memcached';
\$CONFIG['memcache.local'] = '\OC\Memcache\APC';
\$CONFIG['overwrite.cli.url'] = '/cloud';
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
\$CONFIG['logtimezone'] = '$TIMEZONE';
\$CONFIG['logdateformat'] = 'Y-m-d H:i:s';
echo "<?php\n\\\$CONFIG = ";
var_export(\$CONFIG);
echo ";";
@@ -207,6 +281,12 @@ tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
max_execution_time=600 \
short_open_tag=On
# If apc is explicitly disabled we need to enable it
if grep -q apc.enabled=0 /etc/php5/mods-available/apcu.ini; then
tools/editconf.py /etc/php5/mods-available/apcu.ini -c ';' \
apc.enabled=1
fi
# Set up a cron job for owncloud.
cat > /etc/cron.hourly/mailinabox-owncloud << EOF;
#!/bin/bash

View File

@@ -19,17 +19,50 @@ fi
# Check that we have enough memory.
#
# /proc/meminfo reports free memory in kibibytes. Our baseline will be 768 MB,
# which is 750000 kibibytes.
# /proc/meminfo reports free memory in kibibytes. Our baseline will be 512 MB,
# which is 500000 kibibytes.
#
# We will display a warning if the memory is below 768 MB which is 750000 kibibytes
#
# Skip the check if we appear to be running inside of Vagrant, because that's really just for testing.
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}')
if [ $TOTAL_PHYSICAL_MEM -lt 750000 ]; then
if [ $TOTAL_PHYSICAL_MEM -lt 500000 ]; then
if [ ! -d /vagrant ]; then
TOTAL_PHYSICAL_MEM=$(expr \( \( $TOTAL_PHYSICAL_MEM \* 1024 \) / 1000 \) / 1000)
echo "Your Mail-in-a-Box needs more memory (RAM) to function properly."
echo "Please provision a machine with at least 768 MB, 1 GB recommended."
echo "Please provision a machine with at least 512 MB, 1 GB recommended."
echo "This machine has $TOTAL_PHYSICAL_MEM MB memory."
exit
fi
fi
if [ $TOTAL_PHYSICAL_MEM -lt 750000 ]; then
echo "WARNING: Your Mail-in-a-Box has less than 768 MB of memory."
echo " It might run unreliably when under heavy load."
fi
# Check that tempfs is mounted with exec
MOUNTED_TMP_AS_NO_EXEC=$(grep "/tmp.*noexec" /proc/mounts)
if [ -n "$MOUNTED_TMP_AS_NO_EXEC" ]; then
echo "Mail-in-a-Box has to have exec rights on /tmp, please mount /tmp with exec"
exit
fi
# Check that no .wgetrc exists
if [ -e ~/.wgetrc ]; then
echo "Mail-in-a-Box expects no overrides to wget defaults, ~/.wgetrc exists"
exit
fi
# Check that we are running on x86_64 or i686, any other architecture is unsupported and
# will fail later in the setup when we try to install the custom build lucene packages.
#
# Set ARM=1 to ignore this check if you have built the packages yourself. If you do this
# you are on your own!
ARCHITECTURE=$(uname -m)
if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "i686" ]; then
if [ -z "$ARM" ]; then
echo "Mail-in-a-Box only supports x86_64 or i686 and will not work on any other architecture, like ARM."
echo "Your architecture is $ARCHITECTURE"
exit
fi
fi

View File

@@ -3,9 +3,9 @@ if [ -z "$NONINTERACTIVE" ]; then
# this was being able to ask the user for input even if stdin has been redirected,
# e.g. if we piped a bootstrapping install script to bash to get started. In that
# case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we
# use a shell flag instead. Really supress any output from installing dialog.
# use a shell flag instead. Really suppress any output from installing dialog.
#
# Also install depencies needed to validate the email address.
# Also install dependencies needed to validate the email address.
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
@@ -18,7 +18,8 @@ if [ -z "$NONINTERACTIVE" ]; then
message_box "Mail-in-a-Box Installation" \
"Hello and thanks for deploying a Mail-in-a-Box!
\n\nI'm going to ask you a few questions.
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line."
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line.
\n\nNOTE: You should only install this on a brand new Ubuntu installation 100% dedicated to Mail-in-a-Box. Mail-in-a-Box will, for example, remove apache2."
fi
# The box needs a name.

View File

@@ -78,9 +78,13 @@ tools/editconf.py /etc/spamassassin/local.cf -s \
# * Writable by the debian-spamd user, which runs /etc/cron.daily/spamassassin.
#
# We'll have these files owned by spampd and grant access to the other two processes.
#
# Spamassassin will change the access rights back to the defaults, so we must also configure
# the filemode in the config file.
tools/editconf.py /etc/spamassassin/local.cf -s \
bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes
bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes \
bayes_file_mode=0666
mkdir -p $STORAGE_ROOT/mail/spamassassin
chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin

View File

@@ -5,13 +5,14 @@
source setup/functions.sh # load our functions
# Check system setup: Are we running as root on Ubuntu 14.04 on a
# machine with enough memory? If not, this shows an error and exits.
# machine with enough memory? Is /tmp mounted with exec.
# If not, this shows an error and exits.
source setup/preflight.sh
# Ensure Python reads/writes files in UTF-8. If the machine
# triggers some other locale in Python, like ASCII encoding,
# Python may not be able to read/write files. Here and in
# the management daemon startup script.
# Python may not be able to read/write files. This is also
# in the management daemon startup script and the cron script.
if [ -z `locale -a | grep en_US.utf8` ]; then
# Generate locale if not exists
@@ -23,6 +24,9 @@ export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8
# Fix so line drawing characters are shown correctly in Putty on Windows. See #744.
export NCURSES_NO_UTF8_ACS=1
# Recall the last settings used if we're running this a second time.
if [ -f /etc/mailinabox.conf ]; then
# Run any system migrations before proceeding. Since this is a second run,
@@ -107,15 +111,26 @@ source setup/zpush.sh
source setup/management.sh
source setup/munin.sh
# Ping the management daemon to write the DNS and nginx configuration files.
until nc -z -w 4 localhost 10222
# Wait for the management daemon to start...
until nc -z -w 4 127.0.0.1 10222
do
echo Waiting for the Mail-in-a-Box management daemon to start...
sleep 2
done
# ...and then have it write the DNS and nginx configuration files and start those
# services.
tools/dns_update
tools/web_update
# Give fail2ban another restart. The log files may not all have been present when
# fail2ban was first configured, but they should exist now.
restart_service fail2ban
# If DNS is already working, try to provision TLS certficates from Let's Encrypt.
# Suppress extra reasons why domains aren't getting a new certificate.
management/ssl_certificates.py -q
# If there aren't any mail users yet, create one.
source setup/firstuser.sh

View File

@@ -4,6 +4,70 @@ source setup/functions.sh # load our functions
# Basic System Configuration
# -------------------------
# ### Set hostname of the box
# If the hostname is not correctly resolvable sudo can't be used. This will result in
# errors during the install
#
# First set the hostname in the configuration file, then activate the setting
echo $PRIMARY_HOSTNAME > /etc/hostname
hostname $PRIMARY_HOSTNAME
# ### Add swap space to the system
# If the physical memory of the system is below 2GB it is wise to create a
# swap file. This will make the system more resiliant to memory spikes and
# prevent for instance spam filtering from crashing
# We will create a 1G file, this should be a good balance between disk usage
# and buffers for the system. We will only allocate this file if there is more
# than 5GB of disk space available
# The following checks are performed:
# - Check if swap is currently mountend by looking at /proc/swaps
# - Check if the user intents to activate swap on next boot by checking fstab entries.
# - Check if a swapfile already exists
# - Check if the root file system is not btrfs, might be an incompatible version with
# swapfiles. User should hanle it them selves.
# - Check the memory requirements
# - Check available diskspace
# See https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04
# for reference
SWAP_MOUNTED=$(cat /proc/swaps | tail -n+2)
SWAP_IN_FSTAB=$(grep "swap" /etc/fstab)
ROOT_IS_BTRFS=$(grep "\/ .*btrfs" /proc/mounts)
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}')
AVAILABLE_DISK_SPACE=$(df / --output=avail | tail -n 1)
if
[ -z "$SWAP_MOUNTED" ] &&
[ -z "$SWAP_IN_FSTAB" ] &&
[ ! -e /swapfile ] &&
[ -z "$ROOT_IS_BTRFS" ] &&
[ $TOTAL_PHYSICAL_MEM -lt 1900000 ] &&
[ $AVAILABLE_DISK_SPACE -gt 5242880 ]
then
echo "Adding a swap file to the system..."
# Allocate and activate the swap file. Allocate in 1KB chuncks
# doing it in one go, could fail on low memory systems
dd if=/dev/zero of=/swapfile bs=1024 count=$[1024*1024] status=none
if [ -e /swapfile ]; then
chmod 600 /swapfile
hide_output mkswap /swapfile
swapon /swapfile
fi
# Check if swap is mounted then activate on boot
if swapon -s | grep -q "\/swapfile"; then
echo "/swapfile none swap sw 0 0" >> /etc/fstab
else
echo "ERROR: Swap allocation failed"
fi
fi
# ### Add Mail-in-a-Box's PPA.
# We've built several .deb packages on our own that we want to include.
@@ -55,6 +119,14 @@ apt_install python3 python3-dev python3-pip \
haveged pollinate \
unattended-upgrades cron ntp fail2ban
# ### Suppress Upgrade Prompts
# Since Mail-in-a-Box might jump straight to 18.04 LTS, there's no need
# to be reminded about 16.04 on every login.
if [ -f /etc/update-manager/release-upgrades ]; then
tools/editconf.py /etc/update-manager/release-upgrades Prompt=never
rm -f /var/lib/ubuntu-release-upgrader/release-upgrade-available
fi
# ### Set the system timezone
#
# Some systems are missing /etc/timezone, which we cat into the configs for
@@ -144,6 +216,12 @@ pollinate -q -r
# Between these two, we really ought to be all set.
# We need an ssh key to store backups via rsync, if it doesn't exist create one
if [ ! -f /root/.ssh/id_rsa_miab ]; then
echo 'Creating SSH key for backup…'
ssh-keygen -t rsa -b 2048 -a 100 -f /root/.ssh/id_rsa_miab -N '' -q
fi
# ### Package maintenance
#
# Allow apt to install system updates automatically every day.
@@ -227,10 +305,17 @@ restart_service resolvconf
# ### Fail2Ban Service
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix and ssh
cat conf/fail2ban/jail.local \
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix, ssh, etc.
rm -f /etc/fail2ban/jail.local # we used to use this file but don't anymore
cat conf/fail2ban/jails.conf \
| sed "s/PUBLIC_IP/$PUBLIC_IP/g" \
> /etc/fail2ban/jail.local
cp conf/fail2ban/dovecotimap.conf /etc/fail2ban/filter.d/dovecotimap.conf
| sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
> /etc/fail2ban/jail.d/mailinabox.conf
cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/
# On first installation, the log files that the jails look at don't all exist.
# e.g., The roundcube error log isn't normally created until someone logs into
# Roundcube for the first time. This causes fail2ban to fail to start. Later
# scripts will ensure the files exist and then fail2ban is given another
# restart at the very end of setup.
restart_service fail2ban

View File

@@ -34,12 +34,12 @@ 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.3
HASH=4513227bd64eb8564f056817341b1dfe478e215e
VERSION=1.2.1
HASH=81fbfba4683522f6e54006d0300a48e6da3f3bbd
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
HTML5_NOTIFIER_VERSION=046eb388dd63b1ec77a3ee485757fc25ae9e684d
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION
PERSISTENT_LOGIN_VERSION=1e9d724476a370ce917a2fcd5b3217b0c306c24e
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:a
needs_update=0 #NODOC
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
# not installed yet #NODOC
@@ -51,10 +51,10 @@ fi
if [ $needs_update == 1 ]; then
# install roundcube
wget_verify \
https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz \
https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION.tar.gz \
$HASH \
/tmp/roundcube.tgz
tar -C /usr/local/lib -zxf /tmp/roundcube.tgz
tar -C /usr/local/lib --no-same-owner -zxf /tmp/roundcube.tgz
rm -rf /usr/local/lib/roundcubemail
mv /usr/local/lib/roundcubemail-$VERSION/ /usr/local/lib/roundcubemail
rm -f /tmp/roundcube.tgz
@@ -94,12 +94,12 @@ cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
\$config['default_host'] = 'ssl://localhost';
\$config['default_port'] = 993;
\$config['imap_timeout'] = 15;
\$config['smtp_server'] = 'tls://localhost';
\$config['smtp_server'] = 'tls://127.0.0.1';
\$config['smtp_port'] = 587;
\$config['smtp_user'] = '%u';
\$config['smtp_pass'] = '%p';
\$config['support_url'] = 'https://mailinabox.email/';
\$config['product_name'] = 'Mail-in-a-Box/Roundcube Webmail';
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
\$config['des_key'] = '$SECRET_KEY';
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'vacation_sieve', 'persistent_login');
\$config['skin'] = 'classic';
@@ -121,7 +121,7 @@ cat > /usr/local/lib/roundcubemail/plugins/vacation_sieve/config.inc.php <<EOF;
'transfer' => array(
'mode' => 'managesieve',
'ms_activate_script' => true,
'host' => 'localhost',
'host' => '127.0.0.1',
'port' => '4190',
'usetls' => false,
'path' => 'vacation',
@@ -133,6 +133,9 @@ EOF
mkdir -p /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
chown -R www-data.www-data /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
sudo -u www-data touch /var/log/roundcubemail/errors
# Password changing plugin settings
# The config comes empty by default, so we need the settings
# we're not planning to change in config.inc.dist...
@@ -157,6 +160,9 @@ chmod 775 $STORAGE_ROOT/mail
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
chmod 664 $STORAGE_ROOT/mail/users.sqlite
# Run Roundcube database migration script (database is created if it does not exist)
/usr/local/lib/roundcubemail/bin/updatedb.sh --dir /usr/local/lib/roundcubemail/SQL --package roundcube
# Enable PHP modules.
php5enmod mcrypt
restart_service php5-fpm

View File

@@ -53,6 +53,7 @@ cp conf/zpush/backend_combined.php /usr/local/lib/z-push/backend/combined/config
# Configure IMAP
rm -f /usr/local/lib/z-push/backend/imap/config.php
cp conf/zpush/backend_imap.php /usr/local/lib/z-push/backend/imap/config.php
sed -i "s%STORAGE_ROOT%$STORAGE_ROOT%" /usr/local/lib/z-push/backend/imap/config.php
# Configure CardDav
rm -f /usr/local/lib/z-push/backend/carddav/config.php

221
tests/fail2ban.py Normal file
View File

@@ -0,0 +1,221 @@
# Test that a box's fail2ban setting are working
# correctly by attempting a bunch of failed logins.
#
# Specify a SSH login command (which we use to reset
# fail2ban after each test) and the hostname to
# try to log in to.
######################################################################
import sys, os, time, functools
# parse command line
if len(sys.argv) != 4:
print("Usage: tests/fail2ban.py \"ssh user@hostname\" hostname owncloud_user")
sys.exit(1)
ssh_command, hostname, owncloud_user = sys.argv[1:4]
# define some test types
import socket
socket.setdefaulttimeout(10)
class IsBlocked(Exception):
"""Tests raise this exception when it appears that a fail2ban
jail is in effect, i.e. on a connection refused error."""
pass
def smtp_test():
import smtplib
try:
server = smtplib.SMTP(hostname, 587)
except ConnectionRefusedError:
# looks like fail2ban worked
raise IsBlocked()
server.starttls()
server.ehlo_or_helo_if_needed()
try:
server.login("fakeuser", "fakepassword")
raise Exception("authentication didn't fail")
except smtplib.SMTPAuthenticationError:
# athentication should fail
pass
try:
server.quit()
except:
# ignore errors here
pass
def imap_test():
import imaplib
try:
M = imaplib.IMAP4_SSL(hostname)
except ConnectionRefusedError:
# looks like fail2ban worked
raise IsBlocked()
try:
M.login("fakeuser", "fakepassword")
raise Exception("authentication didn't fail")
except imaplib.IMAP4.error:
# authentication should fail
pass
finally:
M.logout() # shuts down connection, has nothing to do with login()
def pop_test():
import poplib
try:
M = poplib.POP3_SSL(hostname)
except ConnectionRefusedError:
# looks like fail2ban worked
raise IsBlocked()
try:
M.user('fakeuser')
try:
M.pass_('fakepassword')
except poplib.error_proto as e:
# Authentication should fail.
M = None # don't .quit()
return
M.list()
raise Exception("authentication didn't fail")
finally:
if M:
M.quit()
def http_test(url, expected_status, postdata=None, qsargs=None, auth=None):
import urllib.parse
import requests
from requests.auth import HTTPBasicAuth
# form request
url = urllib.parse.urljoin("https://" + hostname, url)
if qsargs: url += "?" + urllib.parse.urlencode(qsargs)
urlopen = requests.get if not postdata else requests.post
try:
# issue request
r = urlopen(
url,
auth=HTTPBasicAuth(*auth) if auth else None,
data=postdata,
headers={'User-Agent': 'Mail-in-a-Box fail2ban tester'},
timeout=8,
verify=False) # don't bother with HTTPS validation, it may not be configured yet
except requests.exceptions.ConnectTimeout as e:
raise IsBlocked()
except requests.exceptions.ConnectionError as e:
if "Connection refused" in str(e):
raise IsBlocked()
raise # some other unexpected condition
# return response status code
if r.status_code != expected_status:
r.raise_for_status() # anything but 200
raise IOError("Got unexpected status code %s." % r.status_code)
# define how to run a test
def restart_fail2ban_service(final=False):
# Log in over SSH to restart fail2ban.
command = "sudo fail2ban-client reload"
if not final:
# Stop recidive jails during testing.
command += " && sudo fail2ban-client stop recidive"
os.system("%s \"%s\"" % (ssh_command, command))
def testfunc_runner(i, testfunc, *args):
print(i+1, end=" ", flush=True)
testfunc(*args)
def run_test(testfunc, args, count, within_seconds, parallel):
# Run testfunc count times in within_seconds seconds (and actually
# within a little less time so we're sure we're under the limit).
#
# Because some services are slow, like IMAP, we can't necessarily
# run testfunc sequentially and still get to count requests within
# the required time. So we split the requests across threads.
import requests.exceptions
from multiprocessing import Pool
restart_fail2ban_service()
# Log.
print(testfunc.__name__, " ".join(str(a) for a in args), "...")
# Record the start time so we can know how to evenly space our
# calls to testfunc.
start_time = time.time()
with Pool(parallel) as p:
# Distribute the requests across the pool.
asyncresults = []
for i in range(count):
ar = p.apply_async(testfunc_runner, [i, testfunc] + list(args))
asyncresults.append(ar)
# Wait for all runs to finish.
p.close()
p.join()
# Check for errors.
for ar in asyncresults:
try:
ar.get()
except IsBlocked:
print("Test machine prematurely blocked!")
return False
# Did we make enough requests within the limit?
if (time.time()-start_time) > within_seconds:
raise Exception("Test failed to make %s requests in %d seconds." % (count, within_seconds))
# Wait a moment for the block to be put into place.
time.sleep(4)
# The next call should fail.
print("*", end=" ", flush=True)
try:
testfunc(*args)
except IsBlocked:
# Success -- this one is supposed to be refused.
print("blocked [OK]")
return True # OK
print("not blocked!")
return False
######################################################################
if __name__ == "__main__":
# run tests
# SMTP bans at 10 even though we say 20 in the config because we get
# doubled-up warnings in the logs, we'll let that be for now
run_test(smtp_test, [], 10, 30, 8)
# IMAP
run_test(imap_test, [], 20, 30, 4)
# POP
run_test(pop_test, [], 20, 30, 4)
# Mail-in-a-Box control panel
run_test(http_test, ["/admin/me", 200], 20, 30, 1)
# Munin via the Mail-in-a-Box control panel
run_test(http_test, ["/admin/munin/", 401], 20, 30, 1)
# ownCloud
run_test(http_test, ["/cloud/remote.php/webdav", 401, None, None, [owncloud_user, "aa"]], 20, 120, 1)
# restart fail2ban so that this client machine is no longer blocked
restart_fail2ban_service(final=True)

View File

@@ -33,7 +33,6 @@ PORT 25
AES256-SHA256 - 256 bits 250 2.0.0 Ok
AES256-SHA - 256 bits 250 2.0.0 Ok
AES256-GCM-SHA384 - 256 bits 250 2.0.0 Ok
ECDHE-RSA-RC4-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
ECDHE-RSA-AES128-SHA256 ECDH-256 bits 128 bits 250 2.0.0 Ok
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
ECDHE-RSA-AES128-GCM-SHA256 ECDH-256 bits 128 bits 250 2.0.0 Ok
@@ -43,8 +42,6 @@ PORT 25
DHE-RSA-AES128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-AES128-GCM-SHA256 DH-2048 bits 128 bits 250 2.0.0 Ok
SEED-SHA - 128 bits 250 2.0.0 Ok
RC4-SHA - 128 bits 250 2.0.0 Ok
RC4-MD5 - 128 bits 250 2.0.0 Ok
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
AES128-SHA256 - 128 bits 250 2.0.0 Ok
AES128-SHA - 128 bits 250 2.0.0 Ok
@@ -62,37 +59,11 @@ PORT 25
DHE-RSA-AES256-SHA DH-2048 bits 256 bits 250 2.0.0 Ok
CAMELLIA256-SHA - 256 bits 250 2.0.0 Ok
AES256-SHA - 256 bits 250 2.0.0 Ok
ECDHE-RSA-RC4-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
DHE-RSA-SEED-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-CAMELLIA128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-AES128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
SEED-SHA - 128 bits 250 2.0.0 Ok
RC4-SHA - 128 bits 250 2.0.0 Ok
RC4-MD5 - 128 bits 250 2.0.0 Ok
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
AES128-SHA - 128 bits 250 2.0.0 Ok
ECDHE-RSA-DES-CBC3-SHA ECDH-256 bits 112 bits 250 2.0.0 Ok
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits 250 2.0.0 Ok
DES-CBC3-SHA - 112 bits 250 2.0.0 Ok
* SSLV3 Cipher Suites:
Preferred:
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits 250 2.0.0 Ok
Accepted:
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits 250 2.0.0 Ok
DHE-RSA-CAMELLIA256-SHA DH-2048 bits 256 bits 250 2.0.0 Ok
DHE-RSA-AES256-SHA DH-2048 bits 256 bits 250 2.0.0 Ok
CAMELLIA256-SHA - 256 bits 250 2.0.0 Ok
AES256-SHA - 256 bits 250 2.0.0 Ok
ECDHE-RSA-RC4-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
DHE-RSA-SEED-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-CAMELLIA128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-AES128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
SEED-SHA - 128 bits 250 2.0.0 Ok
RC4-SHA - 128 bits 250 2.0.0 Ok
RC4-MD5 - 128 bits 250 2.0.0 Ok
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
AES128-SHA - 128 bits 250 2.0.0 Ok
ECDHE-RSA-DES-CBC3-SHA ECDH-256 bits 112 bits 250 2.0.0 Ok
@@ -108,23 +79,23 @@ PORT 25
DHE-RSA-AES256-SHA DH-2048 bits 256 bits 250 2.0.0 Ok
CAMELLIA256-SHA - 256 bits 250 2.0.0 Ok
AES256-SHA - 256 bits 250 2.0.0 Ok
ECDHE-RSA-RC4-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits 250 2.0.0 Ok
DHE-RSA-SEED-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-CAMELLIA128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
DHE-RSA-AES128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
SEED-SHA - 128 bits 250 2.0.0 Ok
RC4-SHA - 128 bits 250 2.0.0 Ok
RC4-MD5 - 128 bits 250 2.0.0 Ok
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
AES128-SHA - 128 bits 250 2.0.0 Ok
ECDHE-RSA-DES-CBC3-SHA ECDH-256 bits 112 bits 250 2.0.0 Ok
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits 250 2.0.0 Ok
DES-CBC3-SHA - 112 bits 250 2.0.0 Ok
Should Not Offer: DHE-RSA-SEED-SHA, ECDHE-RSA-RC4-SHA, EDH-RSA-DES-CBC3-SHA, RC4-MD5, RC4-SHA, SEED-SHA
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-DSS-AES256-SHA256, DHE-DSS-CAMELLIA128-SHA, DHE-DSS-CAMELLIA256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-ECDSA-DES-CBC3-SHA, SRP-3DES-EDE-CBC-SHA, SRP-AES-128-CBC-SHA, SRP-AES-256-CBC-SHA, SRP-DSS-3DES-EDE-CBC-SHA, SRP-DSS-AES-128-CBC-SHA, SRP-DSS-AES-256-CBC-SHA, SRP-RSA-3DES-EDE-CBC-SHA, SRP-RSA-AES-128-CBC-SHA, SRP-RSA-AES-256-CBC-SHA
Supported Clients: OpenSSL/1.0.2, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, YandexBot/Jan 2015, Android/4.4.2, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/8/OS X 10.10, Safari/7/iOS 7.1, Safari/6/iOS 6.0.1, Baidu/Jan 2015, Firefox/31.3.0 ESR/Win 7, Android/5.0.0, IE/11/Win 7, Java/8u31, Googlebot/Feb 2015, Chrome/42/OS X, IE Mobile/11/Win Phone 8.1, IE/11/Win 8.1, Android/4.0.4, Android/4.1.1, Safari/6.0.4/OS X 10.8.4, Android/4.3, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, Java/7u25, OpenSSL/0.9.8y, Firefox/37/OS X, IE/7/Vista, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/6u45, Android/2.3.7, IE/8/XP
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
Should Not Offer: DHE-RSA-SEED-SHA, EDH-RSA-DES-CBC3-SHA, SEED-SHA
Could Also Offer: DH-DSS-AES128-GCM-SHA256, DH-DSS-AES128-SHA, DH-DSS-AES128-SHA256, DH-DSS-AES256-GCM-SHA384, DH-DSS-AES256-SHA, DH-DSS-AES256-SHA256, DH-DSS-CAMELLIA128-SHA, DH-DSS-CAMELLIA256-SHA, DH-DSS-DES-CBC3-SHA, DH-RSA-AES128-GCM-SHA256, DH-RSA-AES128-SHA, DH-RSA-AES128-SHA256, DH-RSA-AES256-GCM-SHA384, DH-RSA-AES256-SHA, DH-RSA-AES256-SHA256, DH-RSA-CAMELLIA128-SHA, DH-RSA-CAMELLIA256-SHA, DH-RSA-DES-CBC3-SHA, DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-DSS-AES256-SHA256, DHE-DSS-CAMELLIA128-SHA, DHE-DSS-CAMELLIA256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-ECDSA-DES-CBC3-SHA, SRP-3DES-EDE-CBC-SHA, SRP-AES-128-CBC-SHA, SRP-AES-256-CBC-SHA, SRP-DSS-3DES-EDE-CBC-SHA, SRP-DSS-AES-128-CBC-SHA, SRP-DSS-AES-256-CBC-SHA, SRP-RSA-3DES-EDE-CBC-SHA, SRP-RSA-AES-128-CBC-SHA, SRP-RSA-AES-256-CBC-SHA
Supported Clients: OpenSSL/1.0.2, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, YandexBot/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/OS X 10.10, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/6/iOS 6.0.1, Firefox/31.3.0 ESR/Win 7, Baidu/Jan 2015, IE/11/Win 8.1, IE/11/Win 7, IE Mobile/11/Win Phone 8.1, Android/5.0.0, Java/8u31, Chrome/42/OS X, Googlebot/Feb 2015, Android/4.1.1, Android/4.0.4, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Android/4.3, Safari/5.1.9/OS X 10.6.8, Firefox/37/OS X, OpenSSL/0.9.8y, Java/7u25, IE/8-10/Win 7, IE/7/Vista, IE Mobile/10/Win Phone 8.0, Android/2.3.7, Java/6u45, IE/8/XP
PORT 587
--------
@@ -192,9 +163,6 @@ PORT 587
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
AES128-SHA - 128 bits 250 2.0.0 Ok
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
* TLSV1 Cipher Suites:
Preferred:
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits 250 2.0.0 Ok
@@ -212,9 +180,12 @@ PORT 587
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
AES128-SHA - 128 bits 250 2.0.0 Ok
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
Should Not Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA, DHE-RSA-SEED-SHA, SEED-SHA
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384
Supported Clients: OpenSSL/1.0.2, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, YandexBot/Jan 2015, Android/4.4.2, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/8/OS X 10.10, Safari/7/iOS 7.1, IE Mobile/11/Win Phone 8.1, IE/11/Win 8.1, IE/11/Win 7, Safari/6/iOS 6.0.1, Firefox/31.3.0 ESR/Win 7, Baidu/Jan 2015, Android/5.0.0, Chrome/42/OS X, Java/8u31, Googlebot/Feb 2015, Firefox/37/OS X, Android/4.0.4, Android/4.1.1, Safari/6.0.4/OS X 10.8.4, Android/4.3, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, OpenSSL/0.9.8y, IE/7/Vista, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/7u25, Java/6u45, Android/2.3.7
Supported Clients: OpenSSL/1.0.2, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, YandexBot/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, IE/11/Win 8.1, Safari/8/iOS 8.1.2, IE/11/Win 7, IE Mobile/11/Win Phone 8.1, Safari/8/OS X 10.10, Safari/7/OS X 10.9, Safari/6/iOS 6.0.1, Firefox/31.3.0 ESR/Win 7, Baidu/Jan 2015, Chrome/42/OS X, Android/5.0.0, Java/8u31, Googlebot/Feb 2015, Firefox/37/OS X, Android/4.0.4, Android/4.1.1, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Android/4.3, Safari/5.1.9/OS X 10.6.8, IE/8-10/Win 7, IE/7/Vista, IE Mobile/10/Win Phone 8.0, OpenSSL/0.9.8y, Java/7u25, Java/6u45, Android/2.3.7
PORT 443
--------
@@ -226,22 +197,22 @@ PORT 443
Client-initiated Renegotiations: OK - Rejected
Secure Renegotiation: OK - Supported
* HTTP Strict Transport Security:
OK - HSTS header received: max-age=31536000
* OpenSSL Heartbleed:
OK - Not vulnerable to Heartbleed
* Session Resumption:
With Session IDs: OK - Supported (5 successful, 0 failed, 0 errors, 5 total attempts).
With TLS Session Tickets: OK - Supported
* OpenSSL Heartbleed:
OK - Not vulnerable to Heartbleed
* HTTP Strict Transport Security:
OK - HSTS header received: max-age=31536000
Unhandled exception when processing --chrome_sha1:
exceptions.TypeError - Incorrect padding
* SSLV2 Cipher Suites:
Server rejected all cipher suites.
* Google Chrome SHA-1 Deprecation Status:
OK - Leaf certificate expires before 2016.
* TLSV1_2 Cipher Suites:
Preferred:
ECDHE-RSA-AES128-GCM-SHA256 ECDH-256 bits 128 bits HTTP 200 OK
@@ -270,9 +241,6 @@ PORT 443
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
DES-CBC3-SHA - 112 bits HTTP 200 OK
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
* TLSV1 Cipher Suites:
Preferred:
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits HTTP 200 OK
@@ -283,9 +251,12 @@ PORT 443
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
DES-CBC3-SHA - 112 bits HTTP 200 OK
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
Should Not Offer: (none -- good)
Could Also Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-DSS-AES256-SHA256, DHE-DSS-CAMELLIA128-SHA, DHE-DSS-CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, SRP-AES-128-CBC-SHA, SRP-AES-256-CBC-SHA, SRP-DSS-AES-128-CBC-SHA, SRP-DSS-AES-256-CBC-SHA, SRP-RSA-AES-128-CBC-SHA, SRP-RSA-AES-256-CBC-SHA
Supported Clients: YandexBot/Jan 2015, OpenSSL/1.0.2, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, Android/4.4.2, Safari/8/iOS 8.1.2, Safari/8/OS X 10.10, Safari/7/OS X 10.9, Safari/7/iOS 7.1, Safari/6/iOS 6.0.1, Android/5.0.0, Chrome/42/OS X, IE/11/Win 8.1, IE/11/Win 7, Java/8u31, IE Mobile/11/Win Phone 8.1, Googlebot/Feb 2015, Firefox/37/OS X, Firefox/31.3.0 ESR/Win 7, Android/4.2.2, Android/4.0.4, Baidu/Jan 2015, Safari/5.1.9/OS X 10.6.8, Android/4.1.1, Safari/6.0.4/OS X 10.8.4, Android/4.3, OpenSSL/0.9.8y, IE/7/Vista, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/7u25, Java/6u45, Android/2.3.7, IE/8/XP
Could Also Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, CAMELLIA128-SHA, CAMELLIA256-SHA, DH-DSS-AES128-GCM-SHA256, DH-DSS-AES128-SHA, DH-DSS-AES128-SHA256, DH-DSS-AES256-GCM-SHA384, DH-DSS-AES256-SHA, DH-DSS-AES256-SHA256, DH-DSS-CAMELLIA128-SHA, DH-DSS-CAMELLIA256-SHA, DH-RSA-AES128-GCM-SHA256, DH-RSA-AES128-SHA, DH-RSA-AES128-SHA256, DH-RSA-AES256-GCM-SHA384, DH-RSA-AES256-SHA, DH-RSA-AES256-SHA256, DH-RSA-CAMELLIA128-SHA, DH-RSA-CAMELLIA256-SHA, DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-DSS-AES256-SHA256, DHE-DSS-CAMELLIA128-SHA, DHE-DSS-CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, SRP-AES-128-CBC-SHA, SRP-AES-256-CBC-SHA, SRP-DSS-AES-128-CBC-SHA, SRP-DSS-AES-256-CBC-SHA, SRP-RSA-AES-128-CBC-SHA, SRP-RSA-AES-256-CBC-SHA
Supported Clients: OpenSSL/1.0.2, OpenSSL/1.0.1l, BingPreview/Jan 2015, YandexBot/Jan 2015, Yahoo Slurp/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/OS X 10.10, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/6/iOS 6.0.1, Chrome/42/OS X, IE/11/Win 8.1, IE/11/Win 7, Android/5.0.0, Java/8u31, IE Mobile/11/Win Phone 8.1, Googlebot/Feb 2015, Firefox/31.3.0 ESR/Win 7, Firefox/37/OS X, Android/4.1.1, Android/4.0.4, Baidu/Jan 2015, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Android/4.3, Safari/5.1.9/OS X 10.6.8, IE/8-10/Win 7, IE/7/Vista, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, Java/7u25, Android/2.3.7, Java/6u45, IE/8/XP
PORT 993
--------
@@ -299,13 +270,13 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
* OpenSSL Heartbleed:
OK - Not vulnerable to Heartbleed
* SSLV2 Cipher Suites:
Server rejected all cipher suites.
* Session Resumption:
With Session IDs: NOT SUPPORTED (0 successful, 5 failed, 0 errors, 5 total attempts).
With TLS Session Tickets: NOT SUPPORTED - TLS ticket assigned but not accepted.
* SSLV2 Cipher Suites:
Server rejected all cipher suites.
* TLSV1_2 Cipher Suites:
Preferred:
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
@@ -336,9 +307,6 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
CAMELLIA128-SHA - 128 bits
AES128-SHA - 128 bits
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
* TLSV1 Cipher Suites:
Preferred:
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
@@ -354,9 +322,12 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
CAMELLIA128-SHA - 128 bits
AES128-SHA - 128 bits
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
Should Not Offer: AES128-SHA, AES256-SHA, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-SHA384
Supported Clients: OpenSSL/1.0.2, Baidu/Jan 2015, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, Firefox/31.3.0 ESR/Win 7, Googlebot/Feb 2015, Android/4.2.2, Android/5.0.0, Android/4.0.4, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, YandexBot/Jan 2015, Safari/8/OS X 10.10, Safari/7/iOS 7.1, Chrome/42/OS X, Safari/5.1.9/OS X 10.6.8, Android/4.1.1, Firefox/37/OS X, Safari/6.0.4/OS X 10.8.4, Android/4.3, Safari/6/iOS 6.0.1, Android/4.4.2, OpenSSL/0.9.8y, IE Mobile/11/Win Phone 8.1, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/8u31, Java/7u25, Java/6u45, Android/2.3.7
Supported Clients: OpenSSL/1.0.2, Firefox/31.3.0 ESR/Win 7, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, Baidu/Jan 2015, Safari/7/iOS 7.1, Chrome/42/OS X, Googlebot/Feb 2015, Android/4.0.4, Safari/8/iOS 8.1.2, Android/4.1.1, Android/5.0.0, Safari/6/iOS 6.0.1, YandexBot/Jan 2015, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Safari/8/OS X 10.10, Firefox/37/OS X, Safari/7/OS X 10.9, Android/4.3, Safari/5.1.9/OS X 10.6.8, Android/4.4.2, IE/8-10/Win 7, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, IE Mobile/11/Win Phone 8.1, Java/7u25, Java/8u31, Java/6u45, Android/2.3.7
PORT 995
--------
@@ -370,13 +341,13 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
* OpenSSL Heartbleed:
OK - Not vulnerable to Heartbleed
* SSLV2 Cipher Suites:
Server rejected all cipher suites.
* Session Resumption:
With Session IDs: NOT SUPPORTED (0 successful, 5 failed, 0 errors, 5 total attempts).
With TLS Session Tickets: NOT SUPPORTED - TLS ticket assigned but not accepted.
* SSLV2 Cipher Suites:
Server rejected all cipher suites.
* TLSV1_2 Cipher Suites:
Preferred:
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
@@ -407,9 +378,6 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
CAMELLIA128-SHA - 128 bits
AES128-SHA - 128 bits
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
* TLSV1 Cipher Suites:
Preferred:
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
@@ -425,7 +393,10 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
CAMELLIA128-SHA - 128 bits
AES128-SHA - 128 bits
* SSLV3 Cipher Suites:
Server rejected all cipher suites.
Should Not Offer: AES128-SHA, AES256-SHA, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-SHA384
Supported Clients: OpenSSL/1.0.2, Baidu/Jan 2015, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, Firefox/31.3.0 ESR/Win 7, Googlebot/Feb 2015, Android/4.2.2, Android/5.0.0, Android/4.0.4, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, YandexBot/Jan 2015, Safari/8/OS X 10.10, Safari/7/iOS 7.1, Chrome/42/OS X, Safari/5.1.9/OS X 10.6.8, Android/4.1.1, Firefox/37/OS X, Safari/6.0.4/OS X 10.8.4, Android/4.3, Safari/6/iOS 6.0.1, Android/4.4.2, OpenSSL/0.9.8y, IE Mobile/11/Win Phone 8.1, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/8u31, Java/7u25, Java/6u45, Android/2.3.7
Supported Clients: OpenSSL/1.0.2, Firefox/31.3.0 ESR/Win 7, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, Baidu/Jan 2015, Safari/7/iOS 7.1, Chrome/42/OS X, Googlebot/Feb 2015, Android/4.0.4, Safari/8/iOS 8.1.2, Android/4.1.1, Android/5.0.0, Safari/6/iOS 6.0.1, YandexBot/Jan 2015, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Safari/8/OS X 10.10, Firefox/37/OS X, Safari/7/OS X 10.9, Android/4.3, Safari/5.1.9/OS X 10.6.8, Android/4.4.2, IE/8-10/Win 7, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, IE Mobile/11/Win Phone 8.1, Java/7u25, Java/8u31, Java/6u45, Android/2.3.7

49
tools/owncloud-restore.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
#
# This script will restore the backup made during an installation
source /etc/mailinabox.conf # load global vars
if [ -z "$1" ]; then
echo "Usage: owncloud-restore.sh <backup directory>"
echo
echo "WARNING: This will restore the database to the point of the installation!"
echo " This means that you will lose all changes made by users after that point"
echo
echo
echo "Backups are stored here: $STORAGE_ROOT/owncloud-backup/"
echo
echo "Available backups:"
echo
find $STORAGE_ROOT/owncloud-backup/* -maxdepth 0 -type d
echo
echo "Supply the directory that was created during the last installation as the only commandline argument"
exit
fi
if [ ! -f $1/config.php ]; then
echo "This isn't a valid backup location"
exit
fi
echo "Restoring backup from $1"
service php5-fpm stop
# remove the current owncloud installation
rm -rf /usr/local/lib/owncloud/
# restore the current owncloud application
cp -r "$1/owncloud-install" /usr/local/lib/owncloud
# restore access rights
chmod 750 /usr/local/lib/owncloud/{apps,config}
cp "$1/owncloud.db" $STORAGE_ROOT/owncloud/
cp "$1/config.php" $STORAGE_ROOT/owncloud/
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off
service php5-fpm start
echo "Done"

View File

@@ -2,7 +2,8 @@
#
# This is a tool Josh uses on his box serving mailinabox.email to parse the nginx
# access log to see how many people are installing Mail-in-a-Box each day, by
# looking at accesses to the bootstrap.sh script.
# looking at accesses to the bootstrap.sh script (which is currently at the URL
# .../setup.sh).
import re, glob, gzip, os.path, json
import dateutil.parser
@@ -24,9 +25,10 @@ for fn in glob.glob("/var/log/nginx/access.log*"):
# Loop through the lines in the access log.
with f:
for line in f:
# Find lines that are GETs on /bootstrap.sh by either curl or wget.
# Find lines that are GETs on the bootstrap script by either curl or wget.
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /bootstrap.sh HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
# (Also, the URL changed in January 2016, but we'll accept both.)
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /(bootstrap.sh|setup.sh) HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
if m:
date, time = m.group("date").decode("ascii").split(":", 1)
date = dateutil.parser.parse(date).date().isoformat()