mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-13 17:17:23 +01:00
Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
738e0a6e17 | ||
|
|
e0d46d1eb5 | ||
|
|
7f37abca05 | ||
|
|
2f467556bd | ||
|
|
15583ec10d | ||
|
|
78d1c9be6e | ||
|
|
b0b5d8e792 | ||
|
|
82844ca651 | ||
|
|
2a72c800f6 | ||
|
|
8be23d5ef6 | ||
|
|
f9a0e39cc9 | ||
|
|
0c0a079354 | ||
|
|
42e86610ba | ||
|
|
7c62f4b8e9 | ||
|
|
1eba7b0616 | ||
|
|
9c7820f422 | ||
|
|
87ec4e9f82 | ||
|
|
08becf7fa3 | ||
|
|
5eb4a53de1 | ||
|
|
598ade3f7a | ||
|
|
8f399df5bb | ||
|
|
ae73dc5d30 | ||
|
|
c409b2efd0 | ||
|
|
6961840c0e | ||
|
|
6162a9637c | ||
|
|
47c968e71b | ||
|
|
ed3e2aa712 | ||
|
|
fe597da7aa | ||
|
|
61e9888a85 | ||
|
|
35fed8606e | ||
|
|
ef6f121491 | ||
|
|
ec3aab0eaa | ||
|
|
8c69b9e261 | ||
|
|
e7150e3bc6 | ||
|
|
8d6d84d87f | ||
|
|
a6a1cc7ae0 | ||
|
|
b5c0736d27 | ||
|
|
8ee7de6ff3 | ||
|
|
0088fb4553 | ||
|
|
b2d103145f | ||
|
|
fc9e279cec | ||
|
|
257983d559 | ||
|
|
e924459140 | ||
|
|
441bd35053 | ||
|
|
a0e603a3c6 | ||
|
|
88604074d6 | ||
|
|
d43111eb48 | ||
|
|
6729588d8c | ||
|
|
5f14eca67f | ||
|
|
8944cd7980 | ||
|
|
2bbbc9dfa3 | ||
|
|
544f155948 | ||
|
|
f080eabb3a | ||
|
|
7bf377eed1 | ||
|
|
cd554cf480 | ||
|
|
e5448405ae | ||
|
|
a7eff8fb35 | ||
|
|
341aa8695a | ||
|
|
5efdd72f41 | ||
|
|
f25801e88d | ||
|
|
cc7be13098 | ||
|
|
2556e3fbc2 | ||
|
|
00898b2ff5 | ||
|
|
35b8a149d8 | ||
|
|
d0423afd18 | ||
|
|
edf42df835 | ||
|
|
734745a4a6 | ||
|
|
dbebaba8b9 | ||
|
|
cb765dfe2a | ||
|
|
81258e2189 | ||
|
|
4dd4b4232a | ||
|
|
48ff664ee9 | ||
|
|
a52c56e571 | ||
|
|
6ace97e482 | ||
|
|
19a928e4ec | ||
|
|
78f2fe213e | ||
|
|
a16855ecf0 | ||
|
|
d773140502 | ||
|
|
2c324d0bc9 | ||
|
|
2bd6cc4d6b | ||
|
|
b11157e0b6 | ||
|
|
46ba62b7b1 | ||
|
|
4c36d6e6c9 | ||
|
|
e49c99890b | ||
|
|
a13fd90347 | ||
|
|
18f1689f45 | ||
|
|
8234a5a9f4 | ||
|
|
1d9f9ea617 | ||
|
|
fbb38c3881 | ||
|
|
2caddb41eb | ||
|
|
d2b7204319 | ||
|
|
68ebca8a15 | ||
|
|
9c9dcdbf0a | ||
|
|
0c4c2e51bb | ||
|
|
828512b95a | ||
|
|
add985ce5d | ||
|
|
416dbebf45 | ||
|
|
2a046a22f4 | ||
|
|
b66f12dd4c | ||
|
|
6e04eb490f | ||
|
|
cd39c2b53f | ||
|
|
5da168466d | ||
|
|
a5f39784dd | ||
|
|
a072730fb8 | ||
|
|
00c61dbcdd | ||
|
|
10bf40250b | ||
|
|
453091f1fb | ||
|
|
48e0f39179 | ||
|
|
bb641cdfba | ||
|
|
255a65ac98 | ||
|
|
c7badb80d1 | ||
|
|
653cb7ce10 | ||
|
|
d7d8964afc | ||
|
|
6c3696a54a | ||
|
|
9c9cae2096 | ||
|
|
423f1907d0 | ||
|
|
86621392f6 | ||
|
|
368b9c50d0 | ||
|
|
3830facf78 | ||
|
|
d4baac2363 | ||
|
|
f88c907a29 | ||
|
|
89222d519a | ||
|
|
36bef2ee16 | ||
|
|
f6b20a810f | ||
|
|
f2ff14100e | ||
|
|
2c86fa3755 | ||
|
|
3c05fc94ff | ||
|
|
2e00530944 | ||
|
|
32d6728dc9 | ||
|
|
a3c71fe14f | ||
|
|
a24977a96e | ||
|
|
e694f57673 | ||
|
|
cd59de6314 | ||
|
|
a081d04082 | ||
|
|
09577816f8 | ||
|
|
2647febbf5 | ||
|
|
bd0635728c | ||
|
|
584cfe42c4 | ||
|
|
41601a592f | ||
|
|
18c253eeda | ||
|
|
34d58fb720 | ||
|
|
99d0afd650 | ||
|
|
cd717ec94e | ||
|
|
0b7f477b96 | ||
|
|
ab2367e98a | ||
|
|
384c3b5e3d | ||
|
|
d91368c478 | ||
|
|
61105b1ec3 | ||
|
|
b6f90e10c1 | ||
|
|
3af5e55035 | ||
|
|
e03b071e8b | ||
|
|
df93d82d0f | ||
|
|
59913a5e4c | ||
|
|
c3605f6211 | ||
|
|
96b3a29800 | ||
|
|
abb6a1a070 | ||
|
|
041b5f883f | ||
|
|
3b78a8d9d6 | ||
|
|
6ea1a06a12 | ||
|
|
2b00478b8b | ||
|
|
155bcfc654 | ||
|
|
4b07a6aa8f | ||
|
|
2151d81453 | ||
|
|
fd6226187a | ||
|
|
bbe27df413 | ||
|
|
a658abc95f | ||
|
|
9331dbc519 | ||
|
|
8b5eba21c0 | ||
|
|
da5497cd1c | ||
|
|
a27ec68467 | ||
|
|
3ac4b8aca8 | ||
|
|
02feeafe6a | ||
|
|
5f0376bfbf | ||
|
|
4e4fe90fc7 | ||
|
|
3cd5a6eee7 | ||
|
|
c26bc841a2 | ||
|
|
163daea41c | ||
|
|
d8316119eb | ||
|
|
102b2d46ab | ||
|
|
58541c467f | ||
|
|
00bd23eb04 | ||
|
|
d73d1c6900 | ||
|
|
fc0abd5b4d | ||
|
|
27b4edfc76 | ||
|
|
ba75ff7820 | ||
|
|
a14b17794b | ||
|
|
35a360ef0b | ||
|
|
86457e5bc4 | ||
|
|
7c9f3e0b23 | ||
|
|
83d8dbca3e | ||
|
|
8cf2e468bd | ||
|
|
440a545010 | ||
|
|
942bcfc7c5 | ||
|
|
4f2d16a31d | ||
|
|
e9368de462 | ||
|
|
cdd0a821eb | ||
|
|
6f165d0aeb | ||
|
|
6c22c0533e | ||
|
|
d38b732b0a | ||
|
|
81b5af6b64 | ||
|
|
fc5cc9753b | ||
|
|
1aca6fe08f | ||
|
|
cf3e1cd595 | ||
|
|
b044dda28f | ||
|
|
f66f39b61d | ||
|
|
6de7d59f14 | ||
|
|
9c8f2e75fc | ||
|
|
cbc4bf553d | ||
|
|
4e3cfead46 | ||
|
|
8844a9185f | ||
|
|
3249a55f3a | ||
|
|
b58fb54725 | ||
|
|
82903cd09e | ||
|
|
fb14e30feb | ||
|
|
d9ac321f25 | ||
|
|
bf5e9200f8 | ||
|
|
5f5f00af4a | ||
|
|
6b73bb5d80 | ||
|
|
3055f9a79c | ||
|
|
1c84e0aeb6 | ||
|
|
ae1b56d23f | ||
|
|
946cd63e8e | ||
|
|
01fa8cf72c | ||
|
|
fac8477ba1 | ||
|
|
61744095a8 | ||
|
|
d5b38a27e6 | ||
|
|
6666d28c44 | ||
|
|
66675ff2e9 | ||
|
|
9ee2d946b7 | ||
|
|
ff7d4196a6 | ||
|
|
490b36d86c | ||
|
|
867d9c4669 | ||
|
|
1ad5892acd | ||
|
|
69bd137b4e | ||
|
|
736b3de221 | ||
|
|
42f2e983e5 | ||
|
|
c9f30e8059 |
30
.editorconfig
Normal file
30
.editorconfig
Normal file
@@ -0,0 +1,30 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tabs
|
||||
indent_size = 4
|
||||
|
||||
[Vagrantfile]
|
||||
indent_size = 2
|
||||
|
||||
[*.rb]
|
||||
indent_size = 2
|
||||
|
||||
[*.py]
|
||||
indent_style = tabs
|
||||
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ management/__pycache__/
|
||||
tools/__pycache__/
|
||||
externals/
|
||||
.env
|
||||
.vagrant
|
||||
|
||||
282
CHANGELOG.md
282
CHANGELOG.md
@@ -1,6 +1,285 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
v0.28 (July 30, 2018)
|
||||
---------------------
|
||||
|
||||
System:
|
||||
|
||||
* We now use EFF's `certbot` to provision TLS certificates (from Let's Encrypt) instead of our home-grown ACME library.
|
||||
|
||||
Contacts/Calendar:
|
||||
|
||||
* Fix for Mac OS X autoconfig of the calendar.
|
||||
|
||||
Setup:
|
||||
|
||||
* Installing Z-Push broke because of what looks like a change or problem in their git server HTTPS certificate. That's fixed.
|
||||
|
||||
v0.27 (June 14, 2018)
|
||||
---------------------
|
||||
|
||||
Mail:
|
||||
|
||||
* A report of box activity, including sent/received mail totals and logins by user, is now emailed to the box's administrator user each week.
|
||||
* Update Roundcube to version 1.3.6 and Z-Push to version 2.3.9.
|
||||
|
||||
Control Panel:
|
||||
|
||||
* We now use EFF's `certbot` tool to provision HTTPS certificates instead of our home-grown free_tls_certificates package.
|
||||
* The undocumented feature for proxying web requests to another server now sets X-Forwarded-For.
|
||||
|
||||
v0.26c (February 13, 2018)
|
||||
--------------------------
|
||||
|
||||
Setup:
|
||||
|
||||
* Upgrades from v0.21c (February 1, 2017) or earlier were broken because the intermediate versions of ownCloud used in setup were no longer available from ownCloud.
|
||||
* Some download errors had no output --- there is more output on error now.
|
||||
|
||||
Control Panel:
|
||||
|
||||
* The background service for the control panel was not restarting on updates, leaving the old version running. This was broken in v0.26 and is now fixed.
|
||||
* Installing your own TLS/SSL certificate had been broken since v0.24 because the new version of openssl became stricter about CSR generation parameters.
|
||||
* Fixed password length help text.
|
||||
|
||||
Contacts/Calendar:
|
||||
|
||||
* Upgraded Nextcloud from 12.0.3 to 12.0.5.
|
||||
|
||||
v0.26b (January 25, 2018)
|
||||
-------------------------
|
||||
|
||||
* Fix new installations which broke at the step of asking for the user's desired email address, which was broken by v0.26's changes related to the control panel.
|
||||
* Fix the provisioning of TLS certificates by pinning a Python package we rely on (acme) to an earlier version because our code isn't yet compatible with its current version.
|
||||
* Reduce munin's log_level from debug to warning to prevent massive log files.
|
||||
|
||||
v0.26 (January 18, 2018)
|
||||
------------------------
|
||||
|
||||
Security:
|
||||
|
||||
* HTTPS, IMAP, and POP's TLS settings have been updated to Mozilla's intermediate cipher list recommendation. Some extremely old devices that use less secure TLS ciphers may no longer be able to connect to IMAP/POP.
|
||||
* Updated web HSTS header to use longer six month duration.
|
||||
|
||||
Mail:
|
||||
|
||||
* Adding attachments in Roundcube broke after the last update for some users after rebooting because a temporary directory was deleted on reboot. The temporary directory is now moved from /tmp to /var so that it is persistent.
|
||||
* `X-Spam-Score` header is added to incoming mail.
|
||||
|
||||
Control panel:
|
||||
|
||||
* RSASHA256 is now used for DNSSEC for .lv domains.
|
||||
* Some documentation/links improvements.
|
||||
|
||||
Installer:
|
||||
|
||||
* We now run `apt-get autoremove` at the start of setup to clear out old packages, especially old kernels that take up a lot of space. On the first run, this step may take a long time.
|
||||
* We now fetch Z-Push from its tagged git repository, fixing an installation problem.
|
||||
* Some old PHP5 packages are removed from setup, fixing an installation bug where Apache would get installed.
|
||||
* Python 3 packages for the control panel are now installed using a virtualenv to prevent installation errors due to conflicts in the cryptography/openssl packages between OS-installed packages and pip-installed packages.
|
||||
|
||||
v0.25 (November 15, 2017)
|
||||
-------------------------
|
||||
|
||||
This update is a security update addressing [CVE-2017-16651, a vulnerability in Roundcube webmail that allows logged-in users to access files on the local filesystem](https://roundcube.net/news/2017/11/08/security-updates-1.3.3-1.2.7-and-1.1.10).
|
||||
|
||||
Mail:
|
||||
|
||||
* Update to Roundcube 1.3.3.
|
||||
|
||||
Control Panel:
|
||||
|
||||
* Allow custom DNS records to be set for DNS wildcard subdomains (i.e. `*`).
|
||||
|
||||
v0.24 (October 3, 2017)
|
||||
-----------------------
|
||||
|
||||
System:
|
||||
|
||||
* Install PHP7 via a PPA. Switch to the on-demand process manager.
|
||||
|
||||
Mail:
|
||||
|
||||
* Updated to [Roundcube 1.3.1](https://roundcube.net/news/2017/06/26/roundcube-webmail-1.3.0-released), but unfortunately dropping the Vacation plugin because it has not been supported by its author and is not compatible with Roundcube 1.3, and updated the persistent login plugin.
|
||||
* Updated to [Z-Push 2.3.8](http://download.z-push.org/final/2.3/z-push-2.3.8.txt).
|
||||
* Dovecot now uses stronger 2048 bit DH params for better forward secrecy.
|
||||
|
||||
Nextcloud:
|
||||
|
||||
* Nextcloud updated to 12.0.3, using PHP7.
|
||||
|
||||
Control Panel:
|
||||
|
||||
* Nameserver (NS) records can now be set on custom domains.
|
||||
* Fix an erroneous status check error due to IPv6 address formatting.
|
||||
* Aliases for administrative addresses can now be set to send mail to +tag administrative addresses.
|
||||
|
||||
v0.23a (May 31, 2017)
|
||||
---------------------
|
||||
|
||||
Corrects a problem in the new way third-party assets are downloaded during setup for the control panel, since v0.23.
|
||||
|
||||
v0.23 (May 30, 2017)
|
||||
--------------------
|
||||
|
||||
Mail:
|
||||
|
||||
* The default theme for Roundcube was changed to the nicer Larry theme.
|
||||
* Exchange/ActiveSync support has been replaced with z-push 2.3.6 from z-push.org (rather than z-push-contrib).
|
||||
|
||||
ownCloud (now Nextcloud):
|
||||
|
||||
* ownCloud is replaced with Nextcloud 10.0.5.
|
||||
* Fixed an error in Owncloud/Nextcloud setup not updating domain when changing hostname.
|
||||
|
||||
Control Panel/Management:
|
||||
|
||||
* Fix an error in the control panel showing rsync backup status.
|
||||
* Fix an error in the control panel related to IPv6 addresses.
|
||||
* TLS certificates for internationalized domain names can now be provisioned from Let's Encrypt automatically.
|
||||
* Third-party assets used in the control panel (jQuery/Bootstrap) are now downloaded during setup and served from the box rather than from a CDN.
|
||||
|
||||
DNS:
|
||||
|
||||
* Add support for custom CAA records.
|
||||
|
||||
v0.22 (April 2, 2017)
|
||||
---------------------
|
||||
|
||||
Mail:
|
||||
|
||||
* The CardDAV plugin has been added to Roundcube so that your ownCloud contacts are available in webmail.
|
||||
* Upgraded to Roundcube 1.2.4 and updated the persistent login plugin.
|
||||
* Allow larger messages to be checked by SpamAssassin.
|
||||
* Dovecot's vsz memory limit has been increased proportional to system memory.
|
||||
* Newly set user passwords must be at least eight characters.
|
||||
|
||||
ownCloud:
|
||||
|
||||
* Upgraded to ownCloud 9.1.4.
|
||||
|
||||
Control Panel/Management:
|
||||
|
||||
* The status checks page crashed when the mailinabox.email website was down - that's fixed.
|
||||
* Made nightly re-provisioning of TLS certificates less noisy.
|
||||
* Fixed bugs in rsync backup method and in the list of recent backups.
|
||||
* Fixed incorrect status checks errors about IPv6 addresses.
|
||||
* Fixed incorrect status checks errors for secondary nameservers if round-robin custom A records are set.
|
||||
* The management mail_log.py tool has been rewritten.
|
||||
|
||||
DNS:
|
||||
|
||||
* Added support for DSA, ED25519, and custom SSHFP records.
|
||||
|
||||
System:
|
||||
|
||||
* The SSH fail2ban jail was not activated.
|
||||
|
||||
Installation:
|
||||
|
||||
* At the end of installation, the SHA256 -- rather than SHA1 -- hash of the system's TLS certificate is shown.
|
||||
|
||||
v0.21c (February 1, 2017)
|
||||
-------------------------
|
||||
|
||||
Installations and upgrades started failing about 10 days ago with the error "ImportError: No module named 'packaging'" after an upstream package (Python's setuptools) was updated by its maintainers. The updated package conflicted with Ubuntu 14.04's version of another package (Python's pip). This update upgrades both packages to remove the conflict.
|
||||
|
||||
If you already encountered the error during installation or upgrade of Mail-in-a-Box, this update may not correct the problem on your existing system. See https://discourse.mailinabox.email/t/v0-21c-release-fixes-python-package-installation-issue/1881 for help if the problem persists after upgrading to this version of Mail-in-a-Box.
|
||||
|
||||
v0.21b (December 4, 2016)
|
||||
-------------------------
|
||||
|
||||
This update corrects a first-time installation issue introduced in v0.21 caused by the new Exchange/ActiveSync feature.
|
||||
|
||||
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)
|
||||
--------------------
|
||||
|
||||
@@ -92,7 +371,6 @@ v0.16 (January 30, 2016)
|
||||
------------------------
|
||||
|
||||
This update primarily adds automatic SSL (now "TLS") certificate provisioning from Let's Encrypt (https://letsencrypt.org/).
|
||||
* The Sieve port is now open so tools like the Thunderbird Sieve program can be used to edit mail filters.
|
||||
|
||||
Control Panel:
|
||||
|
||||
@@ -531,4 +809,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
48
CODE_OF_CONDUCT.md
Normal 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).
|
||||
|
||||
@@ -1,3 +1,50 @@
|
||||
# Contributing
|
||||
|
||||
Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome.
|
||||
|
||||
## Development
|
||||
|
||||
To start developing Mail-in-a-Box, [clone the repository](https://github.com/mail-in-a-box/mailinabox) and familiarize yourself with the code.
|
||||
|
||||
$ git clone https://github.com/mail-in-a-box/mailinabox
|
||||
|
||||
### Vagrant and VirtualBox
|
||||
|
||||
We recommend you use [Vagrant](https://www.vagrantup.com/intro/getting-started/install.html) and [VirtualBox](https://www.virtualbox.org/wiki/Downloads) for development. Please install them first.
|
||||
|
||||
With Vagrant set up, the following should boot up Mail-in-a-Box inside a virtual machine:
|
||||
|
||||
$ vagrant up --provision
|
||||
|
||||
_If you're seeing an error message about your *IP address being listed in the Spamhaus Block List*, simply uncomment the `export SKIP_NETWORK_CHECKS=1` line in `Vagrantfile`. It's normal, you're probably using a dynamic IP address assigned by your Internet provider–they're almost all listed._
|
||||
|
||||
### Modifying your `hosts` file
|
||||
|
||||
After a while, Mail-in-a-Box will be available at `192.168.50.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend to add a hostname to your `hosts` file:
|
||||
|
||||
$ echo "192.168.50.4 mailinabox.lan" | sudo tee -a /etc/hosts
|
||||
|
||||
You should now be able to navigate to https://mailinabox.lan/admin using your browser. There should be an initial admin user with the name `me@mailinabox.lan` and the password `12345678`.
|
||||
|
||||
### Making changes
|
||||
|
||||
Your working copy of Mail-in-a-Box will be mounted inside your VM at `/vagrant`. Any change you make locally will appear inside your VM automatically.
|
||||
|
||||
Running `vagrant up --provision` again will repeat the installation with your modifications.
|
||||
|
||||
Alternatively, you can also ssh into the VM using:
|
||||
|
||||
$ vagrant ssh
|
||||
|
||||
Once inside the VM, you can re-run individual parts of the setup like in this example:
|
||||
|
||||
vm$ cd /vagrant
|
||||
vm$ sudo setup/owncloud.sh # replace with script you'd like to re-run
|
||||
|
||||
### Tests
|
||||
|
||||
Mail-in-a-Box needs more tests. If you're still looking for a way to help out, writing and contributing tests would be a great start!
|
||||
|
||||
## Public domain
|
||||
|
||||
This project is in the public domain. Copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication][CC0]. See the LICENSE file in this directory.
|
||||
@@ -5,3 +52,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.
|
||||
|
||||
24
README.md
24
README.md
@@ -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 ([Nextcloud](https://nextcloud.com/)), Exchange ActiveSync ([z-push](http://z-push.org/))
|
||||
* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.org/))
|
||||
* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/))
|
||||
* DNS ([nsd4](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,7 +59,7 @@ 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.18
|
||||
$ git verify-tag v0.28
|
||||
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!
|
||||
@@ -72,7 +72,7 @@ and on my [personal homepage](https://razor.occams.info/). (Of course, if this r
|
||||
|
||||
Checkout the tag corresponding to the most recent release:
|
||||
|
||||
$ git checkout v0.17c
|
||||
$ git checkout v0.28
|
||||
|
||||
Begin the installation.
|
||||
|
||||
@@ -82,10 +82,16 @@ For help, DO NOT contact me directly --- I don't do tech support by email or twe
|
||||
|
||||
Post your question on the [discussion forum](https://discourse.mailinabox.email/) instead, where me and other Mail-in-a-Box users may be able to help you.
|
||||
|
||||
Contributing and Development
|
||||
----------------------------
|
||||
|
||||
Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome. See [CONTRIBUTING](CONTRIBUTING.md) to get started.
|
||||
|
||||
|
||||
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 +101,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.
|
||||
|
||||
17
Vagrantfile
vendored
17
Vagrantfile
vendored
@@ -5,23 +5,30 @@ Vagrant.configure("2") do |config|
|
||||
config.vm.box = "ubuntu14.04"
|
||||
config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box"
|
||||
|
||||
if Vagrant.has_plugin?("vagrant-cachier")
|
||||
# Configure cached packages to be shared between instances of the same base box.
|
||||
# More info on http://fgrehm.viewdocs.io/vagrant-cachier/usage
|
||||
config.cache.scope = :box
|
||||
end
|
||||
|
||||
# Network config: Since it's a mail server, the machine must be connected
|
||||
# to the public web. However, we currently don't want to expose SSH since
|
||||
# the machine's box will let anyone log into it. So instead we'll put the
|
||||
# machine on a private network.
|
||||
config.vm.hostname = "mailinabox"
|
||||
config.vm.hostname = "mailinabox.lan"
|
||||
config.vm.network "private_network", ip: "192.168.50.4"
|
||||
|
||||
config.vm.provision :shell, :inline => <<-SH
|
||||
# Set environment variables so that the setup script does
|
||||
# not ask any questions during provisioning. We'll let the
|
||||
# machine figure out its own public IP and it'll take a
|
||||
# subdomain on our justtesting.email domain so we can get
|
||||
# started quickly.
|
||||
# machine figure out its own public IP.
|
||||
#
|
||||
# Please note: NONINTERACTIVE=1 mode means that you'll automatically agree
|
||||
# to Let's Encrypt's ACME Subscriber Agreement.
|
||||
export NONINTERACTIVE=1
|
||||
export PUBLIC_IP=auto
|
||||
export PUBLIC_IPV6=auto
|
||||
export PRIMARY_HOSTNAME=auto-easy
|
||||
export PRIMARY_HOSTNAME=auto
|
||||
#export SKIP_NETWORK_CHECKS=1
|
||||
|
||||
# Start the setup script.
|
||||
|
||||
12
conf/fail2ban/filter.d/miab-management-daemon.conf
Normal file
12
conf/fail2ban/filter.d/miab-management-daemon.conf
Normal 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 =
|
||||
7
conf/fail2ban/filter.d/miab-munin.conf
Normal file
7
conf/fail2ban/filter.d/miab-munin.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
[INCLUDES]
|
||||
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
failregex=<HOST> - .*GET /admin/munin/.* HTTP/1.1\" 401.*
|
||||
ignoreregex =
|
||||
7
conf/fail2ban/filter.d/miab-owncloud.conf
Normal file
7
conf/fail2ban/filter.d/miab-owncloud.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
[INCLUDES]
|
||||
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
failregex=Login failed: .*Remote IP: '<HOST>[\)']
|
||||
ignoreregex =
|
||||
7
conf/fail2ban/filter.d/miab-postfix-submission.conf
Normal file
7
conf/fail2ban/filter.d/miab-postfix-submission.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
[INCLUDES]
|
||||
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
failregex=postfix/submission/smtpd.*warning.*\[<HOST>\]: .* authentication (failed|aborted)
|
||||
ignoreregex =
|
||||
9
conf/fail2ban/filter.d/miab-roundcube.conf
Normal file
9
conf/fail2ban/filter.d/miab-roundcube.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
[INCLUDES]
|
||||
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = IMAP Error: Login failed for .*? from <HOST>\. AUTHENTICATE.*
|
||||
|
||||
ignoreregex =
|
||||
@@ -1,4 +1,5 @@
|
||||
# Fail2Ban configuration file for Mail-in-a-Box
|
||||
# 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
|
||||
@@ -6,24 +7,53 @@
|
||||
# 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
|
||||
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/nextcloud.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
|
||||
@@ -38,3 +68,14 @@ action = iptables-allports[name=recidive]
|
||||
# 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]
|
||||
enabled = true
|
||||
maxretry = 7
|
||||
bantime = 3600
|
||||
|
||||
[ssh-ddos]
|
||||
enabled = true
|
||||
@@ -18,8 +18,6 @@
|
||||
<string>PRIMARY_HOSTNAME</string>
|
||||
<key>CalDAVPort</key>
|
||||
<real>443</real>
|
||||
<key>CalDAVPrincipalURL</key>
|
||||
<string>/cloud/remote.php/caldav/calendars/</string>
|
||||
<key>CalDAVUseSSL</key>
|
||||
<true/>
|
||||
<key>PayloadDescription</key>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC="Mail-in-a-Box Management Daemon"
|
||||
NAME=mailinabox
|
||||
DAEMON=/usr/local/bin/mailinabox-daemon
|
||||
DAEMON=/usr/local/lib/mailinabox/start
|
||||
PIDFILE=/var/run/$NAME.pid
|
||||
SCRIPTNAME=/etc/init.d/$NAME
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
# takes precedence over all non-regex matches and only regex matches that
|
||||
# come after it (i.e. none of those, since this is the last one.) That means
|
||||
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
|
||||
# handled locations for ownCloud (which serves user-uploaded files that might
|
||||
# handled locations for Nextcloud (which serves user-uploaded files that might
|
||||
# have this pattern, see #414) or some of the other services.
|
||||
location ~ /\.(ht|svn|git|hg|bzr) {
|
||||
log_not_found off;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Control Panel
|
||||
# Proxy /admin to our Python based control panel daemon. It is
|
||||
# listening on IPv4 only so use an IP address and not 'localhost'.
|
||||
location /admin/assets {
|
||||
alias /usr/local/lib/mailinabox/vendor/assets;
|
||||
}
|
||||
rewrite ^/admin$ /admin/;
|
||||
rewrite ^/admin/munin$ /admin/munin/ redirect;
|
||||
location /admin/ {
|
||||
@@ -11,7 +14,7 @@
|
||||
add_header Content-Security-Policy "frame-ancestors 'none';";
|
||||
}
|
||||
|
||||
# ownCloud configuration.
|
||||
# Nextcloud configuration.
|
||||
rewrite ^/cloud$ /cloud/ redirect;
|
||||
rewrite ^/cloud/$ /cloud/index.php;
|
||||
rewrite ^/cloud/(contacts|calendar|files)$ /cloud/index.php/apps/$1/ redirect;
|
||||
@@ -40,13 +43,11 @@
|
||||
fastcgi_param MOD_X_ACCEL_REDIRECT_PREFIX /owncloud-xaccel;
|
||||
fastcgi_read_timeout 630;
|
||||
fastcgi_pass php-fpm;
|
||||
error_page 403 /cloud/core/templates/403.php;
|
||||
error_page 404 /cloud/core/templates/404.php;
|
||||
client_max_body_size 1G;
|
||||
fastcgi_buffers 64 4K;
|
||||
}
|
||||
location ^~ /owncloud-xaccel/ {
|
||||
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. ownCloud sends the full file
|
||||
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. Nextcloud sends the full file
|
||||
# path on disk as a subdirectory under this virtual path.
|
||||
# We must only allow 'internal' redirects within nginx so that the filesystem
|
||||
# is not exposed to the world.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# from: https://gist.github.com/konklone/6532544
|
||||
###################################################################################
|
||||
# from https://gist.github.com/konklone/6532544 and https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
###################################################################################################################
|
||||
|
||||
# Basically the nginx configuration I use at konklone.com.
|
||||
# I check it using https://www.ssllabs.com/ssltest/analyze.html?d=konklone.com
|
||||
@@ -27,17 +27,17 @@
|
||||
#
|
||||
# Reference client: https://www.ssllabs.com/ssltest/analyze.html
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !kECDH !CAMELLIA !RC4 !SEED';
|
||||
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
|
||||
|
||||
# Cut out (the old, broken) SSLv3 entirely.
|
||||
# This **excludes IE6 users** and (apparently) Yandexbot.
|
||||
# Just comment out if you need to support IE6, bless your soul.
|
||||
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
|
||||
|
||||
# Turn on session resumption, using a 10 min cache shared across nginx processes,
|
||||
# Turn on session resumption, using a cache shared across nginx processes,
|
||||
# as recommended by http://nginx.org/en/docs/http/configuring_https_servers.html
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_timeout 1d;
|
||||
#keepalive_timeout 70; # in Ubuntu 14.04/nginx 1.4.6 the default is 65, so plenty good
|
||||
|
||||
# Buffer size of 1400 bytes fits in one MTU.
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
## your own --- please do not ask for help from us.
|
||||
|
||||
upstream php-fpm {
|
||||
server unix:/var/run/php5-fpm.sock;
|
||||
server unix:/var/run/php/php7.0-fpm.sock;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ server {
|
||||
# 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/;
|
||||
alias $STORAGE_ROOT/ssl/lets_encrypt/webroot/.well-known/acme-challenge/;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
* Descr : Autodiscover configuration file
|
||||
************************************************/
|
||||
|
||||
define('TIMEZONE', '');
|
||||
|
||||
// Defines the base path on the server
|
||||
define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/');
|
||||
|
||||
// The Z-Push server location for the autodiscover response
|
||||
define('SERVERURL', 'https://PRIMARY_HOSTNAME/Microsoft-Server-ActiveSync');
|
||||
define('ZPUSH_HOST', 'PRIMARY_HOSTNAME');
|
||||
|
||||
define('USE_FULLEMAIL_FOR_LOGIN', true);
|
||||
|
||||
@@ -18,6 +19,7 @@ define('LOGFILE', LOGFILEDIR . 'autodiscover.log');
|
||||
define('LOGERRORFILE', LOGFILEDIR . 'autodiscover-error.log');
|
||||
define('LOGLEVEL', LOGLEVEL_INFO);
|
||||
define('LOGUSERLEVEL', LOGLEVEL);
|
||||
$specialLogUsers = array();
|
||||
|
||||
// the backend data provider
|
||||
define('BACKEND_PROVIDER', 'BackendCombined');
|
||||
|
||||
@@ -17,7 +17,7 @@ define('CARDDAV_CONTACTS_FOLDER_NAME', '%u Addressbook');
|
||||
define('CARDDAV_SUPPORTS_SYNC', false);
|
||||
|
||||
// If the CardDAV server supports the FN attribute for searches
|
||||
// DAViCal supports it, but SabreDav, Owncloud and SOGo don't
|
||||
// DAViCal supports it, but SabreDav, Nextcloud and SOGo don't
|
||||
// Setting this to true will search by FN. If false will search by sn, givenName and email
|
||||
// It's safe to leave it as false
|
||||
define('CARDDAV_SUPPORTS_FN_SEARCH', false);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
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,19 @@ define('IMAP_FOLDER_TRASH', 'TRASH');
|
||||
define('IMAP_FOLDER_SPAM', 'SPAM');
|
||||
define('IMAP_FOLDER_ARCHIVE', 'ARCHIVE');
|
||||
|
||||
define('IMAP_INLINE_FORWARD', true);
|
||||
define('IMAP_EXCLUDED_FOLDERS', '');
|
||||
|
||||
// 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,6 +44,7 @@ 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');
|
||||
|
||||
@@ -47,5 +52,6 @@ global $imap_smtp_params;
|
||||
$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");
|
||||
define('IMAP_MEETING_USE_CALDAV', true);
|
||||
|
||||
?>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
|
||||
# This script performs a backup of all user data:
|
||||
# 1) System services are stopped.
|
||||
# 2) An incremental encrypted backup is made using duplicity.
|
||||
# 3) The stopped services are restarted.
|
||||
# 4) STORAGE_ROOT/backup/after-backup is executd if it exists.
|
||||
# 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, sys
|
||||
import dateutil.parser, dateutil.relativedelta, dateutil.tz
|
||||
import rtyaml
|
||||
from exclusiveprocess import Lock
|
||||
|
||||
from utils import exclusive_process, load_environment, shell, wait_for_service, fix_boto
|
||||
from utils import 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
|
||||
@@ -32,6 +39,8 @@ def backup_status(env):
|
||||
def reldate(date, ref, clip):
|
||||
if ref < date: return clip
|
||||
rd = dateutil.relativedelta.relativedelta(ref, date)
|
||||
if rd.years > 1: return "%d years, %d months" % (rd.years, rd.months)
|
||||
if rd.years == 1: return "%d year, %d months" % (rd.years, rd.months)
|
||||
if rd.months > 1: return "%d months, %d days" % (rd.months, rd.days)
|
||||
if rd.months == 1: return "%d month, %d days" % (rd.months, rd.days)
|
||||
if rd.days >= 7: return "%d days" % rd.days
|
||||
@@ -51,6 +60,7 @@ def backup_status(env):
|
||||
"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",
|
||||
@@ -58,7 +68,7 @@ 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:
|
||||
@@ -105,7 +115,7 @@ def backup_status(env):
|
||||
# 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:
|
||||
if incremental_count > 0 and incremental_size > 0 and first_full_size is not None:
|
||||
# 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)
|
||||
@@ -197,13 +207,16 @@ def get_target_type(config):
|
||||
def perform_backup(full_backup):
|
||||
env = load_environment()
|
||||
|
||||
exclusive_process("backup")
|
||||
# Create an global exclusive lock so that the backup script
|
||||
# cannot be run more than one.
|
||||
Lock(die=True).forever()
|
||||
|
||||
config = get_backup_config(env)
|
||||
backup_root = os.path.join(env["STORAGE_ROOT"], '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
|
||||
|
||||
@@ -254,10 +267,19 @@ def perform_backup(full_backup):
|
||||
if quit:
|
||||
sys.exit(code)
|
||||
|
||||
service_command("php5-fpm", "stop", quit=True)
|
||||
service_command("php7.0-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
|
||||
# after the first backup. See #396.
|
||||
@@ -273,13 +295,13 @@ def perform_backup(full_backup):
|
||||
env["STORAGE_ROOT"],
|
||||
config["target"],
|
||||
"--allow-source-mismatch"
|
||||
],
|
||||
] + rsync_ssh_options,
|
||||
get_env(env))
|
||||
finally:
|
||||
# Start services again.
|
||||
service_command("dovecot", "start", quit=False)
|
||||
service_command("postfix", "start", quit=False)
|
||||
service_command("php5-fpm", "start", quit=False)
|
||||
service_command("php7.0-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):
|
||||
@@ -295,7 +317,7 @@ def perform_backup(full_backup):
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--force",
|
||||
config["target"]
|
||||
],
|
||||
] + rsync_ssh_options,
|
||||
get_env(env))
|
||||
|
||||
# From duplicity's manual:
|
||||
@@ -310,7 +332,7 @@ def perform_backup(full_backup):
|
||||
"--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
|
||||
@@ -349,7 +371,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()
|
||||
@@ -360,32 +382,75 @@ 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):
|
||||
import urllib.parse
|
||||
try:
|
||||
p = urllib.parse.urlparse(config["target"])
|
||||
target = urllib.parse.urlparse(config["target"])
|
||||
except ValueError:
|
||||
return "invalid target"
|
||||
|
||||
if p.scheme == "file":
|
||||
return [(fn, os.path.getsize(os.path.join(p.path, fn))) for fn in os.listdir(p.path)]
|
||||
if target.scheme == "file":
|
||||
return [(fn, os.path.getsize(os.path.join(target.path, fn))) for fn in os.listdir(target.path)]
|
||||
|
||||
elif p.scheme == "s3":
|
||||
elif target.scheme == "rsync":
|
||||
rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)')
|
||||
rsync_target = '{host}:{path}'
|
||||
|
||||
target_path = target.path
|
||||
if not target_path.endswith('/'):
|
||||
target_path = target_path + '/'
|
||||
if target_path.startswith('/'):
|
||||
target_path = target_path[1:]
|
||||
|
||||
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.netloc,
|
||||
path=target_path)
|
||||
]
|
||||
|
||||
code, listing = shell('check_output', rsync_command, trap=True, capture_stderr=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:
|
||||
if 'Permission denied (publickey).' in listing:
|
||||
reason = "Invalid user or check you correctly copied the SSH key."
|
||||
elif 'No such file or directory' in listing:
|
||||
reason = "Provided path {} is invalid.".format(target_path)
|
||||
elif 'Network is unreachable' in listing:
|
||||
reason = "The IP address {} is unreachable.".format(target.hostname)
|
||||
elif 'Could not resolve hostname':
|
||||
reason = "The hostname {} cannot be resolved.".format(target.hostname)
|
||||
else:
|
||||
reason = "Unknown error." \
|
||||
"Please check running 'python management/backup.py --verify'" \
|
||||
"from mailinabox sources to debug the issue."
|
||||
raise ValueError("Connection to rsync host failed: {}".format(reason))
|
||||
|
||||
elif target.scheme == "s3":
|
||||
# match to a Region
|
||||
fix_boto() # must call prior to importing boto
|
||||
import boto.s3
|
||||
from boto.exception import BotoServerError
|
||||
for region in boto.s3.regions():
|
||||
if region.endpoint == p.hostname:
|
||||
if region.endpoint == target.hostname:
|
||||
break
|
||||
else:
|
||||
raise ValueError("Invalid S3 region/host.")
|
||||
|
||||
bucket = p.path[1:].split('/')[0]
|
||||
path = '/'.join(p.path[1:].split('/')[1:]) + '/'
|
||||
bucket = target.path[1:].split('/')[0]
|
||||
path = '/'.join(target.path[1:].split('/')[1:]) + '/'
|
||||
|
||||
# If no prefix is specified, set the path to '', otherwise boto won't list the files
|
||||
if path == '/':
|
||||
@@ -472,6 +537,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
|
||||
|
||||
@@ -487,6 +555,12 @@ if __name__ == "__main__":
|
||||
# are readable, and b) report if they are up to date.
|
||||
run_duplicity_verification()
|
||||
|
||||
elif sys.argv[-1] == "--list":
|
||||
# Run duplicity's verification command to check a) the backup files
|
||||
# are readable, and b) report if they are up to date.
|
||||
for fn, size in list_target_files(get_backup_config(load_environment())):
|
||||
print("{}\t{}".format(fn, size))
|
||||
|
||||
elif sys.argv[-1] == "--status":
|
||||
# Show backup status.
|
||||
ret = backup_status(load_environment())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/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, make_response
|
||||
@@ -45,6 +44,9 @@ def authorized_personnel_only(viewfunc):
|
||||
privs = []
|
||||
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:
|
||||
# Call view func.
|
||||
@@ -117,6 +119,9 @@ 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": "Incorrect username or password",
|
||||
@@ -328,11 +333,16 @@ def ssl_get_status():
|
||||
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)
|
||||
provision, cant_provision = get_certificates_to_provision(env, show_valid_certs=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 ]
|
||||
domains_status = [
|
||||
{
|
||||
"domain": d["domain"],
|
||||
"status": d["ssl_certificate"][0],
|
||||
"text": d["ssl_certificate"][1] + ((" " + cant_provision[d["domain"]] if d["domain"] in cant_provision else ""))
|
||||
} 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)):
|
||||
@@ -344,7 +354,6 @@ def ssl_get_status():
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -371,11 +380,8 @@ def ssl_install_cert():
|
||||
@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)
|
||||
requests = provision_certificates(env, limit_domains=None)
|
||||
return json_response({ "requests": requests })
|
||||
|
||||
|
||||
# WEB
|
||||
@@ -534,10 +540,9 @@ def munin_cgi(filename):
|
||||
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 as well as passed input to determine
|
||||
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 as well as passed as input to the
|
||||
command.
|
||||
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
|
||||
@@ -545,7 +550,7 @@ def munin_cgi(filename):
|
||||
support infrastructure like spawn-fcgi.
|
||||
"""
|
||||
|
||||
COMMAND = 'su - munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph "%s"'
|
||||
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
|
||||
@@ -557,12 +562,10 @@ def munin_cgi(filename):
|
||||
|
||||
query_str = request.query_string.decode("utf-8", 'ignore')
|
||||
|
||||
env = {'PATH_INFO': '/%s/' % filename, 'QUERY_STRING': query_str}
|
||||
cmd = COMMAND % query_str
|
||||
env = {'PATH_INFO': '/%s/' % filename, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': query_str}
|
||||
code, binout = utils.shell('check_output',
|
||||
cmd.split(' ', 5),
|
||||
# Using a maxsplit of 5 keeps the last 2 arguments together
|
||||
input=query_str.encode('UTF-8'),
|
||||
COMMAND.split(" ", 5),
|
||||
# Using a maxsplit of 5 keeps the last arguments together
|
||||
env=env,
|
||||
return_bytes=True,
|
||||
trap=True)
|
||||
@@ -583,6 +586,22 @@ def munin_cgi(filename):
|
||||
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__':
|
||||
|
||||
@@ -9,11 +9,17 @@ export LC_ALL=en_US.UTF-8
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_TYPE=en_US.UTF-8
|
||||
|
||||
# On Mondays, i.e. once a week, send the administrator a report of total emails
|
||||
# sent and received so the admin might notice server abuse.
|
||||
if [ `date "+%u"` -eq 1 ]; then
|
||||
management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report"
|
||||
fi
|
||||
|
||||
# Take a backup.
|
||||
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"
|
||||
management/ssl_certificates.py -q | management/email_administrator.py "Error Provisioning TLS Certificate"
|
||||
|
||||
# Run status checks and email the administrator if anything changed.
|
||||
management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
|
||||
# Creates DNS zone files for all of the domains of all of the mail users
|
||||
# and mail aliases and restarts nsd.
|
||||
@@ -12,6 +12,12 @@ import dns.resolver
|
||||
from mailconfig import get_mail_domains
|
||||
from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains
|
||||
|
||||
# From https://stackoverflow.com/questions/3026957/how-to-validate-a-domain-name-using-regex-php/16491074#16491074
|
||||
# This regular expression matches domain names according to RFCs, it also accepts fqdn with an leading dot,
|
||||
# underscores, as well as asteriks which are allowed in domain names but not hostnames (i.e. allowed in
|
||||
# DNS but not in URLs), which are common in certain record types like for DKIM.
|
||||
DOMAIN_RE = "^(?!\-)(?:[*][.])?(?:[a-zA-Z\d\-_]{0,62}[a-zA-Z\d_]\.){1,126}(?!\d+)[a-zA-Z\d_]{1,63}(\.?)$"
|
||||
|
||||
def get_dns_domains(env):
|
||||
# Add all domain names in use by email users and mail aliases and ensure
|
||||
# PRIMARY_HOSTNAME is in the list.
|
||||
@@ -274,6 +280,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 ""))
|
||||
@@ -335,13 +348,25 @@ def build_sshfp_records():
|
||||
"ssh-rsa": 1,
|
||||
"ssh-dss": 2,
|
||||
"ecdsa-sha2-nistp256": 3,
|
||||
"ssh-ed25519": 4,
|
||||
}
|
||||
|
||||
# Get our local fingerprints by running ssh-keyscan. The output looks
|
||||
# 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", "-t", "rsa,dsa,ecdsa,ed25519", "-p", ports[0], "localhost"])
|
||||
for key in sorted(keys.split("\n")):
|
||||
if key.strip() == "" or key[0] == "#": continue
|
||||
try:
|
||||
@@ -503,12 +528,13 @@ zone:
|
||||
|
||||
def dnssec_choose_algo(domain, env):
|
||||
if '.' in domain and domain.rsplit('.')[-1] in \
|
||||
("email", "guide", "fund", "be"):
|
||||
("email", "guide", "fund", "be", "lv"):
|
||||
# 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
|
||||
# Nic.lv does not support RSASHA1-NSEC3-SHA1 for .lv tld's
|
||||
return "RSASHA256"
|
||||
|
||||
# For any domain we were able to sign before, don't change the algorithm
|
||||
@@ -743,12 +769,25 @@ def set_custom_dns_record(qname, rtype, value, action, env):
|
||||
# validate rtype
|
||||
rtype = rtype.upper()
|
||||
if value is not None and qname != "_secondary_nameserver":
|
||||
if not re.search(DOMAIN_RE, qname):
|
||||
raise ValueError("Invalid name.")
|
||||
|
||||
if rtype in ("A", "AAAA"):
|
||||
if value != "local": # "local" is a special flag for us
|
||||
v = ipaddress.ip_address(value) # raises a ValueError if there's a problem
|
||||
if rtype == "A" and not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.")
|
||||
if rtype == "AAAA" and not isinstance(v, ipaddress.IPv6Address): raise ValueError("That's an IPv4 address.")
|
||||
elif rtype in ("CNAME", "TXT", "SRV", "MX"):
|
||||
elif rtype in ("CNAME", "NS"):
|
||||
if rtype == "NS" and qname == zone:
|
||||
raise ValueError("NS records can only be set for subdomains.")
|
||||
|
||||
# ensure value has a trailing dot
|
||||
if not value.endswith("."):
|
||||
value = value + "."
|
||||
|
||||
if not re.search(DOMAIN_RE, value):
|
||||
raise ValueError("Invalid value.")
|
||||
elif rtype in ("CNAME", "TXT", "SRV", "MX", "SSHFP", "CAA"):
|
||||
# anything goes
|
||||
pass
|
||||
else:
|
||||
@@ -863,10 +902,10 @@ def set_secondary_dns(hostnames, env):
|
||||
return do_dns_update(env)
|
||||
|
||||
|
||||
def get_custom_dns_record(custom_dns, qname, rtype):
|
||||
def get_custom_dns_records(custom_dns, qname, rtype):
|
||||
for qname1, rtype1, value in custom_dns:
|
||||
if qname1 == qname and rtype1 == rtype:
|
||||
return value
|
||||
yield value
|
||||
return None
|
||||
|
||||
########################################################################
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
|
||||
# Reads in STDIN. If the stream is not empty, mail it to the system administrator.
|
||||
|
||||
import sys
|
||||
|
||||
import html
|
||||
import smtplib
|
||||
from email.message import Message
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
# In Python 3.6:
|
||||
#from email.message import Message
|
||||
|
||||
from utils import load_environment
|
||||
|
||||
@@ -26,11 +32,23 @@ if content == "":
|
||||
sys.exit(0)
|
||||
|
||||
# create MIME message
|
||||
msg = Message()
|
||||
msg = MIMEMultipart('alternative')
|
||||
|
||||
# In Python 3.6:
|
||||
#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")
|
||||
|
||||
content_html = "<html><body><pre>{}</pre></body></html>".format(html.escape(content))
|
||||
|
||||
msg.attach(MIMEText(content, 'plain'))
|
||||
msg.attach(MIMEText(content_html, 'html'))
|
||||
|
||||
# In Python 3.6:
|
||||
#msg.set_content(content)
|
||||
#msg.add_alternative(content_html, "html")
|
||||
|
||||
# send
|
||||
smtpclient = smtplib.SMTP('127.0.0.1', 25)
|
||||
|
||||
@@ -1,136 +1,895 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
import argparse
|
||||
import datetime
|
||||
import gzip
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import textwrap
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from collections import defaultdict
|
||||
import re, os.path
|
||||
import dateutil.parser
|
||||
import time
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import mailconfig
|
||||
import utils
|
||||
|
||||
def scan_mail_log(logger, env):
|
||||
|
||||
LOG_FILES = (
|
||||
'/var/log/mail.log',
|
||||
'/var/log/mail.log.1',
|
||||
'/var/log/mail.log.2.gz',
|
||||
'/var/log/mail.log.3.gz',
|
||||
'/var/log/mail.log.4.gz',
|
||||
'/var/log/mail.log.5.gz',
|
||||
'/var/log/mail.log.6.gz',
|
||||
)
|
||||
|
||||
TIME_DELTAS = OrderedDict([
|
||||
('all', datetime.timedelta(weeks=52)),
|
||||
('month', datetime.timedelta(weeks=4)),
|
||||
('2weeks', datetime.timedelta(days=14)),
|
||||
('week', datetime.timedelta(days=7)),
|
||||
('2days', datetime.timedelta(days=2)),
|
||||
('day', datetime.timedelta(days=1)),
|
||||
('12hours', datetime.timedelta(hours=12)),
|
||||
('6hours', datetime.timedelta(hours=6)),
|
||||
('hour', datetime.timedelta(hours=1)),
|
||||
('30min', datetime.timedelta(minutes=30)),
|
||||
('10min', datetime.timedelta(minutes=10)),
|
||||
('5min', datetime.timedelta(minutes=5)),
|
||||
('min', datetime.timedelta(minutes=1)),
|
||||
('today', datetime.datetime.now() - datetime.datetime.now().replace(hour=0, minute=0, second=0))
|
||||
])
|
||||
|
||||
# Start date > end date!
|
||||
START_DATE = datetime.datetime.now()
|
||||
END_DATE = None
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
# List of strings to filter users with
|
||||
FILTERS = None
|
||||
|
||||
# What to show (with defaults)
|
||||
SCAN_OUT = True # Outgoing email
|
||||
SCAN_IN = True # Incoming email
|
||||
SCAN_DOVECOT_LOGIN = True # Dovecot Logins
|
||||
SCAN_GREY = False # Greylisted email
|
||||
SCAN_BLOCKED = False # Rejected email
|
||||
|
||||
|
||||
def scan_files(collector):
|
||||
""" Scan files until they run out or the earliest date is reached """
|
||||
|
||||
stop_scan = False
|
||||
|
||||
for fn in LOG_FILES:
|
||||
|
||||
tmp_file = None
|
||||
|
||||
if not os.path.exists(fn):
|
||||
continue
|
||||
elif fn[-3:] == '.gz':
|
||||
tmp_file = tempfile.NamedTemporaryFile()
|
||||
shutil.copyfileobj(gzip.open(fn), tmp_file)
|
||||
|
||||
if VERBOSE:
|
||||
print("Processing file", fn, "...")
|
||||
fn = tmp_file.name if tmp_file else fn
|
||||
|
||||
for line in reverse_readline(fn):
|
||||
if scan_mail_log_line(line.strip(), collector) is False:
|
||||
if stop_scan:
|
||||
return
|
||||
stop_scan = True
|
||||
else:
|
||||
stop_scan = False
|
||||
|
||||
|
||||
|
||||
def scan_mail_log(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:
|
||||
env (dict): Dictionary containing MiaB settings
|
||||
|
||||
"""
|
||||
|
||||
collector = {
|
||||
"scan_count": 0, # Number of lines scanned
|
||||
"parse_count": 0, # Number of lines parsed (i.e. that had their contents examined)
|
||||
"scan_time": time.time(), # The time in seconds the scan took
|
||||
"sent_mail": OrderedDict(), # Data about email sent by users
|
||||
"received_mail": OrderedDict(), # Data about email received by users
|
||||
"logins": OrderedDict(), # Data about login activity
|
||||
"postgrey": {}, # Data about greylisting of email addresses
|
||||
"rejected": OrderedDict(), # Emails that were blocked
|
||||
"known_addresses": None, # Addresses handled by the Miab installation
|
||||
"other-services": set(),
|
||||
"imap-logins": { },
|
||||
"postgrey": { },
|
||||
"rejected-mail": { },
|
||||
"activity-by-hour": { "imap-logins": defaultdict(int), "smtp-sends": defaultdict(int) },
|
||||
}
|
||||
|
||||
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
|
||||
with open(fn, 'rb') as log:
|
||||
for line in log:
|
||||
line = line.decode("utf8", errors='replace')
|
||||
scan_mail_log_line(line.strip(), collector)
|
||||
|
||||
if collector["imap-logins"]:
|
||||
logger.add_heading("Recent IMAP Logins")
|
||||
logger.print_block("The most recent login from each remote IP adddress is show.")
|
||||
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]):
|
||||
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")
|
||||
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"))
|
||||
|
||||
if collector["rejected-mail"]:
|
||||
logger.add_heading("Rejected Mail")
|
||||
logger.print_block("The following incoming mail was rejected.")
|
||||
for k in utils.sort_email_addresses(collector["rejected-mail"], env):
|
||||
for date, sender, message in collector["rejected-mail"][k]:
|
||||
logger.print_line(k + "\t" + str(date) + "\t" + sender + "\t" + message)
|
||||
|
||||
logger.add_heading("Activity by Hour")
|
||||
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] ))
|
||||
|
||||
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):
|
||||
m = re.match(r"(\S+ \d+ \d+:\d+:\d+) (\S+) (\S+?)(\[\d+\])?: (.*)", line)
|
||||
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/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"):
|
||||
# nothing to look at
|
||||
try:
|
||||
import mailconfig
|
||||
collector["known_addresses"] = (set(mailconfig.get_mail_users(env)) |
|
||||
set(alias[0] for alias in mailconfig.get_mail_aliases(env)))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
print("Scanning logs from {:%Y-%m-%d %H:%M:%S} to {:%Y-%m-%d %H:%M:%S}".format(
|
||||
END_DATE, START_DATE)
|
||||
)
|
||||
|
||||
# Scan the lines in the log files until the date goes out of range
|
||||
scan_files(collector)
|
||||
|
||||
if not collector["scan_count"]:
|
||||
print("No log lines scanned...")
|
||||
return
|
||||
|
||||
collector["scan_time"] = time.time() - collector["scan_time"]
|
||||
|
||||
print("{scan_count} Log lines scanned, {parse_count} lines parsed in {scan_time:.2f} "
|
||||
"seconds\n".format(**collector))
|
||||
|
||||
# Print Sent Mail report
|
||||
|
||||
if collector["sent_mail"]:
|
||||
msg = "Sent email"
|
||||
print_header(msg)
|
||||
|
||||
data = OrderedDict(sorted(collector["sent_mail"].items(), key=email_sort))
|
||||
|
||||
print_user_table(
|
||||
data.keys(),
|
||||
data=[
|
||||
("sent", [u["sent_count"] for u in data.values()]),
|
||||
("hosts", [len(u["hosts"]) for u in data.values()]),
|
||||
],
|
||||
sub_data=[
|
||||
("sending hosts", [u["hosts"] for u in data.values()]),
|
||||
],
|
||||
activity=[
|
||||
("sent", [u["activity-by-hour"] for u in data.values()]),
|
||||
],
|
||||
earliest=[u["earliest"] for u in data.values()],
|
||||
latest=[u["latest"] for u in data.values()],
|
||||
)
|
||||
|
||||
accum = defaultdict(int)
|
||||
data = collector["sent_mail"].values()
|
||||
|
||||
for h in range(24):
|
||||
accum[h] = sum(d["activity-by-hour"][h] for d in data)
|
||||
|
||||
print_time_table(
|
||||
["sent"],
|
||||
[accum]
|
||||
)
|
||||
|
||||
# Print Received Mail report
|
||||
|
||||
if collector["received_mail"]:
|
||||
msg = "Received email"
|
||||
print_header(msg)
|
||||
|
||||
data = OrderedDict(sorted(collector["received_mail"].items(), key=email_sort))
|
||||
|
||||
print_user_table(
|
||||
data.keys(),
|
||||
data=[
|
||||
("received", [u["received_count"] for u in data.values()]),
|
||||
],
|
||||
activity=[
|
||||
("sent", [u["activity-by-hour"] for u in data.values()]),
|
||||
],
|
||||
earliest=[u["earliest"] for u in data.values()],
|
||||
latest=[u["latest"] for u in data.values()],
|
||||
)
|
||||
|
||||
accum = defaultdict(int)
|
||||
for h in range(24):
|
||||
accum[h] = sum(d["activity-by-hour"][h] for d in data.values())
|
||||
|
||||
print_time_table(
|
||||
["received"],
|
||||
[accum]
|
||||
)
|
||||
|
||||
# Print login report
|
||||
|
||||
if collector["logins"]:
|
||||
msg = "User logins per hour"
|
||||
print_header(msg)
|
||||
|
||||
data = OrderedDict(sorted(collector["logins"].items(), key=email_sort))
|
||||
|
||||
# Get a list of all of the protocols seen in the logs in reverse count order.
|
||||
all_protocols = defaultdict(int)
|
||||
for u in data.values():
|
||||
for protocol_name, count in u["totals_by_protocol"].items():
|
||||
all_protocols[protocol_name] += count
|
||||
all_protocols = [k for k, v in sorted(all_protocols.items(), key=lambda kv : -kv[1])]
|
||||
|
||||
print_user_table(
|
||||
data.keys(),
|
||||
data=[
|
||||
(protocol_name, [
|
||||
round(u["totals_by_protocol"][protocol_name] / (u["latest"]-u["earliest"]).total_seconds() * 60*60, 1)
|
||||
if (u["latest"]-u["earliest"]).total_seconds() > 0
|
||||
else 0 # prevent division by zero
|
||||
for u in data.values()])
|
||||
for protocol_name in all_protocols
|
||||
],
|
||||
sub_data=[
|
||||
("Protocol and Source", [[
|
||||
"{} {}: {} times".format(protocol_name, host, count)
|
||||
for (protocol_name, host), count
|
||||
in sorted(u["totals_by_protocol_and_host"].items(), key=lambda kv:-kv[1])
|
||||
] for u in data.values()])
|
||||
],
|
||||
activity=[
|
||||
(protocol_name, [u["activity-by-hour"][protocol_name] for u in data.values()])
|
||||
for protocol_name in all_protocols
|
||||
],
|
||||
earliest=[u["earliest"] for u in data.values()],
|
||||
latest=[u["latest"] for u in data.values()],
|
||||
numstr=lambda n : str(round(n, 1)),
|
||||
)
|
||||
|
||||
accum = { protocol_name: defaultdict(int) for protocol_name in all_protocols }
|
||||
for h in range(24):
|
||||
for protocol_name in all_protocols:
|
||||
accum[protocol_name][h] = sum(d["activity-by-hour"][protocol_name][h] for d in data.values())
|
||||
|
||||
print_time_table(
|
||||
all_protocols,
|
||||
[accum[protocol_name] for protocol_name in all_protocols]
|
||||
)
|
||||
|
||||
if collector["postgrey"]:
|
||||
msg = "Greylisted Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
|
||||
print_header(msg.format(END_DATE, START_DATE))
|
||||
|
||||
print(textwrap.fill(
|
||||
"The following mail was greylisted, meaning the emails were temporarily rejected. "
|
||||
"Legitimate senders will try again within ten minutes.",
|
||||
width=80, initial_indent=" ", subsequent_indent=" "
|
||||
), end='\n\n')
|
||||
|
||||
data = OrderedDict(sorted(collector["postgrey"].items(), key=email_sort))
|
||||
users = []
|
||||
received = []
|
||||
senders = []
|
||||
sender_clients = []
|
||||
delivered_dates = []
|
||||
|
||||
for recipient in data:
|
||||
sorted_recipients = sorted(data[recipient].items(), key=lambda kv: kv[1][0] or kv[1][1])
|
||||
for (client_address, sender), (first_date, delivered_date) in sorted_recipients:
|
||||
if first_date:
|
||||
users.append(recipient)
|
||||
received.append(first_date)
|
||||
senders.append(sender)
|
||||
delivered_dates.append(delivered_date)
|
||||
sender_clients.append(client_address)
|
||||
|
||||
print_user_table(
|
||||
users,
|
||||
data=[
|
||||
("received", received),
|
||||
("sender", senders),
|
||||
("delivered", [str(d) or "no retry yet" for d in delivered_dates]),
|
||||
("sending host", sender_clients)
|
||||
],
|
||||
delimit=True,
|
||||
)
|
||||
|
||||
if collector["rejected"]:
|
||||
msg = "Blocked Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
|
||||
print_header(msg.format(END_DATE, START_DATE))
|
||||
|
||||
data = OrderedDict(sorted(collector["rejected"].items(), key=email_sort))
|
||||
|
||||
rejects = []
|
||||
|
||||
if VERBOSE:
|
||||
for user_data in data.values():
|
||||
user_rejects = []
|
||||
for date, sender, message in user_data["blocked"]:
|
||||
if len(sender) > 64:
|
||||
sender = sender[:32] + "…" + sender[-32:]
|
||||
user_rejects.append("%s - %s " % (date, sender))
|
||||
user_rejects.append(" %s" % message)
|
||||
rejects.append(user_rejects)
|
||||
|
||||
print_user_table(
|
||||
data.keys(),
|
||||
data=[
|
||||
("blocked", [len(u["blocked"]) for u in data.values()]),
|
||||
],
|
||||
sub_data=[
|
||||
("blocked emails", rejects),
|
||||
],
|
||||
earliest=[u["earliest"] for u in data.values()],
|
||||
latest=[u["latest"] for u in data.values()],
|
||||
)
|
||||
|
||||
if collector["other-services"] and VERBOSE and False:
|
||||
print_header("Other services")
|
||||
print("The following unkown services were found in the log file.")
|
||||
print(" ", *sorted(list(collector["other-services"])), sep='\n│ ')
|
||||
|
||||
|
||||
def scan_mail_log_line(line, collector):
|
||||
""" Scan a log line and extract interesting data """
|
||||
|
||||
m = re.match(r"(\w+[\s]+\d+ \d+:\d+:\d+) ([\w]+ )?([\w\-/]+)[^:]*: (.*)", line)
|
||||
|
||||
if not m:
|
||||
return True
|
||||
|
||||
date, system, service, log = m.groups()
|
||||
collector["scan_count"] += 1
|
||||
|
||||
# print()
|
||||
# print("date:", date)
|
||||
# print("host:", system)
|
||||
# print("service:", service)
|
||||
# print("log:", log)
|
||||
|
||||
# Replaced the dateutil parser for a less clever way of parser that is roughly 4 times faster.
|
||||
# date = dateutil.parser.parse(date)
|
||||
date = datetime.datetime.strptime(date, '%b %d %H:%M:%S')
|
||||
date = date.replace(START_DATE.year)
|
||||
|
||||
# Check if the found date is within the time span we are scanning
|
||||
if date > START_DATE:
|
||||
# Don't process, but continue
|
||||
return True
|
||||
elif date < END_DATE:
|
||||
# Don't process, and halt
|
||||
return False
|
||||
|
||||
if service == "postfix/submission/smtpd":
|
||||
if SCAN_OUT:
|
||||
scan_postfix_submission_line(date, log, collector)
|
||||
elif service == "postfix/lmtp":
|
||||
if SCAN_IN:
|
||||
scan_postfix_lmtp_line(date, log, collector)
|
||||
elif service.endswith("-login"):
|
||||
if SCAN_DOVECOT_LOGIN:
|
||||
scan_dovecot_login_line(date, log, collector, service[:4])
|
||||
elif service == "postgrey":
|
||||
if SCAN_GREY:
|
||||
scan_postgrey_line(date, log, collector)
|
||||
elif service == "postfix/smtpd":
|
||||
if SCAN_BLOCKED:
|
||||
scan_postfix_smtpd_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", "anvil"):
|
||||
# nothing to look at
|
||||
return True
|
||||
else:
|
||||
collector["other-services"].add(service)
|
||||
return True
|
||||
|
||||
collector["parse_count"] += 1
|
||||
return True
|
||||
|
||||
def scan_dovecot_line(date, log, collector):
|
||||
m = re.match("imap-login: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", log)
|
||||
if m:
|
||||
login, ip = m.group(1), m.group(2)
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
action, reason, client_name, client_address, sender, user = m.groups()
|
||||
|
||||
if user_match(user):
|
||||
|
||||
# Might be useful to group services that use a lot of mail different servers on sub
|
||||
# domains like <sub>1.domein.com
|
||||
|
||||
# if '.' in client_name:
|
||||
# addr = client_name.split('.')
|
||||
# if len(addr) > 2:
|
||||
# client_name = '.'.join(addr[1:])
|
||||
|
||||
key = (client_address if client_name == 'unknown' else client_name, sender)
|
||||
|
||||
rep = collector["postgrey"].setdefault(user, {})
|
||||
|
||||
if action == "greylist" and reason == "new":
|
||||
collector["postgrey"].setdefault(recipient, {})[key] = (date, None)
|
||||
elif action == "pass" and reason == "triplet found" and key in collector["postgrey"].get(recipient, {}):
|
||||
collector["postgrey"][recipient][key] = (collector["postgrey"][recipient][key][0], date)
|
||||
rep[key] = (date, rep[key][1] if key in rep else None)
|
||||
elif action == "pass":
|
||||
rep[key] = (rep[key][0] if key in rep else None, date)
|
||||
|
||||
|
||||
def scan_postfix_smtpd_line(date, log, collector):
|
||||
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
|
||||
""" Scan a postfix smtpd log line and extract interesting data """
|
||||
|
||||
# skip this, is reported in the greylisting report
|
||||
# Check if the incoming mail was rejected
|
||||
|
||||
m = re.match("NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
|
||||
|
||||
if m:
|
||||
message, sender, user = m.groups()
|
||||
|
||||
# skip this, if reported in the greylisting report
|
||||
if "Recipient address rejected: Greylisted" in message:
|
||||
return
|
||||
|
||||
# only log mail to known recipients
|
||||
if user_match(user):
|
||||
if collector["known_addresses"] is None or user in collector["known_addresses"]:
|
||||
data = collector["rejected"].get(
|
||||
user,
|
||||
{
|
||||
"blocked": [],
|
||||
"earliest": None,
|
||||
"latest": None,
|
||||
}
|
||||
)
|
||||
# simplify this one
|
||||
m = re.search(r"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)", message)
|
||||
m = re.search(
|
||||
r"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)", message
|
||||
)
|
||||
if m:
|
||||
message = "ip blocked: " + m.group(2)
|
||||
|
||||
else:
|
||||
# simplify this one too
|
||||
m = re.search(r"Sender address \[.*@(.*)\] blocked using dbl.spamhaus.org; (.*)", message)
|
||||
m = re.search(
|
||||
r"Sender address \[.*@(.*)\] blocked using dbl.spamhaus.org; (.*)", message
|
||||
)
|
||||
if m:
|
||||
message = "domain blocked: " + m.group(2)
|
||||
|
||||
collector["rejected-mail"].setdefault(recipient, []).append( (date, sender, message) )
|
||||
if data["latest"] is None:
|
||||
data["latest"] = date
|
||||
data["earliest"] = date
|
||||
data["blocked"].append((date, sender, message))
|
||||
|
||||
collector["rejected"][user] = data
|
||||
|
||||
|
||||
def scan_dovecot_login_line(date, log, collector, protocol_name):
|
||||
""" Scan a dovecot login log line and extract interesting data """
|
||||
|
||||
m = re.match("Info: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", log)
|
||||
|
||||
if m:
|
||||
# TODO: CHECK DIT
|
||||
user, host = m.groups()
|
||||
|
||||
if user_match(user):
|
||||
add_login(user, date, protocol_name, host, collector)
|
||||
|
||||
|
||||
def add_login(user, date, protocol_name, host, collector):
|
||||
# Get the user data, or create it if the user is new
|
||||
data = collector["logins"].get(
|
||||
user,
|
||||
{
|
||||
"earliest": None,
|
||||
"latest": None,
|
||||
"totals_by_protocol": defaultdict(int),
|
||||
"totals_by_protocol_and_host": defaultdict(int),
|
||||
"activity-by-hour": defaultdict(lambda : defaultdict(int)),
|
||||
}
|
||||
)
|
||||
|
||||
if data["latest"] is None:
|
||||
data["latest"] = date
|
||||
data["earliest"] = date
|
||||
|
||||
data["totals_by_protocol"][protocol_name] += 1
|
||||
data["totals_by_protocol_and_host"][(protocol_name, host)] += 1
|
||||
|
||||
if host not in ("127.0.0.1", "::1") or True:
|
||||
data["activity-by-hour"][protocol_name][date.hour] += 1
|
||||
|
||||
collector["logins"][user] = data
|
||||
|
||||
|
||||
def scan_postfix_lmtp_line(date, log, collector):
|
||||
""" Scan a postfix lmtp log line and extract interesting data
|
||||
|
||||
It is assumed that every log of postfix/lmtp indicates an email that was successfully
|
||||
received by Postfix.
|
||||
|
||||
"""
|
||||
|
||||
m = re.match("([A-Z0-9]+): to=<(\S+)>, .* Saved", log)
|
||||
|
||||
if m:
|
||||
_, user = m.groups()
|
||||
|
||||
if user_match(user):
|
||||
# Get the user data, or create it if the user is new
|
||||
data = collector["received_mail"].get(
|
||||
user,
|
||||
{
|
||||
"received_count": 0,
|
||||
"earliest": None,
|
||||
"latest": None,
|
||||
"activity-by-hour": defaultdict(int),
|
||||
}
|
||||
)
|
||||
|
||||
data["received_count"] += 1
|
||||
data["activity-by-hour"][date.hour] += 1
|
||||
|
||||
if data["latest"] is None:
|
||||
data["latest"] = date
|
||||
data["earliest"] = date
|
||||
|
||||
collector["received_mail"][user] = data
|
||||
|
||||
|
||||
def scan_postfix_submission_line(date, log, collector):
|
||||
m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=PLAIN, sasl_username=(\S+)", log)
|
||||
""" Scan a postfix submission log line and extract interesting data
|
||||
|
||||
Lines containing a sasl_method with the values PLAIN or LOGIN are assumed to indicate a sent
|
||||
email.
|
||||
|
||||
"""
|
||||
|
||||
# Match both the 'plain' and 'login' sasl methods, since both authentication methods are
|
||||
# allowed by Dovecot
|
||||
m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=(PLAIN|LOGIN), sasl_username=(\S+)", log)
|
||||
|
||||
if m:
|
||||
procid, client, user = m.groups()
|
||||
collector["activity-by-hour"]["smtp-sends"][date.hour] += 1
|
||||
_, client, method, user = m.groups()
|
||||
|
||||
if user_match(user):
|
||||
# Get the user data, or create it if the user is new
|
||||
data = collector["sent_mail"].get(
|
||||
user,
|
||||
{
|
||||
"sent_count": 0,
|
||||
"hosts": set(),
|
||||
"earliest": None,
|
||||
"latest": None,
|
||||
"activity-by-hour": defaultdict(int),
|
||||
}
|
||||
)
|
||||
|
||||
data["sent_count"] += 1
|
||||
data["hosts"].add(client)
|
||||
data["activity-by-hour"][date.hour] += 1
|
||||
|
||||
if data["latest"] is None:
|
||||
data["latest"] = date
|
||||
data["earliest"] = date
|
||||
|
||||
collector["sent_mail"][user] = data
|
||||
|
||||
# Also log this as a login.
|
||||
add_login(user, date, "smtp", client, collector)
|
||||
|
||||
# Utility functions
|
||||
|
||||
def reverse_readline(filename, buf_size=8192):
|
||||
""" A generator that returns the lines of a file in reverse order
|
||||
|
||||
http://stackoverflow.com/a/23646049/801870
|
||||
|
||||
"""
|
||||
|
||||
with open(filename) as fh:
|
||||
segment = None
|
||||
offset = 0
|
||||
fh.seek(0, os.SEEK_END)
|
||||
file_size = remaining_size = fh.tell()
|
||||
while remaining_size > 0:
|
||||
offset = min(file_size, offset + buf_size)
|
||||
fh.seek(file_size - offset)
|
||||
buff = fh.read(min(remaining_size, buf_size))
|
||||
remaining_size -= buf_size
|
||||
lines = buff.split('\n')
|
||||
# the first line of the buffer is probably not a complete line so
|
||||
# we'll save it and append it to the last line of the next buffer
|
||||
# we read
|
||||
if segment is not None:
|
||||
# if the previous chunk starts right from the beginning of line
|
||||
# do not concat the segment to the last line of new chunk
|
||||
# instead, yield the segment first
|
||||
if buff[-1] is not '\n':
|
||||
lines[-1] += segment
|
||||
else:
|
||||
yield segment
|
||||
segment = lines[0]
|
||||
for index in range(len(lines) - 1, 0, -1):
|
||||
if len(lines[index]):
|
||||
yield lines[index]
|
||||
# Don't yield None if the file was empty
|
||||
if segment is not None:
|
||||
yield segment
|
||||
|
||||
|
||||
def user_match(user):
|
||||
""" Check if the given user matches any of the filters """
|
||||
return FILTERS is None or any(u in user for u in FILTERS)
|
||||
|
||||
|
||||
def email_sort(email):
|
||||
""" Split the given email address into a reverse order tuple, for sorting i.e (domain, name) """
|
||||
return tuple(reversed(email[0].split('@')))
|
||||
|
||||
|
||||
def valid_date(string):
|
||||
""" Validate the given date string fetched from the --startdate argument """
|
||||
try:
|
||||
date = dateutil.parser.parse(string)
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError("Unrecognized date and/or time '%s'" % string)
|
||||
return date
|
||||
|
||||
|
||||
# Print functions
|
||||
|
||||
def print_time_table(labels, data, do_print=True):
|
||||
labels.insert(0, "hour")
|
||||
data.insert(0, [str(h) for h in range(24)])
|
||||
|
||||
temp = "│ {:<%d} " % max(len(l) for l in labels)
|
||||
lines = []
|
||||
|
||||
for label in labels:
|
||||
lines.append(temp.format(label))
|
||||
|
||||
for h in range(24):
|
||||
max_len = max(len(str(d[h])) for d in data)
|
||||
base = "{:>%d} " % max(2, max_len)
|
||||
|
||||
for i, d in enumerate(data):
|
||||
lines[i] += base.format(d[h])
|
||||
|
||||
lines.insert(0, "┬ totals by time of day:")
|
||||
lines.append("└" + (len(lines[-1]) - 2) * "─")
|
||||
|
||||
if do_print:
|
||||
print("\n".join(lines))
|
||||
else:
|
||||
return lines
|
||||
|
||||
|
||||
def print_user_table(users, data=None, sub_data=None, activity=None, latest=None, earliest=None,
|
||||
delimit=False, numstr=str):
|
||||
str_temp = "{:<32} "
|
||||
lines = []
|
||||
data = data or []
|
||||
|
||||
col_widths = len(data) * [0]
|
||||
col_left = len(data) * [False]
|
||||
vert_pos = 0
|
||||
|
||||
do_accum = all(isinstance(n, (int, float)) for _, d in data for n in d)
|
||||
data_accum = len(data) * ([0] if do_accum else [" "])
|
||||
|
||||
last_user = None
|
||||
|
||||
for row, user in enumerate(users):
|
||||
|
||||
if delimit:
|
||||
if last_user and last_user != user:
|
||||
lines.append(len(lines[-1]) * "…")
|
||||
last_user = user
|
||||
|
||||
line = "{:<32} ".format(user[:31] + "…" if len(user) > 32 else user)
|
||||
|
||||
for col, (l, d) in enumerate(data):
|
||||
if isinstance(d[row], str):
|
||||
col_str = str_temp.format(d[row][:31] + "…" if len(d[row]) > 32 else d[row])
|
||||
col_left[col] = True
|
||||
elif isinstance(d[row], datetime.datetime):
|
||||
col_str = "{:<20}".format(str(d[row]))
|
||||
col_left[col] = True
|
||||
else:
|
||||
temp = "{:>%s}" % max(5, len(l) + 1, len(str(d[row])) + 1)
|
||||
col_str = temp.format(str(d[row]))
|
||||
col_widths[col] = max(col_widths[col], len(col_str))
|
||||
line += col_str
|
||||
|
||||
if do_accum:
|
||||
data_accum[col] += d[row]
|
||||
|
||||
try:
|
||||
if None not in [latest, earliest]:
|
||||
vert_pos = len(line)
|
||||
e = earliest[row]
|
||||
l = latest[row]
|
||||
timespan = relativedelta(l, e)
|
||||
if timespan.months:
|
||||
temp = " │ {:0.1f} months"
|
||||
line += temp.format(timespan.months + timespan.days / 30.0)
|
||||
elif timespan.days:
|
||||
temp = " │ {:0.1f} days"
|
||||
line += temp.format(timespan.days + timespan.hours / 24.0)
|
||||
elif (e.hour, e.minute) == (l.hour, l.minute):
|
||||
temp = " │ {:%H:%M}"
|
||||
line += temp.format(e)
|
||||
else:
|
||||
temp = " │ {:%H:%M} - {:%H:%M}"
|
||||
line += temp.format(e, l)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
lines.append(line.rstrip())
|
||||
|
||||
try:
|
||||
if VERBOSE:
|
||||
if sub_data is not None:
|
||||
for l, d in sub_data:
|
||||
if d[row]:
|
||||
lines.append("┬")
|
||||
lines.append("│ %s" % l)
|
||||
lines.append("├─%s─" % (len(l) * "─"))
|
||||
lines.append("│")
|
||||
max_len = 0
|
||||
for v in list(d[row]):
|
||||
lines.append("│ %s" % v)
|
||||
max_len = max(max_len, len(v))
|
||||
lines.append("└" + (max_len + 1) * "─")
|
||||
|
||||
if activity is not None:
|
||||
lines.extend(print_time_table(
|
||||
[label for label, _ in activity],
|
||||
[data[row] for _, data in activity],
|
||||
do_print=False
|
||||
))
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
header = str_temp.format("")
|
||||
|
||||
for col, (l, _) in enumerate(data):
|
||||
if col_left[col]:
|
||||
header += l.ljust(max(5, len(l) + 1, col_widths[col]))
|
||||
else:
|
||||
header += l.rjust(max(5, len(l) + 1, col_widths[col]))
|
||||
|
||||
if None not in (latest, earliest):
|
||||
header += " │ timespan "
|
||||
|
||||
lines.insert(0, header.rstrip())
|
||||
|
||||
table_width = max(len(l) for l in lines)
|
||||
t_line = table_width * "─"
|
||||
b_line = table_width * "─"
|
||||
|
||||
if vert_pos:
|
||||
t_line = t_line[:vert_pos + 1] + "┼" + t_line[vert_pos + 2:]
|
||||
b_line = b_line[:vert_pos + 1] + ("┬" if VERBOSE else "┼") + b_line[vert_pos + 2:]
|
||||
|
||||
lines.insert(1, t_line)
|
||||
lines.append(b_line)
|
||||
|
||||
# Print totals
|
||||
|
||||
data_accum = [numstr(a) for a in data_accum]
|
||||
footer = str_temp.format("Totals:" if do_accum else " ")
|
||||
for row, (l, _) in enumerate(data):
|
||||
temp = "{:>%d}" % max(5, len(l) + 1)
|
||||
footer += temp.format(data_accum[row])
|
||||
|
||||
try:
|
||||
if None not in [latest, earliest]:
|
||||
max_l = max(latest)
|
||||
min_e = min(earliest)
|
||||
timespan = relativedelta(max_l, min_e)
|
||||
if timespan.days:
|
||||
temp = " │ {:0.2f} days"
|
||||
footer += temp.format(timespan.days + timespan.hours / 24.0)
|
||||
elif (min_e.hour, min_e.minute) == (max_l.hour, max_l.minute):
|
||||
temp = " │ {:%H:%M}"
|
||||
footer += temp.format(min_e)
|
||||
else:
|
||||
temp = " │ {:%H:%M} - {:%H:%M}"
|
||||
footer += temp.format(min_e, max_l)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
lines.append(footer)
|
||||
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
def print_header(msg):
|
||||
print('\n' + msg)
|
||||
print("═" * len(msg), '\n')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from status_checks import ConsoleOutput
|
||||
env = utils.load_environment()
|
||||
scan_mail_log(ConsoleOutput(), env)
|
||||
try:
|
||||
env_vars = utils.load_environment()
|
||||
except FileNotFoundError:
|
||||
env_vars = {}
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Scan the mail log files for interesting data. By default, this script "
|
||||
"shows today's incoming and outgoing mail statistics. This script was ("
|
||||
"re)written for the Mail-in-a-box email server."
|
||||
"https://github.com/mail-in-a-box/mailinabox",
|
||||
add_help=False
|
||||
)
|
||||
|
||||
# Switches to determine what to parse and what to ignore
|
||||
|
||||
parser.add_argument("-r", "--received", help="Scan for received emails.",
|
||||
action="store_true")
|
||||
parser.add_argument("-s", "--sent", help="Scan for sent emails.",
|
||||
action="store_true")
|
||||
parser.add_argument("-l", "--logins", help="Scan for user logins to IMAP/POP3.",
|
||||
action="store_true")
|
||||
parser.add_argument("-g", "--grey", help="Scan for greylisted emails.",
|
||||
action="store_true")
|
||||
parser.add_argument("-b", "--blocked", help="Scan for blocked emails.",
|
||||
action="store_true")
|
||||
|
||||
parser.add_argument("-t", "--timespan", choices=TIME_DELTAS.keys(), default='today',
|
||||
metavar='<time span>',
|
||||
help="Time span to scan, going back from the start date. Possible values: "
|
||||
"{}. Defaults to 'today'.".format(", ".join(list(TIME_DELTAS.keys()))))
|
||||
parser.add_argument("-d", "--startdate", action="store", dest="startdate",
|
||||
type=valid_date, metavar='<start date>',
|
||||
help="Date and time to start scanning the log file from. If no date is "
|
||||
"provided, scanning will start from the current date and time.")
|
||||
parser.add_argument("-u", "--users", action="store", dest="users",
|
||||
metavar='<email1,email2,email...>',
|
||||
help="Comma separated list of (partial) email addresses to filter the "
|
||||
"output with.")
|
||||
|
||||
parser.add_argument('-h', '--help', action='help', help="Print this message and exit.")
|
||||
parser.add_argument("-v", "--verbose", help="Output extra data where available.",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.startdate is not None:
|
||||
START_DATE = args.startdate
|
||||
if args.timespan == 'today':
|
||||
args.timespan = 'day'
|
||||
print("Setting start date to {}".format(START_DATE))
|
||||
|
||||
END_DATE = START_DATE - TIME_DELTAS[args.timespan]
|
||||
|
||||
VERBOSE = args.verbose
|
||||
|
||||
if args.received or args.sent or args.logins or args.grey or args.blocked:
|
||||
SCAN_IN = args.received
|
||||
if not SCAN_IN:
|
||||
print("Ignoring received emails")
|
||||
|
||||
SCAN_OUT = args.sent
|
||||
if not SCAN_OUT:
|
||||
print("Ignoring sent emails")
|
||||
|
||||
SCAN_DOVECOT_LOGIN = args.logins
|
||||
if not SCAN_DOVECOT_LOGIN:
|
||||
print("Ignoring logins")
|
||||
|
||||
SCAN_GREY = args.grey
|
||||
if SCAN_GREY:
|
||||
print("Showing greylisted emails")
|
||||
|
||||
SCAN_BLOCKED = args.blocked
|
||||
if SCAN_BLOCKED:
|
||||
print("Showing blocked emails")
|
||||
|
||||
if args.users is not None:
|
||||
FILTERS = args.users.strip().split(',')
|
||||
|
||||
scan_mail_log(env_vars)
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
|
||||
# NOTE:
|
||||
# This script is run both using the system-wide Python 3
|
||||
# interpreter (/usr/bin/python3) as well as through the
|
||||
# virtualenv (/usr/local/lib/mailinabox/env). So only
|
||||
# import packages at the top level of this script that
|
||||
# are installed in *both* contexts. We use the system-wide
|
||||
# Python 3 in setup/questions.sh to validate the email
|
||||
# address entered by the user.
|
||||
|
||||
import subprocess, shutil, os, sqlite3, re
|
||||
import utils
|
||||
@@ -435,9 +444,11 @@ def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exist
|
||||
email = email.strip()
|
||||
if email == "": continue
|
||||
email = sanitize_idn_email_address(email) # Unicode => IDNA
|
||||
# Strip any +tag from email alias and check privileges
|
||||
privileged_email = re.sub(r"(?=\+)[^@]*(?=@)",'',email)
|
||||
if not validate_email(email):
|
||||
return ("Invalid receiver email address (%s)." % email, 400)
|
||||
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(email, env, empty_on_error=True):
|
||||
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(privileged_email, env, empty_on_error=True):
|
||||
# Make domain control validation hijacking a little harder to mess up by
|
||||
# requiring aliases for email addresses typically used in DCV to forward
|
||||
# only to accounts that are administrators on this system.
|
||||
@@ -599,8 +610,8 @@ def validate_password(pw):
|
||||
raise ValueError("No password provided.")
|
||||
if re.search(r"[\s]", pw):
|
||||
raise ValueError("Passwords cannot contain spaces.")
|
||||
if len(pw) < 4:
|
||||
raise ValueError("Passwords must be at least four characters.")
|
||||
if len(pw) < 8:
|
||||
raise ValueError("Passwords must be at least eight characters.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
# Utilities for installing and selecting SSL certificates.
|
||||
|
||||
import os, os.path, re, shutil
|
||||
import os, os.path, re, shutil, subprocess, tempfile
|
||||
|
||||
from utils import shell, safe_domain_name, sort_domains
|
||||
|
||||
import idna
|
||||
|
||||
# SELECTING SSL CERTIFICATES FOR USE IN WEB
|
||||
@@ -25,6 +24,16 @@ def get_ssl_certificates(env):
|
||||
if not os.path.exists(ssl_root):
|
||||
return
|
||||
for fn in os.listdir(ssl_root):
|
||||
if fn == 'ssl_certificate.pem':
|
||||
# This is always a symbolic link
|
||||
# to the certificate to use for
|
||||
# PRIMARY_HOSTNAME. Don't let it
|
||||
# be eligible for use because we
|
||||
# could end up creating a symlink
|
||||
# to itself --- we want to find
|
||||
# the cert that it should be a
|
||||
# symlink to.
|
||||
continue
|
||||
fn = os.path.join(ssl_root, fn)
|
||||
if os.path.isfile(fn):
|
||||
yield fn
|
||||
@@ -75,6 +84,12 @@ def get_ssl_certificates(env):
|
||||
|
||||
# Add this cert to the list of certs usable for the domains.
|
||||
for domain in cert_domains:
|
||||
# The primary hostname can only use a certificate mapped
|
||||
# to the system private key.
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
if cert._private_key._filename != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'):
|
||||
continue
|
||||
|
||||
domains.setdefault(domain, []).append(cert)
|
||||
|
||||
# Sort the certificates to prefer good ones.
|
||||
@@ -82,6 +97,7 @@ def get_ssl_certificates(env):
|
||||
now = datetime.datetime.utcnow()
|
||||
ret = { }
|
||||
for domain, cert_list in domains.items():
|
||||
#for c in cert_list: print(domain, c.not_valid_before, c.not_valid_after, "("+str(now)+")", c.issuer, c.subject, c._filename)
|
||||
cert_list.sort(key = lambda cert : (
|
||||
# must be valid NOW
|
||||
cert.not_valid_before <= now <= cert.not_valid_after,
|
||||
@@ -125,7 +141,8 @@ def get_ssl_certificates(env):
|
||||
|
||||
return ret
|
||||
|
||||
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False, raw=False):
|
||||
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False, use_main_cert=True):
|
||||
if use_main_cert or not allow_missing_cert:
|
||||
# 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'))
|
||||
@@ -136,6 +153,7 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False
|
||||
"certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]),
|
||||
}
|
||||
|
||||
if use_main_cert:
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
# The primary domain must use the server certificate because
|
||||
# it is hard-coded in some service configuration files.
|
||||
@@ -156,127 +174,97 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False
|
||||
|
||||
# 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.
|
||||
def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True):
|
||||
# Get a set of domain names that we can provision certificates for
|
||||
# using certbot. We start with domains that the box is serving web
|
||||
# for and subtract:
|
||||
# * domains not in limit_domains if limit_domains is not empty
|
||||
# * domains with custom "A" records, i.e. they are hosted elsewhere
|
||||
# * domains with actual "A" records that point elsewhere
|
||||
# * domains that already have certificates that will be valid for a while
|
||||
|
||||
from web_update import get_web_domains
|
||||
from status_checks import query_dns, normalize_ip
|
||||
|
||||
import datetime
|
||||
now = datetime.datetime.utcnow()
|
||||
existing_certs = get_ssl_certificates(env)
|
||||
|
||||
# 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)
|
||||
plausible_web_domains = get_web_domains(env, exclude_dns_elsewhere=False)
|
||||
actual_web_domains = get_web_domains(env)
|
||||
|
||||
domains_to_provision = set()
|
||||
domains_cant_provision = { }
|
||||
|
||||
for domain in plausible_web_domains:
|
||||
# Skip domains that the user doesn't want to provision now.
|
||||
if limit_domains and domain not in limit_domains:
|
||||
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)
|
||||
# Check that there isn't an explicit A/AAAA record.
|
||||
if domain not in actual_web_domains:
|
||||
domains_cant_provision[domain] = "The domain has a custom DNS A/AAAA record that points the domain 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)."
|
||||
|
||||
# Check that the DNS resolves to here.
|
||||
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
|
||||
bad_dns = []
|
||||
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
|
||||
if len(response) != 1 or 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(str(r) for r in response))
|
||||
return False
|
||||
response = query_dns(domain, rtype)
|
||||
if response != normalize_ip(value):
|
||||
bad_dns.append("%s (%s)" % (response, rtype))
|
||||
|
||||
return True
|
||||
if bad_dns:
|
||||
domains_cant_provision[domain] = "The domain name does not resolve to this machine: " \
|
||||
+ (", ".join(bad_dns)) \
|
||||
+ "."
|
||||
|
||||
domains = set(filter(can_provision_for_domain, domains))
|
||||
else:
|
||||
# DNS is all good.
|
||||
|
||||
# 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))
|
||||
# Check for a good existing cert.
|
||||
existing_cert = get_domain_ssl_files(domain, existing_certs, env, use_main_cert=False, allow_missing_cert=True)
|
||||
if existing_cert:
|
||||
existing_cert_check = check_certificate(domain, existing_cert['certificate'], existing_cert['private-key'],
|
||||
warn_if_expiring_soon=14)
|
||||
if existing_cert_check[0] == "OK":
|
||||
if show_valid_certs:
|
||||
domains_cant_provision[domain] = "The domain has a valid certificate already. ({} Certificate: {}, private key {})".format(
|
||||
existing_cert_check[1],
|
||||
existing_cert['certificate'],
|
||||
existing_cert['private-key'])
|
||||
continue
|
||||
|
||||
return (domains, problems)
|
||||
domains_to_provision.add(domain)
|
||||
|
||||
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
|
||||
return (domains_to_provision, domains_cant_provision)
|
||||
|
||||
def provision_certificates(env, limit_domains):
|
||||
# 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)
|
||||
domains, domains_cant_provision = get_certificates_to_provision(env, limit_domains=limit_domains)
|
||||
|
||||
# Build a list of what happened on each domain or domain-set.
|
||||
ret = []
|
||||
for domain, error in domains_cant_provision.items():
|
||||
ret.append({
|
||||
"domains": [domain],
|
||||
"log": [error],
|
||||
"result": "skipped",
|
||||
})
|
||||
|
||||
# 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.
|
||||
max_domains_per_group = 100
|
||||
domains = sort_domains(domains, env)
|
||||
certs = []
|
||||
while len(domains) > 0:
|
||||
certs.append( domains[0:100] )
|
||||
domains = domains[100:]
|
||||
certs.append( domains[:max_domains_per_group] )
|
||||
domains = domains[max_domains_per_group:]
|
||||
|
||||
# Prepare to provision.
|
||||
|
||||
@@ -285,259 +273,116 @@ def provision_certificates(env, agree_to_tos_url=None, logger=None, show_extende
|
||||
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 = {
|
||||
ret.append({
|
||||
"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)
|
||||
# Create a CSR file for our master private key so that certbot
|
||||
# uses our private key.
|
||||
key_file = os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem')
|
||||
with tempfile.NamedTemporaryFile() as csr_file:
|
||||
# We could use openssl, but certbot requires
|
||||
# that the CN domain and SAN domains match
|
||||
# the domain list passed to certbot, and adding
|
||||
# SAN domains openssl req is ridiculously complicated.
|
||||
# subprocess.check_output([
|
||||
# "openssl", "req", "-new",
|
||||
# "-key", key_file,
|
||||
# "-out", csr_file.name,
|
||||
# "-subj", "/CN=" + domain_list[0],
|
||||
# "-sha256" ])
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.x509.oid import NameOID
|
||||
builder = x509.CertificateSigningRequestBuilder()
|
||||
builder = builder.subject_name(x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, domain_list[0]) ]))
|
||||
builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
||||
builder = builder.add_extension(x509.SubjectAlternativeName(
|
||||
[x509.DNSName(d) for d in domain_list]
|
||||
), critical=False)
|
||||
request = builder.sign(load_pem(load_cert_chain(key_file)[0]), hashes.SHA256(), default_backend())
|
||||
with open(csr_file.name, "wb") as f:
|
||||
f.write(request.public_bytes(Encoding.PEM))
|
||||
|
||||
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))
|
||||
# Provision, writing to a temporary file.
|
||||
webroot = os.path.join(account_path, 'webroot')
|
||||
os.makedirs(webroot, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
cert_file = os.path.join(d, 'cert_and_chain.pem')
|
||||
print("Provisioning TLS certificates for " + ", ".join(domain_list) + ".")
|
||||
certbotret = subprocess.check_output([
|
||||
"certbot",
|
||||
"certonly",
|
||||
#"-v", # just enough to see ACME errors
|
||||
"--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup
|
||||
|
||||
# Try to provision now that the challenge files are installed.
|
||||
"-d", ",".join(domain_list), # first will be main domain
|
||||
|
||||
cert = client.issue_certificate(
|
||||
domain_list,
|
||||
account_path,
|
||||
private_key=private_key,
|
||||
logger=my_logger)
|
||||
"--csr", csr_file.name, # use our private key; unfortunately this doesn't work with auto-renew so we need to save cert manually
|
||||
"--cert-path", os.path.join(d, 'cert'), # we only use the full chain
|
||||
"--chain-path", os.path.join(d, 'chain'), # we only use the full chain
|
||||
"--fullchain-path", cert_file,
|
||||
|
||||
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,
|
||||
})
|
||||
"--webroot", "--webroot-path", webroot,
|
||||
|
||||
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()
|
||||
})
|
||||
"--config-dir", account_path,
|
||||
#"--staging",
|
||||
], stderr=subprocess.STDOUT).decode("utf8")
|
||||
install_cert_copy_file(cert_file, env)
|
||||
|
||||
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 + ".",
|
||||
})
|
||||
ret[-1]["log"].append(certbotret)
|
||||
ret[-1]["result"] = "installed"
|
||||
except subprocess.CalledProcessError as e:
|
||||
ret[-1]["log"].append(e.output.decode("utf8"))
|
||||
ret[-1]["result"] = "error"
|
||||
except Exception as e:
|
||||
ret[-1]["log"].append(str(e))
|
||||
ret[-1]["result"] = "error"
|
||||
|
||||
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",
|
||||
})
|
||||
# Run post-install steps.
|
||||
ret.extend(post_install_func(env))
|
||||
|
||||
# Return what happened with each certificate request.
|
||||
return {
|
||||
"requests": ret,
|
||||
"problems": problems,
|
||||
}
|
||||
return ret
|
||||
|
||||
def provision_certificates_cmdline():
|
||||
import sys
|
||||
from utils import load_environment, exclusive_process
|
||||
from exclusiveprocess import Lock
|
||||
|
||||
exclusive_process("update_tls_certificates")
|
||||
from utils import load_environment
|
||||
|
||||
Lock(die=True).forever()
|
||||
env = load_environment()
|
||||
|
||||
verbose = False
|
||||
headless = False
|
||||
force_domains = None
|
||||
show_extended_problems = True
|
||||
quiet = False
|
||||
domains = []
|
||||
|
||||
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)
|
||||
for arg in sys.argv[1:]:
|
||||
if arg == "-q":
|
||||
quiet = True
|
||||
else:
|
||||
force_domains = args
|
||||
domains.append(arg)
|
||||
|
||||
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
|
||||
# Go.
|
||||
status = provision_certificates(env, limit_domains=domains)
|
||||
|
||||
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:
|
||||
# Show what happened.
|
||||
for request in status:
|
||||
if isinstance(request, str):
|
||||
print(request)
|
||||
else:
|
||||
if quiet and request['result'] == 'skipped':
|
||||
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(request['result'] + ":", ", ".join(request['domains']) + ":")
|
||||
for line in request["log"]:
|
||||
print(line)
|
||||
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
|
||||
|
||||
@@ -546,7 +391,7 @@ def create_csr(domain, ssl_key, country_code, env):
|
||||
"openssl", "req", "-new",
|
||||
"-key", ssl_key,
|
||||
"-sha256",
|
||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)])
|
||||
"-subj", "/C=%s/CN=%s" % (country_code, domain)])
|
||||
|
||||
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.
|
||||
@@ -567,6 +412,16 @@ def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
|
||||
cert_status += " " + cert_status_details
|
||||
return cert_status
|
||||
|
||||
# Copy certifiate into ssl directory.
|
||||
install_cert_copy_file(fn, env)
|
||||
|
||||
# Run post-install steps.
|
||||
ret = post_install_func(env)
|
||||
if raw: return ret
|
||||
return "\n".join(ret)
|
||||
|
||||
|
||||
def install_cert_copy_file(fn, env):
|
||||
# Where to put it?
|
||||
# Make a unique path for the certificate.
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
@@ -584,14 +439,26 @@ def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
|
||||
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
|
||||
shutil.move(fn, ssl_certificate)
|
||||
|
||||
ret = ["OK"]
|
||||
|
||||
# When updating the cert for PRIMARY_HOSTNAME, symlink it from the system
|
||||
def post_install_func(env):
|
||||
ret = []
|
||||
|
||||
# Get the certificate to use for PRIMARY_HOSTNAME.
|
||||
ssl_certificates = get_ssl_certificates(env)
|
||||
cert = get_domain_ssl_files(env['PRIMARY_HOSTNAME'], ssl_certificates, env, use_main_cert=False)
|
||||
if not cert:
|
||||
# Ruh-row, we don't have any certificate usable
|
||||
# for the primary hostname.
|
||||
ret.append("there is no valid certificate for " + env['PRIMARY_HOSTNAME'])
|
||||
|
||||
# Symlink the best cert for PRIMARY_HOSTNAME to the system
|
||||
# certificate path, which is hard-coded for various purposes, and then
|
||||
# restart postfix and dovecot.
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
# Update symlink.
|
||||
system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
|
||||
if cert and os.readlink(system_ssl_certificate) != cert['certificate']:
|
||||
# Update symlink.
|
||||
ret.append("updating primary certificate")
|
||||
ssl_certificate = cert['certificate']
|
||||
os.unlink(system_ssl_certificate)
|
||||
os.symlink(ssl_certificate, system_ssl_certificate)
|
||||
|
||||
@@ -607,12 +474,12 @@ def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
|
||||
# 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)
|
||||
|
||||
return 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):
|
||||
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=10, rounded_time=False, just_check_domain=False):
|
||||
# Check that the ssl_certificate & ssl_private_key files are good
|
||||
# for the provided domain.
|
||||
|
||||
@@ -718,7 +585,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
||||
# We'll renew it with Lets Encrypt.
|
||||
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
|
||||
|
||||
if ndays <= 10 and warn_if_expiring_soon:
|
||||
if warn_if_expiring_soon and ndays <= 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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/local/lib/mailinabox/env/bin/python
|
||||
#
|
||||
# Checks that the upstream DNS has been set correctly and that
|
||||
# TLS certificates have been signed, etc., and if not tells the user
|
||||
@@ -11,13 +11,36 @@ 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 dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns, get_custom_dns_records
|
||||
from web_update import get_web_domains, get_domains_with_a_records
|
||||
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
||||
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")
|
||||
@@ -61,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": "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, },
|
||||
]
|
||||
|
||||
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
|
||||
@@ -169,6 +168,37 @@ def run_system_checks(rounded_values, 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
|
||||
# package may not be installed so check that before trying to access
|
||||
@@ -210,15 +240,15 @@ 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):
|
||||
@@ -240,6 +270,8 @@ def run_network_checks(env, output):
|
||||
|
||||
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.
|
||||
@@ -361,7 +393,7 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
||||
|
||||
# Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP[V6] in public DNS.
|
||||
ipv6 = query_dns(domain, "AAAA") if env.get("PUBLIC_IPV6") else None
|
||||
if ip == env['PUBLIC_IP'] and ipv6 in (None, env['PUBLIC_IPV6']):
|
||||
if ip == env['PUBLIC_IP'] and not (ipv6 and env['PUBLIC_IPV6'] and ipv6 != normalize_ip(env['PUBLIC_IPV6'])):
|
||||
output.print_ok("Domain resolves to box's IP address. [%s ↦ %s]" % (env['PRIMARY_HOSTNAME'], my_ips))
|
||||
else:
|
||||
output.print_error("""This domain must resolve to your box's IP address (%s) in public DNS but it currently resolves
|
||||
@@ -427,7 +459,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
# half working.)
|
||||
|
||||
custom_dns_records = list(get_custom_dns_config(env)) # generator => list so we can reuse it
|
||||
correct_ip = get_custom_dns_record(custom_dns_records, domain, "A") or env['PUBLIC_IP']
|
||||
correct_ip = "; ".join(sorted(get_custom_dns_records(custom_dns_records, domain, "A"))) or env['PUBLIC_IP']
|
||||
custom_secondary_ns = get_secondary_dns(custom_dns_records, mode="NS")
|
||||
secondary_ns = custom_secondary_ns or ["ns2." + env['PRIMARY_HOSTNAME']]
|
||||
|
||||
@@ -608,7 +640,7 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output):
|
||||
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:
|
||||
if value == normalize_ip(expected):
|
||||
ok_values.append(value)
|
||||
else:
|
||||
output.print_error("""This domain should resolve to your box's IP address (%s %s) if you would like the box to serve
|
||||
@@ -655,6 +687,13 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None):
|
||||
except dns.exception.Timeout:
|
||||
return "[timeout]"
|
||||
|
||||
# Normalize IP addresses. IP address --- especially IPv6 addresses --- can
|
||||
# be expressed in equivalent string forms. Canonicalize the form before
|
||||
# returning them. The caller should normalize any IP addresses the result
|
||||
# of this method is compared with.
|
||||
if rtype in ("A", "AAAA"):
|
||||
response = [normalize_ip(str(r)) for r in response]
|
||||
|
||||
# There may be multiple answers; concatenate the response. Remove trailing
|
||||
# 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
|
||||
@@ -745,8 +784,13 @@ def what_version_is_this(env):
|
||||
def get_latest_miab_version():
|
||||
# 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/setup.sh?ping=1").read()).group(1).decode("utf8")
|
||||
from urllib.request import urlopen, HTTPError, URLError
|
||||
from socket import timeout
|
||||
|
||||
try:
|
||||
return re.search(b'TAG=(.*)', urlopen("https://mailinabox.email/setup.sh?ping=1", timeout=5).read()).group(1).decode("utf8")
|
||||
except (HTTPError, URLError, timeout):
|
||||
return None
|
||||
|
||||
def check_miab_version(env, output):
|
||||
config = load_settings(env)
|
||||
@@ -763,6 +807,8 @@ def check_miab_version(env, output):
|
||||
|
||||
if this_ver == latest_ver:
|
||||
output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver)
|
||||
elif latest_ver is None:
|
||||
output.print_error("Latest Mail-in-a-Box version could not be determined. 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))
|
||||
@@ -835,6 +881,16 @@ def run_and_output_changes(env, pool):
|
||||
with open(cache_fn, "w") as f:
|
||||
json.dump(cur.buf, f, indent=True)
|
||||
|
||||
def normalize_ip(ip):
|
||||
# Use ipaddress module to normalize the IPv6 notation and
|
||||
# ensure we are matching IPv6 addresses written in different
|
||||
# representations according to rfc5952.
|
||||
import ipaddress
|
||||
try:
|
||||
return str(ipaddress.ip_address(ip))
|
||||
except:
|
||||
return ip
|
||||
|
||||
class FileOutput:
|
||||
def __init__(self, buf, width):
|
||||
self.buf = buf
|
||||
|
||||
@@ -106,6 +106,41 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Mail aliases API (advanced)</h3>
|
||||
|
||||
<p>Use your box’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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -31,12 +31,15 @@
|
||||
<label for="customdnsType" class="col-sm-1 control-label">Type</label>
|
||||
<div class="col-sm-10">
|
||||
<select id="customdnsType" class="form-control" style="max-width: 400px" onchange="show_customdns_rtype_hint()">
|
||||
<option value="A" data-hint="Enter an IPv4 address (i.e. a dotted quad, such as 123.456.789.012).">A (IPv4 address)</option>
|
||||
<option value="AAAA" data-hint="Enter an IPv6 address.">AAAA (IPv6 address)</option>
|
||||
<option value="A" data-hint="Enter an IPv4 address (i.e. a dotted quad, such as 123.456.789.012). The 'local' alias sets the record to this box's public IPv4 address.">A (IPv4 address)</option>
|
||||
<option value="AAAA" data-hint="Enter an IPv6 address. The 'local' alias sets the record to this box's public IPv6 address.">AAAA (IPv6 address)</option>
|
||||
<option value="CAA" data-hint="Enter a CA that can issue certificates for this domain in the form of FLAG TAG VALUE. (0 issuewild "letsencrypt.org")">CAA (Certificate Authority Authorization)</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="SRV" data-hint="Enter record in the form of PRIORIY WEIGHT PORT TARGET., including trailing period (e.g. 10 10 5060 sip.example.com.).">SRV (service record)</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>
|
||||
<option value="SSHFP" data-hint="Enter record in the form of ALGORITHM TYPE FINGERPRINT.">SSHFP (SSH fingerprint record)</option>
|
||||
<option value="NS" data-hint="Enter a hostname to which this subdomain should be delegated to">NS (DNS subdomain delegation)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,10 +69,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 “slave”) 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">
|
||||
@@ -124,7 +127,7 @@
|
||||
<tr><td>email</td> <td>The email address of any administrative user here.</td></tr>
|
||||
<tr><td>password</td> <td>That user’s password.</td></tr>
|
||||
<tr><td>qname</td> <td>The fully qualified domain name for the record you are trying to set. It must be one of the domain names or a subdomain of one of the domain names hosted on this box. (Add mail users or aliases to add new domains.)</td></tr>
|
||||
<tr><td>rtype</td> <td>The resource type. Defaults to <code>A</code> if omitted. Possible values: <code>A</code> (an IPv4 address), <code>AAAA</code> (an IPv6 address), <code>TXT</code> (a text string), <code>CNAME</code> (an alias, which is a fully qualified domain name — don’t forget the final period), <code>MX</code>, or <code>SRV</code>.</td></tr>
|
||||
<tr><td>rtype</td> <td>The resource type. Defaults to <code>A</code> if omitted. Possible values: <code>A</code> (an IPv4 address), <code>AAAA</code> (an IPv6 address), <code>TXT</code> (a text string), <code>CNAME</code> (an alias, which is a fully qualified domain name — don’t forget the final period), <code>MX</code>, <code>SRV</code>, <code>SSHFP</code>, <code>CAA</code> or <code>NS</code>.</td></tr>
|
||||
<tr><td>value</td> <td>For PUT, POST, and DELETE, the record’s value. If the <code>rtype</code> is <code>A</code> or <code>AAAA</code> and <code>value</code> is empty or omitted, the IPv4 or IPv6 address of the remote host is used (be sure to use the <code>-4</code> or <code>-6</code> options to curl). This is handy for dynamic DNS!</td></tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
@@ -63,7 +63,7 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap-theme.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<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">
|
||||
@@ -108,7 +108,7 @@
|
||||
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
|
||||
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out</a></li>
|
||||
</ul>
|
||||
</div><!--/.navbar-collapse -->
|
||||
</div>
|
||||
@@ -191,8 +191,8 @@
|
||||
</div>
|
||||
</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.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
|
||||
<script src="/admin/assets/jquery.min.js"></script>
|
||||
<script src="/admin/assets/bootstrap/js/bootstrap.min.js"></script>
|
||||
|
||||
<script>
|
||||
var global_modal_state = null;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<h4>Exchange/ActiveSync settings</h4>
|
||||
|
||||
<p>On iOS devices, devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, or using Outlook 2007 or later on Windows 7 and later, you may set up your mail as an Exchange or ActiveSync server. However, we’ve found this to be more buggy than using IMAP as described above. If you encounter any problems, please use the manual settings above.</p>
|
||||
<p>On iOS devices, devices on this <a href="https://wiki.z-hub.io/display/ZP/Compatibility">compatibility list</a>, or using Outlook 2007 or later on Windows 7 and later, you may set up your mail as an Exchange or ActiveSync server. However, we’ve found this to be more buggy than using IMAP as described above. If you encounter any problems, please use the manual settings above.</p>
|
||||
|
||||
<table class="table">
|
||||
<tr><th>Server</th> <td>{{hostname}}</td></tr>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p>You need a TLS certificate for this box’s hostname ({{hostname}}) and every other domain name and subdomain that this box is hosting a website for (see the list below).</p>
|
||||
|
||||
<div id="ssl_provision">
|
||||
<h3>Provision a Certificate</h3>
|
||||
<h3>Provision certificates</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>
|
||||
@@ -19,24 +19,9 @@
|
||||
<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>
|
||||
<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’s Encrypt</a> 14 days prior to expiration.</p>
|
||||
|
||||
@@ -53,9 +38,9 @@
|
||||
</table>
|
||||
|
||||
|
||||
<h3 id="ssl_install_header">Install Certificate</h3>
|
||||
<h3 id="ssl_install_header">Install certificate</h3>
|
||||
|
||||
<p>There are many other places where you can get a free or cheap certificate. If you don't want to use our automatic Let's Encrypt integration, you can give <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a>, <a href="https://buy.wosign.com/free/">WoSign’s free TLS</a></a> or any other certificate provider a try.</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 a certificate for?</p>
|
||||
|
||||
@@ -103,24 +88,12 @@ function show_tls(keep_provisioning_shown) {
|
||||
// provisioning status
|
||||
|
||||
if (!keep_provisioning_shown)
|
||||
$('#ssl_provision').toggle(res.can_provision.length + res.cant_provision.length > 0)
|
||||
$('#ssl_provision').toggle(res.can_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');
|
||||
@@ -159,7 +132,11 @@ function ssl_install(elem) {
|
||||
}
|
||||
|
||||
function show_csr() {
|
||||
// Can't show a CSR until both inputs are entered.
|
||||
if ($('#ssldomain').val() == "") return;
|
||||
if ($('#sslcc').val() == "") return;
|
||||
|
||||
// Scroll to it and fetch.
|
||||
$('#csr_info').slideDown();
|
||||
$('#ssl_csr').text('Loading...');
|
||||
api(
|
||||
@@ -192,20 +169,15 @@ function install_cert() {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -221,52 +193,33 @@ function provision_tls_cert() {
|
||||
for (var i = 0; i < status.requests.length; i++) {
|
||||
var r = status.requests[i];
|
||||
|
||||
if (r.result == "skipped") {
|
||||
// not interested --- this domain wasn't in the table
|
||||
// to begin with
|
||||
continue;
|
||||
}
|
||||
|
||||
// create an HTML block to display the results of this request
|
||||
var n = $("<div><h4/><p/></div>");
|
||||
$('#ssl_provision_result').append(n);
|
||||
|
||||
// plain log line
|
||||
if (typeof r === "string") {
|
||||
n.find("p").text(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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 & 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") {
|
||||
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
|
||||
@@ -274,7 +227,6 @@ function provision_tls_cert() {
|
||||
n.append(trace);
|
||||
for (var j = 0; j < r.log.length; j++)
|
||||
trace.append($("<div/>").text(r.log[j]));
|
||||
|
||||
}
|
||||
|
||||
if (may_reenable_provision_button)
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<table class="table">
|
||||
<thead><tr><th>For...</th> <th>Use...</th></tr></thead>
|
||||
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/repository/browse/?fdfilter=dav&fdid=at.bitfire.davdroid">here</a>)</td></tr>
|
||||
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/packages/at.bitfire.davdroid/">here</a>)</td></tr>
|
||||
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free beta</a> (free)</td></tr>
|
||||
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.89)</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -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’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’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();
|
||||
}
|
||||
|
||||
@@ -160,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('/');
|
||||
@@ -177,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()
|
||||
})
|
||||
}
|
||||
@@ -196,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(
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<button type="submit" class="btn btn-primary">Add User</button>
|
||||
</form>
|
||||
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
|
||||
<li>Passwords must be at least four characters and may not contain spaces. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li>
|
||||
<li>Passwords must be at least eight characters and may not contain spaces. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li>
|
||||
<li>Use <a href="#" onclick="return show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.</li>
|
||||
<li>Administrators get access to this control panel.</li>
|
||||
<li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#" onclick="return show_panel('aliases');">aliases</a> can.</li>
|
||||
@@ -84,6 +84,48 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Mail user API (advanced)</h3>
|
||||
|
||||
<p>Use your box’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,8 +212,8 @@ 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",
|
||||
$("<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",
|
||||
$("<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 eight characters and may not contain spaces.</small>" + yourpw + "</p>"),
|
||||
"Set Password",
|
||||
function() {
|
||||
api(
|
||||
@@ -254,7 +296,7 @@ function mod_priv(elem, add_remove) {
|
||||
function generate_random_password() {
|
||||
var pw = "";
|
||||
var charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; // confusable characters skipped
|
||||
for (var i = 0; i < 10; i++)
|
||||
for (var i = 0; i < 12; i++)
|
||||
pw += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
show_modal_error("Random Password", "<p>Here, try this:</p> <p><code style='font-size: 110%'>" + pw + "</code></pr");
|
||||
return false; // cancel click
|
||||
|
||||
@@ -106,76 +106,6 @@ def sort_email_addresses(email_addresses, env):
|
||||
ret.extend(sorted(email_addresses)) # whatever is left
|
||||
return ret
|
||||
|
||||
def exclusive_process(name):
|
||||
# Ensure that a process named `name` does not execute multiple
|
||||
# times concurrently.
|
||||
import os, sys, atexit
|
||||
pidfile = '/var/run/mailinabox-%s.pid' % name
|
||||
mypid = os.getpid()
|
||||
|
||||
# Attempt to get a lock on ourself so that the concurrency check
|
||||
# itself is not executed in parallel.
|
||||
with open(__file__, 'r+') as flock:
|
||||
# Try to get a lock. This blocks until a lock is acquired. The
|
||||
# lock is held until the flock file is closed at the end of the
|
||||
# with block.
|
||||
os.lockf(flock.fileno(), os.F_LOCK, 0)
|
||||
|
||||
# While we have a lock, look at the pid file. First attempt
|
||||
# to write our pid to a pidfile if no file already exists there.
|
||||
try:
|
||||
with open(pidfile, 'x') as f:
|
||||
# Successfully opened a new file. Since the file is new
|
||||
# there is no concurrent process. Write our pid.
|
||||
f.write(str(mypid))
|
||||
atexit.register(clear_my_pid, pidfile)
|
||||
return
|
||||
except FileExistsError:
|
||||
# The pid file already exixts, but it may contain a stale
|
||||
# pid of a terminated process.
|
||||
with open(pidfile, 'r+') as f:
|
||||
# Read the pid in the file.
|
||||
existing_pid = None
|
||||
try:
|
||||
existing_pid = int(f.read().strip())
|
||||
except ValueError:
|
||||
pass # No valid integer in the file.
|
||||
|
||||
# Check if the pid in it is valid.
|
||||
if existing_pid:
|
||||
if is_pid_valid(existing_pid):
|
||||
print("Another %s is already running (pid %d)." % (name, existing_pid), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Write our pid.
|
||||
f.seek(0)
|
||||
f.write(str(mypid))
|
||||
f.truncate()
|
||||
atexit.register(clear_my_pid, pidfile)
|
||||
|
||||
|
||||
def clear_my_pid(pidfile):
|
||||
import os
|
||||
os.unlink(pidfile)
|
||||
|
||||
|
||||
def is_pid_valid(pid):
|
||||
"""Checks whether a pid is a valid process ID of a currently running process."""
|
||||
# adapted from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
|
||||
import os, errno
|
||||
if pid <= 0: raise ValueError('Invalid PID.')
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError as err:
|
||||
if err.errno == errno.ESRCH: # No such process
|
||||
return False
|
||||
elif err.errno == errno.EPERM: # Not permitted to send signal
|
||||
return True
|
||||
else: # EINVAL
|
||||
raise
|
||||
else:
|
||||
return True
|
||||
|
||||
def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, trap=False, input=None):
|
||||
# A safe way to execute processes.
|
||||
# Some processes like apt-get require being given a sane PATH.
|
||||
|
||||
@@ -149,7 +149,10 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
||||
|
||||
# any proxy or redirect here?
|
||||
for path, url in yaml.get("proxies", {}).items():
|
||||
nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
|
||||
nginx_conf_extra += "\tlocation %s {" % path
|
||||
nginx_conf_extra += "\n\t\tproxy_pass %s;" % url
|
||||
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
|
||||
nginx_conf_extra += "\n\t}\n"
|
||||
for path, url in yaml.get("redirects", {}).items():
|
||||
nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url)
|
||||
|
||||
@@ -158,9 +161,9 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
||||
|
||||
# Add the HSTS header.
|
||||
if hsts == "yes":
|
||||
nginx_conf_extra += "add_header Strict-Transport-Security max-age=31536000;\n"
|
||||
nginx_conf_extra += "add_header Strict-Transport-Security max-age=15768000;\n"
|
||||
elif hsts == "preload":
|
||||
nginx_conf_extra += "add_header Strict-Transport-Security \"max-age=10886400; includeSubDomains; preload\";\n"
|
||||
nginx_conf_extra += "add_header Strict-Transport-Security \"max-age=15768000; includeSubDomains; preload\";\n"
|
||||
|
||||
# Add in any user customizations in the includes/ folder.
|
||||
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
|
||||
@@ -198,8 +201,11 @@ def get_web_domains_info(env):
|
||||
|
||||
# for the SSL config panel, get cert status
|
||||
def check_cert(domain):
|
||||
try:
|
||||
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
|
||||
if tls_cert is None: return ("danger", "No Certificate Installed")
|
||||
except OSError: # PRIMARY_HOSTNAME cert is missing
|
||||
tls_cert = None
|
||||
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":
|
||||
return ("success", "Signed & valid. " + cert_status_details)
|
||||
|
||||
@@ -14,7 +14,7 @@ build_postgrey: clean
|
||||
git clone git://git.debian.org/git/collab-maint/postgrey.git /tmp/build/postgrey
|
||||
|
||||
# Download the corresponding upstream package.
|
||||
wget -O /tmp/build/postgrey_1.35.orig.tar.gz http://postgrey.schweikert.ch/pub/postgrey-1.35.tar.gz
|
||||
wget -O /tmp/build/postgrey_1.35.orig.tar.gz http://postgrey.schweikert.ch/pub/old/postgrey-1.35.tar.gz
|
||||
|
||||
# Add our source patch to the debian packaging listing.
|
||||
cp postgrey_sources.diff /tmp/build/postgrey/debian/patches/mailinabox
|
||||
|
||||
25
security.md
25
security.md
@@ -40,21 +40,14 @@ The services all follow these rules:
|
||||
|
||||
* 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.
|
||||
* HTTPS, IMAP, and POP track the [Mozilla Intermediate Ciphers Recommendation](https://wiki.mozilla.org/Security/Server_Side_TLS), balancing security with supporting a wide range of mail clients. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy. For more details, see the [output of SSLyze for these ports](tests/tls_results.txt).
|
||||
* SMTP (port 25) uses the Postfix medium grade ciphers and SMTP Submission (port 587) uses the Postfix high grade ciphers ([more info](http://www.postfix.org/postconf.5.html#smtpd_tls_mandatory_ciphers)).
|
||||
|
||||
Additionally:
|
||||
|
||||
* SMTP Submission (port 587) will not accept user credentials without STARTTLS (true also of SMTP on port 25 in case of client misconfiguration), and the submission port won't accept mail without encryption. The minimum cipher key length is 128 bits. (The box is of course configured not to be an open relay. User credentials are required to send outbound mail.) ([source](setup/mail-postfix.sh))
|
||||
* HTTPS (port 443): The HTTPS Strict Transport Security header is set. A redirect from HTTP to HTTPS is offered. The [Qualys SSL Labs test](https://www.ssllabs.com/ssltest) should report an A+ grade. ([source 1](conf/nginx-ssl.conf), [source 2](conf/nginx.conf))
|
||||
|
||||
For more details, see the [output of SSLyze for these ports](tests/tls_results.txt).
|
||||
|
||||
The cipher and protocol selection are chosen to support the following clients:
|
||||
|
||||
* For HTTPS: Firefox 1, Chrome 1, IE 7, Opera 5, Safari 1, Windows XP IE8, Android 2.3, Java 7.
|
||||
* For other protocols: TBD.
|
||||
|
||||
### Password Storage
|
||||
|
||||
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py))
|
||||
@@ -69,6 +62,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), Nextcloud/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 +83,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 +104,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
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#########################################################
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
TAG=v0.18
|
||||
TAG=v0.28
|
||||
fi
|
||||
|
||||
# Are we running as root?
|
||||
|
||||
@@ -23,7 +23,7 @@ apt_install nsd ldnsutils openssh-client
|
||||
mkdir -p /var/run/nsd
|
||||
|
||||
cat > /etc/nsd/nsd.conf << EOF;
|
||||
# No not edit. Overwritten by Mail-in-a-Box setup.
|
||||
# Do not edit. Overwritten by Mail-in-a-Box setup.
|
||||
server:
|
||||
hide-version: yes
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ if [ -z "`tools/mail.py user`" ]; then
|
||||
else
|
||||
# Use me@PRIMARY_HOSTNAME
|
||||
EMAIL_ADDR=me@$PRIMARY_HOSTNAME
|
||||
EMAIL_PW=1234
|
||||
EMAIL_PW=12345678
|
||||
echo
|
||||
echo "Creating a new administrative mail account for $EMAIL_ADDR with password $EMAIL_PW."
|
||||
echo
|
||||
|
||||
@@ -48,6 +48,15 @@ function apt_install {
|
||||
apt_get_quiet install $PACKAGES
|
||||
}
|
||||
|
||||
function apt_add_repository_to_unattended_upgrades {
|
||||
if [ -f /etc/apt/apt.conf.d/50unattended-upgrades ]; then
|
||||
if ! grep -q "$1" /etc/apt/apt.conf.d/50unattended-upgrades; then
|
||||
sed -i "/Allowed-Origins/a \
|
||||
\"$1\";" /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function get_default_hostname {
|
||||
# Guess the machine's hostname. It should be a fully qualified
|
||||
# domain name suitable for DNS. None of these calls may provide
|
||||
@@ -170,7 +179,7 @@ function wget_verify {
|
||||
DEST=$3
|
||||
CHECKSUM="$HASH $DEST"
|
||||
rm -f $DEST
|
||||
wget -q -O $DEST $URL || exit 1
|
||||
hide_output wget -O $DEST $URL
|
||||
if ! echo "$CHECKSUM" | sha1sum --check --strict > /dev/null; then
|
||||
echo "------------------------------------------------------------"
|
||||
echo "Download of $URL did not match expected checksum."
|
||||
|
||||
@@ -37,8 +37,17 @@ apt_install \
|
||||
# of active IMAP connections (at, say, 5 open connections per user that
|
||||
# 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).
|
||||
# The `default_vsz_limit` is the maximum amount of virtual memory that
|
||||
# can be allocated. It should be set *reasonably high* to avoid allocation
|
||||
# issues with larger mailboxes. We're setting it to 1/3 of the total
|
||||
# available memory (physical mem + swap) to be sure.
|
||||
# See here for discussion:
|
||||
# - https://www.dovecot.org/list/dovecot/2012-August/137569.html
|
||||
# - https://www.dovecot.org/list/dovecot/2011-December/132455.html
|
||||
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
|
||||
default_process_limit=$(echo "`nproc` * 250" | bc)
|
||||
default_process_limit=$(echo "`nproc` * 250" | bc) \
|
||||
default_vsz_limit=$(echo "`free -tm | tail -1 | awk '{print $2}'` / 3" | bc)M \
|
||||
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.
|
||||
@@ -70,12 +79,15 @@ tools/editconf.py /etc/dovecot/conf.d/10-auth.conf \
|
||||
|
||||
# Enable SSL, specify the location of the SSL certificate and private key files.
|
||||
# Disable obsolete SSL protocols and allow only good ciphers per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
|
||||
# Enable strong ssl dh parameters
|
||||
tools/editconf.py /etc/dovecot/conf.d/10-ssl.conf \
|
||||
ssl=required \
|
||||
"ssl_cert=<$STORAGE_ROOT/ssl/ssl_certificate.pem" \
|
||||
"ssl_key=<$STORAGE_ROOT/ssl/ssl_private_key.pem" \
|
||||
"ssl_protocols=!SSLv3 !SSLv2" \
|
||||
"ssl_cipher_list=TLSv1+HIGH !SSLv2 !RC4 !aNULL !eNULL !3DES @STRENGTH"
|
||||
"ssl_cipher_list=ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS" \
|
||||
"ssl_prefer_server_ciphers = yes" \
|
||||
"ssl_dh_parameters_length = 2048"
|
||||
|
||||
# Disable in-the-clear IMAP/POP because there is no reason for a user to transmit
|
||||
# login credentials outside of an encrypted connection. Only the over-TLS versions
|
||||
|
||||
@@ -91,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
|
||||
@@ -100,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
|
||||
@@ -122,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
|
||||
@@ -158,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 \
|
||||
|
||||
@@ -49,7 +49,7 @@ 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;
|
||||
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
|
||||
|
||||
@@ -4,25 +4,55 @@ source setup/functions.sh
|
||||
|
||||
echo "Installing Mail-in-a-Box system management daemon..."
|
||||
|
||||
# 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
|
||||
# DEPENDENCIES
|
||||
|
||||
# These are required to pip install cryptography.
|
||||
apt_install build-essential libssl-dev libffi-dev python3-dev
|
||||
# We used to install management daemon-related Python packages
|
||||
# directly to /usr/local/lib. We moved to a virtualenv because
|
||||
# these packages might conflict with apt-installed packages.
|
||||
# We may have a lingering version of acme that conflcits with
|
||||
# certbot, which we're about to install below, so remove it
|
||||
# first. Once acme is installed by an apt package, this might
|
||||
# break the package version and `apt-get install --reinstall python3-acme`
|
||||
# might be needed in that case.
|
||||
while [ -d /usr/local/lib/python3.4/dist-packages/acme ]; do
|
||||
pip3 uninstall -y acme;
|
||||
done
|
||||
|
||||
# duplicity is used to make backups of user data. It uses boto
|
||||
# (via Python 2) to do backups to AWS 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.
|
||||
#
|
||||
# python-virtualenv is used to isolate the Python 3 packages we
|
||||
# install via pip from the system-installed packages.
|
||||
#
|
||||
# certbot installs EFF's certbot which we use to
|
||||
# provision free TLS certificates.
|
||||
apt_install duplicity python-pip python-virtualenv certbot
|
||||
hide_output pip2 install --upgrade boto
|
||||
|
||||
# Create a virtualenv for the installation of Python 3 packages
|
||||
# used by the management daemon.
|
||||
inst_dir=/usr/local/lib/mailinabox
|
||||
mkdir -p $inst_dir
|
||||
venv=$inst_dir/env
|
||||
if [ ! -d $venv ]; then
|
||||
virtualenv -ppython3 $venv
|
||||
fi
|
||||
|
||||
# Upgrade pip because the Ubuntu-packaged version is out of date.
|
||||
hide_output $venv/bin/pip install --upgrade pip
|
||||
|
||||
# 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
|
||||
hide_output $venv/bin/pip install --upgrade \
|
||||
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
|
||||
flask dnspython python-dateutil \
|
||||
"idna>=2.0.0" "cryptography==2.2.2" boto psutil
|
||||
|
||||
# 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
|
||||
# CONFIGURATION
|
||||
|
||||
# Create a backup directory and a random key for encrypting backups.
|
||||
mkdir -p $STORAGE_ROOT/backup
|
||||
@@ -30,12 +60,40 @@ if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then
|
||||
$(umask 077; openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt)
|
||||
fi
|
||||
|
||||
# Link the management server daemon into a well known location.
|
||||
rm -f /usr/local/bin/mailinabox-daemon
|
||||
ln -s `pwd`/management/daemon.py /usr/local/bin/mailinabox-daemon
|
||||
|
||||
# Download jQuery and Bootstrap local files
|
||||
|
||||
# Make sure we have the directory to save to.
|
||||
assets_dir=$inst_dir/vendor/assets
|
||||
rm -rf $assets_dir
|
||||
mkdir -p $assets_dir
|
||||
|
||||
# jQuery CDN URL
|
||||
jquery_version=2.1.4
|
||||
jquery_url=https://code.jquery.com
|
||||
|
||||
# Get jQuery
|
||||
wget_verify $jquery_url/jquery-$jquery_version.min.js 43dc554608df885a59ddeece1598c6ace434d747 $assets_dir/jquery.min.js
|
||||
|
||||
# Bootstrap CDN URL
|
||||
bootstrap_version=3.3.7
|
||||
bootstrap_url=https://github.com/twbs/bootstrap/releases/download/v$bootstrap_version/bootstrap-$bootstrap_version-dist.zip
|
||||
|
||||
# Get Bootstrap
|
||||
wget_verify $bootstrap_url e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a /tmp/bootstrap.zip
|
||||
unzip -q /tmp/bootstrap.zip -d $assets_dir
|
||||
mv $assets_dir/bootstrap-$bootstrap_version-dist $assets_dir/bootstrap
|
||||
rm -f /tmp/bootstrap.zip
|
||||
|
||||
# Create an init script to start the management daemon and keep it
|
||||
# running after a reboot.
|
||||
rm -f /usr/local/bin/mailinabox-daemon # old path
|
||||
cat > $inst_dir/start <<EOF;
|
||||
#!/bin/bash
|
||||
source $venv/bin/activate
|
||||
exec python `pwd`/management/daemon.py
|
||||
EOF
|
||||
chmod +x $inst_dir/start
|
||||
rm -f /etc/init.d/mailinabox
|
||||
ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox
|
||||
hide_output update-rc.d mailinabox defaults
|
||||
|
||||
@@ -137,6 +137,17 @@ def migration_10(env):
|
||||
shutil.move(sslcert, newname)
|
||||
os.rmdir(d)
|
||||
|
||||
def migration_11(env):
|
||||
# Archive the old Let's Encrypt account directory managed by free_tls_certificates
|
||||
# because we'll use that path now for the directory managed by certbot.
|
||||
try:
|
||||
old_path = os.path.join(env["STORAGE_ROOT"], 'ssl', 'lets_encrypt')
|
||||
new_path = os.path.join(env["STORAGE_ROOT"], 'ssl', 'lets_encrypt-old')
|
||||
shutil.move(old_path, new_path)
|
||||
except:
|
||||
# meh
|
||||
pass
|
||||
|
||||
def get_current_migration():
|
||||
ver = 0
|
||||
while True:
|
||||
|
||||
@@ -38,8 +38,10 @@ chown munin. /var/log/munin/munin-cgi-html.log
|
||||
chown munin. /var/log/munin/munin-cgi-graph.log
|
||||
|
||||
# ensure munin-node knows the name of this machine
|
||||
# and reduce logging level to warning
|
||||
tools/editconf.py /etc/munin/munin-node.conf -s \
|
||||
host_name=$PRIMARY_HOSTNAME
|
||||
host_name=$PRIMARY_HOSTNAME \
|
||||
log_level=1
|
||||
|
||||
# Update the activated plugins through munin's autoconfiguration.
|
||||
munin-node-configure --shell --remove-also 2>/dev/null | sh
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Owncloud
|
||||
# Nextcloud
|
||||
##########################
|
||||
|
||||
source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
|
||||
# ### Installing ownCloud
|
||||
# ### Installing Nextcloud
|
||||
|
||||
echo "Installing ownCloud (contacts/calendar)..."
|
||||
echo "Installing Nextcloud (contacts/calendar)..."
|
||||
|
||||
# Keep the php5 dependancies for the owncloud upgrades
|
||||
apt_install \
|
||||
dbconfig-common \
|
||||
php5-cli php5-sqlite php5-gd php5-imap php5-curl php-pear php-apc curl libapr1 libtool libcurl4-openssl-dev php-xml-parser \
|
||||
php5 php5-dev php5-gd php5-fpm memcached php5-memcached unzip
|
||||
php5 php5-dev php5-gd php5-fpm memcached php5-memcached
|
||||
|
||||
apt-get purge -qq -y owncloud*
|
||||
|
||||
# Install ownCloud from source of this version:
|
||||
owncloud_ver=8.2.3
|
||||
owncloud_hash=bfdf6166fbf6fc5438dc358600e7239d1c970613
|
||||
apt_install php7.0 php7.0-fpm \
|
||||
php7.0-cli php7.0-sqlite php7.0-gd php7.0-imap php7.0-curl php-pear php-apc curl \
|
||||
php7.0-dev php7.0-gd memcached php7.0-memcached php7.0-xml php7.0-mbstring php7.0-zip php7.0-apcu
|
||||
|
||||
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
|
||||
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
|
||||
@@ -32,28 +33,37 @@ 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
|
||||
InstallNextcloud() {
|
||||
|
||||
version=$1
|
||||
hash=$2
|
||||
|
||||
echo
|
||||
echo "Upgrading to Nextcloud version $version"
|
||||
echo
|
||||
|
||||
# Remove the current owncloud/Nextcloud
|
||||
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
|
||||
wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.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
|
||||
# Extract ownCloud/Nextcloud
|
||||
unzip -q /tmp/nextcloud.zip -d /usr/local/lib
|
||||
mv /usr/local/lib/nextcloud /usr/local/lib/owncloud
|
||||
rm -f /tmp/nextcloud.zip
|
||||
|
||||
# Extract ownCloud
|
||||
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
|
||||
rm -f /tmp/owncloud.zip
|
||||
|
||||
# The two apps we actually want are not in ownCloud core. Clone them from
|
||||
# The two apps we actually want are not in Nextcloud core. Download the releases from
|
||||
# their github repositories.
|
||||
mkdir -p /usr/local/lib/owncloud/apps
|
||||
git_clone https://github.com/owncloudarchive/contacts 9ba2e667ae8c7ea36d8c4a4c3413c374beb24b1b '' /usr/local/lib/owncloud/apps/contacts
|
||||
git_clone https://github.com/owncloudarchive/calendar 2086e738a3b7b868ec59cd61f0f88b49c3f21dd1 '' /usr/local/lib/owncloud/apps/calendar
|
||||
|
||||
wget_verify https://github.com/nextcloud/contacts/releases/download/v1.5.3/contacts.tar.gz 78c4d49e73f335084feecd4853bd8234cf32615e /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.5.3/calendar.tar.gz b370352d1f280805cc7128f78af4615f623827f8 /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 +79,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,18 +91,174 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
|
||||
echo "...which seemed to work."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# We only install ownCloud intermediate versions to be able to seemlesly upgrade to Nextcloud
|
||||
InstallOwncloud() {
|
||||
|
||||
version=$1
|
||||
hash=$2
|
||||
|
||||
echo
|
||||
echo "Upgrading to OwnCloud version $version"
|
||||
echo
|
||||
|
||||
# Remove the current owncloud/Nextcloud
|
||||
rm -rf /usr/local/lib/owncloud
|
||||
|
||||
# Download and verify
|
||||
wget_verify https://download.owncloud.org/community/owncloud-$version.tar.bz2 $hash /tmp/owncloud.tar.bz2
|
||||
|
||||
|
||||
# Extract ownCloud
|
||||
tar xjf /tmp/owncloud.tar.bz2 -C /usr/local/lib
|
||||
rm -f /tmp/owncloud.tar.bz2
|
||||
|
||||
# The two apps we actually want are not in Nextcloud core. Download the releases from
|
||||
# their github repositories.
|
||||
mkdir -p /usr/local/lib/owncloud/apps
|
||||
|
||||
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}
|
||||
|
||||
# Create a symlink to the config.php in STORAGE_ROOT (for upgrades we're restoring the symlink we previously
|
||||
# put in, and in new installs we're creating a symlink and will create the actual config later).
|
||||
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
|
||||
|
||||
# Make sure permissions are correct or the upgrade step won't run.
|
||||
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
|
||||
# that error.
|
||||
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /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 [ -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 php5 /usr/local/lib/owncloud/occ upgrade
|
||||
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then
|
||||
echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..."
|
||||
sudo -u www-data php5 /usr/local/lib/owncloud/occ upgrade
|
||||
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi
|
||||
sudo -u www-data php5 /usr/local/lib/owncloud/occ maintenance:mode --off
|
||||
echo "...which seemed to work."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
owncloud_ver=12.0.5
|
||||
owncloud_hash=d25afbac977a4e331f5e38df50aed0844498ca86
|
||||
|
||||
# Check if Nextcloud 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 if running. If theyre not running (which happens on a previously failed install), dont bail.
|
||||
service php7.0-fpm stop &> /dev/null || /bin/true
|
||||
service php5-fpm stop &> /dev/null || /bin/true
|
||||
|
||||
# Backup the existing ownCloud/Nextcloud.
|
||||
# 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/Nextcloud to $owncloud_flavor $owncloud_ver (backing up existing 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
|
||||
|
||||
# ### Configuring ownCloud
|
||||
# We only need to check if we do upgrades when owncloud/Nextcloud was previously installed
|
||||
if [ -e /usr/local/lib/owncloud/version.php ]; then
|
||||
if grep -q "OC_VersionString = '8\.1\.[0-9]" /usr/local/lib/owncloud/version.php; then
|
||||
echo "We are running 8.1.x, upgrading to 8.2.11 first"
|
||||
InstallOwncloud 8.2.11 e4794938fc2f15a095018ba9d6ee18b53f6f299c
|
||||
fi
|
||||
|
||||
# Setup ownCloud if the ownCloud database does not yet exist. Running setup when
|
||||
# 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 "OC_VersionString = '8\.2\.[0-9]" /usr/local/lib/owncloud/version.php; then
|
||||
echo "We are running version 8.2.x, upgrading to 9.0.11 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\APCu';
|
||||
|
||||
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.11
|
||||
InstallOwncloud 9.0.11 fc8bad8a62179089bc58c406b28997fb0329337b
|
||||
|
||||
# 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 php5 /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 php5 /usr/local/lib/owncloud/occ dav:migrate-calendar $username
|
||||
done
|
||||
sudo -u www-data php5 /usr/local/lib/owncloud/occ dav:sync-birthday-calendar
|
||||
fi
|
||||
|
||||
# If we are upgrading from 9.0.x we should go to 9.1 first.
|
||||
if grep -q "OC_VersionString = '9\.0\.[0-9]" /usr/local/lib/owncloud/version.php; then
|
||||
echo "We are running ownCloud 9.0.x, upgrading to ownCloud 9.1.7 first"
|
||||
InstallOwncloud 9.1.7 1307d997d0b23dc42742d315b3e2f11423a9c808
|
||||
fi
|
||||
|
||||
# Newer ownCloud 9.1.x versions cannot be upgraded to Nextcloud 10 and have to be
|
||||
# upgraded to Nextcloud 11 straight away, see:
|
||||
# https://github.com/nextcloud/server/issues/2203
|
||||
# However, for some reason, upgrading to the latest Nextcloud 11.0.7 doesn't
|
||||
# work either. Therefore, we're upgrading to Nextcloud 11.0.0 in the interim.
|
||||
# This should not be a problem since we're upgrading to the latest Nextcloud 12
|
||||
# in the next step.
|
||||
if grep -q "OC_VersionString = '9\.1\.[0-9]" /usr/local/lib/owncloud/version.php; then
|
||||
echo "We are running ownCloud 9.1.x, upgrading to Nextcloud 11.0.0 first"
|
||||
InstallNextcloud 11.0.0 e8c9ebe72a4a76c047080de94743c5c11735e72e
|
||||
fi
|
||||
|
||||
# If we are upgrading from 10.0.x we should go to Nextcloud 11.0 first.
|
||||
if grep -q "OC_VersionString = '10\.0\.[0-9]" /usr/local/lib/owncloud/version.php; then
|
||||
echo "We are running Nextcloud 10.0.x, upgrading to Nextcloud 11.0.7 first"
|
||||
InstallNextcloud 11.0.7 f936ddcb2ae3dbb66ee4926eb8b2ebbddc3facbe
|
||||
fi
|
||||
fi
|
||||
|
||||
InstallNextcloud $owncloud_ver $owncloud_hash
|
||||
fi
|
||||
|
||||
# ### Configuring Nextcloud
|
||||
|
||||
# Setup Nextcloud if the Nextcloud database does not yet exist. Running setup when
|
||||
# the database does exist wipes the database and user data.
|
||||
if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
||||
# Create user data directory
|
||||
mkdir -p $STORAGE_ROOT/owncloud
|
||||
|
||||
# Create 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
|
||||
@@ -101,7 +267,7 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
||||
|
||||
'instanceid' => '$instanceid',
|
||||
|
||||
'forcessl' => true, # if unset/false, ownCloud sends a HSTS=0 header, which conflicts with nginx config
|
||||
'forcessl' => true, # if unset/false, Nextcloud sends a HSTS=0 header, which conflicts with nginx config
|
||||
|
||||
'overwritewebroot' => '/cloud',
|
||||
'overwrite.cli.url' => '/cloud',
|
||||
@@ -111,10 +277,7 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
||||
'arguments'=>array('{127.0.0.1:993/imap/ssl/novalidate-cert}')
|
||||
)
|
||||
),
|
||||
'memcache.local' => '\\OC\\Memcache\\Memcached',
|
||||
"memcached_servers" => array (
|
||||
array('127.0.0.1', 11211),
|
||||
),
|
||||
'memcache.local' => '\OC\Memcache\APCu',
|
||||
'mail_smtpmode' => 'sendmail',
|
||||
'mail_smtpsecure' => '',
|
||||
'mail_smtpauthtype' => 'LOGIN',
|
||||
@@ -124,8 +287,6 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
||||
'mail_smtpname' => '',
|
||||
'mail_smtppassword' => '',
|
||||
'mail_from_address' => 'owncloud',
|
||||
'mail_domain' => '$PRIMARY_HOSTNAME',
|
||||
'logtimezone' => '$TIMEZONE',
|
||||
);
|
||||
?>
|
||||
EOF
|
||||
@@ -142,7 +303,7 @@ EOF
|
||||
'dbtype' => 'sqlite3',
|
||||
|
||||
# create an administrator account with a random password so that
|
||||
# the user does not have to enter anything on first load of ownCloud
|
||||
# the user does not have to enter anything on first load of Nextcloud
|
||||
'adminlogin' => 'root',
|
||||
'adminpass' => '$adminpassword',
|
||||
);
|
||||
@@ -152,7 +313,7 @@ EOF
|
||||
# Set permissions
|
||||
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
|
||||
|
||||
# Execute ownCloud's setup step, which creates the ownCloud sqlite database.
|
||||
# Execute Nextcloud's setup step, which creates the Nextcloud sqlite database.
|
||||
# It also wipes it if it exists. And it updates config.php with database
|
||||
# settings and deletes the autoconfig.php file.
|
||||
(cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;)
|
||||
@@ -163,7 +324,13 @@ 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
|
||||
# * mail_domain' needs to be set every time we run the setup. Making sure we are setting
|
||||
# the correct domain name if the domain is being change from the previous setup.
|
||||
# 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 +338,15 @@ include("$STORAGE_ROOT/owncloud/config.php");
|
||||
|
||||
\$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME');
|
||||
|
||||
\$CONFIG['memcache.local'] = '\\OC\\Memcache\\Memcached';
|
||||
\$CONFIG['memcache.local'] = '\OC\Memcache\APCu';
|
||||
\$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';
|
||||
|
||||
\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME';
|
||||
|
||||
echo "<?php\n\\\$CONFIG = ";
|
||||
var_export(\$CONFIG);
|
||||
echo ";";
|
||||
@@ -182,9 +354,9 @@ echo ";";
|
||||
EOF
|
||||
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
|
||||
|
||||
# Enable/disable apps. Note that this must be done after the ownCloud setup.
|
||||
# Enable/disable apps. Note that this must be done after the Nextcloud setup.
|
||||
# The firstrunwizard gave Josh all sorts of problems, so disabling that.
|
||||
# user_external is what allows ownCloud to use IMAP for login. The contacts
|
||||
# user_external is what allows Nextcloud to use IMAP for login. The contacts
|
||||
# and calendar apps are the extensions we really care about here.
|
||||
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable firstrunwizard
|
||||
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable user_external
|
||||
@@ -199,7 +371,7 @@ if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi
|
||||
|
||||
# Set PHP FPM values to support large file uploads
|
||||
# (semicolon is the comment character in this file, hashes produce deprecation warnings)
|
||||
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
|
||||
tools/editconf.py /etc/php/7.0/fpm/php.ini -c ';' \
|
||||
upload_max_filesize=16G \
|
||||
post_max_size=16G \
|
||||
output_buffering=16384 \
|
||||
@@ -207,7 +379,27 @@ tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
|
||||
max_execution_time=600 \
|
||||
short_open_tag=On
|
||||
|
||||
# Set up a cron job for owncloud.
|
||||
# Set Nextcloud recommended opcache settings
|
||||
tools/editconf.py /etc/php/7.0/cli/conf.d/10-opcache.ini -c ';' \
|
||||
opcache.enable=1 \
|
||||
opcache.enable_cli=1 \
|
||||
opcache.interned_strings_buffer=8 \
|
||||
opcache.max_accelerated_files=10000 \
|
||||
opcache.memory_consumption=128 \
|
||||
opcache.save_comments=1 \
|
||||
opcache.revalidate_freq=1
|
||||
|
||||
# Configure the path environment for php-fpm
|
||||
tools/editconf.py /etc/php/7.0/fpm/pool.d/www.conf -c ';' \
|
||||
env[PATH]=/usr/local/bin:/usr/bin:/bin
|
||||
|
||||
# If apc is explicitly disabled we need to enable it
|
||||
if grep -q apc.enabled=0 /etc/php/7.0/mods-available/apcu.ini; then
|
||||
tools/editconf.py /etc/php/7.0/mods-available/apcu.ini -c ';' \
|
||||
apc.enabled=1
|
||||
fi
|
||||
|
||||
# Set up a cron job for Nextcloud.
|
||||
cat > /etc/cron.hourly/mailinabox-owncloud << EOF;
|
||||
#!/bin/bash
|
||||
# Mail-in-a-Box
|
||||
@@ -215,8 +407,8 @@ sudo -u www-data php -f /usr/local/lib/owncloud/cron.php
|
||||
EOF
|
||||
chmod +x /etc/cron.hourly/mailinabox-owncloud
|
||||
|
||||
# There's nothing much of interest that a user could do as an admin for ownCloud,
|
||||
# and there's a lot they could mess up, so we don't make any users admins of ownCloud.
|
||||
# There's nothing much of interest that a user could do as an admin for Nextcloud,
|
||||
# and there's a lot they could mess up, so we don't make any users admins of Nextcloud.
|
||||
# But if we wanted to, we would do this:
|
||||
# ```
|
||||
# for user in $(tools/mail.py user admins); do
|
||||
@@ -225,5 +417,4 @@ chmod +x /etc/cron.hourly/mailinabox-owncloud
|
||||
# ```
|
||||
|
||||
# Enable PHP modules and restart PHP.
|
||||
php5enmod imap
|
||||
restart_service php5-fpm
|
||||
restart_service php7.0-fpm
|
||||
|
||||
@@ -19,20 +19,26 @@ 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)
|
||||
@@ -47,15 +53,15 @@ if [ -e ~/.wgetrc ]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check that we are running on x86_64, any other architecture is unsupported and
|
||||
# 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" ]; then
|
||||
if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "i686" ]; then
|
||||
if [ -z "$ARM" ]; then
|
||||
echo "Mail-in-a-Box only supports x86_64 and will not work on any other architecture, like ARM."
|
||||
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
|
||||
|
||||
@@ -12,7 +12,9 @@ if [ -z "$NONINTERACTIVE" ]; then
|
||||
apt_get_quiet install dialog python3 python3-pip || exit 1
|
||||
fi
|
||||
|
||||
# email_validator is repeated in setup/management.sh
|
||||
# Installing email_validator is repeated in setup/management.sh, but in setup/management.sh
|
||||
# we install it inside a virtualenv. In this script, we don't have the virtualenv yet
|
||||
# so we install the python package globally.
|
||||
hide_output pip3 install "email_validator>=1.0.0" || exit 1
|
||||
|
||||
message_box "Mail-in-a-Box Installation" \
|
||||
@@ -49,7 +51,7 @@ you really want.
|
||||
# user hit ESC/cancel
|
||||
exit
|
||||
fi
|
||||
while ! management/mailconfig.py validate-email "$EMAIL_ADDR"
|
||||
while ! python3 management/mailconfig.py validate-email "$EMAIL_ADDR"
|
||||
do
|
||||
input_box "Your Email Address" \
|
||||
"That's not a valid email address.\n\nWhat email address are you setting this box up to manage?" \
|
||||
@@ -180,9 +182,6 @@ if [ "$PUBLIC_IPV6" = "auto" ]; then
|
||||
fi
|
||||
if [ "$PRIMARY_HOSTNAME" = "auto" ]; then
|
||||
PRIMARY_HOSTNAME=$(get_default_hostname)
|
||||
elif [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
|
||||
# Generate a probably-unique subdomain under our justtesting.email domain.
|
||||
PRIMARY_HOSTNAME=`echo $PUBLIC_IP | sha1sum | cut -c1-5`.justtesting.email
|
||||
fi
|
||||
|
||||
# Set STORAGE_USER and STORAGE_ROOT to default values (user-data and /home/user-data), unless
|
||||
|
||||
@@ -48,7 +48,7 @@ echo "public.pyzor.org:24441" > /etc/spamassassin/pyzor/servers
|
||||
# * Disable localmode so Pyzor, DKIM and DNS checks can be used.
|
||||
tools/editconf.py /etc/default/spampd \
|
||||
DESTPORT=10026 \
|
||||
ADDOPTS="\"--maxsize=500\"" \
|
||||
ADDOPTS="\"--maxsize=2000\"" \
|
||||
LOCALONLY=0
|
||||
|
||||
# Spamassassin normally wraps spam as an attachment inside a fresh
|
||||
@@ -61,9 +61,11 @@ tools/editconf.py /etc/default/spampd \
|
||||
# content or execute scripts, and it is probably confusing to most users.
|
||||
#
|
||||
# Tell Spamassassin not to modify the original message except for adding
|
||||
# the X-Spam-Status mail header and related headers.
|
||||
# the X-Spam-Status & X-Spam-Score mail headers and related headers.
|
||||
tools/editconf.py /etc/spamassassin/local.cf -s \
|
||||
report_safe=0
|
||||
report_safe=0 \
|
||||
add_header="all Report _REPORT_" \
|
||||
add_header="all Score _SCORE_"
|
||||
|
||||
# Bayesean learning
|
||||
# -----------------
|
||||
@@ -84,7 +86,7 @@ tools/editconf.py /etc/spamassassin/local.cf -s \
|
||||
|
||||
tools/editconf.py /etc/spamassassin/local.cf -s \
|
||||
bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes \
|
||||
bayes_file_mode=0660
|
||||
bayes_file_mode=0666
|
||||
|
||||
mkdir -p $STORAGE_ROOT/mail/spamassassin
|
||||
chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin
|
||||
|
||||
@@ -74,7 +74,7 @@ if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
||||
CSR=/tmp/ssl_cert_sign_req-$$.csr
|
||||
hide_output \
|
||||
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CSR \
|
||||
-sha256 -subj "/C=/ST=/L=/O=/CN=$PRIMARY_HOSTNAME"
|
||||
-sha256 -subj "/CN=$PRIMARY_HOSTNAME"
|
||||
|
||||
# Generate the self-signed certificate.
|
||||
CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem
|
||||
|
||||
@@ -111,22 +111,38 @@ source setup/zpush.sh
|
||||
source setup/management.sh
|
||||
source setup/munin.sh
|
||||
|
||||
# Ping the management daemon to write the DNS and nginx configuration files.
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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 there aren't any mail users yet, create one.
|
||||
source setup/firstuser.sh
|
||||
|
||||
# Register with Let's Encrypt, including agreeing to the Terms of Service. This
|
||||
# is an interactive command.
|
||||
if [ ! -d $STORAGE_ROOT/ssl/lets_encrypt/accounts/acme-v01.api.letsencrypt.org/ ]; then
|
||||
echo
|
||||
echo "-----------------------------------------------"
|
||||
echo "Mail-in-a-Box uses Let's Encrypt to provision free certificates"
|
||||
echo "to enable HTTPS connections to your box. You'll now be asked to agree"
|
||||
echo "to Let's Encrypt's terms of service."
|
||||
echo
|
||||
certbot register $([ "$NONINTERACTIVE" == 1 ] && echo "--agree-tos") \
|
||||
--register-unsafely-without-email --config-dir $STORAGE_ROOT/ssl/lets_encrypt
|
||||
fi
|
||||
|
||||
# Done.
|
||||
echo
|
||||
echo "-----------------------------------------------"
|
||||
@@ -140,17 +156,17 @@ if management/status_checks.py --check-primary-hostname; then
|
||||
echo https://$PRIMARY_HOSTNAME/admin
|
||||
echo
|
||||
echo "If you have a DNS problem put the box's IP address in the URL"
|
||||
echo "(https://$PUBLIC_IP/admin) but then check the SSL fingerprint:"
|
||||
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint \
|
||||
| sed "s/SHA1 Fingerprint=//"
|
||||
echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:"
|
||||
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\
|
||||
| sed "s/SHA256 Fingerprint=//"
|
||||
else
|
||||
echo https://$PUBLIC_IP/admin
|
||||
echo
|
||||
echo You will be alerted that the website has an invalid certificate. Check that
|
||||
echo the certificate fingerprint matches:
|
||||
echo
|
||||
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint \
|
||||
| sed "s/SHA1 Fingerprint=//"
|
||||
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\
|
||||
| sed "s/SHA256 Fingerprint=//"
|
||||
echo
|
||||
echo Then you can confirm the security exception and continue.
|
||||
echo
|
||||
|
||||
@@ -68,17 +68,10 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ### Add Mail-in-a-Box's PPA.
|
||||
|
||||
# We've built several .deb packages on our own that we want to include.
|
||||
# One is a replacement for Ubuntu's stock postgrey package that makes
|
||||
# some enhancements. The other is dovecot-lucene, a Lucene-based full
|
||||
# text search plugin for (and by) dovecot, which is not available in
|
||||
# Ubuntu currently.
|
||||
#
|
||||
# So, first ensure add-apt-repository is installed, then use it to install
|
||||
# the [mail-in-a-box ppa](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa).
|
||||
# ### Add PPAs.
|
||||
|
||||
# We install some non-standard Ubuntu packages maintained by us and other
|
||||
# third-party providers. First ensure add-apt-repository is installed.
|
||||
|
||||
if [ ! -f /usr/bin/add-apt-repository ]; then
|
||||
echo "Installing add-apt-repository..."
|
||||
@@ -86,16 +79,32 @@ if [ ! -f /usr/bin/add-apt-repository ]; then
|
||||
apt_install software-properties-common
|
||||
fi
|
||||
|
||||
# [Main-in-a-Box's own PPA](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa)
|
||||
# holds several .deb packages that we built on our own.
|
||||
# One is a replacement for Ubuntu's stock postgrey package that makes
|
||||
# some enhancements. The other is dovecot-lucene, a Lucene-based full
|
||||
# text search plugin for (and by) dovecot, which is not available in
|
||||
# Ubuntu currently.
|
||||
|
||||
hide_output add-apt-repository -y ppa:mail-in-a-box/ppa
|
||||
hide_output add-apt-repository -y ppa:certbot/certbot
|
||||
|
||||
# ### Update Packages
|
||||
|
||||
# Update system packages to make sure we have the latest upstream versions of things from Ubuntu.
|
||||
# Update system packages to make sure we have the latest upstream versions
|
||||
# of things from Ubuntu, as well as the directory of packages provide by the
|
||||
# PPAs so we can install those packages later.
|
||||
|
||||
echo Updating system packages...
|
||||
hide_output apt-get update
|
||||
apt_get_quiet upgrade
|
||||
|
||||
# Old kernels pile up over time and take up a lot of disk space, and because of Mail-in-a-Box
|
||||
# changes there may be other packages that are no longer needed. Clear out anything apt knows
|
||||
# is safe to delete.
|
||||
|
||||
apt_get_quiet autoremove
|
||||
|
||||
# ### Install System Packages
|
||||
|
||||
# Install basic utilities.
|
||||
@@ -116,9 +125,29 @@ apt_get_quiet upgrade
|
||||
echo Installing system packages...
|
||||
apt_install python3 python3-dev python3-pip \
|
||||
netcat-openbsd wget curl git sudo coreutils bc \
|
||||
haveged pollinate \
|
||||
haveged pollinate unzip \
|
||||
unattended-upgrades cron ntp fail2ban
|
||||
|
||||
# ### Add PHP7 PPA
|
||||
|
||||
# Nextcloud requires PHP7, we will install the ppa from ubuntu php maintainer Ondřej Surý
|
||||
# The PPA is located here https://launchpad.net/%7Eondrej/+archive/ubuntu/php
|
||||
# Unattended upgrades are activated for the repository If it appears it's already
|
||||
# installed, don't do it again so we can avoid an unnecessary call to apt-get update.
|
||||
if [ ! -f /etc/apt/sources.list.d/ondrej-php-trusty.list ]; then
|
||||
hide_output add-apt-repository -y ppa:ondrej/php
|
||||
apt_add_repository_to_unattended_upgrades LP-PPA-ondrej-php:trusty
|
||||
hide_output apt-get update
|
||||
fi
|
||||
|
||||
# ### 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
|
||||
@@ -208,6 +237,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.
|
||||
@@ -216,7 +251,7 @@ cat > /etc/apt/apt.conf.d/02periodic <<EOF;
|
||||
APT::Periodic::MaxAge "7";
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::Verbose "1";
|
||||
APT::Periodic::Verbose "0";
|
||||
EOF
|
||||
|
||||
# ### Firewall
|
||||
@@ -291,10 +326,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
|
||||
|
||||
18
setup/web.sh
18
setup/web.sh
@@ -18,7 +18,11 @@ fi
|
||||
# Turn off nginx's default website.
|
||||
|
||||
echo "Installing Nginx (web server)..."
|
||||
apt_install nginx php5-fpm
|
||||
|
||||
apt_install nginx php7.0-cli php7.0-fpm
|
||||
|
||||
# Set PHP7 as the default
|
||||
update-alternatives --set php /usr/bin/php7.0
|
||||
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
@@ -40,15 +44,19 @@ tools/editconf.py /etc/nginx/nginx.conf -s \
|
||||
server_names_hash_bucket_size="128;"
|
||||
|
||||
# Tell PHP not to expose its version number in the X-Powered-By header.
|
||||
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
|
||||
tools/editconf.py /etc/php/7.0/fpm/php.ini -c ';' \
|
||||
expose_php=Off
|
||||
|
||||
# Set PHPs default charset to UTF-8, since we use it. See #367.
|
||||
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
|
||||
tools/editconf.py /etc/php/7.0/fpm/php.ini -c ';' \
|
||||
default_charset="UTF-8"
|
||||
|
||||
# Switch from the dynamic process manager to the ondemand manager see #1216
|
||||
tools/editconf.py /etc/php/7.0/fpm/pool.d/www.conf -c ';' \
|
||||
pm=ondemand
|
||||
|
||||
# Bump up PHP's max_children to support more concurrent connections
|
||||
tools/editconf.py /etc/php5/fpm/pool.d/www.conf -c ';' \
|
||||
tools/editconf.py /etc/php/7.0/fpm/pool.d/www.conf -c ';' \
|
||||
pm.max_children=8
|
||||
|
||||
# Other nginx settings will be configured by the management service
|
||||
@@ -103,7 +111,7 @@ done #NODOC
|
||||
|
||||
# Start services.
|
||||
restart_service nginx
|
||||
restart_service php5-fpm
|
||||
restart_service php7.0-fpm
|
||||
|
||||
# Open ports.
|
||||
ufw_allow http
|
||||
|
||||
131
setup/webmail.sh
131
setup/webmail.sh
@@ -22,8 +22,9 @@ source /etc/mailinabox.conf # load global vars
|
||||
echo "Installing Roundcube (webmail)..."
|
||||
apt_install \
|
||||
dbconfig-common \
|
||||
php5 php5-sqlite php5-mcrypt php5-intl php5-json php5-common php-auth php-net-smtp php-net-socket php-net-sieve php-mail-mime php-crypt-gpg php5-gd php5-pspell \
|
||||
tinymce libjs-jquery libjs-jquery-mousewheel libmagic1
|
||||
php7.0-cli php7.0-sqlite php7.0-mcrypt php7.0-intl php7.0-json php7.0-common php7.0-curl \
|
||||
php7.0-gd php7.0-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php7.0-mbstring
|
||||
|
||||
apt_get_quiet remove php-mail-mimedecode # no longer needed since Roundcube 1.1.3
|
||||
|
||||
# We used to install Roundcube from Ubuntu, without triggering the dependencies #NODOC
|
||||
@@ -32,14 +33,22 @@ apt_get_quiet remove php-mail-mimedecode # no longer needed since Roundcube 1.1.
|
||||
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.5
|
||||
HASH=8A59D196EF0AA6D9C717B00699215135ABCB99CF
|
||||
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
||||
PERSISTENT_LOGIN_VERSION=1e9d724476a370ce917a2fcd5b3217b0c306c24e
|
||||
# Combine the Roundcube version number with the commit hash of plugins to track
|
||||
# whether we have the latest version of everything.
|
||||
VERSION=1.3.6
|
||||
HASH=ece5cfc9c7af0cbe90c0065ef33e85ed42991830
|
||||
PERSISTENT_LOGIN_VERSION=dc5ca3d3f4415cc41edb2fde533c8a8628a94c76
|
||||
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
|
||||
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:a
|
||||
CARDDAV_VERSION=2.0.4
|
||||
CARDDAV_HASH=d93f3cfb3038a519e71c7c3212c1d16f5da609a4
|
||||
|
||||
UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION
|
||||
|
||||
# paths that are often reused.
|
||||
RCM_DIR=/usr/local/lib/roundcubemail
|
||||
RCM_PLUGIN_DIR=${RCM_DIR}/plugins
|
||||
RCM_CONFIG=${RCM_DIR}/config/config.inc.php
|
||||
|
||||
needs_update=0 #NODOC
|
||||
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
||||
# not installed yet #NODOC
|
||||
@@ -51,25 +60,32 @@ fi
|
||||
if [ $needs_update == 1 ]; then
|
||||
# install roundcube
|
||||
wget_verify \
|
||||
https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION.tar.gz \
|
||||
https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION-complete.tar.gz \
|
||||
$HASH \
|
||||
/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
|
||||
mv /usr/local/lib/roundcubemail-$VERSION/ $RCM_DIR
|
||||
rm -f /tmp/roundcube.tgz
|
||||
|
||||
# install roundcube autoreply/vacation plugin
|
||||
git_clone https://github.com/arodier/Roundcube-Plugins.git $VACATION_SIEVE_VERSION plugins/vacation_sieve /usr/local/lib/roundcubemail/plugins/vacation_sieve
|
||||
|
||||
# install roundcube persistent_login plugin
|
||||
git_clone https://github.com/mfreiholz/Roundcube-Persistent-Login-Plugin.git $PERSISTENT_LOGIN_VERSION '' /usr/local/lib/roundcubemail/plugins/persistent_login
|
||||
git_clone https://github.com/mfreiholz/Roundcube-Persistent-Login-Plugin.git $PERSISTENT_LOGIN_VERSION '' ${RCM_PLUGIN_DIR}/persistent_login
|
||||
|
||||
# install roundcube html5_notifier plugin
|
||||
git_clone https://github.com/kitist/html5_notifier.git $HTML5_NOTIFIER_VERSION '' /usr/local/lib/roundcubemail/plugins/html5_notifier
|
||||
git_clone https://github.com/kitist/html5_notifier.git $HTML5_NOTIFIER_VERSION '' ${RCM_PLUGIN_DIR}/html5_notifier
|
||||
|
||||
# download and verify the full release of the carddav plugin
|
||||
wget_verify \
|
||||
https://github.com/blind-coder/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-${CARDDAV_VERSION}.zip \
|
||||
$CARDDAV_HASH \
|
||||
/tmp/carddav.zip
|
||||
|
||||
# unzip and cleanup
|
||||
unzip -q /tmp/carddav.zip -d ${RCM_PLUGIN_DIR}
|
||||
rm -f /tmp/carddav.zip
|
||||
|
||||
# record the version we've installed
|
||||
echo $UPDATE_KEY > /usr/local/lib/roundcubemail/version
|
||||
echo $UPDATE_KEY > ${RCM_DIR}/version
|
||||
fi
|
||||
|
||||
# ### Configuring Roundcube
|
||||
@@ -82,65 +98,80 @@ SECRET_KEY=$(dd if=/dev/urandom bs=1 count=18 2>/dev/null | base64 | fold -w 24
|
||||
# For security, temp and log files are not stored in the default locations
|
||||
# which are inside the roundcube sources directory. We put them instead
|
||||
# in normal places.
|
||||
cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
|
||||
cat > $RCM_CONFIG <<EOF;
|
||||
<?php
|
||||
/*
|
||||
* Do not edit. Written by Mail-in-a-Box. Regenerated on updates.
|
||||
*/
|
||||
\$config = array();
|
||||
\$config['log_dir'] = '/var/log/roundcubemail/';
|
||||
\$config['temp_dir'] = '/tmp/roundcubemail/';
|
||||
\$config['temp_dir'] = '/var/tmp/roundcubemail/';
|
||||
\$config['db_dsnw'] = 'sqlite:///$STORAGE_ROOT/mail/roundcube/roundcube.sqlite?mode=0640';
|
||||
\$config['default_host'] = 'ssl://127.0.0.1';
|
||||
\$config['default_host'] = 'ssl://localhost';
|
||||
\$config['default_port'] = 993;
|
||||
\$config['imap_conn_options'] = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
),
|
||||
);
|
||||
\$config['imap_timeout'] = 15;
|
||||
\$config['smtp_server'] = 'tls://127.0.0.1';
|
||||
\$config['smtp_port'] = 587;
|
||||
\$config['smtp_user'] = '%u';
|
||||
\$config['smtp_pass'] = '%p';
|
||||
\$config['smtp_conn_options'] = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
),
|
||||
);
|
||||
\$config['support_url'] = 'https://mailinabox.email/';
|
||||
\$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';
|
||||
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
|
||||
\$config['skin'] = 'larry';
|
||||
\$config['login_autocomplete'] = 2;
|
||||
\$config['password_charset'] = 'UTF-8';
|
||||
\$config['junk_mbox'] = 'Spam';
|
||||
?>
|
||||
EOF
|
||||
|
||||
# Configure vaction_sieve.
|
||||
cat > /usr/local/lib/roundcubemail/plugins/vacation_sieve/config.inc.php <<EOF;
|
||||
# Configure CardDav
|
||||
cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
|
||||
<?php
|
||||
/* Do not edit. Written by Mail-in-a-Box. Regenerated on updates. */
|
||||
\$rcmail_config['vacation_sieve'] = array(
|
||||
'date_format' => 'd/m/Y',
|
||||
'working_hours' => array(8,18),
|
||||
'msg_format' => 'text',
|
||||
'logon_transform' => array('#([a-z])[a-z]+(\.|\s)([a-z])#i', '\$1\$3'),
|
||||
'transfer' => array(
|
||||
'mode' => 'managesieve',
|
||||
'ms_activate_script' => true,
|
||||
'host' => '127.0.0.1',
|
||||
'port' => '4190',
|
||||
'usetls' => false,
|
||||
'path' => 'vacation',
|
||||
)
|
||||
\$prefs['_GLOBAL']['hide_preferences'] = true;
|
||||
\$prefs['_GLOBAL']['suppress_version_warning'] = true;
|
||||
\$prefs['ownCloud'] = array(
|
||||
'name' => 'ownCloud',
|
||||
'username' => '%u', // login username
|
||||
'password' => '%p', // login password
|
||||
'url' => 'https://${PRIMARY_HOSTNAME}/cloud/remote.php/carddav/addressbooks/%u/contacts',
|
||||
'active' => true,
|
||||
'readonly' => false,
|
||||
'refresh_time' => '02:00:00',
|
||||
'fixed' => array('username','password'),
|
||||
'preemptive_auth' => '1',
|
||||
'hide' => false,
|
||||
);
|
||||
EOF
|
||||
|
||||
# Create writable directories.
|
||||
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
|
||||
mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
||||
chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
||||
|
||||
# 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...
|
||||
cp /usr/local/lib/roundcubemail/plugins/password/config.inc.php.dist \
|
||||
/usr/local/lib/roundcubemail/plugins/password/config.inc.php
|
||||
cp ${RCM_PLUGIN_DIR}/password/config.inc.php.dist \
|
||||
${RCM_PLUGIN_DIR}/password/config.inc.php
|
||||
|
||||
tools/editconf.py /usr/local/lib/roundcubemail/plugins/password/config.inc.php \
|
||||
"\$config['password_minimum_length']=6;" \
|
||||
tools/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php \
|
||||
"\$config['password_minimum_length']=8;" \
|
||||
"\$config['password_db_dsn']='sqlite:///$STORAGE_ROOT/mail/users.sqlite';" \
|
||||
"\$config['password_query']='UPDATE users SET password=%D WHERE email=%u';" \
|
||||
"\$config['password_dovecotpw']='/usr/bin/doveadm pw';" \
|
||||
@@ -157,6 +188,16 @@ chmod 775 $STORAGE_ROOT/mail
|
||||
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
|
||||
chmod 664 $STORAGE_ROOT/mail/users.sqlite
|
||||
|
||||
# Fix Carddav permissions:
|
||||
chown -f -R root.www-data ${RCM_PLUGIN_DIR}/carddav
|
||||
# root.www-data need all permissions, others only read
|
||||
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
|
||||
|
||||
# Run Roundcube database migration script (database is created if it does not exist)
|
||||
${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube
|
||||
chown www-data:www-data $STORAGE_ROOT/mail/roundcube/roundcube.sqlite
|
||||
chmod 664 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite
|
||||
|
||||
# Enable PHP modules.
|
||||
php5enmod mcrypt
|
||||
restart_service php5-fpm
|
||||
phpenmod -v php7.0 mcrypt imap
|
||||
restart_service php7.0-fpm
|
||||
|
||||
@@ -17,25 +17,32 @@ source /etc/mailinabox.conf # load global vars
|
||||
|
||||
echo "Installing Z-Push (Exchange/ActiveSync server)..."
|
||||
apt_install \
|
||||
php-soap php5-imap libawl-php php5-xsl
|
||||
php7.0-soap php7.0-imap libawl-php php7.0-xsl
|
||||
|
||||
php5enmod imap
|
||||
phpenmod -v php7.0 imap
|
||||
|
||||
# Copy Z-Push into place.
|
||||
TARGETHASH=80cbe53de4ab8dd598d1f2af6f0a23fa396c529a
|
||||
VERSION=2.3.9
|
||||
TARGETHASH=60087b97e4b1c73db096e252cf893c75df556907
|
||||
needs_update=0 #NODOC
|
||||
if [ ! -f /usr/local/lib/z-push/version ]; then
|
||||
needs_update=1 #NODOC
|
||||
elif [[ $TARGETHASH != `cat /usr/local/lib/z-push/version` ]]; then
|
||||
elif [[ $VERSION != `cat /usr/local/lib/z-push/version` ]]; then
|
||||
# checks if the version
|
||||
needs_update=1 #NODOC
|
||||
fi
|
||||
if [ $needs_update == 1 ]; then
|
||||
git_clone https://github.com/fmbiete/Z-Push-contrib $TARGETHASH '' /usr/local/lib/z-push
|
||||
wget_verify http://download.z-push.org/final/2.3/z-push-$VERSION.tar.gz $TARGETHASH /tmp/z-push.tar.gz
|
||||
|
||||
rm -rf /usr/local/lib/z-push
|
||||
tar -xzf /tmp/z-push.tar.gz -C /usr/local/lib/
|
||||
rm /tmp/z-push.tar.gz
|
||||
mv /usr/local/lib/z-push-$VERSION /usr/local/lib/z-push
|
||||
|
||||
rm -f /usr/sbin/z-push-{admin,top}
|
||||
ln -s /usr/local/lib/z-push/z-push-admin.php /usr/sbin/z-push-admin
|
||||
ln -s /usr/local/lib/z-push/z-push-top.php /usr/sbin/z-push-top
|
||||
echo $TARGETHASH > /usr/local/lib/z-push/version
|
||||
echo $VERSION > /usr/local/lib/z-push/version
|
||||
fi
|
||||
|
||||
# Configure default config.
|
||||
@@ -53,6 +60,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
|
||||
@@ -66,6 +74,7 @@ cp conf/zpush/backend_caldav.php /usr/local/lib/z-push/backend/caldav/config.php
|
||||
rm -f /usr/local/lib/z-push/autodiscover/config.php
|
||||
cp conf/zpush/autodiscover_config.php /usr/local/lib/z-push/autodiscover/config.php
|
||||
sed -i "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" /usr/local/lib/z-push/autodiscover/config.php
|
||||
sed -i "s^define('TIMEZONE', .*^define('TIMEZONE', '$(cat /etc/timezone)');^" /usr/local/lib/z-push/autodiscover/config.php
|
||||
|
||||
# Some directories it will use.
|
||||
|
||||
@@ -91,4 +100,8 @@ EOF
|
||||
|
||||
# Restart service.
|
||||
|
||||
restart_service php5-fpm
|
||||
restart_service php7.0-fpm
|
||||
|
||||
# Fix states after upgrade
|
||||
|
||||
hide_output z-push-admin -a fixstates
|
||||
|
||||
221
tests/fail2ban.py
Normal file
221
tests/fail2ban.py
Normal 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)
|
||||
@@ -61,9 +61,9 @@ common_opts = ["--sslv2", "--sslv3", "--tlsv1", "--tlsv1_1", "--tlsv1_2", "--ren
|
||||
# Assumes TLSv1, TLSv1.1, TLSv1.2.
|
||||
#
|
||||
# The 'old' ciphers bring compatibility back to Win XP IE 6.
|
||||
MOZILLA_CIPHERS_MODERN = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK"
|
||||
MOZILLA_CIPHERS_INTERMEDIATE = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
|
||||
MOZILLA_CIPHERS_OLD = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
|
||||
MOZILLA_CIPHERS_MODERN = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
|
||||
MOZILLA_CIPHERS_INTERMEDIATE = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"
|
||||
MOZILLA_CIPHERS_OLD = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP"
|
||||
|
||||
######################################################################
|
||||
|
||||
|
||||
@@ -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: (none -- good)
|
||||
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, DHE-DSS-SEED-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
|
||||
Supported Clients: BingPreview/Jan 2015, OpenSSL/1.0.2, Yahoo Slurp/Jan 2015, OpenSSL/1.0.1l, YandexBot/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/iOS 8.1.2, Safari/6/iOS 6.0.1, Safari/7/OS X 10.9, Safari/8/OS X 10.10, Baidu/Jan 2015, Firefox/31.3.0 ESR/Win 7, IE/11/Win 7, IE/11/Win 8.1, IE Mobile/11/Win Phone 8.1, Java/8u31, Android/5.0.0, Googlebot/Feb 2015, Chrome/42/OS X, Android/4.1.1, Android/4.3, Android/4.0.4, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, Safari/6.0.4/OS X 10.8.4, Firefox/37/OS X, OpenSSL/0.9.8y, Java/7u25, IE Mobile/10/Win Phone 8.0, IE/8-10/Win 7, IE/7/Vista, 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
|
||||
|
||||
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
|
||||
* 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-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA, DHE-RSA-SEED-SHA, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA, SEED-SHA
|
||||
Could Also Offer: ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA384
|
||||
Supported Clients: BingPreview/Jan 2015, OpenSSL/1.0.2, Yahoo Slurp/Jan 2015, OpenSSL/1.0.1l, YandexBot/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, IE/11/Win 7, IE/11/Win 8.1, Safari/8/iOS 8.1.2, Safari/6/iOS 6.0.1, Safari/7/OS X 10.9, IE Mobile/11/Win Phone 8.1, Safari/8/OS X 10.10, Baidu/Jan 2015, Firefox/31.3.0 ESR/Win 7, Java/8u31, Android/5.0.0, Chrome/42/OS X, Googlebot/Feb 2015, Firefox/37/OS X, Android/4.1.1, Android/4.3, Android/4.0.4, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, Safari/6.0.4/OS X 10.8.4, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, IE/8-10/Win 7, IE/7/Vista, Java/7u25, Android/2.3.7, Java/6u45
|
||||
|
||||
PORT 443
|
||||
--------
|
||||
@@ -226,22 +197,22 @@ PORT 443
|
||||
Client-initiated Renegotiations: OK - Rejected
|
||||
Secure Renegotiation: OK - Supported
|
||||
|
||||
* OpenSSL Heartbleed:
|
||||
OK - Not vulnerable to Heartbleed
|
||||
|
||||
* HTTP Strict Transport Security:
|
||||
OK - HSTS header received: max-age=31536000
|
||||
OK - HSTS header received: max-age=15768000
|
||||
|
||||
Unhandled exception when processing --chrome_sha1:
|
||||
exceptions.TypeError - Incorrect padding
|
||||
|
||||
* 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
|
||||
|
||||
* 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
|
||||
@@ -252,12 +223,20 @@ PORT 443
|
||||
DHE-RSA-AES256-SHA256 DH-2048 bits 256 bits HTTP 200 OK
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits HTTP 200 OK
|
||||
DHE-RSA-AES256-GCM-SHA384 DH-2048 bits 256 bits HTTP 200 OK
|
||||
AES256-SHA256 - 256 bits HTTP 200 OK
|
||||
AES256-SHA - 256 bits HTTP 200 OK
|
||||
AES256-GCM-SHA384 - 256 bits HTTP 200 OK
|
||||
ECDHE-RSA-AES128-SHA256 ECDH-256 bits 128 bits HTTP 200 OK
|
||||
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits HTTP 200 OK
|
||||
ECDHE-RSA-AES128-GCM-SHA256 ECDH-256 bits 128 bits HTTP 200 OK
|
||||
DHE-RSA-AES128-SHA256 DH-2048 bits 128 bits HTTP 200 OK
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
|
||||
DHE-RSA-AES128-GCM-SHA256 DH-2048 bits 128 bits HTTP 200 OK
|
||||
AES128-SHA256 - 128 bits HTTP 200 OK
|
||||
AES128-SHA - 128 bits HTTP 200 OK
|
||||
AES128-GCM-SHA256 - 128 bits HTTP 200 OK
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-256 bits 112 bits HTTP 200 OK
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits HTTP 200 OK
|
||||
DES-CBC3-SHA - 112 bits HTTP 200 OK
|
||||
|
||||
* TLSV1_1 Cipher Suites:
|
||||
@@ -266,26 +245,34 @@ PORT 443
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits HTTP 200 OK
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits HTTP 200 OK
|
||||
AES256-SHA - 256 bits HTTP 200 OK
|
||||
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits HTTP 200 OK
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
|
||||
AES128-SHA - 128 bits HTTP 200 OK
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-256 bits 112 bits HTTP 200 OK
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 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
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits HTTP 200 OK
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits HTTP 200 OK
|
||||
AES256-SHA - 256 bits HTTP 200 OK
|
||||
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits HTTP 200 OK
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
|
||||
AES128-SHA - 128 bits HTTP 200 OK
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-256 bits 112 bits HTTP 200 OK
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 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: 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
|
||||
Supported Clients: BingPreview/Jan 2015, OpenSSL/1.0.2, YandexBot/Jan 2015, OpenSSL/1.0.1l, Yahoo Slurp/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/iOS 8.1.2, Safari/6/iOS 6.0.1, Safari/7/OS X 10.9, Safari/8/OS X 10.10, IE/11/Win 7, IE/11/Win 8.1, IE Mobile/11/Win Phone 8.1, Java/8u31, Android/5.0.0, Googlebot/Feb 2015, Firefox/31.3.0 ESR/Win 7, Chrome/42/OS X, Baidu/Jan 2015, Android/4.1.1, Android/4.3, Android/4.0.4, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, Safari/6.0.4/OS X 10.8.4, Firefox/37/OS X, OpenSSL/0.9.8y, Java/7u25, IE Mobile/10/Win Phone 8.0, IE/8-10/Win 7, IE/7/Vista, Java/6u45, Android/2.3.7, IE/8/XP
|
||||
|
||||
PORT 993
|
||||
--------
|
||||
@@ -308,55 +295,64 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
||||
|
||||
* TLSV1_2 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
ECDHE-RSA-AES128-GCM-SHA256 ECDH-384 bits 128 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA384 ECDH-384 bits 256 bits
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-CAMELLIA256-SHA DH-1024 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-1024 bits 256 bits
|
||||
CAMELLIA256-SHA - 256 bits
|
||||
ECDHE-RSA-AES256-GCM-SHA384 ECDH-384 bits 256 bits
|
||||
DHE-RSA-AES256-SHA256 DH-2048 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits
|
||||
DHE-RSA-AES256-GCM-SHA384 DH-2048 bits 256 bits
|
||||
AES256-SHA256 - 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
AES256-GCM-SHA384 - 256 bits
|
||||
ECDHE-RSA-AES128-SHA256 ECDH-384 bits 128 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||
CAMELLIA128-SHA - 128 bits
|
||||
ECDHE-RSA-AES128-GCM-SHA256 ECDH-384 bits 128 bits
|
||||
DHE-RSA-AES128-SHA256 DH-2048 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits
|
||||
DHE-RSA-AES128-GCM-SHA256 DH-2048 bits 128 bits
|
||||
AES128-SHA256 - 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
AES128-GCM-SHA256 - 128 bits
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-384 bits 112 bits
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits
|
||||
DES-CBC3-SHA - 112 bits
|
||||
|
||||
* TLSV1_1 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-CAMELLIA256-SHA DH-1024 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-1024 bits 256 bits
|
||||
CAMELLIA256-SHA - 256 bits
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||
CAMELLIA128-SHA - 128 bits
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-384 bits 112 bits
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits
|
||||
DES-CBC3-SHA - 112 bits
|
||||
|
||||
* TLSV1 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-384 bits 112 bits
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits
|
||||
DES-CBC3-SHA - 112 bits
|
||||
|
||||
* SSLV3 Cipher Suites:
|
||||
Server rejected all cipher suites.
|
||||
|
||||
* TLSV1 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-CAMELLIA256-SHA DH-1024 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-1024 bits 256 bits
|
||||
CAMELLIA256-SHA - 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||
CAMELLIA128-SHA - 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
|
||||
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
|
||||
Should Not Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, DES-CBC3-SHA, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||
Could Also Offer: ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA384
|
||||
Supported Clients: BingPreview/Jan 2015, OpenSSL/1.0.2, YandexBot/Jan 2015, OpenSSL/1.0.1l, Yahoo Slurp/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/iOS 8.1.2, Safari/6/iOS 6.0.1, Safari/7/OS X 10.9, Safari/8/OS X 10.10, IE/11/Win 7, IE/11/Win 8.1, IE Mobile/11/Win Phone 8.1, Java/8u31, Android/5.0.0, Googlebot/Feb 2015, Firefox/31.3.0 ESR/Win 7, Chrome/42/OS X, Baidu/Jan 2015, Android/4.1.1, Android/4.3, Android/4.0.4, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, Safari/6.0.4/OS X 10.8.4, Firefox/37/OS X, OpenSSL/0.9.8y, Java/7u25, IE Mobile/10/Win Phone 8.0, IE/8-10/Win 7, IE/7/Vista, Java/6u45, Android/2.3.7, IE/8/XP
|
||||
|
||||
PORT 995
|
||||
--------
|
||||
@@ -379,53 +375,62 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
||||
|
||||
* TLSV1_2 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
ECDHE-RSA-AES128-GCM-SHA256 ECDH-384 bits 128 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA384 ECDH-384 bits 256 bits
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-CAMELLIA256-SHA DH-1024 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-1024 bits 256 bits
|
||||
CAMELLIA256-SHA - 256 bits
|
||||
ECDHE-RSA-AES256-GCM-SHA384 ECDH-384 bits 256 bits
|
||||
DHE-RSA-AES256-SHA256 DH-2048 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits
|
||||
DHE-RSA-AES256-GCM-SHA384 DH-2048 bits 256 bits
|
||||
AES256-SHA256 - 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
AES256-GCM-SHA384 - 256 bits
|
||||
ECDHE-RSA-AES128-SHA256 ECDH-384 bits 128 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||
CAMELLIA128-SHA - 128 bits
|
||||
ECDHE-RSA-AES128-GCM-SHA256 ECDH-384 bits 128 bits
|
||||
DHE-RSA-AES128-SHA256 DH-2048 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits
|
||||
DHE-RSA-AES128-GCM-SHA256 DH-2048 bits 128 bits
|
||||
AES128-SHA256 - 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
AES128-GCM-SHA256 - 128 bits
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-384 bits 112 bits
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits
|
||||
DES-CBC3-SHA - 112 bits
|
||||
|
||||
* TLSV1_1 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-CAMELLIA256-SHA DH-1024 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-1024 bits 256 bits
|
||||
CAMELLIA256-SHA - 256 bits
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||
CAMELLIA128-SHA - 128 bits
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-384 bits 112 bits
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits
|
||||
DES-CBC3-SHA - 112 bits
|
||||
|
||||
* TLSV1 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
ECDHE-RSA-DES-CBC3-SHA ECDH-384 bits 112 bits
|
||||
EDH-RSA-DES-CBC3-SHA DH-2048 bits 112 bits
|
||||
DES-CBC3-SHA - 112 bits
|
||||
|
||||
* SSLV3 Cipher Suites:
|
||||
Server rejected all cipher suites.
|
||||
|
||||
* TLSV1 Cipher Suites:
|
||||
Preferred:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
Accepted:
|
||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||
DHE-RSA-CAMELLIA256-SHA DH-1024 bits 256 bits
|
||||
DHE-RSA-AES256-SHA DH-1024 bits 256 bits
|
||||
CAMELLIA256-SHA - 256 bits
|
||||
AES256-SHA - 256 bits
|
||||
ECDHE-RSA-AES128-SHA ECDH-384 bits 128 bits
|
||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||
CAMELLIA128-SHA - 128 bits
|
||||
AES128-SHA - 128 bits
|
||||
|
||||
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
|
||||
Should Not Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, DES-CBC3-SHA, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||
Could Also Offer: ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA384
|
||||
Supported Clients: BingPreview/Jan 2015, OpenSSL/1.0.2, YandexBot/Jan 2015, OpenSSL/1.0.1l, Yahoo Slurp/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/iOS 8.1.2, Safari/6/iOS 6.0.1, Safari/7/OS X 10.9, Safari/8/OS X 10.10, IE/11/Win 7, IE/11/Win 8.1, IE Mobile/11/Win Phone 8.1, Java/8u31, Android/5.0.0, Googlebot/Feb 2015, Firefox/31.3.0 ESR/Win 7, Chrome/42/OS X, Baidu/Jan 2015, Android/4.1.1, Android/4.3, Android/4.0.4, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, Safari/6.0.4/OS X 10.8.4, Firefox/37/OS X, OpenSSL/0.9.8y, Java/7u25, IE Mobile/10/Win Phone 8.0, IE/8-10/Win 7, IE/7/Vista, Java/6u45, Android/2.3.7, IE/8/XP
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ def mgmt(cmd, data=None, is_json=False):
|
||||
def read_password():
|
||||
while True:
|
||||
first = getpass.getpass('password: ')
|
||||
if len(first) < 4:
|
||||
print("Passwords must be at least four characters.")
|
||||
if len(first) < 8:
|
||||
print("Passwords must be at least eight characters.")
|
||||
continue
|
||||
if re.search(r'[\s]', first):
|
||||
print("Passwords cannot contain spaces.")
|
||||
|
||||
51
tools/owncloud-restore.sh
Executable file
51
tools/owncloud-restore.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/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
|
||||
service php7.0-fpm stop
|
||||
|
||||
# remove the current ownCloud/Nextcloud installation
|
||||
rm -rf /usr/local/lib/owncloud/
|
||||
# restore the current ownCloud/Nextcloud 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
|
||||
service php7.0-fpm start
|
||||
echo "Done"
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# This script will give you administrative access to the ownCloud
|
||||
# This script will give you administrative access to the Nextcloud
|
||||
# instance running here.
|
||||
#
|
||||
# Run this at your own risk. This is for testing & experimentation
|
||||
@@ -14,10 +14,10 @@ test -z "$1" || ADMIN=$1
|
||||
echo I am going to unlock admin features for $ADMIN.
|
||||
echo You can provide another user to unlock as the first argument of this script.
|
||||
echo
|
||||
echo WARNING: you could break mail-in-a-box when fiddling around with owncloud\'s admin interface
|
||||
echo WARNING: you could break mail-in-a-box when fiddling around with Nextcloud\'s admin interface
|
||||
echo If in doubt, press CTRL-C to cancel.
|
||||
echo
|
||||
echo Press enter to continue.
|
||||
read
|
||||
|
||||
sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$ADMIN')" && echo Done.
|
||||
sudo -u www-data php /usr/local/lib/owncloud/occ group:adduser admin $ADMIN && echo Done.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# Updates subresource integrity attributes in management/templates/index.html
|
||||
# to prevent CDN-hosted resources from being used as an attack vector. Run this
|
||||
# after updating the Bootstrap and jQuery <link> and <script> to compute the
|
||||
# appropriate hash and insert it into the template.
|
||||
|
||||
import re, urllib.request, hashlib, base64
|
||||
|
||||
fn = "management/templates/index.html"
|
||||
|
||||
with open(fn, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
def make_integrity(url):
|
||||
resource = urllib.request.urlopen(url).read()
|
||||
return "sha256-" + base64.b64encode(hashlib.sha256(resource).digest()).decode('ascii')
|
||||
|
||||
content = re.sub(
|
||||
r'<(link rel="stylesheet" href|script src)="(.*?)" integrity="(.*?)"',
|
||||
lambda m : '<' + m.group(1) + '="' + m.group(2) + '" integrity="' + make_integrity(m.group(2)) + '"',
|
||||
content)
|
||||
|
||||
with open(fn, 'w') as f:
|
||||
f.write(content)
|
||||
Reference in New Issue
Block a user