mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-12 17:07:23 +01:00
Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdd0a821eb | ||
|
|
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 | ||
|
|
94b7c80792 | ||
|
|
69bd137b4e | ||
|
|
ae8cd4efdf | ||
|
|
6d259a6e12 | ||
|
|
e7fffc66c7 | ||
|
|
8548ede638 | ||
|
|
6eeb107ee3 | ||
|
|
31eefa18da | ||
|
|
20adbb51cb | ||
|
|
79a39d86f9 | ||
|
|
0ebf33e9df | ||
|
|
d3818d1db6 | ||
|
|
f65d9d3196 | ||
|
|
74fea6b93e | ||
|
|
7a935d8385 | ||
|
|
7e0f534aea | ||
|
|
736b3de221 | ||
|
|
5628f8eecb | ||
|
|
9cc5160c38 | ||
|
|
42f2e983e5 | ||
|
|
bc40134b7b | ||
|
|
3649ba1ce9 | ||
|
|
22395bdb8b | ||
|
|
30c89be982 | ||
|
|
853b641d1b | ||
|
|
703a963ae5 | ||
|
|
c9f30e8059 | ||
|
|
1a1d125b31 | ||
|
|
86881c0107 | ||
|
|
e65c77588e | ||
|
|
3843f63416 | ||
|
|
703e6795e8 | ||
|
|
b3223136f4 | ||
|
|
aa1fdaddaf | ||
|
|
7fa9baf308 | ||
|
|
36d51bbde0 | ||
|
|
eb8cfaab75 | ||
|
|
c5e8a975cd | ||
|
|
3210ccdcac | ||
|
|
252c35c66e | ||
|
|
c910a58f07 | ||
|
|
f292e8fc5b | ||
|
|
4d7229ccb0 | ||
|
|
1e1c3cbd00 | ||
|
|
611e9cc84d | ||
|
|
454a2b167b | ||
|
|
f6e0af124f | ||
|
|
d7d8bda0a4 | ||
|
|
df92a10eba | ||
|
|
74a0359cec | ||
|
|
336b95b3d5 | ||
|
|
56591abbc2 | ||
|
|
313a86d0fa | ||
|
|
083e3cf755 | ||
|
|
696bbe4e82 | ||
|
|
3d4cabbcd5 | ||
|
|
cdedaed3b0 | ||
|
|
c01f903413 | ||
|
|
5edefbec27 | ||
|
|
67555679bd | ||
|
|
546d6f0026 | ||
|
|
bd86d44c8b | ||
|
|
72fcb005b2 | ||
|
|
84638ab11e | ||
|
|
84f4509b48 | ||
|
|
35a593af13 | ||
|
|
f69d6e9015 | ||
|
|
44705a32b7 | ||
|
|
e343061cf4 | ||
|
|
65add24e2a | ||
|
|
33a9fb6aa2 | ||
|
|
0bc5d20e8f | ||
|
|
49ea9cddd1 | ||
|
|
6a48cdcdf3 | ||
|
|
f78f039776 | ||
|
|
d881487d68 | ||
|
|
33d07b2b54 | ||
|
|
3bbec18ac6 | ||
|
|
fc5c198646 | ||
|
|
2be373fd06 | ||
|
|
b71ad85e9f | ||
|
|
86d3e9da86 | ||
|
|
f53d3bc390 | ||
|
|
f9ca440ce8 | ||
|
|
8ea2f5a766 | ||
|
|
6c1357e16c | ||
|
|
d880f088be | ||
|
|
5cabfd591b | ||
|
|
721730f0e8 | ||
|
|
af80849857 | ||
|
|
7a191e67b8 | ||
|
|
4b2e48f2c0 | ||
|
|
eb545d7941 | ||
|
|
a2e6e81697 | ||
|
|
1b24e2cbaf | ||
|
|
0843159fb4 | ||
|
|
a7e60af93f | ||
|
|
42f879687f | ||
|
|
057903a303 | ||
|
|
b8e99c30a2 | ||
|
|
3d933c16d0 | ||
|
|
e785886447 | ||
|
|
23ecff04b8 | ||
|
|
a0bae5db5c | ||
|
|
86368ed165 | ||
|
|
5e4c0ed825 | ||
|
|
ffa9dc5d67 | ||
|
|
43cb6c4995 | ||
|
|
36cb2ef41d | ||
|
|
098e250cc4 | ||
|
|
3d5a35b184 | ||
|
|
87d3f2641d | ||
|
|
c6c75c5a17 | ||
|
|
1ba44b02d4 | ||
|
|
6fd4cd85ca | ||
|
|
6182347641 | ||
|
|
401b0526a3 | ||
|
|
2f24328608 | ||
|
|
8ea42847da | ||
|
|
4ed23f44e6 | ||
|
|
178527dab1 | ||
|
|
f5c376dca8 | ||
|
|
239eac662c | ||
|
|
4e18f66db6 | ||
|
|
77937df955 | ||
|
|
4db8efa0df | ||
|
|
66c80bd16a | ||
|
|
5895aeecd7 | ||
|
|
83ffc99b9c | ||
|
|
3615772b2d | ||
|
|
78729bd277 | ||
|
|
85a9a1608c | ||
|
|
2e693f7011 | ||
|
|
6f0220da4b | ||
|
|
09a45b4397 | ||
|
|
6b408ef824 | ||
|
|
62b6117638 | ||
|
|
70111dafbc | ||
|
|
faaa74c3a7 | ||
|
|
8932aaf4ef | ||
|
|
6d6f3ea391 | ||
|
|
2ad7d0830e | ||
|
|
5045e206c2 | ||
|
|
07f9228694 | ||
|
|
50b5b91216 | ||
|
|
72bfc0915c | ||
|
|
a7d7a9adbd | ||
|
|
413af1fe67 | ||
|
|
b09cbb0ca4 | ||
|
|
5b135738b4 | ||
|
|
36e5772a8e | ||
|
|
2b9fb9643d | ||
|
|
2882e63dd8 | ||
|
|
812ef024ef | ||
|
|
40cdc5aa30 | ||
|
|
b8d6226a9a | ||
|
|
bac15d3919 | ||
|
|
4b4f670adf | ||
|
|
b1b57f9bfd | ||
|
|
b6933a73fa | ||
|
|
5033042b8c | ||
|
|
89a46089ee | ||
|
|
e288d7730b | ||
|
|
5ae75e723c | ||
|
|
d6d5009d23 | ||
|
|
06a0e7f3fe | ||
|
|
a9cd72bbf9 | ||
|
|
f184a74fa0 | ||
|
|
682b1dea5e | ||
|
|
2835d9b468 | ||
|
|
afd401c3d4 | ||
|
|
59f8aa1c31 |
153
CHANGELOG.md
153
CHANGELOG.md
@@ -1,6 +1,159 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
v0.19 (August 13, 2016)
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
|
||||||
|
* Roundcube is updated to version 1.2.1.
|
||||||
|
* SSLv3 and RC4 are now no longer supported in incoming and outgoing mail (SMTP port 25).
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* The users and aliases APIs are now documented on their control panel pages.
|
||||||
|
* The HSTS header was missing.
|
||||||
|
* New status checks were added for the ufw firewall.
|
||||||
|
|
||||||
|
DNS:
|
||||||
|
|
||||||
|
* Add SRV records for CardDAV/CalDAV to facilitate autoconfiguration (e.g. in DavDroid, whose latest version didn't seem to work to configure with entering just a hostname).
|
||||||
|
|
||||||
|
System:
|
||||||
|
|
||||||
|
* fail2ban jails added for SMTP submission, Roundcube, ownCloud, the control panel, and munin.
|
||||||
|
* Mail-in-a-Box can now be installed on the i686 architecture.
|
||||||
|
|
||||||
|
v0.18c (June 2, 2016)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* Domain aliases (and misconfigured aliases/catch-alls with non-existent local targets) would accept mail and deliver it to new mailbox folders on disk even if the target address didn't correspond with an existing mail user, instead of rejecting the mail. This issue was introduced in v0.18.
|
||||||
|
* The Munin Monitoring link in the control panel now opens a new window.
|
||||||
|
* Added an undocumented before-backup script.
|
||||||
|
|
||||||
|
v0.18b (May 16, 2016)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* Fixed a Roundcube user accounts issue introduced in v0.18.
|
||||||
|
|
||||||
|
v0.18 (May 15, 2016)
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
ownCloud:
|
||||||
|
|
||||||
|
* Updated to ownCloud to 8.2.3
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
|
||||||
|
* Roundcube is updated to version 1.1.5 and the Roundcube login screen now says "[hostname] Webmail" instead of "Mail-in-a-Box/Roundcube webmail".
|
||||||
|
* Fixed a long-standing issue with training the spam filter not working (because of a file permissions issue).
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* Munin system monitoring graphs are now zoomable.
|
||||||
|
* When a reboot is required (due to Ubuntu security updates automatically installed), a Reboot Box button now appears on the System Status Checks page of the control panel.
|
||||||
|
* It is now possible to add SRV and secondary MX records in the Custom DNS page.
|
||||||
|
* Other minor fixes.
|
||||||
|
|
||||||
|
System:
|
||||||
|
|
||||||
|
* The fail2ban recidive jail, which blocks long-duration brute force attacks, now no longer sends the administrator emails (which were not helpful).
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
* The system hostname is now set during setup.
|
||||||
|
* A swap file is now created if system memory is less than 2GB, 5GB of free disk space is available, and if no swap file yet exists.
|
||||||
|
* We now install Roundcube from the official GitHub repository instead of our own mirror, which we had previously created to solve problems with SourceForge.
|
||||||
|
* DKIM was incorrectly set up on machines where "localhost" was defined as something other than "127.0.0.1".
|
||||||
|
|
||||||
|
v0.17c (April 1, 2016)
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
This update addresses some minor security concerns and some installation issues.
|
||||||
|
|
||||||
|
ownCoud:
|
||||||
|
|
||||||
|
* Block web access to the configuration parameters (config.php). There is no immediate impact (see [#776](https://github.com/mail-in-a-box/mailinabox/pull/776)), although advanced users may want to take note.
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
|
||||||
|
* Roundcube html5_notifier plugin updated from version 0.6 to 0.6.2 to fix Roundcube getting stuck for some people.
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* Prevent click-jacking of the management interface by adding HTTP headers.
|
||||||
|
* Failed login no longer reveals whether an account exists on the system.
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
* Setup dialogs did not appear correctly when connecting to SSH using Putty on Windows.
|
||||||
|
* We now install Roundcube from our own mirror because Sourceforge's downloads experience frequent intermittant unavailability.
|
||||||
|
|
||||||
|
v0.17b (March 1, 2016)
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
ownCloud moved their source code to a new location, breaking our installation script.
|
||||||
|
|
||||||
|
v0.17 (February 25, 2016)
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
|
||||||
|
* Roundcube updated to version 1.1.4.
|
||||||
|
* When there's a problem delivering an outgoing message, a new 'warning' bounce will come after 3 hours and the box will stop trying after 2 days (instead of 5).
|
||||||
|
* On multi-homed machines, Postfix now binds to the right network interface when sending outbound mail so that SPF checks on the receiving end will pass.
|
||||||
|
* Mail sent from addresses on subdomains of other domains hosted by this box would not be DKIM-signed and so would fail DMARC checks by recipients, since version v0.15.
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* TLS certificate provisioning would crash if DNS propagation was in progress and a challenge failed; might have shown the wrong error when provisioning fails.
|
||||||
|
* Backup times were displayed with the wrong time zone.
|
||||||
|
* Thresholds for displaying messages when the system is running low on memory have been reduced from 30% to 20% for a warning and from 15% to 10% for an error.
|
||||||
|
* Other minor fixes.
|
||||||
|
|
||||||
|
System:
|
||||||
|
|
||||||
|
* Backups to some AWS S3 regions broke in version 0.15 because we reverted the version of boto. That's now fixed.
|
||||||
|
* On low-usage systems, don't hold backups for quite so long by taking a full backup more often.
|
||||||
|
* Nightly status checks might fail on systems not configured with a default Unicode locale.
|
||||||
|
* If domains need a TLS certificate and the user hasn't installed one yet using Let's Encrypt, the administrator would get a nightly email with weird interactive text asking them to agree to Let's Encrypt's ToS. Now just say that the provisioning can't be done automatically.
|
||||||
|
* Reduce the number of background processes used by the management daemon to lower memory consumption.
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
* The first screen now warns users not to install on a machine used for other things.
|
||||||
|
|
||||||
|
v0.16 (January 30, 2016)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
This update primarily adds automatic SSL (now "TLS") certificate provisioning from Let's Encrypt (https://letsencrypt.org/).
|
||||||
|
* The Sieve port is now open so tools like the Thunderbird Sieve program can be used to edit mail filters.
|
||||||
|
|
||||||
|
Control Panel:
|
||||||
|
|
||||||
|
* The SSL certificates (now referred to as "TLS ccertificates") page now supports provisioning free certificates from Let's Encrypt.
|
||||||
|
* Report free memory usage.
|
||||||
|
* Fix a crash when the git directory is not checked out to a tag.
|
||||||
|
* When IPv6 is enabled, check that all domains (besides the system hostname) resolve over IPv6.
|
||||||
|
* When a domain doesn't resolve to the box, don't bother checking if the TLS certificate is valid.
|
||||||
|
* Remove rounded border on the menu bar.
|
||||||
|
|
||||||
|
Other:
|
||||||
|
|
||||||
|
* The Sieve port is now open so tools like the Thunderbird Sieve extension can be used to edit mail filters.
|
||||||
|
* .be domains now offer DNSSEC options supported by the TLD
|
||||||
|
* The daily backup will now email the administrator if there is a problem.
|
||||||
|
* Expiring TLS certificates are now automatically renewed via Let's Encrypt.
|
||||||
|
* File ownership for installed Roundcube files is fixed.
|
||||||
|
* Typos fixed.
|
||||||
|
|
||||||
|
v0.15a (January 9, 2016)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
|
||||||
|
* Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14 in some setups. This is now fixed.
|
||||||
|
|
||||||
v0.15 (January 1, 2016)
|
v0.15 (January 1, 2016)
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -59,20 +59,20 @@ by me:
|
|||||||
$ curl -s https://keybase.io/joshdata/key.asc | gpg --import
|
$ curl -s https://keybase.io/joshdata/key.asc | gpg --import
|
||||||
gpg: key C10BDD81: public key "Joshua Tauberer <jt@occams.info>" imported
|
gpg: key C10BDD81: public key "Joshua Tauberer <jt@occams.info>" imported
|
||||||
|
|
||||||
$ git verify-tag v0.15
|
$ git verify-tag v0.19
|
||||||
gpg: Signature made ..... using RSA key ID C10BDD81
|
gpg: Signature made ..... using RSA key ID C10BDD81
|
||||||
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
|
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
|
||||||
gpg: WARNING: This key is not certified with a trusted signature!
|
gpg: WARNING: This key is not certified with a trusted signature!
|
||||||
gpg: There is no indication that the signature belongs to the owner.
|
gpg: There is no indication that the signature belongs to the owner.
|
||||||
Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81
|
Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81
|
||||||
|
|
||||||
You'll get a lot of warnings, but that's OK. Check that the primary key fingerprint matchs the
|
You'll get a lot of warnings, but that's OK. Check that the primary key fingerprint matches the
|
||||||
fingerprint in the key details at [https://keybase.io/joshdata](https://keybase.io/joshdata)
|
fingerprint in the key details at [https://keybase.io/joshdata](https://keybase.io/joshdata)
|
||||||
and on my [personal homepage](https://razor.occams.info/). (Of course, if this repository has been compromised you can't trust these instructions.)
|
and on my [personal homepage](https://razor.occams.info/). (Of course, if this repository has been compromised you can't trust these instructions.)
|
||||||
|
|
||||||
Checkout the tag corresponding to the most recent release:
|
Checkout the tag corresponding to the most recent release:
|
||||||
|
|
||||||
$ git checkout v0.15
|
$ git checkout v0.19
|
||||||
|
|
||||||
Begin the installation.
|
Begin the installation.
|
||||||
|
|
||||||
|
|||||||
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,29 +0,0 @@
|
|||||||
# Fail2Ban configuration file for Mail-in-a-Box
|
|
||||||
|
|
||||||
[DEFAULT]
|
|
||||||
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
|
||||||
# ping services over the public interface so we should whitelist that address of
|
|
||||||
# ours too. The string is substituted during installation.
|
|
||||||
ignoreip = 127.0.0.1/8 PUBLIC_IP
|
|
||||||
|
|
||||||
# JAILS
|
|
||||||
|
|
||||||
[ssh]
|
|
||||||
maxretry = 7
|
|
||||||
bantime = 3600
|
|
||||||
|
|
||||||
[ssh-ddos]
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[sasl]
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[dovecot]
|
|
||||||
enabled = true
|
|
||||||
filter = dovecotimap
|
|
||||||
findtime = 30
|
|
||||||
maxretry = 20
|
|
||||||
|
|
||||||
[recidive]
|
|
||||||
enabled = true
|
|
||||||
maxretry = 10
|
|
||||||
80
conf/fail2ban/jails.conf
Normal file
80
conf/fail2ban/jails.conf
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Fail2Ban configuration file for Mail-in-a-Box. Do not edit.
|
||||||
|
# This file is re-generated on updates.
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
||||||
|
# ping services over the public interface so we should whitelist that address of
|
||||||
|
# ours too. The string is substituted during installation.
|
||||||
|
ignoreip = 127.0.0.1/8 PUBLIC_IP
|
||||||
|
|
||||||
|
[dovecot]
|
||||||
|
enabled = true
|
||||||
|
filter = dovecotimap
|
||||||
|
logpath = /var/log/mail.log
|
||||||
|
findtime = 30
|
||||||
|
maxretry = 20
|
||||||
|
|
||||||
|
[miab-management]
|
||||||
|
enabled = true
|
||||||
|
filter = miab-management-daemon
|
||||||
|
port = http,https
|
||||||
|
logpath = /var/log/syslog
|
||||||
|
maxretry = 20
|
||||||
|
findtime = 30
|
||||||
|
|
||||||
|
[miab-munin]
|
||||||
|
enabled = true
|
||||||
|
port = http,https
|
||||||
|
filter = miab-munin
|
||||||
|
logpath = /var/log/nginx/access.log
|
||||||
|
maxretry = 20
|
||||||
|
findtime = 30
|
||||||
|
|
||||||
|
[miab-owncloud]
|
||||||
|
enabled = true
|
||||||
|
port = http,https
|
||||||
|
filter = miab-owncloud
|
||||||
|
logpath = STORAGE_ROOT/owncloud/owncloud.log
|
||||||
|
maxretry = 20
|
||||||
|
findtime = 120
|
||||||
|
|
||||||
|
[miab-postfix587]
|
||||||
|
enabled = true
|
||||||
|
port = 587
|
||||||
|
filter = miab-postfix-submission
|
||||||
|
logpath = /var/log/mail.log
|
||||||
|
maxretry = 20
|
||||||
|
findtime = 30
|
||||||
|
|
||||||
|
[miab-roundcube]
|
||||||
|
enabled = true
|
||||||
|
port = http,https
|
||||||
|
filter = miab-roundcube
|
||||||
|
logpath = /var/log/roundcubemail/errors
|
||||||
|
maxretry = 20
|
||||||
|
findtime = 30
|
||||||
|
|
||||||
|
[recidive]
|
||||||
|
enabled = true
|
||||||
|
maxretry = 10
|
||||||
|
action = iptables-allports[name=recidive]
|
||||||
|
# In the recidive section of jail.conf the action contains:
|
||||||
|
#
|
||||||
|
# action = iptables-allports[name=recidive]
|
||||||
|
# sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log]
|
||||||
|
#
|
||||||
|
# The last line on the action will sent an email to the configured address. This mail will
|
||||||
|
# notify the administrator that someone has been repeatedly triggering one of the other jails.
|
||||||
|
# By default we don't configure this address and no action is required from the admin anyway.
|
||||||
|
# So the notification is ommited. This will prevent message appearing in the mail.log that mail
|
||||||
|
# can't be delivered to fail2ban@$HOSTNAME.
|
||||||
|
|
||||||
|
[sasl]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[ssh]
|
||||||
|
maxretry = 7
|
||||||
|
bantime = 3600
|
||||||
|
|
||||||
|
[ssh-ddos]
|
||||||
|
enabled = true
|
||||||
@@ -27,9 +27,9 @@ EXEC_AS_USER=root
|
|||||||
|
|
||||||
# Ensure Python reads/writes files in UTF-8. If the machine
|
# Ensure Python reads/writes files in UTF-8. If the machine
|
||||||
# triggers some other locale in Python, like ASCII encoding,
|
# triggers some other locale in Python, like ASCII encoding,
|
||||||
# Python may not be able to read/write files. Here and in
|
# Python may not be able to read/write files. Set also
|
||||||
# setup/start.sh (where the locale is also installed if not
|
# setup/start.sh (where the locale is also installed if not
|
||||||
# already present).
|
# already present) and management/daily_tasks.sh.
|
||||||
export LANGUAGE=en_US.UTF-8
|
export LANGUAGE=en_US.UTF-8
|
||||||
export LC_ALL=en_US.UTF-8
|
export LC_ALL=en_US.UTF-8
|
||||||
export LANG=en_US.UTF-8
|
export LANG=en_US.UTF-8
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
location /admin/ {
|
location /admin/ {
|
||||||
proxy_pass http://127.0.0.1:10222/;
|
proxy_pass http://127.0.0.1:10222/;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
add_header X-Frame-Options "DENY";
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none';";
|
||||||
|
add_header Strict-Transport-Security max-age=31536000;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ownCloud configuration.
|
# ownCloud configuration.
|
||||||
@@ -15,8 +19,11 @@
|
|||||||
rewrite ^(/cloud/core/doc/[^\/]+/)$ $1/index.html;
|
rewrite ^(/cloud/core/doc/[^\/]+/)$ $1/index.html;
|
||||||
location /cloud/ {
|
location /cloud/ {
|
||||||
alias /usr/local/lib/owncloud/;
|
alias /usr/local/lib/owncloud/;
|
||||||
location ~ ^/(data|config|\.ht|db_structure\.xml|README) {
|
location ~ ^/cloud/(build|tests|config|lib|3rdparty|templates|data|README)/ {
|
||||||
deny all;
|
deny all;
|
||||||
|
}
|
||||||
|
location ~ ^/cloud/(?:\.|autotest|occ|issue|indie|db_|console) {
|
||||||
|
deny all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ {
|
location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
## $HOSTNAME
|
## $HOSTNAME
|
||||||
|
|
||||||
# Redirect all HTTP to HTTPS.
|
# Redirect all HTTP to HTTPS *except* the ACME challenges (Let's Encrypt TLS certificate
|
||||||
|
# domain validation challenges) path, which must be served over HTTP per the ACME spec
|
||||||
|
# (due to some Apache vulnerability).
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
@@ -12,10 +14,19 @@ server {
|
|||||||
# error pages and in the "Server" HTTP-Header.
|
# error pages and in the "Server" HTTP-Header.
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
# Redirect using the 'return' directive and the built-in
|
location / {
|
||||||
# variable '$request_uri' to avoid any capturing, matching
|
# Redirect using the 'return' directive and the built-in
|
||||||
# or evaluation of regular expressions.
|
# variable '$request_uri' to avoid any capturing, matching
|
||||||
return 301 https://$HOSTNAME$request_uri;
|
# or evaluation of regular expressions.
|
||||||
|
return 301 https://$HOSTNAME$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
# This path must be served over HTTP for ACME domain validation.
|
||||||
|
# We map this to a special path where our TLS cert provisioning
|
||||||
|
# tool knows to store challenge response files.
|
||||||
|
alias $STORAGE_ROOT/ssl/lets_encrypt/acme_challenges/;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# The secure HTTPS server.
|
# The secure HTTPS server.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
************************************************/
|
************************************************/
|
||||||
|
|
||||||
define('CALDAV_PROTOCOL', 'https');
|
define('CALDAV_PROTOCOL', 'https');
|
||||||
define('CALDAV_SERVER', 'localhost');
|
define('CALDAV_SERVER', '127.0.0.1');
|
||||||
define('CALDAV_PORT', '443');
|
define('CALDAV_PORT', '443');
|
||||||
define('CALDAV_PATH', '/caldav/calendars/%u/');
|
define('CALDAV_PATH', '/caldav/calendars/%u/');
|
||||||
define('CALDAV_PERSONAL', 'PRINCIPAL');
|
define('CALDAV_PERSONAL', 'PRINCIPAL');
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
|
|
||||||
define('CARDDAV_PROTOCOL', 'https'); /* http or https */
|
define('CARDDAV_PROTOCOL', 'https'); /* http or https */
|
||||||
define('CARDDAV_SERVER', 'localhost');
|
define('CARDDAV_SERVER', '127.0.0.1');
|
||||||
define('CARDDAV_PORT', '443');
|
define('CARDDAV_PORT', '443');
|
||||||
define('CARDDAV_PATH', '/carddav/addressbooks/%u/');
|
define('CARDDAV_PATH', '/carddav/addressbooks/%u/');
|
||||||
define('CARDDAV_DEFAULT_PATH', '/carddav/addressbooks/%u/contacts/'); /* subdirectory of the main path */
|
define('CARDDAV_DEFAULT_PATH', '/carddav/addressbooks/%u/contacts/'); /* subdirectory of the main path */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Descr : IMAP backend configuration file
|
* Descr : IMAP backend configuration file
|
||||||
************************************************/
|
************************************************/
|
||||||
|
|
||||||
define('IMAP_SERVER', 'localhost');
|
define('IMAP_SERVER', '127.0.0.1');
|
||||||
define('IMAP_PORT', 993);
|
define('IMAP_PORT', 993);
|
||||||
define('IMAP_OPTIONS', '/ssl/norsh/novalidate-cert');
|
define('IMAP_OPTIONS', '/ssl/norsh/novalidate-cert');
|
||||||
define('IMAP_DEFAULTFROM', '');
|
define('IMAP_DEFAULTFROM', '');
|
||||||
@@ -41,9 +41,10 @@ define('IMAP_FROM_LDAP_QUERY', '(mail=#username@#domain)');
|
|||||||
define('IMAP_FROM_LDAP_FIELDS', serialize(array('givenname', 'sn', 'mail')));
|
define('IMAP_FROM_LDAP_FIELDS', serialize(array('givenname', 'sn', 'mail')));
|
||||||
define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>');
|
define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>');
|
||||||
|
|
||||||
|
define('IMAP_SMTP_METHOD', 'sendmail');
|
||||||
|
|
||||||
global $imap_smtp_params;
|
global $imap_smtp_params;
|
||||||
$imap_smtp_params = array('host' => 'ssl://localhost', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
|
$imap_smtp_params = array('host' => 'ssl://127.0.0.1', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
|
||||||
|
|
||||||
define('MAIL_MIMEPART_CRLF', "\r\n");
|
define('MAIL_MIMEPART_CRLF', "\r\n");
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
# This script performs a backup of all user data:
|
# This script performs a backup of all user data:
|
||||||
# 1) System services are stopped while a copy of user data is made.
|
# 1) System services are stopped.
|
||||||
# 2) An incremental encrypted backup is made using duplicity into the
|
# 2) STORAGE_ROOT/backup/before-backup is executed if it exists.
|
||||||
# directory STORAGE_ROOT/backup/encrypted. The password used for
|
# 3) An incremental encrypted backup is made using duplicity.
|
||||||
# encryption is stored in backup/secret_key.txt.
|
# 4) The stopped services are restarted.
|
||||||
# 3) The stopped services are restarted.
|
# 5) STORAGE_ROOT/backup/after-backup is executed if it exists.
|
||||||
# 5) STORAGE_ROOT/backup/after-backup is executd if it exists.
|
|
||||||
|
|
||||||
import os, os.path, shutil, glob, re, datetime
|
import os, os.path, shutil, glob, re, datetime, sys
|
||||||
import dateutil.parser, dateutil.relativedelta, dateutil.tz
|
import dateutil.parser, dateutil.relativedelta, dateutil.tz
|
||||||
import rtyaml
|
import rtyaml
|
||||||
|
|
||||||
@@ -44,10 +43,10 @@ def backup_status(env):
|
|||||||
# Get duplicity collection status and parse for a list of backups.
|
# Get duplicity collection status and parse for a list of backups.
|
||||||
def parse_line(line):
|
def parse_line(line):
|
||||||
keys = line.strip().split()
|
keys = line.strip().split()
|
||||||
date = dateutil.parser.parse(keys[1])
|
date = dateutil.parser.parse(keys[1]).astimezone(dateutil.tz.tzlocal())
|
||||||
return {
|
return {
|
||||||
"date": keys[1],
|
"date": keys[1],
|
||||||
"date_str": date.strftime("%x %X"),
|
"date_str": date.strftime("%x %X") + " " + now.tzname(),
|
||||||
"date_delta": reldate(date, now, "the future?"),
|
"date_delta": reldate(date, now, "the future?"),
|
||||||
"full": keys[0] == "full",
|
"full": keys[0] == "full",
|
||||||
"size": 0, # collection-status doesn't give us the size
|
"size": 0, # collection-status doesn't give us the size
|
||||||
@@ -65,8 +64,8 @@ def backup_status(env):
|
|||||||
trap=True)
|
trap=True)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
# Command failed. This is likely due to an improperly configured remote
|
# Command failed. This is likely due to an improperly configured remote
|
||||||
# destination for the backups.
|
# destination for the backups or the last backup job terminated unexpectedly.
|
||||||
return { }
|
raise Exception("Something is wrong with the backup: " + collection_status)
|
||||||
for line in collection_status.split('\n'):
|
for line in collection_status.split('\n'):
|
||||||
if line.startswith(" full") or line.startswith(" inc"):
|
if line.startswith(" full") or line.startswith(" inc"):
|
||||||
backup = parse_line(line)
|
backup = parse_line(line)
|
||||||
@@ -83,50 +82,66 @@ def backup_status(env):
|
|||||||
# This is relied on by should_force_full() and the next step.
|
# This is relied on by should_force_full() and the next step.
|
||||||
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
|
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
|
||||||
|
|
||||||
# Get the average size of incremental backups and the size of the
|
# Get the average size of incremental backups, the size of the
|
||||||
# most recent full backup.
|
# most recent full backup, and the date of the most recent
|
||||||
|
# backup and the most recent full backup.
|
||||||
incremental_count = 0
|
incremental_count = 0
|
||||||
incremental_size = 0
|
incremental_size = 0
|
||||||
|
first_date = None
|
||||||
first_full_size = None
|
first_full_size = None
|
||||||
|
first_full_date = None
|
||||||
for bak in backups:
|
for bak in backups:
|
||||||
|
if first_date is None:
|
||||||
|
first_date = dateutil.parser.parse(bak["date"])
|
||||||
if bak["full"]:
|
if bak["full"]:
|
||||||
first_full_size = bak["size"]
|
first_full_size = bak["size"]
|
||||||
|
first_full_date = dateutil.parser.parse(bak["date"])
|
||||||
break
|
break
|
||||||
incremental_count += 1
|
incremental_count += 1
|
||||||
incremental_size += bak["size"]
|
incremental_size += bak["size"]
|
||||||
|
|
||||||
# Predict how many more increments until the next full backup,
|
# When will the most recent backup be deleted? It won't be deleted if the next
|
||||||
# and add to that the time we hold onto backups, to predict
|
# backup is incremental, because the increments rely on all past increments.
|
||||||
# how long the most recent full backup+increments will be held
|
# So first guess how many more incremental backups will occur until the next
|
||||||
# onto. Round up since the backup occurs on the night following
|
# full backup. That full backup frees up this one to be deleted. But, the backup
|
||||||
# when the threshold is met.
|
# must also be at least min_age_in_days old too.
|
||||||
deleted_in = None
|
deleted_in = None
|
||||||
if incremental_count > 0 and first_full_size is not None:
|
if incremental_count > 0 and first_full_size is not None:
|
||||||
deleted_in = "approx. %d days" % round(config["min_age_in_days"] + (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count) + .5)
|
# How many days until the next incremental backup? First, the part of
|
||||||
|
# the algorithm based on increment sizes:
|
||||||
|
est_days_to_next_full = (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count)
|
||||||
|
est_time_of_next_full = first_date + datetime.timedelta(days=est_days_to_next_full)
|
||||||
|
|
||||||
# When will a backup be deleted?
|
# ...And then the part of the algorithm based on full backup age:
|
||||||
|
est_time_of_next_full = min(est_time_of_next_full, first_full_date + datetime.timedelta(days=config["min_age_in_days"]*10+1))
|
||||||
|
|
||||||
|
# It still can't be deleted until it's old enough.
|
||||||
|
est_deleted_on = max(est_time_of_next_full, first_date + datetime.timedelta(days=config["min_age_in_days"]))
|
||||||
|
|
||||||
|
deleted_in = "approx. %d days" % round((est_deleted_on-now).total_seconds()/60/60/24 + .5)
|
||||||
|
|
||||||
|
# When will a backup be deleted? Set the deleted_in field of each backup.
|
||||||
saw_full = False
|
saw_full = False
|
||||||
days_ago = now - datetime.timedelta(days=config["min_age_in_days"])
|
|
||||||
for bak in backups:
|
for bak in backups:
|
||||||
if deleted_in:
|
if deleted_in:
|
||||||
# Subsequent backups are deleted when the most recent increment
|
# The most recent increment in a chain and all of the previous backups
|
||||||
# in the chain would be deleted.
|
# it relies on are deleted at the same time.
|
||||||
bak["deleted_in"] = deleted_in
|
bak["deleted_in"] = deleted_in
|
||||||
if bak["full"]:
|
if bak["full"]:
|
||||||
# Reset when we get to a full backup. A new chain start next.
|
# Reset when we get to a full backup. A new chain start *next*.
|
||||||
saw_full = True
|
saw_full = True
|
||||||
deleted_in = None
|
deleted_in = None
|
||||||
elif saw_full and not deleted_in:
|
elif saw_full and not deleted_in:
|
||||||
# Mark deleted_in only on the first increment after a full backup.
|
# We're now on backups prior to the most recent full backup. These are
|
||||||
deleted_in = reldate(days_ago, dateutil.parser.parse(bak["date"]), "on next daily backup")
|
# free to be deleted as soon as they are min_age_in_days old.
|
||||||
|
deleted_in = reldate(now, dateutil.parser.parse(bak["date"]) + datetime.timedelta(days=config["min_age_in_days"]), "on next daily backup")
|
||||||
bak["deleted_in"] = deleted_in
|
bak["deleted_in"] = deleted_in
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tz": now.tzname(),
|
|
||||||
"backups": backups,
|
"backups": backups,
|
||||||
}
|
}
|
||||||
|
|
||||||
def should_force_full(env):
|
def should_force_full(config, env):
|
||||||
# Force a full backup when the total size of the increments
|
# Force a full backup when the total size of the increments
|
||||||
# since the last full backup is greater than half the size
|
# since the last full backup is greater than half the size
|
||||||
# of that full backup.
|
# of that full backup.
|
||||||
@@ -138,8 +153,14 @@ def should_force_full(env):
|
|||||||
inc_size += bak["size"]
|
inc_size += bak["size"]
|
||||||
else:
|
else:
|
||||||
# ...until we reach the most recent full backup.
|
# ...until we reach the most recent full backup.
|
||||||
# Return if we should to a full backup.
|
# Return if we should to a full backup, which is based
|
||||||
return inc_size > .5*bak["size"]
|
# on the size of the increments relative to the full
|
||||||
|
# backup, as well as the age of the full backup.
|
||||||
|
if inc_size > .5*bak["size"]:
|
||||||
|
return True
|
||||||
|
if dateutil.parser.parse(bak["date"]) + datetime.timedelta(days=config["min_age_in_days"]*10+1) < datetime.datetime.now(dateutil.tz.tzlocal()):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
# If we got here there are no (full) backups, so make one.
|
# If we got here there are no (full) backups, so make one.
|
||||||
# (I love for/else blocks. Here it's just to show off.)
|
# (I love for/else blocks. Here it's just to show off.)
|
||||||
@@ -217,12 +238,35 @@ def perform_backup(full_backup):
|
|||||||
# will fail. Otherwise do a full backup when the size of
|
# will fail. Otherwise do a full backup when the size of
|
||||||
# the increments since the most recent full backup are
|
# the increments since the most recent full backup are
|
||||||
# large.
|
# large.
|
||||||
full_backup = full_backup or should_force_full(env)
|
try:
|
||||||
|
full_backup = full_backup or should_force_full(config, env)
|
||||||
|
except Exception as e:
|
||||||
|
# This was the first call to duplicity, and there might
|
||||||
|
# be an error already.
|
||||||
|
print(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Stop services.
|
# Stop services.
|
||||||
shell('check_call', ["/usr/sbin/service", "php5-fpm", "stop"])
|
def service_command(service, command, quit=None):
|
||||||
shell('check_call', ["/usr/sbin/service", "postfix", "stop"])
|
# Execute silently, but if there is an error then display the output & exit.
|
||||||
shell('check_call', ["/usr/sbin/service", "dovecot", "stop"])
|
code, ret = shell('check_output', ["/usr/sbin/service", service, command], capture_stderr=True, trap=True)
|
||||||
|
if code != 0:
|
||||||
|
print(ret)
|
||||||
|
if quit:
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
service_command("php5-fpm", "stop", quit=True)
|
||||||
|
service_command("postfix", "stop", quit=True)
|
||||||
|
service_command("dovecot", "stop", quit=True)
|
||||||
|
|
||||||
|
# Execute a pre-backup script that copies files outside the homedir.
|
||||||
|
# Run as the STORAGE_USER user, not as root. Pass our settings in
|
||||||
|
# environment variables so the script has access to STORAGE_ROOT.
|
||||||
|
pre_script = os.path.join(backup_root, 'before-backup')
|
||||||
|
if os.path.exists(pre_script):
|
||||||
|
shell('check_call',
|
||||||
|
['su', env['STORAGE_USER'], '-c', pre_script, config["target"]],
|
||||||
|
env=env)
|
||||||
|
|
||||||
# Run a backup of STORAGE_ROOT (but excluding the backups themselves!).
|
# Run a backup of STORAGE_ROOT (but excluding the backups themselves!).
|
||||||
# --allow-source-mismatch is needed in case the box's hostname is changed
|
# --allow-source-mismatch is needed in case the box's hostname is changed
|
||||||
@@ -231,6 +275,7 @@ def perform_backup(full_backup):
|
|||||||
shell('check_call', [
|
shell('check_call', [
|
||||||
"/usr/bin/duplicity",
|
"/usr/bin/duplicity",
|
||||||
"full" if full_backup else "incr",
|
"full" if full_backup else "incr",
|
||||||
|
"--verbosity", "warning", "--no-print-statistics",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--exclude", backup_root,
|
"--exclude", backup_root,
|
||||||
"--volsize", "250",
|
"--volsize", "250",
|
||||||
@@ -242,9 +287,9 @@ def perform_backup(full_backup):
|
|||||||
get_env(env))
|
get_env(env))
|
||||||
finally:
|
finally:
|
||||||
# Start services again.
|
# Start services again.
|
||||||
shell('check_call', ["/usr/sbin/service", "dovecot", "start"])
|
service_command("dovecot", "start", quit=False)
|
||||||
shell('check_call', ["/usr/sbin/service", "postfix", "start"])
|
service_command("postfix", "start", quit=False)
|
||||||
shell('check_call', ["/usr/sbin/service", "php5-fpm", "start"])
|
service_command("php5-fpm", "start", quit=False)
|
||||||
|
|
||||||
# Once the migrated backup is included in a new backup, it can be deleted.
|
# Once the migrated backup is included in a new backup, it can be deleted.
|
||||||
if os.path.isdir(migrated_unencrypted_backup_dir):
|
if os.path.isdir(migrated_unencrypted_backup_dir):
|
||||||
@@ -256,6 +301,7 @@ def perform_backup(full_backup):
|
|||||||
"/usr/bin/duplicity",
|
"/usr/bin/duplicity",
|
||||||
"remove-older-than",
|
"remove-older-than",
|
||||||
"%dD" % config["min_age_in_days"],
|
"%dD" % config["min_age_in_days"],
|
||||||
|
"--verbosity", "error",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--force",
|
"--force",
|
||||||
config["target"]
|
config["target"]
|
||||||
@@ -270,6 +316,7 @@ def perform_backup(full_backup):
|
|||||||
shell('check_call', [
|
shell('check_call', [
|
||||||
"/usr/bin/duplicity",
|
"/usr/bin/duplicity",
|
||||||
"cleanup",
|
"cleanup",
|
||||||
|
"--verbosity", "error",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--force",
|
"--force",
|
||||||
config["target"]
|
config["target"]
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os, os.path, re, json
|
import os, os.path, re, json, time
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Flask, request, render_template, abort, Response, send_from_directory
|
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
|
||||||
|
|
||||||
import auth, utils
|
import auth, utils, multiprocessing.pool
|
||||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||||
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
||||||
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||||
|
|
||||||
# Create a worker pool for the status checks. The pool should
|
|
||||||
# live across http requests so we don't baloon the system with
|
|
||||||
# processes.
|
|
||||||
import multiprocessing.pool
|
|
||||||
pool = multiprocessing.pool.Pool(processes=10)
|
|
||||||
|
|
||||||
env = utils.load_environment()
|
env = utils.load_environment()
|
||||||
|
|
||||||
auth_service = auth.KeyAuthService()
|
auth_service = auth.KeyAuthService()
|
||||||
@@ -49,7 +44,10 @@ def authorized_personnel_only(viewfunc):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# Authentication failed.
|
# Authentication failed.
|
||||||
privs = []
|
privs = []
|
||||||
error = str(e)
|
error = "Incorrect username or password"
|
||||||
|
|
||||||
|
# Write a line in the log recording the failed login
|
||||||
|
log_failed_login(request)
|
||||||
|
|
||||||
# Authorized to access an API view?
|
# Authorized to access an API view?
|
||||||
if "admin" in privs:
|
if "admin" in privs:
|
||||||
@@ -123,9 +121,12 @@ def me():
|
|||||||
try:
|
try:
|
||||||
email, privs = auth_service.authenticate(request, env)
|
email, privs = auth_service.authenticate(request, env)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
# Log the failed login
|
||||||
|
log_failed_login(request)
|
||||||
|
|
||||||
return json_response({
|
return json_response({
|
||||||
"status": "invalid",
|
"status": "invalid",
|
||||||
"reason": str(e),
|
"reason": "Incorrect username or password",
|
||||||
})
|
})
|
||||||
|
|
||||||
resp = {
|
resp = {
|
||||||
@@ -327,6 +328,33 @@ def dns_get_dump():
|
|||||||
|
|
||||||
# SSL
|
# SSL
|
||||||
|
|
||||||
|
@app.route('/ssl/status')
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_get_status():
|
||||||
|
from ssl_certificates import get_certificates_to_provision
|
||||||
|
from web_update import get_web_domains_info, get_web_domains
|
||||||
|
|
||||||
|
# What domains can we provision certificates for? What unexpected problems do we have?
|
||||||
|
provision, cant_provision = get_certificates_to_provision(env, show_extended_problems=False)
|
||||||
|
|
||||||
|
# What's the current status of TLS certificates on all of the domain?
|
||||||
|
domains_status = get_web_domains_info(env)
|
||||||
|
domains_status = [{ "domain": d["domain"], "status": d["ssl_certificate"][0], "text": d["ssl_certificate"][1] } for d in domains_status ]
|
||||||
|
|
||||||
|
# Warn the user about domain names not hosted here because of other settings.
|
||||||
|
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
||||||
|
domains_status.append({
|
||||||
|
"domain": domain,
|
||||||
|
"status": "not-applicable",
|
||||||
|
"text": "The domain's website is hosted elsewhere.",
|
||||||
|
})
|
||||||
|
|
||||||
|
return json_response({
|
||||||
|
"can_provision": utils.sort_domains(provision, env),
|
||||||
|
"cant_provision": [{ "domain": domain, "problem": cant_provision[domain] } for domain in utils.sort_domains(cant_provision, env) ],
|
||||||
|
"status": domains_status,
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def ssl_get_csr(domain):
|
def ssl_get_csr(domain):
|
||||||
@@ -346,6 +374,17 @@ def ssl_install_cert():
|
|||||||
return "Invalid domain name."
|
return "Invalid domain name."
|
||||||
return install_cert(domain, ssl_cert, ssl_chain, env)
|
return install_cert(domain, ssl_cert, ssl_chain, env)
|
||||||
|
|
||||||
|
@app.route('/ssl/provision', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_provision_certs():
|
||||||
|
from ssl_certificates import provision_certificates
|
||||||
|
agree_to_tos_url = request.form.get('agree_to_tos_url')
|
||||||
|
status = provision_certificates(env,
|
||||||
|
agree_to_tos_url=agree_to_tos_url,
|
||||||
|
jsonable=True)
|
||||||
|
return json_response(status)
|
||||||
|
|
||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
|
|
||||||
@app.route('/web/domains')
|
@app.route('/web/domains')
|
||||||
@@ -398,7 +437,10 @@ def system_status():
|
|||||||
def print_line(self, message, monospace=False):
|
def print_line(self, message, monospace=False):
|
||||||
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
|
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
|
||||||
output = WebOutput()
|
output = WebOutput()
|
||||||
|
# Create a temporary pool of processes for the status checks
|
||||||
|
pool = multiprocessing.pool.Pool(processes=5)
|
||||||
run_checks(False, env, output, pool)
|
run_checks(False, env, output, pool)
|
||||||
|
pool.terminate()
|
||||||
return json_response(output.items)
|
return json_response(output.items)
|
||||||
|
|
||||||
@app.route('/system/updates')
|
@app.route('/system/updates')
|
||||||
@@ -418,11 +460,35 @@ def do_updates():
|
|||||||
"DEBIAN_FRONTEND": "noninteractive"
|
"DEBIAN_FRONTEND": "noninteractive"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/system/reboot', methods=["GET"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def needs_reboot():
|
||||||
|
from status_checks import is_reboot_needed_due_to_package_installation
|
||||||
|
if is_reboot_needed_due_to_package_installation():
|
||||||
|
return json_response(True)
|
||||||
|
else:
|
||||||
|
return json_response(False)
|
||||||
|
|
||||||
|
@app.route('/system/reboot', methods=["POST"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def do_reboot():
|
||||||
|
# To keep the attack surface low, we don't allow a remote reboot if one isn't necessary.
|
||||||
|
from status_checks import is_reboot_needed_due_to_package_installation
|
||||||
|
if is_reboot_needed_due_to_package_installation():
|
||||||
|
return utils.shell("check_output", ["/sbin/shutdown", "-r", "now"], capture_stderr=True)
|
||||||
|
else:
|
||||||
|
return "No reboot is required, so it is not allowed."
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/backup/status')
|
@app.route('/system/backup/status')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def backup_status():
|
def backup_status():
|
||||||
from backup import backup_status
|
from backup import backup_status
|
||||||
return json_response(backup_status(env))
|
try:
|
||||||
|
return json_response(backup_status(env))
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({ "error": str(e) })
|
||||||
|
|
||||||
@app.route('/system/backup/config', methods=["GET"])
|
@app.route('/system/backup/config', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
@@ -466,6 +532,80 @@ def munin(filename=""):
|
|||||||
if filename == "": filename = "index.html"
|
if filename == "": filename = "index.html"
|
||||||
return send_from_directory("/var/cache/munin/www", filename)
|
return send_from_directory("/var/cache/munin/www", filename)
|
||||||
|
|
||||||
|
@app.route('/munin/cgi-graph/<path:filename>')
|
||||||
|
@authorized_personnel_only
|
||||||
|
def munin_cgi(filename):
|
||||||
|
""" Relay munin cgi dynazoom requests
|
||||||
|
/usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package
|
||||||
|
that is responsible for generating binary png images _and_ associated HTTP
|
||||||
|
headers based on parameters in the requesting URL. All output is written
|
||||||
|
to stdout which munin_cgi splits into response headers and binary response
|
||||||
|
data.
|
||||||
|
munin-cgi-graph reads environment variables as well as passed input 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.
|
||||||
|
munin-cgi-graph has several failure modes. Some write HTTP Status headers and
|
||||||
|
others return nonzero exit codes.
|
||||||
|
Situating munin_cgi between the user-agent and munin-cgi-graph enables keeping
|
||||||
|
the cgi script behind mailinabox's auth mechanisms and avoids additional
|
||||||
|
support infrastructure like spawn-fcgi.
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMMAND = 'su - munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph "%s"'
|
||||||
|
# su changes user, we use the munin user here
|
||||||
|
# --preserve-environment retains the environment, which is where Popen's `env` data is
|
||||||
|
# --shell=/bin/bash ensures the shell used is bash
|
||||||
|
# -c "/usr/lib/munin/cgi/munin-cgi-graph" passes the command to run as munin
|
||||||
|
# "%s" is a placeholder for where the request's querystring will be added
|
||||||
|
|
||||||
|
if filename == "":
|
||||||
|
return ("a path must be specified", 404)
|
||||||
|
|
||||||
|
query_str = request.query_string.decode("utf-8", 'ignore')
|
||||||
|
|
||||||
|
env = {'PATH_INFO': '/%s/' % filename, 'QUERY_STRING': query_str}
|
||||||
|
cmd = COMMAND % 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'),
|
||||||
|
env=env,
|
||||||
|
return_bytes=True,
|
||||||
|
trap=True)
|
||||||
|
|
||||||
|
if code != 0:
|
||||||
|
# nonzero returncode indicates error
|
||||||
|
app.logger.error("munin_cgi: munin-cgi-graph returned nonzero exit code, %s", process.returncode)
|
||||||
|
return ("error processing graph image", 500)
|
||||||
|
|
||||||
|
# /usr/lib/munin/cgi/munin-cgi-graph returns both headers and binary png when successful.
|
||||||
|
# A double-Windows-style-newline always indicates the end of HTTP headers.
|
||||||
|
headers, image_bytes = binout.split(b'\r\n\r\n', 1)
|
||||||
|
response = make_response(image_bytes)
|
||||||
|
for line in headers.splitlines():
|
||||||
|
name, value = line.decode("utf8").split(':', 1)
|
||||||
|
response.headers[name] = value
|
||||||
|
if 'Status' in response.headers and '404' in response.headers['Status']:
|
||||||
|
app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO'])
|
||||||
|
return response
|
||||||
|
|
||||||
|
def log_failed_login(request):
|
||||||
|
# We need to figure out the ip to list in the message, all our calls are routed
|
||||||
|
# through nginx who will put the original ip in X-Forwarded-For.
|
||||||
|
# During setup we call the management interface directly to determine the user
|
||||||
|
# status. So we can't always use X-Forwarded-For because during setup that header
|
||||||
|
# will not be present.
|
||||||
|
if request.headers.getlist("X-Forwarded-For"):
|
||||||
|
ip = request.headers.getlist("X-Forwarded-For")[0]
|
||||||
|
else:
|
||||||
|
ip = request.remote_addr
|
||||||
|
|
||||||
|
# We need to add a timestamp to the log message, otherwise /dev/log will eat the "duplicate"
|
||||||
|
# message.
|
||||||
|
app.logger.warning( "Mail-in-a-Box Management Daemon: Failed login attempt from ip %s - timestamp %s" % (ip, time.time()))
|
||||||
|
|
||||||
|
|
||||||
# APP
|
# APP
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This script is run daily (at 3am each night).
|
# This script is run daily (at 3am each night).
|
||||||
|
|
||||||
|
# Set character encoding flags to ensure that any non-ASCII
|
||||||
|
# characters don't cause problems. See setup/start.sh and
|
||||||
|
# the management daemon startup script.
|
||||||
|
export LANGUAGE=en_US.UTF-8
|
||||||
|
export LC_ALL=en_US.UTF-8
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
export LC_TYPE=en_US.UTF-8
|
||||||
|
|
||||||
# Take a backup.
|
# Take a backup.
|
||||||
management/backup.py
|
management/backup.py | management/email_administrator.py "Backup Status"
|
||||||
|
|
||||||
|
# Provision any new certificates for new domains or domains with expiring certificates.
|
||||||
|
management/ssl_certificates.py --headless | management/email_administrator.py "Error Provisioning TLS Certificate"
|
||||||
|
|
||||||
# Run status checks and email the administrator if anything changed.
|
# Run status checks and email the administrator if anything changed.
|
||||||
management/status_checks.py --show-changes --smtp
|
management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ def do_dns_update(env, force=False):
|
|||||||
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
|
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
|
||||||
|
|
||||||
# Write the OpenDKIM configuration tables for all of the domains.
|
# Write the OpenDKIM configuration tables for all of the domains.
|
||||||
if write_opendkim_tables([domain for domain, zonefile in zonefiles], env):
|
if write_opendkim_tables(get_mail_domains(env), env):
|
||||||
# Settings changed. Kick opendkim.
|
# Settings changed. Kick opendkim.
|
||||||
shell('check_call', ["/usr/sbin/service", "opendkim", "restart"])
|
shell('check_call', ["/usr/sbin/service", "opendkim", "restart"])
|
||||||
if len(updated_domains) == 0:
|
if len(updated_domains) == 0:
|
||||||
@@ -175,9 +175,6 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
|
|||||||
for value in build_sshfp_records():
|
for value in build_sshfp_records():
|
||||||
records.append((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh."))
|
records.append((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh."))
|
||||||
|
|
||||||
# The MX record says where email for the domain should be delivered: Here!
|
|
||||||
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
|
|
||||||
|
|
||||||
# Add DNS records for any subdomains of this domain. We should not have a zone for
|
# Add DNS records for any subdomains of this domain. We should not have a zone for
|
||||||
# both a domain and one of its subdomains.
|
# both a domain and one of its subdomains.
|
||||||
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
||||||
@@ -244,6 +241,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
|
|||||||
# Don't pin the list of records that has_rec checks against anymore.
|
# Don't pin the list of records that has_rec checks against anymore.
|
||||||
has_rec_base = records
|
has_rec_base = records
|
||||||
|
|
||||||
|
# The MX record says where email for the domain should be delivered: Here!
|
||||||
|
if not has_rec(None, "MX", prefix="10 "):
|
||||||
|
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
|
||||||
|
|
||||||
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
||||||
# the domain, and no one else.
|
# the domain, and no one else.
|
||||||
# Skip if the user has set a custom SPF record.
|
# Skip if the user has set a custom SPF record.
|
||||||
@@ -273,6 +274,13 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en
|
|||||||
if not has_rec(dmarc_qname, "TXT", prefix="v=DMARC1; "):
|
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)))
|
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.
|
# 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 ""))
|
records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else ""))
|
||||||
@@ -502,11 +510,12 @@ zone:
|
|||||||
|
|
||||||
def dnssec_choose_algo(domain, env):
|
def dnssec_choose_algo(domain, env):
|
||||||
if '.' in domain and domain.rsplit('.')[-1] in \
|
if '.' in domain and domain.rsplit('.')[-1] in \
|
||||||
("email", "guide", "fund"):
|
("email", "guide", "fund", "be"):
|
||||||
# At GoDaddy, RSASHA256 is the only algorithm supported
|
# At GoDaddy, RSASHA256 is the only algorithm supported
|
||||||
# for .email and .guide.
|
# for .email and .guide.
|
||||||
# A variety of algorithms are supported for .fund. This
|
# A variety of algorithms are supported for .fund. This
|
||||||
# is preferred.
|
# is preferred.
|
||||||
|
# Gandi tells me that .be does not support RSASHA1-NSEC3-SHA1
|
||||||
return "RSASHA256"
|
return "RSASHA256"
|
||||||
|
|
||||||
# For any domain we were able to sign before, don't change the algorithm
|
# For any domain we were able to sign before, don't change the algorithm
|
||||||
|
|||||||
42
management/email_administrator.py
Executable file
42
management/email_administrator.py
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# Reads in STDIN. If the stream is not empty, mail it to the system administrator.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
from email.message import Message
|
||||||
|
|
||||||
|
from utils import load_environment
|
||||||
|
|
||||||
|
# Load system environment info.
|
||||||
|
env = load_environment()
|
||||||
|
|
||||||
|
# Process command line args.
|
||||||
|
subject = sys.argv[1]
|
||||||
|
|
||||||
|
# Administrator's email address.
|
||||||
|
admin_addr = "administrator@" + env['PRIMARY_HOSTNAME']
|
||||||
|
|
||||||
|
# Read in STDIN.
|
||||||
|
content = sys.stdin.read().strip()
|
||||||
|
|
||||||
|
# If there's nothing coming in, just exit.
|
||||||
|
if content == "":
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# create MIME message
|
||||||
|
msg = Message()
|
||||||
|
msg['From'] = "\"%s\" <%s>" % (env['PRIMARY_HOSTNAME'], admin_addr)
|
||||||
|
msg['To'] = admin_addr
|
||||||
|
msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject)
|
||||||
|
msg.set_payload(content, "UTF-8")
|
||||||
|
|
||||||
|
# send
|
||||||
|
smtpclient = smtplib.SMTP('127.0.0.1', 25)
|
||||||
|
smtpclient.ehlo()
|
||||||
|
smtpclient.sendmail(
|
||||||
|
admin_addr, # MAIL FROM
|
||||||
|
admin_addr, # RCPT TO
|
||||||
|
msg.as_string())
|
||||||
|
smtpclient.quit()
|
||||||
@@ -1,136 +1,211 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import re, os.path
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
|
|
||||||
import mailconfig
|
import mailconfig
|
||||||
import utils
|
import utils
|
||||||
|
|
||||||
|
|
||||||
def scan_mail_log(logger, env):
|
def scan_mail_log(logger, env):
|
||||||
collector = {
|
""" Scan the system's mail log files and collect interesting data
|
||||||
"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))
|
This function scans the 2 most recent mail log files in /var/log/.
|
||||||
|
|
||||||
for fn in ('/var/log/mail.log.1', '/var/log/mail.log'):
|
Args:
|
||||||
if not os.path.exists(fn): continue
|
logger (ConsoleOutput): Object used for writing messages to the console
|
||||||
with open(fn, 'rb') as log:
|
env (dict): Dictionary containing MiaB settings
|
||||||
for line in log:
|
"""
|
||||||
line = line.decode("utf8", errors='replace')
|
|
||||||
scan_mail_log_line(line.strip(), collector)
|
|
||||||
|
|
||||||
if collector["imap-logins"]:
|
collector = {
|
||||||
logger.add_heading("Recent IMAP Logins")
|
"other-services": set(),
|
||||||
logger.print_block("The most recent login from each remote IP adddress is show.")
|
"imap-logins": {},
|
||||||
for k in utils.sort_email_addresses(collector["imap-logins"], env):
|
"pop3-logins": {},
|
||||||
for ip, date in sorted(collector["imap-logins"][k].items(), key = lambda kv : kv[1]):
|
"postgrey": {},
|
||||||
logger.print_line(k + "\t" + str(date) + "\t" + ip)
|
"rejected-mail": {},
|
||||||
|
"activity-by-hour": {
|
||||||
|
"imap-logins": defaultdict(int),
|
||||||
|
"pop3-logins": defaultdict(int),
|
||||||
|
"smtp-sends": defaultdict(int),
|
||||||
|
"smtp-receives": defaultdict(int),
|
||||||
|
},
|
||||||
|
"real_mail_addresses": (
|
||||||
|
set(mailconfig.get_mail_users(env)) | set(alias[0] for alias in mailconfig.get_mail_aliases(env))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if collector["postgrey"]:
|
for fn in ('/var/log/mail.log.1', '/var/log/mail.log'):
|
||||||
logger.add_heading("Greylisted Mail")
|
if not os.path.exists(fn):
|
||||||
logger.print_block("The following mail was greylisted, meaning the emails were temporarily rejected. Legitimate senders will try again within ten minutes.")
|
continue
|
||||||
logger.print_line("recipient" + "\t" + "received" + "\t" + "sender" + "\t" + "delivered")
|
with open(fn, 'rb') as log:
|
||||||
for recipient in utils.sort_email_addresses(collector["postgrey"], env):
|
for line in log:
|
||||||
for (client_address, sender), (first_date, delivered_date) in sorted(collector["postgrey"][recipient].items(), key = lambda kv : kv[1][0]):
|
line = line.decode("utf8", errors='replace')
|
||||||
logger.print_line(recipient + "\t" + str(first_date) + "\t" + sender + "\t" + (("delivered " + str(delivered_date)) if delivered_date else "no retry yet"))
|
scan_mail_log_line(line.strip(), collector)
|
||||||
|
|
||||||
if collector["rejected-mail"]:
|
if collector["imap-logins"]:
|
||||||
logger.add_heading("Rejected Mail")
|
logger.add_heading("Recent IMAP Logins")
|
||||||
logger.print_block("The following incoming mail was rejected.")
|
logger.print_block("The most recent login from each remote IP adddress is shown.")
|
||||||
for k in utils.sort_email_addresses(collector["rejected-mail"], env):
|
for k in utils.sort_email_addresses(collector["imap-logins"], env):
|
||||||
for date, sender, message in collector["rejected-mail"][k]:
|
for ip, date in sorted(collector["imap-logins"][k].items(), key=lambda kv: kv[1]):
|
||||||
logger.print_line(k + "\t" + str(date) + "\t" + sender + "\t" + message)
|
logger.print_line(k + "\t" + str(date) + "\t" + ip)
|
||||||
|
|
||||||
logger.add_heading("Activity by Hour")
|
if collector["pop3-logins"]:
|
||||||
for h in range(24):
|
logger.add_heading("Recent POP3 Logins")
|
||||||
logger.print_line("%d\t%d\t%d" % (h, collector["activity-by-hour"]["imap-logins"][h], collector["activity-by-hour"]["smtp-sends"][h] ))
|
logger.print_block("The most recent login from each remote IP adddress is shown.")
|
||||||
|
for k in utils.sort_email_addresses(collector["pop3-logins"], env):
|
||||||
|
for ip, date in sorted(collector["pop3-logins"][k].items(), key=lambda kv: kv[1]):
|
||||||
|
logger.print_line(k + "\t" + str(date) + "\t" + ip)
|
||||||
|
|
||||||
|
if collector["postgrey"]:
|
||||||
|
logger.add_heading("Greylisted Mail")
|
||||||
|
logger.print_block("The following mail was greylisted, meaning the emails were temporarily rejected. "
|
||||||
|
"Legitimate senders will try again within ten minutes.")
|
||||||
|
logger.print_line("recipient" + "\t" + "received" + 3 * "\t" + "sender" + 6 * "\t" + "delivered")
|
||||||
|
for recipient in utils.sort_email_addresses(collector["postgrey"], env):
|
||||||
|
sorted_recipients = sorted(collector["postgrey"][recipient].items(), key=lambda kv: kv[1][0])
|
||||||
|
for (client_address, sender), (first_date, delivered_date) in sorted_recipients:
|
||||||
|
logger.print_line(
|
||||||
|
recipient + "\t" + str(first_date) + "\t" + sender + "\t" +
|
||||||
|
(("delivered " + str(delivered_date)) if delivered_date else "no retry yet")
|
||||||
|
)
|
||||||
|
|
||||||
|
if collector["rejected-mail"]:
|
||||||
|
logger.add_heading("Rejected Mail")
|
||||||
|
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")
|
||||||
|
logger.print_block("Dovecot logins and Postfix mail traffic per hour.")
|
||||||
|
logger.print_block("Hour\tIMAP\tPOP3\tSent\tReceived")
|
||||||
|
for h in range(24):
|
||||||
|
logger.print_line(
|
||||||
|
"%d\t%d\t\t%d\t\t%d\t\t%d" % (
|
||||||
|
h,
|
||||||
|
collector["activity-by-hour"]["imap-logins"][h],
|
||||||
|
collector["activity-by-hour"]["pop3-logins"][h],
|
||||||
|
collector["activity-by-hour"]["smtp-sends"][h],
|
||||||
|
collector["activity-by-hour"]["smtp-receives"][h],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(collector["other-services"]) > 0:
|
||||||
|
logger.add_heading("Other")
|
||||||
|
logger.print_block("Unrecognized services in the log: " + ", ".join(collector["other-services"]))
|
||||||
|
|
||||||
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):
|
def scan_mail_log_line(line, collector):
|
||||||
m = re.match(r"(\S+ \d+ \d+:\d+:\d+) (\S+) (\S+?)(\[\d+\])?: (.*)", line)
|
""" Scan a log line and extract interesting data """
|
||||||
if not m: return
|
|
||||||
|
|
||||||
date, system, service, pid, log = m.groups()
|
m = re.match(r"(\S+ \d+ \d+:\d+:\d+) (\S+) (\S+?)(\[\d+\])?: (.*)", line)
|
||||||
date = dateutil.parser.parse(date)
|
|
||||||
|
|
||||||
if service == "dovecot":
|
|
||||||
scan_dovecot_line(date, log, collector)
|
|
||||||
|
|
||||||
elif service == "postgrey":
|
if not m:
|
||||||
scan_postgrey_line(date, log, collector)
|
return
|
||||||
|
|
||||||
elif service == "postfix/smtpd":
|
date, system, service, pid, log = m.groups()
|
||||||
scan_postfix_smtpd_line(date, log, collector)
|
date = dateutil.parser.parse(date)
|
||||||
|
|
||||||
elif service == "postfix/submission/smtpd":
|
if service == "dovecot":
|
||||||
scan_postfix_submission_line(date, log, collector)
|
scan_dovecot_line(date, log, collector)
|
||||||
|
elif service == "postgrey":
|
||||||
|
scan_postgrey_line(date, log, collector)
|
||||||
|
elif service == "postfix/smtpd":
|
||||||
|
scan_postfix_smtpd_line(date, log, collector)
|
||||||
|
elif service == "postfix/cleanup":
|
||||||
|
scan_postfix_cleanup_line(date, log, collector)
|
||||||
|
elif service == "postfix/submission/smtpd":
|
||||||
|
scan_postfix_submission_line(date, log, collector)
|
||||||
|
elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup", "postfix/scache", "spampd", "postfix/anvil",
|
||||||
|
"postfix/master", "opendkim", "postfix/lmtp", "postfix/tlsmgr"):
|
||||||
|
# nothing to look at
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
collector["other-services"].add(service)
|
||||||
|
|
||||||
elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup",
|
|
||||||
"postfix/scache", "spampd", "postfix/anvil", "postfix/master",
|
|
||||||
"opendkim", "postfix/lmtp", "postfix/tlsmgr"):
|
|
||||||
# nothing to look at
|
|
||||||
pass
|
|
||||||
|
|
||||||
else:
|
def scan_dovecot_line(date, line, collector):
|
||||||
collector["other-services"].add(service)
|
""" Scan a dovecot log line and extract interesting data """
|
||||||
|
|
||||||
|
m = re.match("(imap|pop3)-login: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", line)
|
||||||
|
|
||||||
|
if m:
|
||||||
|
prot, login, ip = m.group(1), m.group(2), m.group(3)
|
||||||
|
logins_key = "%s-logins" % prot
|
||||||
|
if ip != "127.0.0.1": # local login from webmail/zpush
|
||||||
|
collector[logins_key].setdefault(login, {})[ip] = date
|
||||||
|
collector["activity-by-hour"][logins_key][date.hour] += 1
|
||||||
|
|
||||||
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):
|
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 """
|
||||||
if m:
|
|
||||||
action, reason, client_name, client_address, sender, recipient = m.groups()
|
m = re.match("action=(greylist|pass), reason=(.*?), (?:delay=\d+, )?client_name=(.*), client_address=(.*), "
|
||||||
key = (client_address, sender)
|
"sender=(.*), recipient=(.*)",
|
||||||
if action == "greylist" and reason == "new":
|
log)
|
||||||
collector["postgrey"].setdefault(recipient, {})[key] = (date, None)
|
|
||||||
elif action == "pass" and reason == "triplet found" and key in collector["postgrey"].get(recipient, {}):
|
if m:
|
||||||
collector["postgrey"][recipient][key] = (collector["postgrey"][recipient][key][0], date)
|
action, reason, client_name, client_address, sender, recipient = m.groups()
|
||||||
|
key = (client_address, sender)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def scan_postfix_smtpd_line(date, log, collector):
|
def scan_postfix_smtpd_line(date, log, collector):
|
||||||
m = re.match("NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
|
""" Scan a postfix smtpd log line and extract interesting data """
|
||||||
if m:
|
|
||||||
message, sender, recipient = m.groups()
|
|
||||||
if recipient in collector["real_mail_addresses"]:
|
|
||||||
# only log mail to real recipients
|
|
||||||
|
|
||||||
# skip this, is reported in the greylisting report
|
# Check if the incomming mail was rejected
|
||||||
if "Recipient address rejected: Greylisted" in message:
|
|
||||||
return
|
|
||||||
|
|
||||||
# simplify this one
|
m = re.match("NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
|
||||||
m = re.search(r"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)", message)
|
|
||||||
if m:
|
|
||||||
message = "ip blocked: " + m.group(2)
|
|
||||||
|
|
||||||
# simplify this one too
|
if m:
|
||||||
m = re.search(r"Sender address \[.*@(.*)\] blocked using dbl.spamhaus.org; (.*)", message)
|
message, sender, recipient = m.groups()
|
||||||
if m:
|
if recipient in collector["real_mail_addresses"]:
|
||||||
message = "domain blocked: " + m.group(2)
|
# only log mail to real recipients
|
||||||
|
|
||||||
collector["rejected-mail"].setdefault(recipient, []).append( (date, sender, message) )
|
# skip this, if reported in the greylisting report
|
||||||
|
if "Recipient address rejected: Greylisted" in message:
|
||||||
|
return
|
||||||
|
|
||||||
|
# simplify this one
|
||||||
|
m = re.search(r"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)", message)
|
||||||
|
if m:
|
||||||
|
message = "ip blocked: " + m.group(2)
|
||||||
|
|
||||||
|
# simplify this one too
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
def scan_postfix_cleanup_line(date, _, collector):
|
||||||
|
""" Scan a postfix cleanup log line and extract interesting data
|
||||||
|
|
||||||
|
It is assumed that every log of postfix/cleanup indicates an email that was successfulfy received by Postfix.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
collector["activity-by-hour"]["smtp-receives"][date.hour] += 1
|
||||||
|
|
||||||
def scan_postfix_submission_line(date, log, collector):
|
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 """
|
||||||
if m:
|
|
||||||
procid, client, user = m.groups()
|
m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=PLAIN, sasl_username=(\S+)", log)
|
||||||
collector["activity-by-hour"]["smtp-sends"][date.hour] += 1
|
|
||||||
|
if m:
|
||||||
|
# procid, client, user = m.groups()
|
||||||
|
collector["activity-by-hour"]["smtp-sends"][date.hour] += 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from status_checks import ConsoleOutput
|
from status_checks import ConsoleOutput
|
||||||
env = utils.load_environment()
|
|
||||||
scan_mail_log(ConsoleOutput(), env)
|
env_vars = utils.load_environment()
|
||||||
|
scan_mail_log(ConsoleOutput(), env_vars)
|
||||||
|
|||||||
@@ -137,19 +137,20 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
|
|||||||
if with_archived:
|
if with_archived:
|
||||||
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
|
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
|
||||||
for domain in os.listdir(root):
|
for domain in os.listdir(root):
|
||||||
for user in os.listdir(os.path.join(root, domain)):
|
if os.path.isdir(os.path.join(root, domain)):
|
||||||
email = user + "@" + domain
|
for user in os.listdir(os.path.join(root, domain)):
|
||||||
mbox = os.path.join(root, domain, user)
|
email = user + "@" + domain
|
||||||
if email in active_accounts: continue
|
mbox = os.path.join(root, domain, user)
|
||||||
user = {
|
if email in active_accounts: continue
|
||||||
"email": email,
|
user = {
|
||||||
"privileges": "",
|
"email": email,
|
||||||
"status": "inactive",
|
"privileges": "",
|
||||||
"mailbox": mbox,
|
"status": "inactive",
|
||||||
}
|
"mailbox": mbox,
|
||||||
users.append(user)
|
}
|
||||||
if with_slow_info:
|
users.append(user)
|
||||||
user["mailbox_size"] = utils.du(mbox)
|
if with_slow_info:
|
||||||
|
user["mailbox_size"] = utils.du(mbox)
|
||||||
|
|
||||||
# Group by domain.
|
# Group by domain.
|
||||||
domains = { }
|
domains = { }
|
||||||
|
|||||||
470
management/ssl_certificates.py
Normal file → Executable file
470
management/ssl_certificates.py
Normal file → Executable file
@@ -1,8 +1,13 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
# Utilities for installing and selecting SSL certificates.
|
# Utilities for installing and selecting SSL certificates.
|
||||||
|
|
||||||
import os, os.path, re, shutil
|
import os, os.path, re, shutil
|
||||||
|
|
||||||
from utils import shell, safe_domain_name
|
from utils import shell, safe_domain_name, sort_domains
|
||||||
|
|
||||||
|
import idna
|
||||||
|
|
||||||
|
# SELECTING SSL CERTIFICATES FOR USE IN WEB
|
||||||
|
|
||||||
def get_ssl_certificates(env):
|
def get_ssl_certificates(env):
|
||||||
# Scan all of the installed SSL certificates and map every domain
|
# Scan all of the installed SSL certificates and map every domain
|
||||||
@@ -17,6 +22,8 @@ def get_ssl_certificates(env):
|
|||||||
|
|
||||||
# List all of the files in the SSL directory and one level deep.
|
# List all of the files in the SSL directory and one level deep.
|
||||||
def get_file_list():
|
def get_file_list():
|
||||||
|
if not os.path.exists(ssl_root):
|
||||||
|
return
|
||||||
for fn in os.listdir(ssl_root):
|
for fn in os.listdir(ssl_root):
|
||||||
fn = os.path.join(ssl_root, fn)
|
fn = os.path.join(ssl_root, fn)
|
||||||
if os.path.isfile(fn):
|
if os.path.isfile(fn):
|
||||||
@@ -82,10 +89,27 @@ def get_ssl_certificates(env):
|
|||||||
# prefer one that is not self-signed
|
# prefer one that is not self-signed
|
||||||
cert.issuer != cert.subject,
|
cert.issuer != cert.subject,
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
# The above lines ensure that valid certificates are chosen
|
||||||
|
# over invalid certificates. The lines below choose between
|
||||||
|
# multiple valid certificates available for this domain.
|
||||||
|
###########################################################
|
||||||
|
|
||||||
# prefer one with the expiration furthest into the future so
|
# prefer one with the expiration furthest into the future so
|
||||||
# that we can easily rotate to new certs as we get them
|
# that we can easily rotate to new certs as we get them
|
||||||
cert.not_valid_after,
|
cert.not_valid_after,
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
# We always choose the certificate that is good for the
|
||||||
|
# longest period of time. This is important for how we
|
||||||
|
# provision certificates for Let's Encrypt. To ensure that
|
||||||
|
# we don't re-provision every night, we have to ensure that
|
||||||
|
# if we choose to provison a certificate that it will
|
||||||
|
# *actually* be used so the provisioning logic knows it
|
||||||
|
# doesn't still need to provision a certificate for the
|
||||||
|
# domain.
|
||||||
|
###########################################################
|
||||||
|
|
||||||
# in case a certificate is installed in multiple paths,
|
# in case a certificate is installed in multiple paths,
|
||||||
# prefer the... lexicographically last one?
|
# prefer the... lexicographically last one?
|
||||||
cert._filename,
|
cert._filename,
|
||||||
@@ -96,46 +120,426 @@ def get_ssl_certificates(env):
|
|||||||
"private-key": cert._private_key._filename,
|
"private-key": cert._private_key._filename,
|
||||||
"certificate": cert._filename,
|
"certificate": cert._filename,
|
||||||
"primary-domain": cert._primary_domain,
|
"primary-domain": cert._primary_domain,
|
||||||
|
"certificate_object": cert,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False):
|
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False, raw=False):
|
||||||
# Get the default paths.
|
# Get the system certificate info.
|
||||||
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
|
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'))
|
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
|
||||||
|
system_certificate = {
|
||||||
|
"private-key": ssl_private_key,
|
||||||
|
"certificate": ssl_certificate,
|
||||||
|
"primary-domain": env['PRIMARY_HOSTNAME'],
|
||||||
|
"certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]),
|
||||||
|
}
|
||||||
|
|
||||||
if domain == env['PRIMARY_HOSTNAME']:
|
if domain == env['PRIMARY_HOSTNAME']:
|
||||||
# The primary domain must use the server certificate because
|
# The primary domain must use the server certificate because
|
||||||
# it is hard-coded in some service configuration files.
|
# it is hard-coded in some service configuration files.
|
||||||
return ssl_private_key, ssl_certificate, None
|
return system_certificate
|
||||||
|
|
||||||
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
||||||
|
|
||||||
if domain in ssl_certificates:
|
if domain in ssl_certificates:
|
||||||
cert_info = ssl_certificates[domain]
|
return ssl_certificates[domain]
|
||||||
cert_type = "multi-domain"
|
|
||||||
elif wildcard_domain in ssl_certificates:
|
elif wildcard_domain in ssl_certificates:
|
||||||
cert_info = ssl_certificates[wildcard_domain]
|
return ssl_certificates[wildcard_domain]
|
||||||
cert_type = "wildcard"
|
|
||||||
elif not allow_missing_cert:
|
elif not allow_missing_cert:
|
||||||
# No certificate is available for this domain! Return default files.
|
# No valid certificate is available for this domain! Return default files.
|
||||||
ssl_via = "Using certificate for %s." % env['PRIMARY_HOSTNAME']
|
return system_certificate
|
||||||
return ssl_private_key, ssl_certificate, ssl_via
|
|
||||||
else:
|
else:
|
||||||
# No certificate is available - and warn appropriately.
|
# No valid certificate is available for this domain.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 'via' is a hint to the user about which certificate is in use for the domain
|
|
||||||
if cert_info['certificate'] == os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'):
|
|
||||||
# Using the server certificate.
|
|
||||||
via = "Using same %s certificate as for %s." % (cert_type, env['PRIMARY_HOSTNAME'])
|
|
||||||
elif cert_info['primary-domain'] != domain and cert_info['primary-domain'] in ssl_certificates and cert_info == ssl_certificates[cert_info['primary-domain']]:
|
|
||||||
via = "Using same %s certificate as for %s." % (cert_type, cert_info['primary-domain'])
|
|
||||||
else:
|
|
||||||
via = None # don't show a hint - show expiration info instead
|
|
||||||
|
|
||||||
return cert_info['private-key'], cert_info['certificate'], via
|
# PROVISIONING CERTIFICATES FROM LETSENCRYPT
|
||||||
|
|
||||||
|
def get_certificates_to_provision(env, show_extended_problems=True, force_domains=None):
|
||||||
|
# Get a set of domain names that we should now provision certificates
|
||||||
|
# for. Provision if a domain name has no valid certificate or if any
|
||||||
|
# certificate is expiring in 14 days. If provisioning anything, also
|
||||||
|
# provision certificates expiring within 30 days. The period between
|
||||||
|
# 14 and 30 days allows us to consolidate domains into multi-domain
|
||||||
|
# certificates for domains expiring around the same time.
|
||||||
|
|
||||||
|
from web_update import get_web_domains
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
# Get domains with missing & expiring certificates.
|
||||||
|
certs = get_ssl_certificates(env)
|
||||||
|
domains = set()
|
||||||
|
domains_if_any = set()
|
||||||
|
problems = { }
|
||||||
|
for domain in get_web_domains(env):
|
||||||
|
# If the user really wants a cert for certain domains, include it.
|
||||||
|
if force_domains:
|
||||||
|
if force_domains == "ALL" or (isinstance(force_domains, list) and domain in force_domains):
|
||||||
|
domains.add(domain)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Include this domain if its certificate is missing, self-signed, or expiring soon.
|
||||||
|
try:
|
||||||
|
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
# system certificate is not present
|
||||||
|
problems[domain] = "Error: " + str(e)
|
||||||
|
continue
|
||||||
|
if cert is None:
|
||||||
|
# No valid certificate available.
|
||||||
|
domains.add(domain)
|
||||||
|
else:
|
||||||
|
cert = cert["certificate_object"]
|
||||||
|
if cert.issuer == cert.subject:
|
||||||
|
# This is self-signed. Get a real one.
|
||||||
|
domains.add(domain)
|
||||||
|
|
||||||
|
# Valid certificate today, but is it expiring soon?
|
||||||
|
elif cert.not_valid_after-now < datetime.timedelta(days=14):
|
||||||
|
domains.add(domain)
|
||||||
|
elif cert.not_valid_after-now < datetime.timedelta(days=30):
|
||||||
|
domains_if_any.add(domain)
|
||||||
|
|
||||||
|
# It's valid. Should we report its validness?
|
||||||
|
elif show_extended_problems:
|
||||||
|
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
|
||||||
|
|
||||||
|
# Warn the user about domains hosted elsewhere.
|
||||||
|
if not force_domains and show_extended_problems:
|
||||||
|
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
||||||
|
problems[domain] = "The domain's DNS is pointed elsewhere, so there is no point to installing a TLS certificate here and we could not automatically provision one anyway because provisioning requires access to the website (which isn't here)."
|
||||||
|
|
||||||
|
# Filter out domains that we can't provision a certificate for.
|
||||||
|
def can_provision_for_domain(domain):
|
||||||
|
# Let's Encrypt doesn't yet support IDNA domains.
|
||||||
|
# We store domains in IDNA (ASCII). To see if this domain is IDNA,
|
||||||
|
# we'll see if its IDNA-decoded form is different.
|
||||||
|
if idna.decode(domain.encode("ascii")) != domain:
|
||||||
|
problems[domain] = "Let's Encrypt does not yet support provisioning certificates for internationalized domains."
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the domain resolve to this machine in public DNS? If not,
|
||||||
|
# we can't do domain control validation. For IPv6 is configured,
|
||||||
|
# make sure both IPv4 and IPv6 are correct because we don't know
|
||||||
|
# how Let's Encrypt will connect.
|
||||||
|
import dns.resolver
|
||||||
|
for rtype, value in [("A", env["PUBLIC_IP"]), ("AAAA", env.get("PUBLIC_IPV6"))]:
|
||||||
|
if not value: continue # IPv6 is not configured
|
||||||
|
try:
|
||||||
|
# Must make the qname absolute to prevent a fall-back lookup with a
|
||||||
|
# search domain appended, by adding a period to the end.
|
||||||
|
response = dns.resolver.query(domain + ".", rtype)
|
||||||
|
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
|
||||||
|
problems[domain] = "DNS isn't configured properly for this domain: DNS resolution failed (%s: %s)." % (rtype, str(e) or repr(e)) # NoAnswer's str is empty
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
problems[domain] = "DNS isn't configured properly for this domain: DNS lookup had an error: %s." % str(e)
|
||||||
|
return False
|
||||||
|
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
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
domains = set(filter(can_provision_for_domain, domains))
|
||||||
|
|
||||||
|
# If there are any domains we definitely will provision for, add in
|
||||||
|
# additional domains to do at this time.
|
||||||
|
if len(domains) > 0:
|
||||||
|
domains |= set(filter(can_provision_for_domain, domains_if_any))
|
||||||
|
|
||||||
|
return (domains, problems)
|
||||||
|
|
||||||
|
def provision_certificates(env, agree_to_tos_url=None, logger=None, show_extended_problems=True, force_domains=None, jsonable=False):
|
||||||
|
import requests.exceptions
|
||||||
|
import acme.messages
|
||||||
|
|
||||||
|
from free_tls_certificates import client
|
||||||
|
|
||||||
|
# What domains should we provision certificates for? And what
|
||||||
|
# errors prevent provisioning for other domains.
|
||||||
|
domains, problems = get_certificates_to_provision(env, force_domains=force_domains, show_extended_problems=show_extended_problems)
|
||||||
|
|
||||||
|
# Exit fast if there is nothing to do.
|
||||||
|
if len(domains) == 0:
|
||||||
|
return {
|
||||||
|
"requests": [],
|
||||||
|
"problems": problems,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
|
||||||
|
# limit for a single certificate. We'll sort to put related domains together.
|
||||||
|
domains = sort_domains(domains, env)
|
||||||
|
certs = []
|
||||||
|
while len(domains) > 0:
|
||||||
|
certs.append( domains[0:100] )
|
||||||
|
domains = domains[100:]
|
||||||
|
|
||||||
|
# Prepare to provision.
|
||||||
|
|
||||||
|
# Where should we put our Let's Encrypt account info and state cache.
|
||||||
|
account_path = os.path.join(env['STORAGE_ROOT'], 'ssl/lets_encrypt')
|
||||||
|
if not os.path.exists(account_path):
|
||||||
|
os.mkdir(account_path)
|
||||||
|
|
||||||
|
# Where should we put ACME challenge files. This is mapped to /.well-known/acme_challenge
|
||||||
|
# by the nginx configuration.
|
||||||
|
challenges_path = os.path.join(account_path, 'acme_challenges')
|
||||||
|
if not os.path.exists(challenges_path):
|
||||||
|
os.mkdir(challenges_path)
|
||||||
|
|
||||||
|
# Read in the private key that we use for all TLS certificates. We'll need that
|
||||||
|
# to generate a CSR (done by free_tls_certificates).
|
||||||
|
with open(os.path.join(env['STORAGE_ROOT'], 'ssl/ssl_private_key.pem'), 'rb') as f:
|
||||||
|
private_key = f.read()
|
||||||
|
|
||||||
|
# Provision certificates.
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for domain_list in certs:
|
||||||
|
# For return.
|
||||||
|
ret_item = {
|
||||||
|
"domains": domain_list,
|
||||||
|
"log": [],
|
||||||
|
}
|
||||||
|
ret.append(ret_item)
|
||||||
|
|
||||||
|
# Logging for free_tls_certificates.
|
||||||
|
def my_logger(message):
|
||||||
|
if logger: logger(message)
|
||||||
|
ret_item["log"].append(message)
|
||||||
|
|
||||||
|
# Attempt to provision a certificate.
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
cert = client.issue_certificate(
|
||||||
|
domain_list,
|
||||||
|
account_path,
|
||||||
|
agree_to_tos_url=agree_to_tos_url,
|
||||||
|
private_key=private_key,
|
||||||
|
logger=my_logger)
|
||||||
|
|
||||||
|
except client.NeedToTakeAction as e:
|
||||||
|
# Write out the ACME challenge files.
|
||||||
|
for action in e.actions:
|
||||||
|
if isinstance(action, client.NeedToInstallFile):
|
||||||
|
fn = os.path.join(challenges_path, action.file_name)
|
||||||
|
with open(fn, 'w') as f:
|
||||||
|
f.write(action.contents)
|
||||||
|
else:
|
||||||
|
raise ValueError(str(action))
|
||||||
|
|
||||||
|
# Try to provision now that the challenge files are installed.
|
||||||
|
|
||||||
|
cert = client.issue_certificate(
|
||||||
|
domain_list,
|
||||||
|
account_path,
|
||||||
|
private_key=private_key,
|
||||||
|
logger=my_logger)
|
||||||
|
|
||||||
|
except client.NeedToAgreeToTOS as e:
|
||||||
|
# The user must agree to the Let's Encrypt terms of service agreement
|
||||||
|
# before any further action can be taken.
|
||||||
|
ret_item.update({
|
||||||
|
"result": "agree-to-tos",
|
||||||
|
"url": e.url,
|
||||||
|
})
|
||||||
|
|
||||||
|
except client.WaitABit as e:
|
||||||
|
# We need to hold on for a bit before querying again to see if we can
|
||||||
|
# acquire a provisioned certificate.
|
||||||
|
import time, datetime
|
||||||
|
ret_item.update({
|
||||||
|
"result": "wait",
|
||||||
|
"until": e.until_when if not jsonable else e.until_when.isoformat(),
|
||||||
|
"seconds": (e.until_when - datetime.datetime.now()).total_seconds()
|
||||||
|
})
|
||||||
|
|
||||||
|
except client.AccountDataIsCorrupt as e:
|
||||||
|
# This is an extremely rare condition.
|
||||||
|
ret_item.update({
|
||||||
|
"result": "error",
|
||||||
|
"message": "Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file " + e.account_file_path + ".",
|
||||||
|
})
|
||||||
|
|
||||||
|
except (client.InvalidDomainName, client.NeedToTakeAction, client.ChallengeFailed, client.RateLimited, acme.messages.Error, requests.exceptions.RequestException) as e:
|
||||||
|
ret_item.update({
|
||||||
|
"result": "error",
|
||||||
|
"message": "Something unexpected went wrong: " + str(e),
|
||||||
|
})
|
||||||
|
|
||||||
|
else:
|
||||||
|
# A certificate was issued.
|
||||||
|
|
||||||
|
install_status = install_cert(domain_list[0], cert['cert'].decode("ascii"), b"\n".join(cert['chain']).decode("ascii"), env, raw=True)
|
||||||
|
|
||||||
|
# str indicates the certificate was not installed.
|
||||||
|
if isinstance(install_status, str):
|
||||||
|
ret_item.update({
|
||||||
|
"result": "error",
|
||||||
|
"message": "Something unexpected was wrong with the provisioned certificate: " + install_status,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# A list indicates success and what happened next.
|
||||||
|
ret_item["log"].extend(install_status)
|
||||||
|
ret_item.update({
|
||||||
|
"result": "installed",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Return what happened with each certificate request.
|
||||||
|
return {
|
||||||
|
"requests": ret,
|
||||||
|
"problems": problems,
|
||||||
|
}
|
||||||
|
|
||||||
|
def provision_certificates_cmdline():
|
||||||
|
import sys
|
||||||
|
from utils import load_environment, exclusive_process
|
||||||
|
|
||||||
|
exclusive_process("update_tls_certificates")
|
||||||
|
env = load_environment()
|
||||||
|
|
||||||
|
verbose = False
|
||||||
|
headless = False
|
||||||
|
force_domains = None
|
||||||
|
show_extended_problems = True
|
||||||
|
|
||||||
|
args = list(sys.argv)
|
||||||
|
args.pop(0) # program name
|
||||||
|
if args and args[0] == "-v":
|
||||||
|
verbose = True
|
||||||
|
args.pop(0)
|
||||||
|
if args and args[0] == "q":
|
||||||
|
show_extended_problems = False
|
||||||
|
args.pop(0)
|
||||||
|
if args and args[0] == "--headless":
|
||||||
|
headless = True
|
||||||
|
args.pop(0)
|
||||||
|
if args and args[0] == "--force":
|
||||||
|
force_domains = "ALL"
|
||||||
|
args.pop(0)
|
||||||
|
else:
|
||||||
|
force_domains = args
|
||||||
|
|
||||||
|
agree_to_tos_url = None
|
||||||
|
while True:
|
||||||
|
# Run the provisioning script. This installs certificates. If there are
|
||||||
|
# a very large number of domains on this box, it issues separate
|
||||||
|
# certificates for groups of domains. We have to check the result for
|
||||||
|
# each group.
|
||||||
|
def my_logger(message):
|
||||||
|
if verbose:
|
||||||
|
print(">", message)
|
||||||
|
status = provision_certificates(env, agree_to_tos_url=agree_to_tos_url, logger=my_logger, force_domains=force_domains, show_extended_problems=show_extended_problems)
|
||||||
|
agree_to_tos_url = None # reset to prevent infinite looping
|
||||||
|
|
||||||
|
if not status["requests"]:
|
||||||
|
# No domains need certificates.
|
||||||
|
if not headless or verbose:
|
||||||
|
if len(status["problems"]) == 0:
|
||||||
|
print("No domains hosted on this box need a new TLS certificate at this time.")
|
||||||
|
elif len(status["problems"]) > 0:
|
||||||
|
print("No TLS certificates could be provisoned at this time:")
|
||||||
|
print()
|
||||||
|
for domain in sort_domains(status["problems"], env):
|
||||||
|
print("%s: %s" % (domain, status["problems"][domain]))
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# What happened?
|
||||||
|
wait_until = None
|
||||||
|
wait_domains = []
|
||||||
|
for request in status["requests"]:
|
||||||
|
if request["result"] == "agree-to-tos":
|
||||||
|
# We may have asked already in a previous iteration.
|
||||||
|
if agree_to_tos_url is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Can't ask the user a question in this mode. Warn the user that something
|
||||||
|
# needs to be done.
|
||||||
|
if headless:
|
||||||
|
print(", ".join(request["domains"]) + " need a new or renewed TLS certificate.")
|
||||||
|
print()
|
||||||
|
print("This box can't do that automatically for you until you agree to Let's Encrypt's")
|
||||||
|
print("Terms of Service agreement. Use the Mail-in-a-Box control panel to provision")
|
||||||
|
print("certificates for these domains.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
I'm going to provision a TLS certificate (formerly called a SSL certificate)
|
||||||
|
for you from Let's Encrypt (letsencrypt.org).
|
||||||
|
|
||||||
|
TLS certificates are cryptographic keys that ensure communication between
|
||||||
|
you and this box are secure when getting and sending mail and visiting
|
||||||
|
websites hosted on this box. Let's Encrypt is a free provider of TLS
|
||||||
|
certificates.
|
||||||
|
|
||||||
|
Please open this document in your web browser:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
It is Let's Encrypt's terms of service agreement. If you agree, I can
|
||||||
|
provision that TLS certificate. If you don't agree, you will have an
|
||||||
|
opportunity to install your own TLS certificate from the Mail-in-a-Box
|
||||||
|
control panel.
|
||||||
|
|
||||||
|
Do you agree to the agreement? Type Y or N and press <ENTER>: """
|
||||||
|
% request["url"], end='', flush=True)
|
||||||
|
|
||||||
|
if sys.stdin.readline().strip().upper() != "Y":
|
||||||
|
print("\nYou didn't agree. Quitting.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Okay, indicate agreement on next iteration.
|
||||||
|
agree_to_tos_url = request["url"]
|
||||||
|
|
||||||
|
if request["result"] == "wait":
|
||||||
|
# Must wait. We'll record until when. The wait occurs below.
|
||||||
|
if wait_until is None:
|
||||||
|
wait_until = request["until"]
|
||||||
|
else:
|
||||||
|
wait_until = max(wait_until, request["until"])
|
||||||
|
wait_domains += request["domains"]
|
||||||
|
|
||||||
|
if request["result"] == "error":
|
||||||
|
print(", ".join(request["domains"]) + ":")
|
||||||
|
print(request["message"])
|
||||||
|
|
||||||
|
if request["result"] == "installed":
|
||||||
|
print("A TLS certificate was successfully installed for " + ", ".join(request["domains"]) + ".")
|
||||||
|
|
||||||
|
if wait_until:
|
||||||
|
# Wait, then loop.
|
||||||
|
import time, datetime
|
||||||
|
print()
|
||||||
|
print("A TLS certificate was requested for: " + ", ".join(wait_domains) + ".")
|
||||||
|
first = True
|
||||||
|
while wait_until > datetime.datetime.now():
|
||||||
|
if not headless or first:
|
||||||
|
print ("We have to wait", int(round((wait_until - datetime.datetime.now()).total_seconds())), "seconds for the certificate to be issued...")
|
||||||
|
time.sleep(10)
|
||||||
|
first = False
|
||||||
|
|
||||||
|
continue # Loop!
|
||||||
|
|
||||||
|
if agree_to_tos_url:
|
||||||
|
# The user agrees to the TOS. Loop to try again by agreeing.
|
||||||
|
continue # Loop!
|
||||||
|
|
||||||
|
# Unless we were instructed to wait, or we just agreed to the TOS,
|
||||||
|
# we're done for now.
|
||||||
|
break
|
||||||
|
|
||||||
|
# And finally show the domains with problems.
|
||||||
|
if len(status["problems"]) > 0:
|
||||||
|
print("TLS certificates could not be provisoned for:")
|
||||||
|
for domain in sort_domains(status["problems"], env):
|
||||||
|
print("%s: %s" % (domain, status["problems"][domain]))
|
||||||
|
|
||||||
|
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
|
||||||
|
|
||||||
def create_csr(domain, ssl_key, country_code, env):
|
def create_csr(domain, ssl_key, country_code, env):
|
||||||
return shell("check_output", [
|
return shell("check_output", [
|
||||||
@@ -144,7 +548,7 @@ def create_csr(domain, ssl_key, country_code, env):
|
|||||||
"-sha256",
|
"-sha256",
|
||||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)])
|
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)])
|
||||||
|
|
||||||
def install_cert(domain, ssl_cert, ssl_chain, env):
|
def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
|
||||||
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
||||||
# The certificate always goes above the chain.
|
# The certificate always goes above the chain.
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -203,8 +607,10 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
|
|||||||
# Update the web configuration so nginx picks up the new certificate file.
|
# Update the web configuration so nginx picks up the new certificate file.
|
||||||
from web_update import do_web_update
|
from web_update import do_web_update
|
||||||
ret.append( do_web_update(env) )
|
ret.append( do_web_update(env) )
|
||||||
|
if raw: return ret
|
||||||
return "\n".join(ret)
|
return "\n".join(ret)
|
||||||
|
|
||||||
|
# VALIDATION OF CERTIFICATES
|
||||||
|
|
||||||
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
|
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
|
||||||
# Check that the ssl_certificate & ssl_private_key files are good
|
# Check that the ssl_certificate & ssl_private_key files are good
|
||||||
@@ -296,7 +702,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
|||||||
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
|
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
|
||||||
|
|
||||||
# There is some unknown problem. Return the `openssl verify` raw output.
|
# There is some unknown problem. Return the `openssl verify` raw output.
|
||||||
return ("There is a problem with the SSL certificate.", verifyoutput.strip())
|
return ("There is a problem with the certificate.", verifyoutput.strip())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# `openssl verify` returned a zero exit status so the cert is currently
|
# `openssl verify` returned a zero exit status so the cert is currently
|
||||||
@@ -305,16 +711,16 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
|||||||
# But is it expiring soon?
|
# But is it expiring soon?
|
||||||
cert_expiration_date = cert.not_valid_after
|
cert_expiration_date = cert.not_valid_after
|
||||||
ndays = (cert_expiration_date-now).days
|
ndays = (cert_expiration_date-now).days
|
||||||
if not rounded_time or ndays < 7:
|
if not rounded_time or ndays <= 10:
|
||||||
|
# Yikes better renew soon!
|
||||||
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
||||||
elif ndays <= 14:
|
|
||||||
expiry_info = "The certificate expires in less than two weeks, on %s." % cert_expiration_date.strftime("%x")
|
|
||||||
elif ndays <= 31:
|
|
||||||
expiry_info = "The certificate expires in less than a month, on %s." % cert_expiration_date.strftime("%x")
|
|
||||||
else:
|
else:
|
||||||
|
# We'll renew it with Lets Encrypt.
|
||||||
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
|
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
|
||||||
|
|
||||||
if ndays <= 31 and warn_if_expiring_soon:
|
if ndays <= 10 and warn_if_expiring_soon:
|
||||||
|
# Warn on day 10 to give 4 days for us to automatically renew the
|
||||||
|
# certificate, which occurs on day 14.
|
||||||
return ("The certificate is expiring soon: " + expiry_info, None)
|
return ("The certificate is expiring soon: " + expiry_info, None)
|
||||||
|
|
||||||
# Return the special OK code.
|
# Return the special OK code.
|
||||||
@@ -381,3 +787,7 @@ def get_certificate_domains(cert):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return names, cn
|
return names, cn
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Provision certificates.
|
||||||
|
provision_certificates_cmdline()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
#
|
#
|
||||||
# Checks that the upstream DNS has been set correctly and that
|
# Checks that the upstream DNS has been set correctly and that
|
||||||
# SSL certificates have been signed, etc., and if not tells the user
|
# TLS certificates have been signed, etc., and if not tells the user
|
||||||
# what to do next.
|
# what to do next.
|
||||||
|
|
||||||
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
||||||
@@ -9,6 +9,7 @@ import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
|||||||
import dns.reversename, dns.resolver
|
import dns.reversename, dns.resolver
|
||||||
import dateutil.parser, dateutil.tz
|
import dateutil.parser, dateutil.tz
|
||||||
import idna
|
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_record
|
||||||
from web_update import get_web_domains, get_domains_with_a_records
|
from web_update import get_web_domains, get_domains_with_a_records
|
||||||
@@ -17,6 +18,29 @@ from mailconfig import get_mail_domains, get_mail_aliases
|
|||||||
|
|
||||||
from utils import shell, sort_domains, load_env_vars_from_file, load_settings
|
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):
|
def run_checks(rounded_values, env, output, pool):
|
||||||
# run systems checks
|
# run systems checks
|
||||||
output.add_heading("System")
|
output.add_heading("System")
|
||||||
@@ -60,33 +84,9 @@ def get_ssh_port():
|
|||||||
|
|
||||||
def run_services_checks(env, output, pool):
|
def run_services_checks(env, output, pool):
|
||||||
# Check that system services are running.
|
# Check that system services are running.
|
||||||
|
|
||||||
services = [
|
|
||||||
{ "name": "Local DNS (bind9)", "port": 53, "public": False, },
|
|
||||||
#{ "name": "NSD Control", "port": 8952, "public": False, },
|
|
||||||
{ "name": "Local DNS Control (bind9/rndc)", "port": 953, "public": False, },
|
|
||||||
{ "name": "Dovecot LMTP LDA", "port": 10026, "public": False, },
|
|
||||||
{ "name": "Postgrey", "port": 10023, "public": False, },
|
|
||||||
{ "name": "Spamassassin", "port": 10025, "public": False, },
|
|
||||||
{ "name": "OpenDKIM", "port": 8891, "public": False, },
|
|
||||||
{ "name": "OpenDMARC", "port": 8893, "public": False, },
|
|
||||||
{ "name": "Memcached", "port": 11211, "public": False, },
|
|
||||||
{ "name": "Sieve (dovecot)", "port": 4190, "public": False, },
|
|
||||||
{ "name": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, },
|
|
||||||
|
|
||||||
{ "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
|
|
||||||
{ "name": "Public DNS (nsd4)", "port": 53, "public": True, },
|
|
||||||
{ "name": "Incoming Mail (SMTP/postfix)", "port": 25, "public": True, },
|
|
||||||
{ "name": "Outgoing Mail (SMTP 587/postfix)", "port": 587, "public": True, },
|
|
||||||
#{ "name": "Postfix/master", "port": 10587, "public": True, },
|
|
||||||
{ "name": "IMAPS (dovecot)", "port": 993, "public": True, },
|
|
||||||
{ "name": "HTTP Web (nginx)", "port": 80, "public": True, },
|
|
||||||
{ "name": "HTTPS Web (nginx)", "port": 443, "public": True, },
|
|
||||||
]
|
|
||||||
|
|
||||||
all_running = True
|
all_running = True
|
||||||
fatal = False
|
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):
|
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)
|
if output2 is None: continue # skip check (e.g. no port was set, e.g. no sshd)
|
||||||
all_running = all_running and running
|
all_running = all_running and running
|
||||||
@@ -166,6 +166,27 @@ def run_system_checks(rounded_values, env, output):
|
|||||||
check_miab_version(env, output)
|
check_miab_version(env, output)
|
||||||
check_system_aliases(env, output)
|
check_system_aliases(env, output)
|
||||||
check_free_disk_space(rounded_values, env, output)
|
check_free_disk_space(rounded_values, env, output)
|
||||||
|
check_free_memory(rounded_values, env, output)
|
||||||
|
|
||||||
|
def check_ufw(env, output):
|
||||||
|
ufw = shell('check_output', ['ufw', 'status']).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):
|
def check_ssh_password(env, output):
|
||||||
# Check that SSH login with password is disabled. The openssh-server
|
# Check that SSH login with password is disabled. The openssh-server
|
||||||
@@ -183,10 +204,13 @@ def check_ssh_password(env, output):
|
|||||||
else:
|
else:
|
||||||
output.print_ok("SSH disallows password-based login.")
|
output.print_ok("SSH disallows password-based login.")
|
||||||
|
|
||||||
|
def is_reboot_needed_due_to_package_installation():
|
||||||
|
return os.path.exists("/var/run/reboot-required")
|
||||||
|
|
||||||
def check_software_updates(env, output):
|
def check_software_updates(env, output):
|
||||||
# Check for any software package updates.
|
# Check for any software package updates.
|
||||||
pkgs = list_apt_updates(apt_update=False)
|
pkgs = list_apt_updates(apt_update=False)
|
||||||
if os.path.exists("/var/run/reboot-required"):
|
if is_reboot_needed_due_to_package_installation():
|
||||||
output.print_error("System updates have been installed and a reboot of the machine is required.")
|
output.print_error("System updates have been installed and a reboot of the machine is required.")
|
||||||
elif len(pkgs) == 0:
|
elif len(pkgs) == 0:
|
||||||
output.print_ok("System software is up to date.")
|
output.print_ok("System software is up to date.")
|
||||||
@@ -216,11 +240,27 @@ def check_free_disk_space(rounded_values, env, output):
|
|||||||
else:
|
else:
|
||||||
output.print_error(disk_msg)
|
output.print_error(disk_msg)
|
||||||
|
|
||||||
|
def check_free_memory(rounded_values, env, output):
|
||||||
|
# Check free memory.
|
||||||
|
percent_free = 100 - psutil.virtual_memory().percent
|
||||||
|
memory_msg = "System memory is %s%% free." % str(round(percent_free))
|
||||||
|
if percent_free >= 20:
|
||||||
|
if rounded_values: memory_msg = "System free memory is at least 20%."
|
||||||
|
output.print_ok(memory_msg)
|
||||||
|
elif percent_free >= 10:
|
||||||
|
if rounded_values: memory_msg = "System free memory is below 20%."
|
||||||
|
output.print_warning(memory_msg)
|
||||||
|
else:
|
||||||
|
if rounded_values: memory_msg = "System free memory is below 10%."
|
||||||
|
output.print_error(memory_msg)
|
||||||
|
|
||||||
def run_network_checks(env, output):
|
def run_network_checks(env, output):
|
||||||
# Also see setup/network-checks.sh.
|
# Also see setup/network-checks.sh.
|
||||||
|
|
||||||
output.add_heading("Network")
|
output.add_heading("Network")
|
||||||
|
|
||||||
|
check_ufw(env, output)
|
||||||
|
|
||||||
# Stop if we cannot make an outbound connection on port 25. Many residential
|
# 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.
|
# 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.
|
# See if we can reach one of Google's MTAs with a 5-second timeout.
|
||||||
@@ -262,23 +302,24 @@ def run_domain_checks(rounded_time, env, output, pool):
|
|||||||
# Get the list of domains that we don't serve web for because of a custom CNAME/A record.
|
# Get the list of domains that we don't serve web for because of a custom CNAME/A record.
|
||||||
domains_with_a_records = get_domains_with_a_records(env)
|
domains_with_a_records = get_domains_with_a_records(env)
|
||||||
|
|
||||||
ssl_certificates = get_ssl_certificates(env)
|
|
||||||
|
|
||||||
# Serial version:
|
# Serial version:
|
||||||
#for domain in sort_domains(domains_to_check, env):
|
#for domain in sort_domains(domains_to_check, env):
|
||||||
# run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
|
# run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
|
||||||
|
|
||||||
# Parallelize the checks across a worker pool.
|
# Parallelize the checks across a worker pool.
|
||||||
args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records, ssl_certificates)
|
args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records)
|
||||||
for domain in domains_to_check)
|
for domain in domains_to_check)
|
||||||
ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1)
|
ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1)
|
||||||
ret = dict(ret) # (domain, output) => { domain: output }
|
ret = dict(ret) # (domain, output) => { domain: output }
|
||||||
for domain in sort_domains(ret, env):
|
for domain in sort_domains(ret, env):
|
||||||
ret[domain].playback(output)
|
ret[domain].playback(output)
|
||||||
|
|
||||||
def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records, ssl_certificates):
|
def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records):
|
||||||
output = BufferedOutput()
|
output = BufferedOutput()
|
||||||
|
|
||||||
|
# we'd move this up, but this returns non-pickleable values
|
||||||
|
ssl_certificates = get_ssl_certificates(env)
|
||||||
|
|
||||||
# The domain is IDNA-encoded in the database, but for display use Unicode.
|
# The domain is IDNA-encoded in the database, but for display use Unicode.
|
||||||
try:
|
try:
|
||||||
domain_display = idna.decode(domain.encode('ascii'))
|
domain_display = idna.decode(domain.encode('ascii'))
|
||||||
@@ -447,7 +488,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
|||||||
elif ip is None:
|
elif ip is None:
|
||||||
output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns)
|
output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns)
|
||||||
else:
|
else:
|
||||||
output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, env['PUBLIC_IP']))
|
output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, correct_ip))
|
||||||
|
|
||||||
def check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records):
|
def check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records):
|
||||||
# Warn if a custom DNS record is preventing this or the automatic www redirect from
|
# Warn if a custom DNS record is preventing this or the automatic www redirect from
|
||||||
@@ -584,15 +625,23 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output):
|
|||||||
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
|
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
|
||||||
# other domains, it is required to access its website.
|
# other domains, it is required to access its website.
|
||||||
if domain != env['PRIMARY_HOSTNAME']:
|
if domain != env['PRIMARY_HOSTNAME']:
|
||||||
ip = query_dns(domain, "A")
|
ok_values = []
|
||||||
if ip == env['PUBLIC_IP']:
|
for (rtype, expected) in (("A", env['PUBLIC_IP']), ("AAAA", env.get('PUBLIC_IPV6'))):
|
||||||
output.print_ok("Domain resolves to this box's IP address. [%s ↦ %s]" % (domain, env['PUBLIC_IP']))
|
if not expected: continue # IPv6 is not configured
|
||||||
else:
|
value = query_dns(domain, rtype)
|
||||||
output.print_error("""This domain should resolve to your box's IP address (%s) if you would like the box to serve
|
if value == expected:
|
||||||
webmail or a website on this domain. The domain currently resolves to %s in public DNS. It may take several hours for
|
ok_values.append(value)
|
||||||
public DNS to update after a change. This problem may result from other issues listed here.""" % (env['PUBLIC_IP'], ip))
|
else:
|
||||||
|
output.print_error("""This domain should resolve to your box's IP address (%s %s) if you would like the box to serve
|
||||||
|
webmail or a website on this domain. The domain currently resolves to %s in public DNS. It may take several hours for
|
||||||
|
public DNS to update after a change. This problem may result from other issues listed here.""" % (rtype, expected, value))
|
||||||
|
return
|
||||||
|
|
||||||
# We need a SSL certificate for PRIMARY_HOSTNAME because that's where the
|
# If both A and AAAA are correct...
|
||||||
|
output.print_ok("Domain resolves to this box's IP address. [%s ↦ %s]" % (domain, '; '.join(ok_values)))
|
||||||
|
|
||||||
|
|
||||||
|
# We need a TLS certificate for PRIMARY_HOSTNAME because that's where the
|
||||||
# user will log in with IMAP or webmail. Any other domain we serve a
|
# user will log in with IMAP or webmail. Any other domain we serve a
|
||||||
# website for also needs a signed certificate.
|
# website for also needs a signed certificate.
|
||||||
check_ssl_cert(domain, rounded_time, ssl_certificates, env, output)
|
check_ssl_cert(domain, rounded_time, ssl_certificates, env, output)
|
||||||
@@ -634,56 +683,39 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None):
|
|||||||
return "; ".join(sorted(str(r).rstrip('.') for r in response))
|
return "; ".join(sorted(str(r).rstrip('.') for r in response))
|
||||||
|
|
||||||
def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
|
def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
|
||||||
# Check that SSL certificate is signed.
|
# Check that TLS certificate is signed.
|
||||||
|
|
||||||
# Skip the check if the A record is not pointed here.
|
# Skip the check if the A record is not pointed here.
|
||||||
if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return
|
if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return
|
||||||
|
|
||||||
# Where is the SSL stored?
|
# Where is the certificate file stored?
|
||||||
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
|
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
|
||||||
|
if tls_cert is None:
|
||||||
if x is None:
|
output.print_warning("""No TLS (SSL) certificate is installed for this domain. Visitors to a website on
|
||||||
output.print_warning("""No SSL certificate is installed for this domain. Visitors to a website on
|
|
||||||
this domain will get a security warning. If you are not serving a website on this domain, you do
|
this domain will get a security warning. If you are not serving a website on this domain, you do
|
||||||
not need to take any action. Use the SSL Certificates page in the control panel to install a
|
not need to take any action. Use the TLS Certificates page in the control panel to install a
|
||||||
SSL certificate.""")
|
TLS certificate.""")
|
||||||
return
|
return
|
||||||
|
|
||||||
ssl_key, ssl_certificate, ssl_via = x
|
|
||||||
|
|
||||||
# Check that the certificate is good.
|
# Check that the certificate is good.
|
||||||
|
|
||||||
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, rounded_time=rounded_time)
|
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"], rounded_time=rounded_time)
|
||||||
|
|
||||||
if cert_status == "OK":
|
if cert_status == "OK":
|
||||||
# The certificate is ok. The details has expiry info.
|
# The certificate is ok. The details has expiry info.
|
||||||
output.print_ok("SSL certificate is signed & valid. %s %s" % (ssl_via if ssl_via else "", cert_status_details))
|
output.print_ok("TLS (SSL) certificate is signed & valid. " + cert_status_details)
|
||||||
|
|
||||||
elif cert_status == "SELF-SIGNED":
|
elif cert_status == "SELF-SIGNED":
|
||||||
# Offer instructions for purchasing a signed certificate.
|
# Offer instructions for purchasing a signed certificate.
|
||||||
|
|
||||||
fingerprint = shell('check_output', [
|
|
||||||
"openssl",
|
|
||||||
"x509",
|
|
||||||
"-in", ssl_certificate,
|
|
||||||
"-noout",
|
|
||||||
"-fingerprint"
|
|
||||||
])
|
|
||||||
fingerprint = re.sub(".*Fingerprint=", "", fingerprint).strip()
|
|
||||||
|
|
||||||
if domain == env['PRIMARY_HOSTNAME']:
|
if domain == env['PRIMARY_HOSTNAME']:
|
||||||
output.print_error("""The SSL certificate for this domain is currently self-signed. You will get a security
|
output.print_error("""The TLS (SSL) certificate for this domain is currently self-signed. You will get a security
|
||||||
warning when you check or send email and when visiting this domain in a web browser (for webmail or
|
warning when you check or send email and when visiting this domain in a web browser (for webmail or
|
||||||
static site hosting). Use the SSL Certificates page in the control panel to install a signed SSL certificate.
|
static site hosting).""")
|
||||||
You may choose to leave the self-signed certificate in place and confirm the security exception, but check that
|
|
||||||
the certificate fingerprint matches the following:""")
|
|
||||||
output.print_line("")
|
|
||||||
output.print_line(" " + fingerprint, monospace=True)
|
|
||||||
else:
|
else:
|
||||||
output.print_error("""The SSL certificate for this domain is self-signed.""")
|
output.print_error("""The TLS (SSL) certificate for this domain is self-signed.""")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
output.print_error("The SSL certificate has a problem: " + cert_status)
|
output.print_error("The TLS (SSL) certificate has a problem: " + cert_status)
|
||||||
if cert_status_details:
|
if cert_status_details:
|
||||||
output.print_line("")
|
output.print_line("")
|
||||||
output.print_line(cert_status_details)
|
output.print_line(cert_status_details)
|
||||||
@@ -732,10 +764,10 @@ def what_version_is_this(env):
|
|||||||
return tag
|
return tag
|
||||||
|
|
||||||
def get_latest_miab_version():
|
def get_latest_miab_version():
|
||||||
# This pings https://mailinabox.email/bootstrap.sh and extracts the tag named in
|
# This pings https://mailinabox.email/setup.sh and extracts the tag named in
|
||||||
# the script to determine the current product version.
|
# the script to determine the current product version.
|
||||||
import urllib.request
|
import urllib.request
|
||||||
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/bootstrap.sh?ping=1").read()).group(1).decode("utf8")
|
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/setup.sh?ping=1").read()).group(1).decode("utf8")
|
||||||
|
|
||||||
def check_miab_version(env, output):
|
def check_miab_version(env, output):
|
||||||
config = load_settings(env)
|
config = load_settings(env)
|
||||||
@@ -743,23 +775,24 @@ def check_miab_version(env, output):
|
|||||||
if config.get("privacy", True):
|
if config.get("privacy", True):
|
||||||
output.print_warning("Mail-in-a-Box version check disabled by privacy setting.")
|
output.print_warning("Mail-in-a-Box version check disabled by privacy setting.")
|
||||||
else:
|
else:
|
||||||
this_ver = what_version_is_this(env)
|
try:
|
||||||
|
this_ver = what_version_is_this(env)
|
||||||
|
except:
|
||||||
|
this_ver = "Unknown"
|
||||||
|
|
||||||
latest_ver = get_latest_miab_version()
|
latest_ver = get_latest_miab_version()
|
||||||
|
|
||||||
if this_ver == latest_ver:
|
if this_ver == latest_ver:
|
||||||
output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver)
|
output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver)
|
||||||
else:
|
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. "
|
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))
|
% (this_ver, latest_ver))
|
||||||
|
|
||||||
def run_and_output_changes(env, pool, send_via_email):
|
def run_and_output_changes(env, pool):
|
||||||
import json
|
import json
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
if not send_via_email:
|
out = ConsoleOutput()
|
||||||
out = ConsoleOutput()
|
|
||||||
else:
|
|
||||||
import io
|
|
||||||
out = FileOutput(io.StringIO(""), 70)
|
|
||||||
|
|
||||||
# Run status checks.
|
# Run status checks.
|
||||||
cur = BufferedOutput()
|
cur = BufferedOutput()
|
||||||
@@ -818,28 +851,6 @@ def run_and_output_changes(env, pool, send_via_email):
|
|||||||
out.add_heading(category)
|
out.add_heading(category)
|
||||||
out.print_warning("This section was removed.")
|
out.print_warning("This section was removed.")
|
||||||
|
|
||||||
if send_via_email:
|
|
||||||
# If there were changes, send off an email.
|
|
||||||
buf = out.buf.getvalue()
|
|
||||||
if len(buf) > 0:
|
|
||||||
# create MIME message
|
|
||||||
from email.message import Message
|
|
||||||
msg = Message()
|
|
||||||
msg['From'] = "\"%s\" <administrator@%s>" % (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'])
|
|
||||||
msg['To'] = "administrator@%s" % env['PRIMARY_HOSTNAME']
|
|
||||||
msg['Subject'] = "[%s] Status Checks Change Notice" % env['PRIMARY_HOSTNAME']
|
|
||||||
msg.set_payload(buf, "UTF-8")
|
|
||||||
|
|
||||||
# send to administrator@
|
|
||||||
import smtplib
|
|
||||||
mailserver = smtplib.SMTP('localhost', 25)
|
|
||||||
mailserver.ehlo()
|
|
||||||
mailserver.sendmail(
|
|
||||||
"administrator@%s" % env['PRIMARY_HOSTNAME'], # MAIL FROM
|
|
||||||
"administrator@%s" % env['PRIMARY_HOSTNAME'], # RCPT TO
|
|
||||||
msg.as_string())
|
|
||||||
mailserver.quit()
|
|
||||||
|
|
||||||
# Store the current status checks output for next time.
|
# Store the current status checks output for next time.
|
||||||
os.makedirs(os.path.dirname(cache_fn), exist_ok=True)
|
os.makedirs(os.path.dirname(cache_fn), exist_ok=True)
|
||||||
with open(cache_fn, "w") as f:
|
with open(cache_fn, "w") as f:
|
||||||
@@ -870,7 +881,7 @@ class FileOutput:
|
|||||||
words = re.split("(\s+)", message)
|
words = re.split("(\s+)", message)
|
||||||
linelen = 0
|
linelen = 0
|
||||||
for w in words:
|
for w in words:
|
||||||
if linelen + len(w) > self.width-1-len(first_line):
|
if self.width and (linelen + len(w) > self.width-1-len(first_line)):
|
||||||
print(file=self.buf)
|
print(file=self.buf)
|
||||||
print(" ", end="", file=self.buf)
|
print(" ", end="", file=self.buf)
|
||||||
linelen = 0
|
linelen = 0
|
||||||
@@ -886,10 +897,22 @@ class FileOutput:
|
|||||||
class ConsoleOutput(FileOutput):
|
class ConsoleOutput(FileOutput):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.buf = sys.stdout
|
self.buf = sys.stdout
|
||||||
try:
|
|
||||||
self.width = int(shell('check_output', ['stty', 'size']).split()[1])
|
# Do nice line-wrapping according to the size of the terminal.
|
||||||
except:
|
# The 'stty' program queries standard input for terminal information.
|
||||||
self.width = 76
|
if sys.stdin.isatty():
|
||||||
|
try:
|
||||||
|
self.width = int(shell('check_output', ['stty', 'size']).split()[1])
|
||||||
|
except:
|
||||||
|
self.width = 76
|
||||||
|
|
||||||
|
else:
|
||||||
|
# However if standard input is not a terminal, we would get
|
||||||
|
# "stty: standard input: Inappropriate ioctl for device". So
|
||||||
|
# we test with sys.stdin.isatty first, and if it is not a
|
||||||
|
# terminal don't do any line wrapping. When this script is
|
||||||
|
# run from cron, or if stdin has been redirected, this happens.
|
||||||
|
self.width = None
|
||||||
|
|
||||||
class BufferedOutput:
|
class BufferedOutput:
|
||||||
# Record all of the instance method calls so we can play them back later.
|
# Record all of the instance method calls so we can play them back later.
|
||||||
@@ -917,7 +940,7 @@ if __name__ == "__main__":
|
|||||||
run_checks(False, env, ConsoleOutput(), pool)
|
run_checks(False, env, ConsoleOutput(), pool)
|
||||||
|
|
||||||
elif sys.argv[1] == "--show-changes":
|
elif sys.argv[1] == "--show-changes":
|
||||||
run_and_output_changes(env, pool, sys.argv[-1] == "--smtp")
|
run_and_output_changes(env, pool)
|
||||||
|
|
||||||
elif sys.argv[1] == "--check-primary-hostname":
|
elif sys.argv[1] == "--check-primary-hostname":
|
||||||
# See if the primary hostname appears resolvable and has a signed certificate.
|
# See if the primary hostname appears resolvable and has a signed certificate.
|
||||||
@@ -925,10 +948,10 @@ if __name__ == "__main__":
|
|||||||
if query_dns(domain, "A") != env['PUBLIC_IP']:
|
if query_dns(domain, "A") != env['PUBLIC_IP']:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
ssl_certificates = get_ssl_certificates(env)
|
ssl_certificates = get_ssl_certificates(env)
|
||||||
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, ssl_certificates, env)
|
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
|
||||||
if not os.path.exists(ssl_certificate):
|
if not os.path.exists(tls_cert["certificate"]):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, warn_if_expiring_soon=False)
|
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"], warn_if_expiring_soon=False)
|
||||||
if cert_status != "OK":
|
if cert_status != "OK":
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@@ -106,6 +106,41 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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>forward_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 "forward_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>
|
<script>
|
||||||
function show_aliases() {
|
function show_aliases() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<p>It is possible to set custom DNS records on domains hosted here.</p>
|
<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>
|
<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>
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
<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="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="TXT" data-hint="Enter arbitrary text.">TXT (text record)</option>
|
||||||
<option value="MX" data-hint="Enter record in the form of PRIORIY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">MX (mail exchanger)</option>
|
<option value="MX" data-hint="Enter record in the form of 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>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 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 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>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc=" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -63,14 +63,14 @@
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css" integrity="sha256-bHQiqcFbnJb1Qhh61RY9cMh6kR0gTuQY6iFOBj1yj00=" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!--[if lt IE 8]><p>Internet Explorer version 8 or any modern web browser is required to use this website, sorry.<![endif]-->
|
<!--[if lt IE 8]><p>Internet Explorer version 8 or any modern web browser is required to use this website, sorry.<![endif]-->
|
||||||
<!--[if gt IE 7]><!-->
|
<!--[if gt IE 7]><!-->
|
||||||
|
|
||||||
<div class="navbar navbar-inverse" role="navigation">
|
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
|
||||||
@@ -87,13 +87,13 @@
|
|||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
|
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
|
||||||
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
|
<li><a href="#tls" onclick="return show_panel(this);">TLS (SSL) Certificates</a></li>
|
||||||
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Advanced Pages</li>
|
<li class="dropdown-header">Advanced Pages</li>
|
||||||
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
|
<li><a href="#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="#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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
{% include "web.html" %}
|
{% include "web.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="panel_ssl" class="admin_panel">
|
<div id="panel_tls" class="admin_panel">
|
||||||
{% include "ssl.html" %}
|
{% include "ssl.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
</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://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js" integrity="sha256-rsPUGdUPBXgalvIj4YKJrrUlmLXbOb6Cp7cdxn1qeUc=" crossorigin="anonymous"></script>
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha256-Sk3nkD6mLTMOF0EOpNtsIry+s1CsaqQC1rVLTAy+0yc=" crossorigin="anonymous"></script>
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var global_modal_state = null;
|
var global_modal_state = null;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function do_login() {
|
|||||||
// Open the next panel the user wants to go to. Do this after the XHR response
|
// Open the next panel the user wants to go to. Do this after the XHR response
|
||||||
// is over so that we don't start a new XHR request while this one is finishing,
|
// is over so that we don't start a new XHR request while this one is finishing,
|
||||||
// which confuses the loading indicator.
|
// which confuses the loading indicator.
|
||||||
setTimeout(function() { show_panel(!switch_back_to_panel ? 'system_status' : switch_back_to_panel) }, 300);
|
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,46 @@
|
|||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<h2>SSL Certificates</h2>
|
<h2>TLS (SSL) Certificates</h2>
|
||||||
|
|
||||||
<h3>Certificate Status</h3>
|
<p>A TLS (formerly called SSL) certificate is a cryptographic file that proves to anyone connecting to a web address that the connection is secure between you and the owner of that address.</p>
|
||||||
|
|
||||||
|
<p>You need a TLS certificate for this box’s hostname ({{hostname}}) and every other domain name and subdomain that this box is hosting a website for (see the list below).</p>
|
||||||
|
|
||||||
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;">
|
<div id="ssl_provision">
|
||||||
|
<h3>Provision a certificate</h3>
|
||||||
|
|
||||||
|
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
|
||||||
|
<button onclick='return provision_tls_cert();' class='btn btn-primary' style="float: left; margin: 0 1.5em 1em 0;">Provision</button>
|
||||||
|
<p>A TLS certificate can be automatically provisioned from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>, a free TLS certificate provider, for:<br>
|
||||||
|
<span class="text-primary"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clearfix"> </div>
|
||||||
|
|
||||||
|
<div id="ssl_provision_result"></div>
|
||||||
|
|
||||||
|
<div id="ssl_provision_problems_div" style="display: none;">
|
||||||
|
<p style="margin-bottom: .5em;">Certificates cannot be automatically provisioned for:</p>
|
||||||
|
<table id="ssl_provision_problems" style="margin-top: 0;" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Problem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>Use the <em>Install Certificate</em> button below for these domains.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Certificate status</h3>
|
||||||
|
|
||||||
|
<p style="margin-top: 1.5em">Certificates expire after a period of time. All certificates will be automatically renewed through <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a> 14 days prior to expiration.</p>
|
||||||
|
|
||||||
|
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto; display: none">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Domain</th>
|
<th>Domain</th>
|
||||||
@@ -18,17 +52,18 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for.</p>
|
|
||||||
|
|
||||||
<h3 id="ssl_install_header">Install SSL Certificate</h3>
|
<h3 id="ssl_install_header">Install certificate</h3>
|
||||||
|
|
||||||
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a> or <a href="https://buy.wosign.com/free/">WoSign’s free SSL</a></a>.</p>
|
<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>Which domain are you getting an SSL certificate for?</p>
|
<p>Which domain are you getting a certificate for?</p>
|
||||||
|
|
||||||
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
||||||
|
|
||||||
<p>What country are you in? This is required by some SSL certificate providers. You may leave this blank if you know your SSL certificate provider doesn't require it.</p>
|
<p>(A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for besides the one you choose above.)</p>
|
||||||
|
|
||||||
|
<p>What country are you in? This is required by some TLS certificate providers. You may leave this blank if you know your TLS certificate provider doesn't require it.</p>
|
||||||
|
|
||||||
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
||||||
<option value="">(Select)</option>
|
<option value="">(Select)</option>
|
||||||
@@ -38,18 +73,18 @@
|
|||||||
</select></p>
|
</select></p>
|
||||||
|
|
||||||
<div id="csr_info" style="display: none">
|
<div id="csr_info" style="display: none">
|
||||||
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
|
<p>You will need to provide the certificate provider this Certificate Signing Request (CSR):</p>
|
||||||
|
|
||||||
<pre id="ssl_csr"></pre>
|
<pre id="ssl_csr"></pre>
|
||||||
|
|
||||||
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
|
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
|
||||||
|
|
||||||
<p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
|
<p>The certificate provider will then provide you with a TLS/SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
|
||||||
|
|
||||||
<p style="margin-bottom: .5em">SSL certificate:</p>
|
<p style="margin-bottom: .5em">TLS/SSL certificate:</p>
|
||||||
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----"></textarea></p>
|
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----"></textarea></p>
|
||||||
|
|
||||||
<p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p>
|
<p style="margin-bottom: .5em">TLS/SSL intermediate chain (if provided):</p>
|
||||||
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
more stuff here
-----END CERTIFICATE-----"></textarea></p>
|
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
more stuff here
-----END CERTIFICATE-----"></textarea></p>
|
||||||
|
|
||||||
<p>After you paste in the information, click the install button.</p>
|
<p>After you paste in the information, click the install button.</p>
|
||||||
@@ -58,26 +93,53 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function show_ssl() {
|
function show_tls(keep_provisioning_shown) {
|
||||||
api(
|
api(
|
||||||
"/web/domains",
|
"/ssl/status",
|
||||||
"GET",
|
"GET",
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
function(domains) {
|
function(res) {
|
||||||
|
// provisioning status
|
||||||
|
|
||||||
|
if (!keep_provisioning_shown)
|
||||||
|
$('#ssl_provision').toggle(res.can_provision.length + res.cant_provision.length > 0)
|
||||||
|
|
||||||
|
$('#ssl_provision_p').toggle(res.can_provision.length > 0);
|
||||||
|
if (res.can_provision.length > 0)
|
||||||
|
$('#ssl_provision_p span').text(res.can_provision.join(", "));
|
||||||
|
|
||||||
|
$('#ssl_provision_problems_div').toggle(res.cant_provision.length > 0);
|
||||||
|
$('#ssl_provision_problems tbody').text("");
|
||||||
|
for (var i = 0; i < res.cant_provision.length; i++) {
|
||||||
|
var domain = res.cant_provision[i];
|
||||||
|
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td></tr>");
|
||||||
|
$('#ssl_provision_problems tbody').append(row);
|
||||||
|
row.attr('data-domain', domain.domain);
|
||||||
|
row.find('.domain a').text(domain.domain);
|
||||||
|
row.find('.domain a').attr('href', 'https://' + domain.domain);
|
||||||
|
row.find('.status').text(domain.problem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificate status
|
||||||
|
var domains = res.status;
|
||||||
var tb = $('#ssl_domains tbody');
|
var tb = $('#ssl_domains tbody');
|
||||||
tb.text('');
|
tb.text('');
|
||||||
$('#ssldomain').html('<option value="">(select)</option>');
|
$('#ssldomain').html('<option value="">(select)</option>');
|
||||||
|
$('#ssl_domains').show();
|
||||||
for (var i = 0; i < domains.length; i++) {
|
for (var i = 0; i < domains.length; i++) {
|
||||||
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
|
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
|
||||||
tb.append(row);
|
tb.append(row);
|
||||||
row.attr('data-domain', domains[i].domain);
|
row.attr('data-domain', domains[i].domain);
|
||||||
row.find('.domain a').text(domains[i].domain);
|
row.find('.domain a').text(domains[i].domain);
|
||||||
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
||||||
row.addClass("text-" + domains[i].ssl_certificate[0]);
|
if (domains[i].status == "not-applicable") {
|
||||||
row.find('.status').text(domains[i].ssl_certificate[1]);
|
domains[i].status = "muted"; // text-muted css class
|
||||||
if (domains[i].ssl_certificate[0] == "success") {
|
row.find('.actions a').remove(); // no actions applicable
|
||||||
|
}
|
||||||
|
row.addClass("text-" + domains[i].status);
|
||||||
|
row.find('.status').text(domains[i].text);
|
||||||
|
if (domains[i].status == "success") {
|
||||||
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
|
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
|
||||||
} else {
|
} else {
|
||||||
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
|
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
|
||||||
@@ -91,14 +153,15 @@ function show_ssl() {
|
|||||||
function ssl_install(elem) {
|
function ssl_install(elem) {
|
||||||
var domain = $(elem).parents('tr').attr('data-domain');
|
var domain = $(elem).parents('tr').attr('data-domain');
|
||||||
$('#ssldomain').val(domain);
|
$('#ssldomain').val(domain);
|
||||||
$('#csr_info').slideDown();
|
|
||||||
$('#ssl_csr').text('Loading...');
|
|
||||||
show_csr();
|
show_csr();
|
||||||
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
|
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function show_csr() {
|
function show_csr() {
|
||||||
|
if ($('#ssldomain').val() == "") return;
|
||||||
|
$('#csr_info').slideDown();
|
||||||
|
$('#ssl_csr').text('Loading...');
|
||||||
api(
|
api(
|
||||||
"/ssl/csr/" + $('#ssldomain').val(),
|
"/ssl/csr/" + $('#ssldomain').val(),
|
||||||
"POST",
|
"POST",
|
||||||
@@ -122,10 +185,100 @@ function install_cert() {
|
|||||||
function(status) {
|
function(status) {
|
||||||
if (/^OK($|\n)/.test(status)) {
|
if (/^OK($|\n)/.test(status)) {
|
||||||
console.log(status)
|
console.log(status)
|
||||||
show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
|
show_modal_error("TLS Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
|
||||||
} else {
|
} else {
|
||||||
show_modal_error("SSL Certificate Installation", status);
|
show_modal_error("TLS Certificate Installation", status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var agree_to_tos_url_prompt = null;
|
||||||
|
var agree_to_tos_url = null;
|
||||||
|
function provision_tls_cert() {
|
||||||
|
// Automatically provision any certs.
|
||||||
|
$('#ssl_provision_p .btn').attr('disabled', '1'); // prevent double-clicks
|
||||||
|
api(
|
||||||
|
"/ssl/provision",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
agree_to_tos_url: agree_to_tos_url
|
||||||
|
},
|
||||||
|
function(status) {
|
||||||
|
// Clear last attempt.
|
||||||
|
agree_to_tos_url = null;
|
||||||
|
$('#ssl_provision_result').text("");
|
||||||
|
may_reenable_provision_button = true;
|
||||||
|
|
||||||
|
// Nothing was done. There might also be problem domains, but we've already displayed those.
|
||||||
|
if (status.requests.length == 0) {
|
||||||
|
show_modal_error("TLS Certificate Provisioning", "There were no domain names to provision certificates for.");
|
||||||
|
// don't return - haven't re-enabled the provision button
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each provisioning API call returns zero or more "requests" which represent
|
||||||
|
// a request to Let's Encrypt for a single certificate. Normally there is just
|
||||||
|
// one request (for a single multi-domain certificate).
|
||||||
|
for (var i = 0; i < status.requests.length; i++) {
|
||||||
|
var r = status.requests[i];
|
||||||
|
|
||||||
|
// create an HTML block to display the results of this request
|
||||||
|
var n = $("<div><h4/><p/></div>");
|
||||||
|
$('#ssl_provision_result').append(n);
|
||||||
|
|
||||||
|
// show a header only to disambiguate request blocks
|
||||||
|
if (status.requests.length > 0)
|
||||||
|
n.find("h4").text(r.domains.join(", "));
|
||||||
|
|
||||||
|
if (r.result == "agree-to-tos") {
|
||||||
|
// user needs to agree to Let's Encrypt's TOS
|
||||||
|
agree_to_tos_url_prompt = r.url;
|
||||||
|
$('#ssl_provision_p .btn').attr('disabled', '1');
|
||||||
|
n.find("p").html("Please open and review <a href='" + r.url + "' target='_blank'>Let's Encrypt's terms of service agreement</a>. You must agree to their terms for a certificate to be automatically provisioned from them.");
|
||||||
|
n.append($('<button onclick="agree_to_tos_url = agree_to_tos_url_prompt; return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Agree & Try Again</button>'));
|
||||||
|
|
||||||
|
// don't re-enable the Provision button -- user must use the Agree button
|
||||||
|
may_reenable_provision_button = false;
|
||||||
|
|
||||||
|
} else if (r.result == "error") {
|
||||||
|
n.find("p").addClass("text-danger").text(r.message);
|
||||||
|
|
||||||
|
} else if (r.result == "wait") {
|
||||||
|
// Show a button that counts down to zero, at which point it becomes enabled.
|
||||||
|
n.find("p").text("A certificate is now in the process of being provisioned, but it takes some time. Please wait until the Finish button is enabled, and then click it to acquire the certificate.");
|
||||||
|
var b = $('<button onclick="return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Finish</button>');
|
||||||
|
b.attr("disabled", "1");
|
||||||
|
var now = new Date();
|
||||||
|
n.append(b);
|
||||||
|
function ready_to_finish() {
|
||||||
|
var remaining = Math.round(r.seconds - (new Date() - now)/1000);
|
||||||
|
if (remaining > 0) {
|
||||||
|
setTimeout(ready_to_finish, 1000);
|
||||||
|
b.text("Finish (" + remaining + "...)")
|
||||||
|
} else {
|
||||||
|
b.text("Finish (ready)")
|
||||||
|
b.removeAttr("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready_to_finish();
|
||||||
|
|
||||||
|
// don't re-enable the Provision button -- user must use the Retry button when it becomes enabled
|
||||||
|
may_reenable_provision_button = false;
|
||||||
|
|
||||||
|
} else if (r.result == "installed") {
|
||||||
|
n.find("p").addClass("text-success").text("The TLS certificate was provisioned and installed.");
|
||||||
|
setTimeout("show_tls(true)", 1); // update main table of certificate statuses, call with arg keep_provisioning_shown true so that we don't clear what we just outputted
|
||||||
|
}
|
||||||
|
|
||||||
|
// display the detailed log info in case of problems
|
||||||
|
var trace = $("<div class='small text-muted' style='margin-top: 1.5em'>Log:</div>");
|
||||||
|
n.append(trace);
|
||||||
|
for (var j = 0; j < r.log.length; j++)
|
||||||
|
trace.append($("<div/>").text(r.log[j]));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (may_reenable_provision_button)
|
||||||
|
$('#ssl_provision_p .btn').removeAttr("disabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
<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>
|
||||||
|
|
||||||
@@ -121,6 +121,11 @@ function show_system_backup() {
|
|||||||
"GET",
|
"GET",
|
||||||
{ },
|
{ },
|
||||||
function(r) {
|
function(r) {
|
||||||
|
if (r.error) {
|
||||||
|
show_modal_error("Backup Error", $("<pre/>").text(r.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$('#backup-status tbody').html("");
|
$('#backup-status tbody').html("");
|
||||||
var total_disk_size = 0;
|
var total_disk_size = 0;
|
||||||
|
|
||||||
@@ -137,7 +142,7 @@ function show_system_backup() {
|
|||||||
var b = r.backups[i];
|
var b = r.backups[i];
|
||||||
var tr = $('<tr/>');
|
var tr = $('<tr/>');
|
||||||
if (b.full) tr.addClass("full-backup");
|
if (b.full) tr.addClass("full-backup");
|
||||||
tr.append( $('<td/>').text(b.date_str + " " + r.tz) );
|
tr.append( $('<td/>').text(b.date_str) );
|
||||||
tr.append( $('<td/>').text(b.date_delta + " ago") );
|
tr.append( $('<td/>').text(b.date_delta + " ago") );
|
||||||
tr.append( $('<td/>').text(b.full ? "full" : "increment") );
|
tr.append( $('<td/>').text(b.full ? "full" : "increment") );
|
||||||
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.size)) );
|
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.size)) );
|
||||||
|
|||||||
@@ -34,19 +34,23 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#system-privacy-setting {
|
|
||||||
float: right;
|
|
||||||
max-width: 20em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-push-9 col-md-3">
|
||||||
|
|
||||||
|
<div id="system-reboot-required" style="display: none; margin-bottom: 1em;">
|
||||||
|
<button type="button" class="btn btn-danger" onclick="confirm_reboot(); return false;">Reboot Box</button>
|
||||||
|
<div>No reboot is necessary.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="system-privacy-setting" style="display: none">
|
<div id="system-privacy-setting" style="display: none">
|
||||||
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span> New-Version Check</a></div>
|
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span> New-Version Check</a></div>
|
||||||
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of Mail-in-a-Box.)</small></p>
|
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of Mail-in-a-Box.)</small></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- /col -->
|
||||||
|
<div class="col-md-pull-3 col-md-8">
|
||||||
|
|
||||||
<table id="system-checks" class="table" style="max-width: 60em">
|
<table id="system-checks" class="table" style="max-width: 60em">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -55,6 +59,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</div> <!-- /col -->
|
||||||
|
</div> <!-- /row -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function show_system_status() {
|
function show_system_status() {
|
||||||
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
|
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
|
||||||
@@ -70,6 +77,16 @@ function show_system_status() {
|
|||||||
$('#system-privacy-setting p').toggle(r);
|
$('#system-privacy-setting p').toggle(r);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api(
|
||||||
|
"/system/reboot",
|
||||||
|
"GET",
|
||||||
|
{ },
|
||||||
|
function(r) {
|
||||||
|
$('#system-reboot-required').show(); // show when r becomes available
|
||||||
|
$('#system-reboot-required').find('button').toggle(r);
|
||||||
|
$('#system-reboot-required').find('div').toggle(!r);
|
||||||
|
});
|
||||||
|
|
||||||
api(
|
api(
|
||||||
"/system/status",
|
"/system/status",
|
||||||
"POST",
|
"POST",
|
||||||
@@ -122,4 +139,22 @@ function enable_privacy(status) {
|
|||||||
});
|
});
|
||||||
return false; // disable link
|
return false; // disable link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirm_reboot() {
|
||||||
|
show_modal_confirm(
|
||||||
|
"Reboot",
|
||||||
|
$("<p>This will reboot your Mail-in-a-Box <code>{{hostname}}</code>.</p> <p>Until the machine is fully restarted, your users will not be able to send and receive email, and you will not be able to connect to this control panel or with SSH. The reboot cannot be cancelled.</p>"),
|
||||||
|
"Reboot Now",
|
||||||
|
function() {
|
||||||
|
api(
|
||||||
|
"/system/reboot",
|
||||||
|
"POST",
|
||||||
|
{ },
|
||||||
|
function(r) {
|
||||||
|
var msg = "<p>Please reload this page after a minute or so.</p>";
|
||||||
|
if (r) msg = "<p>The reboot command said:</p> <pre>" + $("<pre/>").text(r).html() + "</pre>"; // successful reboots don't produce any output; the output must be HTML-escaped
|
||||||
|
show_modal_error("Reboot", msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -84,6 +84,48 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
function show_users() {
|
function show_users() {
|
||||||
@@ -170,7 +212,7 @@ function users_set_password(elem) {
|
|||||||
yourpw = "<p class='text-danger'>If you change your own password, you will be logged out of this control panel and will need to log in again.</p>";
|
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(
|
show_modal_confirm(
|
||||||
"Archive User",
|
"Set Password",
|
||||||
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small>" + yourpw + "</p>"),
|
$("<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",
|
"Set Password",
|
||||||
function() {
|
function() {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function show_change_web_root(elem) {
|
|||||||
var root = $(elem).parents('tr').attr('data-custom-web-root');
|
var root = $(elem).parents('tr').attr('data-custom-web-root');
|
||||||
show_modal_confirm(
|
show_modal_confirm(
|
||||||
'Change Root Directory for ' + domain,
|
'Change Root Directory for ' + domain,
|
||||||
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.'),
|
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.</p>'),
|
||||||
'Update',
|
'Update',
|
||||||
function() { do_web_update(); });
|
function() { do_web_update(); });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from dns_update import get_custom_dns_config, get_dns_zones
|
|||||||
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
||||||
from utils import shell, safe_domain_name, sort_domains
|
from utils import shell, safe_domain_name, sort_domains
|
||||||
|
|
||||||
def get_web_domains(env, include_www_redirects=True):
|
def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True):
|
||||||
# What domains should we serve HTTP(S) for?
|
# What domains should we serve HTTP(S) for?
|
||||||
domains = set()
|
domains = set()
|
||||||
|
|
||||||
@@ -24,9 +24,10 @@ def get_web_domains(env, include_www_redirects=True):
|
|||||||
# the topmost of each domain we serve.
|
# the topmost of each domain we serve.
|
||||||
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
||||||
|
|
||||||
# ...Unless the domain has an A/AAAA record that maps it to a different
|
if exclude_dns_elsewhere:
|
||||||
# IP address than this box. Remove those domains from our list.
|
# ...Unless the domain has an A/AAAA record that maps it to a different
|
||||||
domains -= get_domains_with_a_records(env)
|
# IP address than this box. Remove those domains from our list.
|
||||||
|
domains -= get_domains_with_a_records(env)
|
||||||
|
|
||||||
# Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail
|
# Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail
|
||||||
# as well as Z-Push for Exchange ActiveSync. This can't be removed
|
# as well as Z-Push for Exchange ActiveSync. This can't be removed
|
||||||
@@ -119,7 +120,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
|||||||
root = get_web_root(domain, env)
|
root = get_web_root(domain, env)
|
||||||
|
|
||||||
# What private key and SSL certificate will we use for this domain?
|
# What private key and SSL certificate will we use for this domain?
|
||||||
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, ssl_certificates, env)
|
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
|
||||||
|
|
||||||
# ADDITIONAL DIRECTIVES.
|
# ADDITIONAL DIRECTIVES.
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
|||||||
finally:
|
finally:
|
||||||
f.close()
|
f.close()
|
||||||
return sha1.hexdigest()
|
return sha1.hexdigest()
|
||||||
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
|
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
|
||||||
|
|
||||||
# Add in any user customizations in YAML format.
|
# Add in any user customizations in YAML format.
|
||||||
hsts = "yes"
|
hsts = "yes"
|
||||||
@@ -177,8 +178,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
|||||||
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
|
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
|
||||||
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
|
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
|
||||||
nginx_conf = nginx_conf.replace("$ROOT", root)
|
nginx_conf = nginx_conf.replace("$ROOT", root)
|
||||||
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
|
nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"])
|
||||||
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"])
|
||||||
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
|
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
|
||||||
|
|
||||||
return nginx_conf
|
return nginx_conf
|
||||||
@@ -193,20 +194,15 @@ def get_web_root(domain, env, test_exists=True):
|
|||||||
def get_web_domains_info(env):
|
def get_web_domains_info(env):
|
||||||
www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
|
www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
|
||||||
has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
|
has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
|
||||||
|
ssl_certificates = get_ssl_certificates(env)
|
||||||
|
|
||||||
# for the SSL config panel, get cert status
|
# for the SSL config panel, get cert status
|
||||||
def check_cert(domain):
|
def check_cert(domain):
|
||||||
ssl_certificates = get_ssl_certificates(env)
|
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
|
||||||
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
|
if tls_cert is None: return ("danger", "No Certificate Installed")
|
||||||
if x is None: return ("danger", "No Certificate Installed")
|
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"])
|
||||||
ssl_key, ssl_certificate, ssl_via = x
|
|
||||||
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
|
|
||||||
if cert_status == "OK":
|
if cert_status == "OK":
|
||||||
if not ssl_via:
|
return ("success", "Signed & valid. " + cert_status_details)
|
||||||
return ("success", "Signed & valid. " + cert_status_details)
|
|
||||||
else:
|
|
||||||
# This is an alternate domain but using the same cert as the primary domain.
|
|
||||||
return ("success", "Signed & valid. " + ssl_via)
|
|
||||||
elif cert_status == "SELF-SIGNED":
|
elif cert_status == "SELF-SIGNED":
|
||||||
return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
|
return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
|
||||||
else:
|
else:
|
||||||
@@ -221,4 +217,4 @@ def get_web_domains_info(env):
|
|||||||
"static_enabled": domain not in (www_redirects | has_root_proxy_or_redirect),
|
"static_enabled": domain not in (www_redirects | has_root_proxy_or_redirect),
|
||||||
}
|
}
|
||||||
for domain in get_web_domains(env)
|
for domain in get_web_domains(env)
|
||||||
]
|
]
|
||||||
|
|||||||
16
security.md
16
security.md
@@ -38,7 +38,7 @@ These services are protected by [TLS](https://en.wikipedia.org/wiki/Transport_La
|
|||||||
|
|
||||||
The services all follow these rules:
|
The services all follow these rules:
|
||||||
|
|
||||||
* SSL certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
|
* TLS certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
|
||||||
* Only TLSv1, TLSv1.1 and TLSv1.2 are offered (the older SSL protocols are not offered).
|
* 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.
|
* 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.
|
* The minimum cipher key length offered is 112 bits. The maximum is 256 bits. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy.
|
||||||
@@ -69,6 +69,16 @@ The [setup guide video](https://mailinabox.email/) explains how to verify the ho
|
|||||||
|
|
||||||
If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that the box automatically puts into DNS can also be used to verify the host key fingerprint by setting `VerifyHostKeyDNS yes` in your `ssh/.config` file or by logging in with `ssh -o VerifyHostKeyDNS=yes`. ([source](management/dns_update.py))
|
If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that the box automatically puts into DNS can also be used to verify the host key fingerprint by setting `VerifyHostKeyDNS yes` in your `ssh/.config` file or by logging in with `ssh -o VerifyHostKeyDNS=yes`. ([source](management/dns_update.py))
|
||||||
|
|
||||||
|
### Brute-force attack mitigation
|
||||||
|
|
||||||
|
`fail2ban` provides some protection from brute-force login attacks (repeated logins that guess account passwords) by blocking offending IP addresses at the network level.
|
||||||
|
|
||||||
|
The following services are protected: SSH, IMAP (dovecot), SMTP submission (postfix), webmail (roundcube), ownCloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel & munin (over HTTP).
|
||||||
|
|
||||||
|
Some other services running on the box may be missing fail2ban filters.
|
||||||
|
|
||||||
|
`fail2ban` only blocks IPv4 addresses, however. If the box has a public IPv6 address, it is not protected from these attacks.
|
||||||
|
|
||||||
Outbound Mail
|
Outbound Mail
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -80,7 +90,7 @@ The first step in resolving the destination server for an email address is perfo
|
|||||||
|
|
||||||
### Encryption
|
### 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
|
### DANE
|
||||||
|
|
||||||
@@ -101,7 +111,7 @@ Incoming Mail
|
|||||||
|
|
||||||
### Encryption
|
### 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
|
### DANE
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
#########################################################
|
#########################################################
|
||||||
# This script is intended to be run like this:
|
# This script is intended to be run like this:
|
||||||
#
|
#
|
||||||
# curl https://.../bootstrap.sh | sudo bash
|
# curl https://mailinabox.email/setup.sh | sudo bash
|
||||||
#
|
#
|
||||||
#########################################################
|
#########################################################
|
||||||
|
|
||||||
if [ -z "$TAG" ]; then
|
if [ -z "$TAG" ]; then
|
||||||
TAG=v0.15
|
TAG=v0.19
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Are we running as root?
|
# Are we running as root?
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
|
|||||||
InternalHosts refile:/etc/opendkim/TrustedHosts
|
InternalHosts refile:/etc/opendkim/TrustedHosts
|
||||||
KeyTable refile:/etc/opendkim/KeyTable
|
KeyTable refile:/etc/opendkim/KeyTable
|
||||||
SigningTable refile:/etc/opendkim/SigningTable
|
SigningTable refile:/etc/opendkim/SigningTable
|
||||||
Socket inet:8891@localhost
|
Socket inet:8891@127.0.0.1
|
||||||
RequireSafeKeys false
|
RequireSafeKeys false
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
@@ -39,7 +39,7 @@ fi
|
|||||||
# Create a new DKIM key. This creates mail.private and mail.txt
|
# Create a new DKIM key. This creates mail.private and mail.txt
|
||||||
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
|
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
|
||||||
# the latter is the suggested DNS TXT entry which we'll include
|
# the latter is the suggested DNS TXT entry which we'll include
|
||||||
# in our DNS setup. Note tha the files are named after the
|
# in our DNS setup. Note that the files are named after the
|
||||||
# 'selector' of the key, which we can change later on to support
|
# 'selector' of the key, which we can change later on to support
|
||||||
# key rotation.
|
# key rotation.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ apt_install \
|
|||||||
# would be 20 users). Set it to 250 times the number of cores this
|
# 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).
|
# machine has, so on a two-core machine that's 500 processes/100 users).
|
||||||
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
|
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
|
||||||
default_process_limit=$(echo "`nproc` * 250" | bc)
|
default_process_limit=$(echo "`nproc` * 250" | bc) \
|
||||||
|
log_path=/var/log/mail.log
|
||||||
|
|
||||||
# The inotify `max_user_instances` default is 128, which constrains
|
# The inotify `max_user_instances` default is 128, which constrains
|
||||||
# the total number of watched (IMAP IDLE push) folders by open connections.
|
# the total number of watched (IMAP IDLE push) folders by open connections.
|
||||||
@@ -203,5 +204,8 @@ chown -R mail.mail $STORAGE_ROOT/mail/sieve
|
|||||||
ufw_allow imaps
|
ufw_allow imaps
|
||||||
ufw_allow pop3s
|
ufw_allow pop3s
|
||||||
|
|
||||||
|
# Allow the Sieve port in the firewall.
|
||||||
|
ufw_allow sieve
|
||||||
|
|
||||||
# Restart services.
|
# Restart services.
|
||||||
restart_service dovecot
|
restart_service dovecot
|
||||||
|
|||||||
@@ -57,15 +57,26 @@ apt_install postfix postfix-pcre postgrey ca-certificates
|
|||||||
# Set some basic settings...
|
# Set some basic settings...
|
||||||
#
|
#
|
||||||
# * Have postfix listen on all network interfaces.
|
# * Have postfix listen on all network interfaces.
|
||||||
|
# * Make outgoing connections on a particular interface (if multihomed) so that SPF passes on the receiving side.
|
||||||
# * Set our name (the Debian default seems to be "localhost" but make it our hostname).
|
# * Set our name (the Debian default seems to be "localhost" but make it our hostname).
|
||||||
# * Set the name of the local machine to localhost, which means xxx@localhost is delivered locally, although we don't use it.
|
# * Set the name of the local machine to localhost, which means xxx@localhost is delivered locally, although we don't use it.
|
||||||
# * Set the SMTP banner (which must have the hostname first, then anything).
|
# * Set the SMTP banner (which must have the hostname first, then anything).
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
inet_interfaces=all \
|
inet_interfaces=all \
|
||||||
|
smtp_bind_address=$PRIVATE_IP \
|
||||||
|
smtp_bind_address6=$PRIVATE_IPV6 \
|
||||||
myhostname=$PRIMARY_HOSTNAME\
|
myhostname=$PRIMARY_HOSTNAME\
|
||||||
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
|
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
|
||||||
mydestination=localhost
|
mydestination=localhost
|
||||||
|
|
||||||
|
# Tweak some queue settings:
|
||||||
|
# * Inform users when their e-mail delivery is delayed more than 3 hours (default is not to warn).
|
||||||
|
# * Stop trying to send an undeliverable e-mail after 2 days (instead of 5), and for bounce messages just try for 1 day.
|
||||||
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
|
delay_warning_time=3h \
|
||||||
|
maximal_queue_lifetime=2d \
|
||||||
|
bounce_queue_lifetime=1d
|
||||||
|
|
||||||
# ### Outgoing Mail
|
# ### Outgoing Mail
|
||||||
|
|
||||||
# Enable the 'submission' port 587 smtpd server and tweak its settings.
|
# Enable the 'submission' port 587 smtpd server and tweak its settings.
|
||||||
@@ -111,8 +122,9 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||||||
smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \
|
smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \
|
||||||
smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \
|
smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \
|
||||||
smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \
|
smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \
|
||||||
|
smtpd_tls_protocols=\!SSLv2,\!SSLv3 \
|
||||||
smtpd_tls_ciphers=medium \
|
smtpd_tls_ciphers=medium \
|
||||||
smtpd_tls_exclude_ciphers=aNULL \
|
smtpd_tls_exclude_ciphers=aNULL,RC4 \
|
||||||
smtpd_tls_received_header=yes
|
smtpd_tls_received_header=yes
|
||||||
|
|
||||||
# Prevent non-authenticated users from sending mail that requires being
|
# Prevent non-authenticated users from sending mail that requires being
|
||||||
@@ -147,6 +159,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
|
# 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`.
|
# now see notices about trusted certs. The CA file is provided by the package `ca-certificates`.
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
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_tls_security_level=dane \
|
||||||
smtp_dns_support_level=dnssec \
|
smtp_dns_support_level=dnssec \
|
||||||
smtp_tls_CAfile=/etc/ssl/certs/ca-certificates.crt \
|
smtp_tls_CAfile=/etc/ssl/certs/ca-certificates.crt \
|
||||||
|
|||||||
@@ -38,17 +38,19 @@ passdb {
|
|||||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||||
}
|
}
|
||||||
userdb {
|
userdb {
|
||||||
driver = static
|
driver = sql
|
||||||
args = uid=mail gid=mail home=$STORAGE_ROOT/mail/mailboxes/%d/%n
|
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Configure the SQL to query for a user's password.
|
# Configure the SQL to query for a user's metadata and password.
|
||||||
cat > /etc/dovecot/dovecot-sql.conf.ext << EOF;
|
cat > /etc/dovecot/dovecot-sql.conf.ext << EOF;
|
||||||
driver = sqlite
|
driver = sqlite
|
||||||
connect = $db_path
|
connect = $db_path
|
||||||
default_pass_scheme = SHA512-CRYPT
|
default_pass_scheme = SHA512-CRYPT
|
||||||
password_query = SELECT email as user, password FROM users WHERE email='%u';
|
password_query = SELECT email as user, password FROM users WHERE email='%u';
|
||||||
|
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users WHERE email='%u';
|
||||||
|
iterate_query = SELECT email AS user FROM users;
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,25 @@ source setup/functions.sh
|
|||||||
|
|
||||||
echo "Installing Mail-in-a-Box system management daemon..."
|
echo "Installing Mail-in-a-Box system management daemon..."
|
||||||
|
|
||||||
# Switching python 2 boto to package manager's, not pypi's.
|
# Install packages.
|
||||||
if [ -f /usr/local/lib/python2.7/dist-packages/boto/__init__.py ]; then hide_output pip uninstall -y boto; fi
|
# flask, yaml, dnspython, and dateutil are all for our Python 3 management daemon itself.
|
||||||
|
# duplicity does backups. python-pip is so we can 'pip install boto' for Python 2, for duplicity, so it can do backups to AWS S3.
|
||||||
|
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil python-pip
|
||||||
|
|
||||||
# duplicity uses python 2 so we need to use the python 2 package of boto
|
# These are required to pip install cryptography.
|
||||||
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
|
apt_install build-essential libssl-dev libffi-dev python3-dev
|
||||||
apt_install python3-flask links duplicity python-boto libyaml-dev python3-dnspython python3-dateutil \
|
|
||||||
build-essential libssl-dev libffi-dev python3-dev python-pip
|
|
||||||
hide_output pip3 install --upgrade rtyaml "email_validator>=1.0.0" "idna>=2.0.0" "cryptography>=1.0.2" boto
|
|
||||||
|
|
||||||
# email_validator is repeated in setup/questions.sh
|
# Install other Python 3 packages used by the management daemon.
|
||||||
|
# The first line is the packages that Josh maintains himself!
|
||||||
|
# NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced.
|
||||||
|
hide_output pip3 install --upgrade \
|
||||||
|
rtyaml "email_validator>=1.0.0" "free_tls_certificates>=0.1.3" \
|
||||||
|
"idna>=2.0.0" "cryptography>=1.0.2" boto psutil
|
||||||
|
|
||||||
|
# duplicity uses python 2 so we need to get the python 2 package of boto to have backups to S3.
|
||||||
|
# boto from the Ubuntu package manager is too out-of-date -- it doesn't support the newer
|
||||||
|
# S3 api used in some regions, which breaks backups to those regions. See #627, #653.
|
||||||
|
hide_output pip install --upgrade boto
|
||||||
|
|
||||||
# Create a backup directory and a random key for encrypting backups.
|
# Create a backup directory and a random key for encrypting backups.
|
||||||
mkdir -p $STORAGE_ROOT/backup
|
mkdir -p $STORAGE_ROOT/backup
|
||||||
@@ -44,5 +53,5 @@ cat > /etc/cron.d/mailinabox-nightly << EOF;
|
|||||||
0 3 * * * root (cd `pwd` && management/daily_tasks.sh)
|
0 3 * * * root (cd `pwd` && management/daily_tasks.sh)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start it.
|
# Start the management server.
|
||||||
restart_service mailinabox
|
restart_service mailinabox
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ source /etc/mailinabox.conf # load global vars
|
|||||||
|
|
||||||
# install Munin
|
# install Munin
|
||||||
echo "Installing Munin (system monitoring)..."
|
echo "Installing Munin (system monitoring)..."
|
||||||
apt_install munin munin-node
|
apt_install munin munin-node libcgi-fast-perl
|
||||||
|
# libcgi-fast-perl is needed by /usr/lib/munin/cgi/munin-cgi-graph
|
||||||
|
|
||||||
# edit config
|
# edit config
|
||||||
cat > /etc/munin/munin.conf <<EOF;
|
cat > /etc/munin/munin.conf <<EOF;
|
||||||
@@ -19,6 +20,9 @@ tmpldir /etc/munin/templates
|
|||||||
|
|
||||||
includedir /etc/munin/munin-conf.d
|
includedir /etc/munin/munin-conf.d
|
||||||
|
|
||||||
|
# path dynazoom uses for requests
|
||||||
|
cgiurl_graph /admin/munin/cgi-graph
|
||||||
|
|
||||||
# a simple host tree
|
# a simple host tree
|
||||||
[$PRIMARY_HOSTNAME]
|
[$PRIMARY_HOSTNAME]
|
||||||
address 127.0.0.1
|
address 127.0.0.1
|
||||||
@@ -29,6 +33,10 @@ contact.admin.command mail -s "Munin notification ${var:host}" administrator@$PR
|
|||||||
contact.admin.always_send warning critical
|
contact.admin.always_send warning critical
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi
|
||||||
|
chown munin. /var/log/munin/munin-cgi-html.log
|
||||||
|
chown munin. /var/log/munin/munin-cgi-graph.log
|
||||||
|
|
||||||
# ensure munin-node knows the name of this machine
|
# ensure munin-node knows the name of this machine
|
||||||
tools/editconf.py /etc/munin/munin-node.conf -s \
|
tools/editconf.py /etc/munin/munin-node.conf -s \
|
||||||
host_name=$PRIMARY_HOSTNAME
|
host_name=$PRIMARY_HOSTNAME
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ apt_install \
|
|||||||
apt-get purge -qq -y owncloud*
|
apt-get purge -qq -y owncloud*
|
||||||
|
|
||||||
# Install ownCloud from source of this version:
|
# Install ownCloud from source of this version:
|
||||||
owncloud_ver=8.1.1
|
owncloud_ver=8.2.3
|
||||||
owncloud_hash=34077e78575a3e689825a00964ee37fbf83fbdda
|
owncloud_hash=bfdf6166fbf6fc5438dc358600e7239d1c970613
|
||||||
|
|
||||||
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
|
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
|
||||||
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
|
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
|
||||||
@@ -52,8 +52,8 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
|
|||||||
# The two apps we actually want are not in ownCloud core. Clone them from
|
# The two apps we actually want are not in ownCloud core. Clone them from
|
||||||
# their github repositories.
|
# their github repositories.
|
||||||
mkdir -p /usr/local/lib/owncloud/apps
|
mkdir -p /usr/local/lib/owncloud/apps
|
||||||
git_clone https://github.com/owncloud/contacts 4ff855e7c2075309041bead09fbb9eb7df678244 '' /usr/local/lib/owncloud/apps/contacts
|
git_clone https://github.com/owncloudarchive/contacts 9ba2e667ae8c7ea36d8c4a4c3413c374beb24b1b '' /usr/local/lib/owncloud/apps/contacts
|
||||||
git_clone https://github.com/owncloud/calendar ec53139b144c0f842c33813305612e8006c42ea5 '' /usr/local/lib/owncloud/apps/calendar
|
git_clone https://github.com/owncloudarchive/calendar 2086e738a3b7b868ec59cd61f0f88b49c3f21dd1 '' /usr/local/lib/owncloud/apps/calendar
|
||||||
|
|
||||||
# Fix weird permissions.
|
# Fix weird permissions.
|
||||||
chmod 750 /usr/local/lib/owncloud/{apps,config}
|
chmod 750 /usr/local/lib/owncloud/{apps,config}
|
||||||
@@ -92,7 +92,6 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
|||||||
mkdir -p $STORAGE_ROOT/owncloud
|
mkdir -p $STORAGE_ROOT/owncloud
|
||||||
|
|
||||||
# Create an initial configuration file.
|
# Create an initial configuration file.
|
||||||
TIMEZONE=$(cat /etc/timezone)
|
|
||||||
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
|
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
|
||||||
cat > $STORAGE_ROOT/owncloud/config.php <<EOF;
|
cat > $STORAGE_ROOT/owncloud/config.php <<EOF;
|
||||||
<?php
|
<?php
|
||||||
@@ -108,12 +107,12 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
|||||||
'user_backends' => array(
|
'user_backends' => array(
|
||||||
array(
|
array(
|
||||||
'class'=>'OC_User_IMAP',
|
'class'=>'OC_User_IMAP',
|
||||||
'arguments'=>array('{localhost:993/imap/ssl/novalidate-cert}')
|
'arguments'=>array('{127.0.0.1:993/imap/ssl/novalidate-cert}')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
'memcache.local' => '\\OC\\Memcache\\Memcached',
|
'memcache.local' => '\\OC\\Memcache\\Memcached',
|
||||||
"memcached_servers" => array (
|
"memcached_servers" => array (
|
||||||
array('localhost', 11211),
|
array('127.0.0.1', 11211),
|
||||||
),
|
),
|
||||||
'mail_smtpmode' => 'sendmail',
|
'mail_smtpmode' => 'sendmail',
|
||||||
'mail_smtpsecure' => '',
|
'mail_smtpsecure' => '',
|
||||||
@@ -125,7 +124,6 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
|||||||
'mail_smtppassword' => '',
|
'mail_smtppassword' => '',
|
||||||
'mail_from_address' => 'owncloud',
|
'mail_from_address' => 'owncloud',
|
||||||
'mail_domain' => '$PRIMARY_HOSTNAME',
|
'mail_domain' => '$PRIMARY_HOSTNAME',
|
||||||
'logtimezone' => '$TIMEZONE',
|
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
EOF
|
EOF
|
||||||
@@ -163,7 +161,11 @@ fi
|
|||||||
# so set it here. It also can change if the box's PRIMARY_HOSTNAME changes, so
|
# 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.
|
# this will make sure it has the right value.
|
||||||
# * Some settings weren't included in previous versions of Mail-in-a-Box.
|
# * Some settings weren't included in previous versions of Mail-in-a-Box.
|
||||||
|
# * We need to set the timezone to the system timezone to allow fail2ban to ban
|
||||||
|
# users within the proper timeframe
|
||||||
|
# * We need to set the logdateformat to something that will work correctly with fail2ban
|
||||||
# Use PHP to read the settings file, modify it, and write out the new settings array.
|
# Use PHP to read the settings file, modify it, and write out the new settings array.
|
||||||
|
TIMEZONE=$(cat /etc/timezone)
|
||||||
CONFIG_TEMP=$(/bin/mktemp)
|
CONFIG_TEMP=$(/bin/mktemp)
|
||||||
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
|
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
|
||||||
<?php
|
<?php
|
||||||
@@ -175,6 +177,9 @@ include("$STORAGE_ROOT/owncloud/config.php");
|
|||||||
\$CONFIG['overwrite.cli.url'] = '/cloud';
|
\$CONFIG['overwrite.cli.url'] = '/cloud';
|
||||||
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
|
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
|
||||||
|
|
||||||
|
\$CONFIG['logtimezone'] = '$TIMEZONE';
|
||||||
|
\$CONFIG['logdateformat'] = 'Y-m-d H:i:s';
|
||||||
|
|
||||||
echo "<?php\n\\\$CONFIG = ";
|
echo "<?php\n\\\$CONFIG = ";
|
||||||
var_export(\$CONFIG);
|
var_export(\$CONFIG);
|
||||||
echo ";";
|
echo ";";
|
||||||
|
|||||||
@@ -33,3 +33,30 @@ if [ ! -d /vagrant ]; then
|
|||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check that tempfs is mounted with exec
|
||||||
|
MOUNTED_TMP_AS_NO_EXEC=$(grep "/tmp.*noexec" /proc/mounts)
|
||||||
|
if [ -n "$MOUNTED_TMP_AS_NO_EXEC" ]; then
|
||||||
|
echo "Mail-in-a-Box has to have exec rights on /tmp, please mount /tmp with exec"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that no .wgetrc exists
|
||||||
|
if [ -e ~/.wgetrc ]; then
|
||||||
|
echo "Mail-in-a-Box expects no overrides to wget defaults, ~/.wgetrc exists"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that we are running on x86_64 or i686, any other architecture is unsupported and
|
||||||
|
# will fail later in the setup when we try to install the custom build lucene packages.
|
||||||
|
#
|
||||||
|
# Set ARM=1 to ignore this check if you have built the packages yourself. If you do this
|
||||||
|
# you are on your own!
|
||||||
|
ARCHITECTURE=$(uname -m)
|
||||||
|
if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "i686" ]; then
|
||||||
|
if [ -z "$ARM" ]; then
|
||||||
|
echo "Mail-in-a-Box only supports x86_64 or i686 and will not work on any other architecture, like ARM."
|
||||||
|
echo "Your architecture is $ARCHITECTURE"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ if [ -z "$NONINTERACTIVE" ]; then
|
|||||||
# this was being able to ask the user for input even if stdin has been redirected,
|
# this was being able to ask the user for input even if stdin has been redirected,
|
||||||
# e.g. if we piped a bootstrapping install script to bash to get started. In that
|
# e.g. if we piped a bootstrapping install script to bash to get started. In that
|
||||||
# case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we
|
# case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we
|
||||||
# use a shell flag instead. Really supress any output from installing dialog.
|
# use a shell flag instead. Really suppress any output from installing dialog.
|
||||||
#
|
#
|
||||||
# Also install depencies needed to validate the email address.
|
# Also install dependencies needed to validate the email address.
|
||||||
if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then
|
if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then
|
||||||
echo Installing packages needed for setup...
|
echo Installing packages needed for setup...
|
||||||
apt-get -q -q update
|
apt-get -q -q update
|
||||||
@@ -18,7 +18,8 @@ if [ -z "$NONINTERACTIVE" ]; then
|
|||||||
message_box "Mail-in-a-Box Installation" \
|
message_box "Mail-in-a-Box Installation" \
|
||||||
"Hello and thanks for deploying a Mail-in-a-Box!
|
"Hello and thanks for deploying a Mail-in-a-Box!
|
||||||
\n\nI'm going to ask you a few questions.
|
\n\nI'm going to ask you a few questions.
|
||||||
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line."
|
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line.
|
||||||
|
\n\nNOTE: You should only install this on a brand new Ubuntu installation 100% dedicated to Mail-in-a-Box. Mail-in-a-Box will, for example, remove apache2."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The box needs a name.
|
# The box needs a name.
|
||||||
|
|||||||
@@ -78,9 +78,13 @@ tools/editconf.py /etc/spamassassin/local.cf -s \
|
|||||||
# * Writable by the debian-spamd user, which runs /etc/cron.daily/spamassassin.
|
# * Writable by the debian-spamd user, which runs /etc/cron.daily/spamassassin.
|
||||||
#
|
#
|
||||||
# We'll have these files owned by spampd and grant access to the other two processes.
|
# We'll have these files owned by spampd and grant access to the other two processes.
|
||||||
|
#
|
||||||
|
# Spamassassin will change the access rights back to the defaults, so we must also configure
|
||||||
|
# the filemode in the config file.
|
||||||
|
|
||||||
tools/editconf.py /etc/spamassassin/local.cf -s \
|
tools/editconf.py /etc/spamassassin/local.cf -s \
|
||||||
bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes
|
bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes \
|
||||||
|
bayes_file_mode=0660
|
||||||
|
|
||||||
mkdir -p $STORAGE_ROOT/mail/spamassassin
|
mkdir -p $STORAGE_ROOT/mail/spamassassin
|
||||||
chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin
|
chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
source setup/functions.sh # load our functions
|
source setup/functions.sh # load our functions
|
||||||
|
|
||||||
# Check system setup: Are we running as root on Ubuntu 14.04 on a
|
# Check system setup: Are we running as root on Ubuntu 14.04 on a
|
||||||
# machine with enough memory? If not, this shows an error and exits.
|
# machine with enough memory? Is /tmp mounted with exec.
|
||||||
|
# If not, this shows an error and exits.
|
||||||
source setup/preflight.sh
|
source setup/preflight.sh
|
||||||
|
|
||||||
# Ensure Python reads/writes files in UTF-8. If the machine
|
# Ensure Python reads/writes files in UTF-8. If the machine
|
||||||
# triggers some other locale in Python, like ASCII encoding,
|
# triggers some other locale in Python, like ASCII encoding,
|
||||||
# Python may not be able to read/write files. Here and in
|
# Python may not be able to read/write files. This is also
|
||||||
# the management daemon startup script.
|
# in the management daemon startup script and the cron script.
|
||||||
|
|
||||||
if [ -z `locale -a | grep en_US.utf8` ]; then
|
if [ -z `locale -a | grep en_US.utf8` ]; then
|
||||||
# Generate locale if not exists
|
# Generate locale if not exists
|
||||||
@@ -23,6 +24,9 @@ export LC_ALL=en_US.UTF-8
|
|||||||
export LANG=en_US.UTF-8
|
export LANG=en_US.UTF-8
|
||||||
export LC_TYPE=en_US.UTF-8
|
export LC_TYPE=en_US.UTF-8
|
||||||
|
|
||||||
|
# Fix so line drawing characters are shown correctly in Putty on Windows. See #744.
|
||||||
|
export NCURSES_NO_UTF8_ACS=1
|
||||||
|
|
||||||
# Recall the last settings used if we're running this a second time.
|
# Recall the last settings used if we're running this a second time.
|
||||||
if [ -f /etc/mailinabox.conf ]; then
|
if [ -f /etc/mailinabox.conf ]; then
|
||||||
# Run any system migrations before proceeding. Since this is a second run,
|
# Run any system migrations before proceeding. Since this is a second run,
|
||||||
@@ -108,7 +112,7 @@ source setup/management.sh
|
|||||||
source setup/munin.sh
|
source setup/munin.sh
|
||||||
|
|
||||||
# Ping the management daemon to write the DNS and nginx configuration files.
|
# Ping the management daemon to write the DNS and nginx configuration files.
|
||||||
until nc -z -w 4 localhost 10222
|
until nc -z -w 4 127.0.0.1 10222
|
||||||
do
|
do
|
||||||
echo Waiting for the Mail-in-a-Box management daemon to start...
|
echo Waiting for the Mail-in-a-Box management daemon to start...
|
||||||
sleep 2
|
sleep 2
|
||||||
@@ -116,6 +120,10 @@ done
|
|||||||
tools/dns_update
|
tools/dns_update
|
||||||
tools/web_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
|
||||||
|
|
||||||
# If there aren't any mail users yet, create one.
|
# If there aren't any mail users yet, create one.
|
||||||
source setup/firstuser.sh
|
source setup/firstuser.sh
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,70 @@ source setup/functions.sh # load our functions
|
|||||||
# Basic System Configuration
|
# Basic System Configuration
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
|
# ### Set hostname of the box
|
||||||
|
|
||||||
|
# If the hostname is not correctly resolvable sudo can't be used. This will result in
|
||||||
|
# errors during the install
|
||||||
|
#
|
||||||
|
# First set the hostname in the configuration file, then activate the setting
|
||||||
|
|
||||||
|
echo $PRIMARY_HOSTNAME > /etc/hostname
|
||||||
|
hostname $PRIMARY_HOSTNAME
|
||||||
|
|
||||||
|
# ### Add swap space to the system
|
||||||
|
|
||||||
|
# If the physical memory of the system is below 2GB it is wise to create a
|
||||||
|
# swap file. This will make the system more resiliant to memory spikes and
|
||||||
|
# prevent for instance spam filtering from crashing
|
||||||
|
|
||||||
|
# We will create a 1G file, this should be a good balance between disk usage
|
||||||
|
# and buffers for the system. We will only allocate this file if there is more
|
||||||
|
# than 5GB of disk space available
|
||||||
|
|
||||||
|
# The following checks are performed:
|
||||||
|
# - Check if swap is currently mountend by looking at /proc/swaps
|
||||||
|
# - Check if the user intents to activate swap on next boot by checking fstab entries.
|
||||||
|
# - Check if a swapfile already exists
|
||||||
|
# - Check if the root file system is not btrfs, might be an incompatible version with
|
||||||
|
# swapfiles. User should hanle it them selves.
|
||||||
|
# - Check the memory requirements
|
||||||
|
# - Check available diskspace
|
||||||
|
|
||||||
|
# See https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04
|
||||||
|
# for reference
|
||||||
|
|
||||||
|
SWAP_MOUNTED=$(cat /proc/swaps | tail -n+2)
|
||||||
|
SWAP_IN_FSTAB=$(grep "swap" /etc/fstab)
|
||||||
|
ROOT_IS_BTRFS=$(grep "\/ .*btrfs" /proc/mounts)
|
||||||
|
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}')
|
||||||
|
AVAILABLE_DISK_SPACE=$(df / --output=avail | tail -n 1)
|
||||||
|
if
|
||||||
|
[ -z "$SWAP_MOUNTED" ] &&
|
||||||
|
[ -z "$SWAP_IN_FSTAB" ] &&
|
||||||
|
[ ! -e /swapfile ] &&
|
||||||
|
[ -z "$ROOT_IS_BTRFS" ] &&
|
||||||
|
[ $TOTAL_PHYSICAL_MEM -lt 1900000 ] &&
|
||||||
|
[ $AVAILABLE_DISK_SPACE -gt 5242880 ]
|
||||||
|
then
|
||||||
|
echo "Adding a swap file to the system..."
|
||||||
|
|
||||||
|
# Allocate and activate the swap file. Allocate in 1KB chuncks
|
||||||
|
# doing it in one go, could fail on low memory systems
|
||||||
|
dd if=/dev/zero of=/swapfile bs=1024 count=$[1024*1024] status=none
|
||||||
|
if [ -e /swapfile ]; then
|
||||||
|
chmod 600 /swapfile
|
||||||
|
hide_output mkswap /swapfile
|
||||||
|
swapon /swapfile
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if swap is mounted then activate on boot
|
||||||
|
if swapon -s | grep -q "\/swapfile"; then
|
||||||
|
echo "/swapfile none swap sw 0 0" >> /etc/fstab
|
||||||
|
else
|
||||||
|
echo "ERROR: Swap allocation failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ### Add Mail-in-a-Box's PPA.
|
# ### Add Mail-in-a-Box's PPA.
|
||||||
|
|
||||||
# We've built several .deb packages on our own that we want to include.
|
# We've built several .deb packages on our own that we want to include.
|
||||||
@@ -227,10 +291,12 @@ restart_service resolvconf
|
|||||||
|
|
||||||
# ### Fail2Ban Service
|
# ### Fail2Ban Service
|
||||||
|
|
||||||
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix and ssh
|
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix, ssh, etc.
|
||||||
cat conf/fail2ban/jail.local \
|
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" \
|
| sed "s/PUBLIC_IP/$PUBLIC_IP/g" \
|
||||||
> /etc/fail2ban/jail.local
|
| sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
|
||||||
cp conf/fail2ban/dovecotimap.conf /etc/fail2ban/filter.d/dovecotimap.conf
|
> /etc/fail2ban/jail.d/mailinabox.conf
|
||||||
|
cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/
|
||||||
|
|
||||||
restart_service fail2ban
|
restart_service fail2ban
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ apt-get purge -qq -y roundcube* #NODOC
|
|||||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
# 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
|
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
|
||||||
# whether we have the latest version.
|
# whether we have the latest version.
|
||||||
VERSION=1.1.3
|
VERSION=1.2.1
|
||||||
HASH=4513227bd64eb8564f056817341b1dfe478e215e
|
HASH=81fbfba4683522f6e54006d0300a48e6da3f3bbd
|
||||||
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
||||||
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
|
PERSISTENT_LOGIN_VERSION=1e9d724476a370ce917a2fcd5b3217b0c306c24e
|
||||||
HTML5_NOTIFIER_VERSION=046eb388dd63b1ec77a3ee485757fc25ae9e684d
|
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
|
||||||
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION
|
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:a
|
||||||
needs_update=0 #NODOC
|
needs_update=0 #NODOC
|
||||||
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
||||||
# not installed yet #NODOC
|
# not installed yet #NODOC
|
||||||
@@ -51,10 +51,10 @@ fi
|
|||||||
if [ $needs_update == 1 ]; then
|
if [ $needs_update == 1 ]; then
|
||||||
# install roundcube
|
# install roundcube
|
||||||
wget_verify \
|
wget_verify \
|
||||||
https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz \
|
https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION.tar.gz \
|
||||||
$HASH \
|
$HASH \
|
||||||
/tmp/roundcube.tgz
|
/tmp/roundcube.tgz
|
||||||
tar -C /usr/local/lib -zxf /tmp/roundcube.tgz
|
tar -C /usr/local/lib --no-same-owner -zxf /tmp/roundcube.tgz
|
||||||
rm -rf /usr/local/lib/roundcubemail
|
rm -rf /usr/local/lib/roundcubemail
|
||||||
mv /usr/local/lib/roundcubemail-$VERSION/ /usr/local/lib/roundcubemail
|
mv /usr/local/lib/roundcubemail-$VERSION/ /usr/local/lib/roundcubemail
|
||||||
rm -f /tmp/roundcube.tgz
|
rm -f /tmp/roundcube.tgz
|
||||||
@@ -94,12 +94,12 @@ cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
|
|||||||
\$config['default_host'] = 'ssl://localhost';
|
\$config['default_host'] = 'ssl://localhost';
|
||||||
\$config['default_port'] = 993;
|
\$config['default_port'] = 993;
|
||||||
\$config['imap_timeout'] = 15;
|
\$config['imap_timeout'] = 15;
|
||||||
\$config['smtp_server'] = 'tls://localhost';
|
\$config['smtp_server'] = 'tls://127.0.0.1';
|
||||||
\$config['smtp_port'] = 587;
|
\$config['smtp_port'] = 587;
|
||||||
\$config['smtp_user'] = '%u';
|
\$config['smtp_user'] = '%u';
|
||||||
\$config['smtp_pass'] = '%p';
|
\$config['smtp_pass'] = '%p';
|
||||||
\$config['support_url'] = 'https://mailinabox.email/';
|
\$config['support_url'] = 'https://mailinabox.email/';
|
||||||
\$config['product_name'] = 'Mail-in-a-Box/Roundcube Webmail';
|
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
|
||||||
\$config['des_key'] = '$SECRET_KEY';
|
\$config['des_key'] = '$SECRET_KEY';
|
||||||
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'vacation_sieve', 'persistent_login');
|
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'vacation_sieve', 'persistent_login');
|
||||||
\$config['skin'] = 'classic';
|
\$config['skin'] = 'classic';
|
||||||
@@ -121,7 +121,7 @@ cat > /usr/local/lib/roundcubemail/plugins/vacation_sieve/config.inc.php <<EOF;
|
|||||||
'transfer' => array(
|
'transfer' => array(
|
||||||
'mode' => 'managesieve',
|
'mode' => 'managesieve',
|
||||||
'ms_activate_script' => true,
|
'ms_activate_script' => true,
|
||||||
'host' => 'localhost',
|
'host' => '127.0.0.1',
|
||||||
'port' => '4190',
|
'port' => '4190',
|
||||||
'usetls' => false,
|
'usetls' => false,
|
||||||
'path' => 'vacation',
|
'path' => 'vacation',
|
||||||
@@ -157,6 +157,12 @@ chmod 775 $STORAGE_ROOT/mail
|
|||||||
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
|
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
|
||||||
chmod 664 $STORAGE_ROOT/mail/users.sqlite
|
chmod 664 $STORAGE_ROOT/mail/users.sqlite
|
||||||
|
|
||||||
|
# Run Roundcube database migration script, if the database exists (it's created by
|
||||||
|
# Roundcube on first use).
|
||||||
|
if [ -f $STORAGE_ROOT/mail/roundcube/roundcube.sqlite ]; then
|
||||||
|
/usr/local/lib/roundcubemail/bin/updatedb.sh --dir /usr/local/lib/roundcubemail/SQL --package roundcube
|
||||||
|
fi
|
||||||
|
|
||||||
# Enable PHP modules.
|
# Enable PHP modules.
|
||||||
php5enmod mcrypt
|
php5enmod mcrypt
|
||||||
restart_service php5-fpm
|
restart_service php5-fpm
|
||||||
|
|||||||
195
tests/fail2ban.py
Normal file
195
tests/fail2ban.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Test that a box's fail2ban setting are working
|
||||||
|
# correctly by attempting a bunch of failed logins.
|
||||||
|
# Specify SSH login information the command line -
|
||||||
|
# we use that to reset fail2ban after each test,
|
||||||
|
# and we extract the hostname from that to open
|
||||||
|
# connections to.
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
import sys, os, time, functools
|
||||||
|
|
||||||
|
# parse command line
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: tests/fail2ban.py user@hostname")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ssh_user, hostname = sys.argv[1].split("@", 1)
|
||||||
|
|
||||||
|
# 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 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)
|
||||||
|
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("ssh %s@%s \"%s\"" % (ssh_user, hostname, 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)
|
||||||
|
|
||||||
|
# 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, ["aa", "aa"]], 20, 120, 1)
|
||||||
|
|
||||||
|
# restart fail2ban so that this client machine is no longer blocked
|
||||||
|
restart_fail2ban_service(final=True)
|
||||||
@@ -33,7 +33,6 @@ PORT 25
|
|||||||
AES256-SHA256 - 256 bits 250 2.0.0 Ok
|
AES256-SHA256 - 256 bits 250 2.0.0 Ok
|
||||||
AES256-SHA - 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
|
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-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-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
|
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-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
|
DHE-RSA-AES128-GCM-SHA256 DH-2048 bits 128 bits 250 2.0.0 Ok
|
||||||
SEED-SHA - 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
|
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
|
||||||
AES128-SHA256 - 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
|
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
|
DHE-RSA-AES256-SHA DH-2048 bits 256 bits 250 2.0.0 Ok
|
||||||
CAMELLIA256-SHA - 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
|
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
|
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-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-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
|
DHE-RSA-AES128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
|
||||||
SEED-SHA - 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
|
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
|
||||||
AES128-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
|
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
|
DHE-RSA-AES256-SHA DH-2048 bits 256 bits 250 2.0.0 Ok
|
||||||
CAMELLIA256-SHA - 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
|
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
|
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-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-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
|
DHE-RSA-AES128-SHA DH-2048 bits 128 bits 250 2.0.0 Ok
|
||||||
SEED-SHA - 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
|
CAMELLIA128-SHA - 128 bits 250 2.0.0 Ok
|
||||||
AES128-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
|
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
|
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
|
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
|
* SSLV3 Cipher Suites:
|
||||||
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
|
Server rejected all cipher suites.
|
||||||
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
|
|
||||||
|
Should Not Offer: DHE-RSA-SEED-SHA, EDH-RSA-DES-CBC3-SHA, SEED-SHA
|
||||||
|
Could Also Offer: DH-DSS-AES128-GCM-SHA256, DH-DSS-AES128-SHA, DH-DSS-AES128-SHA256, DH-DSS-AES256-GCM-SHA384, DH-DSS-AES256-SHA, DH-DSS-AES256-SHA256, DH-DSS-CAMELLIA128-SHA, DH-DSS-CAMELLIA256-SHA, DH-DSS-DES-CBC3-SHA, DH-RSA-AES128-GCM-SHA256, DH-RSA-AES128-SHA, DH-RSA-AES128-SHA256, DH-RSA-AES256-GCM-SHA384, DH-RSA-AES256-SHA, DH-RSA-AES256-SHA256, DH-RSA-CAMELLIA128-SHA, DH-RSA-CAMELLIA256-SHA, DH-RSA-DES-CBC3-SHA, DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-DSS-AES256-SHA256, DHE-DSS-CAMELLIA128-SHA, DHE-DSS-CAMELLIA256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-ECDSA-DES-CBC3-SHA, SRP-3DES-EDE-CBC-SHA, SRP-AES-128-CBC-SHA, SRP-AES-256-CBC-SHA, SRP-DSS-3DES-EDE-CBC-SHA, SRP-DSS-AES-128-CBC-SHA, SRP-DSS-AES-256-CBC-SHA, SRP-RSA-3DES-EDE-CBC-SHA, SRP-RSA-AES-128-CBC-SHA, SRP-RSA-AES-256-CBC-SHA
|
||||||
|
Supported Clients: OpenSSL/1.0.2, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, YandexBot/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/OS X 10.10, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/6/iOS 6.0.1, Firefox/31.3.0 ESR/Win 7, Baidu/Jan 2015, IE/11/Win 8.1, IE/11/Win 7, IE Mobile/11/Win Phone 8.1, Android/5.0.0, Java/8u31, Chrome/42/OS X, Googlebot/Feb 2015, Android/4.1.1, Android/4.0.4, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Android/4.3, Safari/5.1.9/OS X 10.6.8, Firefox/37/OS X, OpenSSL/0.9.8y, Java/7u25, IE/8-10/Win 7, IE/7/Vista, IE Mobile/10/Win Phone 8.0, Android/2.3.7, Java/6u45, IE/8/XP
|
||||||
|
|
||||||
PORT 587
|
PORT 587
|
||||||
--------
|
--------
|
||||||
@@ -192,9 +163,6 @@ PORT 587
|
|||||||
CAMELLIA128-SHA - 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
|
AES128-SHA - 128 bits 250 2.0.0 Ok
|
||||||
|
|
||||||
* SSLV3 Cipher Suites:
|
|
||||||
Server rejected all cipher suites.
|
|
||||||
|
|
||||||
* TLSV1 Cipher Suites:
|
* TLSV1 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits 250 2.0.0 Ok
|
ECDHE-RSA-AES256-SHA ECDH-256 bits 256 bits 250 2.0.0 Ok
|
||||||
@@ -210,11 +178,14 @@ PORT 587
|
|||||||
DHE-RSA-AES128-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
|
SEED-SHA - 128 bits 250 2.0.0 Ok
|
||||||
CAMELLIA128-SHA - 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
|
AES128-SHA - 128 bits 250 2.0.0 Ok
|
||||||
|
|
||||||
|
* SSLV3 Cipher Suites:
|
||||||
|
Server rejected all cipher suites.
|
||||||
|
|
||||||
Should Not Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA, DHE-RSA-SEED-SHA, SEED-SHA
|
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
|
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384
|
||||||
Supported Clients: OpenSSL/1.0.2, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, YandexBot/Jan 2015, Android/4.4.2, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/8/OS X 10.10, Safari/7/iOS 7.1, IE Mobile/11/Win Phone 8.1, IE/11/Win 8.1, IE/11/Win 7, Safari/6/iOS 6.0.1, Firefox/31.3.0 ESR/Win 7, Baidu/Jan 2015, Android/5.0.0, Chrome/42/OS X, Java/8u31, Googlebot/Feb 2015, Firefox/37/OS X, Android/4.0.4, Android/4.1.1, Safari/6.0.4/OS X 10.8.4, Android/4.3, Android/4.2.2, Safari/5.1.9/OS X 10.6.8, OpenSSL/0.9.8y, IE/7/Vista, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/7u25, Java/6u45, Android/2.3.7
|
Supported Clients: OpenSSL/1.0.2, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, YandexBot/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, IE/11/Win 8.1, Safari/8/iOS 8.1.2, IE/11/Win 7, IE Mobile/11/Win Phone 8.1, Safari/8/OS X 10.10, Safari/7/OS X 10.9, Safari/6/iOS 6.0.1, Firefox/31.3.0 ESR/Win 7, Baidu/Jan 2015, Chrome/42/OS X, Android/5.0.0, Java/8u31, Googlebot/Feb 2015, Firefox/37/OS X, Android/4.0.4, Android/4.1.1, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Android/4.3, Safari/5.1.9/OS X 10.6.8, IE/8-10/Win 7, IE/7/Vista, IE Mobile/10/Win Phone 8.0, OpenSSL/0.9.8y, Java/7u25, Java/6u45, Android/2.3.7
|
||||||
|
|
||||||
PORT 443
|
PORT 443
|
||||||
--------
|
--------
|
||||||
@@ -226,22 +197,22 @@ PORT 443
|
|||||||
Client-initiated Renegotiations: OK - Rejected
|
Client-initiated Renegotiations: OK - Rejected
|
||||||
Secure Renegotiation: OK - Supported
|
Secure Renegotiation: OK - Supported
|
||||||
|
|
||||||
* HTTP Strict Transport Security:
|
* OpenSSL Heartbleed:
|
||||||
OK - HSTS header received: max-age=31536000
|
OK - Not vulnerable to Heartbleed
|
||||||
|
|
||||||
* Session Resumption:
|
* Session Resumption:
|
||||||
With Session IDs: OK - Supported (5 successful, 0 failed, 0 errors, 5 total attempts).
|
With Session IDs: OK - Supported (5 successful, 0 failed, 0 errors, 5 total attempts).
|
||||||
With TLS Session Tickets: OK - Supported
|
With TLS Session Tickets: OK - Supported
|
||||||
|
|
||||||
* OpenSSL Heartbleed:
|
* HTTP Strict Transport Security:
|
||||||
OK - Not vulnerable to Heartbleed
|
OK - HSTS header received: max-age=31536000
|
||||||
|
|
||||||
|
Unhandled exception when processing --chrome_sha1:
|
||||||
|
exceptions.TypeError - Incorrect padding
|
||||||
|
|
||||||
* SSLV2 Cipher Suites:
|
* SSLV2 Cipher Suites:
|
||||||
Server rejected all cipher suites.
|
Server rejected all cipher suites.
|
||||||
|
|
||||||
* Google Chrome SHA-1 Deprecation Status:
|
|
||||||
OK - Leaf certificate expires before 2016.
|
|
||||||
|
|
||||||
* TLSV1_2 Cipher Suites:
|
* TLSV1_2 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES128-GCM-SHA256 ECDH-256 bits 128 bits HTTP 200 OK
|
ECDHE-RSA-AES128-GCM-SHA256 ECDH-256 bits 128 bits HTTP 200 OK
|
||||||
@@ -270,9 +241,6 @@ PORT 443
|
|||||||
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
|
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
|
||||||
DES-CBC3-SHA - 112 bits HTTP 200 OK
|
DES-CBC3-SHA - 112 bits HTTP 200 OK
|
||||||
|
|
||||||
* SSLV3 Cipher Suites:
|
|
||||||
Server rejected all cipher suites.
|
|
||||||
|
|
||||||
* TLSV1 Cipher Suites:
|
* TLSV1 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits HTTP 200 OK
|
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 bits HTTP 200 OK
|
||||||
@@ -281,11 +249,14 @@ PORT 443
|
|||||||
DHE-RSA-AES256-SHA DH-2048 bits 256 bits HTTP 200 OK
|
DHE-RSA-AES256-SHA DH-2048 bits 256 bits HTTP 200 OK
|
||||||
ECDHE-RSA-AES128-SHA ECDH-256 bits 128 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
|
DHE-RSA-AES128-SHA DH-2048 bits 128 bits HTTP 200 OK
|
||||||
DES-CBC3-SHA - 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)
|
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
|
Could Also Offer: AES128-GCM-SHA256, AES128-SHA, AES128-SHA256, AES256-GCM-SHA384, AES256-SHA, AES256-SHA256, CAMELLIA128-SHA, CAMELLIA256-SHA, DH-DSS-AES128-GCM-SHA256, DH-DSS-AES128-SHA, DH-DSS-AES128-SHA256, DH-DSS-AES256-GCM-SHA384, DH-DSS-AES256-SHA, DH-DSS-AES256-SHA256, DH-DSS-CAMELLIA128-SHA, DH-DSS-CAMELLIA256-SHA, DH-RSA-AES128-GCM-SHA256, DH-RSA-AES128-SHA, DH-RSA-AES128-SHA256, DH-RSA-AES256-GCM-SHA384, DH-RSA-AES256-SHA, DH-RSA-AES256-SHA256, DH-RSA-CAMELLIA128-SHA, DH-RSA-CAMELLIA256-SHA, DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-DSS-AES256-SHA256, DHE-DSS-CAMELLIA128-SHA, DHE-DSS-CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, SRP-AES-128-CBC-SHA, SRP-AES-256-CBC-SHA, SRP-DSS-AES-128-CBC-SHA, SRP-DSS-AES-256-CBC-SHA, SRP-RSA-AES-128-CBC-SHA, SRP-RSA-AES-256-CBC-SHA
|
||||||
Supported Clients: 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
|
Supported Clients: OpenSSL/1.0.2, OpenSSL/1.0.1l, BingPreview/Jan 2015, YandexBot/Jan 2015, Yahoo Slurp/Jan 2015, Android/4.4.2, Safari/7/iOS 7.1, Safari/8/OS X 10.10, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, Safari/6/iOS 6.0.1, Chrome/42/OS X, IE/11/Win 8.1, IE/11/Win 7, Android/5.0.0, Java/8u31, IE Mobile/11/Win Phone 8.1, Googlebot/Feb 2015, Firefox/31.3.0 ESR/Win 7, Firefox/37/OS X, Android/4.1.1, Android/4.0.4, Baidu/Jan 2015, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Android/4.3, Safari/5.1.9/OS X 10.6.8, IE/8-10/Win 7, IE/7/Vista, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, Java/7u25, Android/2.3.7, Java/6u45, IE/8/XP
|
||||||
|
|
||||||
PORT 993
|
PORT 993
|
||||||
--------
|
--------
|
||||||
@@ -299,13 +270,13 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
|||||||
* OpenSSL Heartbleed:
|
* OpenSSL Heartbleed:
|
||||||
OK - Not vulnerable to Heartbleed
|
OK - Not vulnerable to Heartbleed
|
||||||
|
|
||||||
* SSLV2 Cipher Suites:
|
|
||||||
Server rejected all cipher suites.
|
|
||||||
|
|
||||||
* Session Resumption:
|
* Session Resumption:
|
||||||
With Session IDs: NOT SUPPORTED (0 successful, 5 failed, 0 errors, 5 total attempts).
|
With Session IDs: NOT SUPPORTED (0 successful, 5 failed, 0 errors, 5 total attempts).
|
||||||
With TLS Session Tickets: NOT SUPPORTED - TLS ticket assigned but not accepted.
|
With TLS Session Tickets: NOT SUPPORTED - TLS ticket assigned but not accepted.
|
||||||
|
|
||||||
|
* SSLV2 Cipher Suites:
|
||||||
|
Server rejected all cipher suites.
|
||||||
|
|
||||||
* TLSV1_2 Cipher Suites:
|
* TLSV1_2 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||||
@@ -336,9 +307,6 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
|||||||
CAMELLIA128-SHA - 128 bits
|
CAMELLIA128-SHA - 128 bits
|
||||||
AES128-SHA - 128 bits
|
AES128-SHA - 128 bits
|
||||||
|
|
||||||
* SSLV3 Cipher Suites:
|
|
||||||
Server rejected all cipher suites.
|
|
||||||
|
|
||||||
* TLSV1 Cipher Suites:
|
* TLSV1 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||||
@@ -352,11 +320,14 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
|||||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||||
CAMELLIA128-SHA - 128 bits
|
CAMELLIA128-SHA - 128 bits
|
||||||
AES128-SHA - 128 bits
|
AES128-SHA - 128 bits
|
||||||
|
|
||||||
|
* SSLV3 Cipher Suites:
|
||||||
|
Server rejected all cipher suites.
|
||||||
|
|
||||||
Should Not Offer: AES128-SHA, AES256-SHA, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA
|
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
|
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-SHA384
|
||||||
Supported Clients: OpenSSL/1.0.2, Baidu/Jan 2015, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, Firefox/31.3.0 ESR/Win 7, Googlebot/Feb 2015, Android/4.2.2, Android/5.0.0, Android/4.0.4, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, YandexBot/Jan 2015, Safari/8/OS X 10.10, Safari/7/iOS 7.1, Chrome/42/OS X, Safari/5.1.9/OS X 10.6.8, Android/4.1.1, Firefox/37/OS X, Safari/6.0.4/OS X 10.8.4, Android/4.3, Safari/6/iOS 6.0.1, Android/4.4.2, OpenSSL/0.9.8y, IE Mobile/11/Win Phone 8.1, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/8u31, Java/7u25, Java/6u45, Android/2.3.7
|
Supported Clients: OpenSSL/1.0.2, Firefox/31.3.0 ESR/Win 7, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, Baidu/Jan 2015, Safari/7/iOS 7.1, Chrome/42/OS X, Googlebot/Feb 2015, Android/4.0.4, Safari/8/iOS 8.1.2, Android/4.1.1, Android/5.0.0, Safari/6/iOS 6.0.1, YandexBot/Jan 2015, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Safari/8/OS X 10.10, Firefox/37/OS X, Safari/7/OS X 10.9, Android/4.3, Safari/5.1.9/OS X 10.6.8, Android/4.4.2, IE/8-10/Win 7, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, IE Mobile/11/Win Phone 8.1, Java/7u25, Java/8u31, Java/6u45, Android/2.3.7
|
||||||
|
|
||||||
PORT 995
|
PORT 995
|
||||||
--------
|
--------
|
||||||
@@ -370,13 +341,13 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
|||||||
* OpenSSL Heartbleed:
|
* OpenSSL Heartbleed:
|
||||||
OK - Not vulnerable to Heartbleed
|
OK - Not vulnerable to Heartbleed
|
||||||
|
|
||||||
* SSLV2 Cipher Suites:
|
|
||||||
Server rejected all cipher suites.
|
|
||||||
|
|
||||||
* Session Resumption:
|
* Session Resumption:
|
||||||
With Session IDs: NOT SUPPORTED (0 successful, 5 failed, 0 errors, 5 total attempts).
|
With Session IDs: NOT SUPPORTED (0 successful, 5 failed, 0 errors, 5 total attempts).
|
||||||
With TLS Session Tickets: NOT SUPPORTED - TLS ticket assigned but not accepted.
|
With TLS Session Tickets: NOT SUPPORTED - TLS ticket assigned but not accepted.
|
||||||
|
|
||||||
|
* SSLV2 Cipher Suites:
|
||||||
|
Server rejected all cipher suites.
|
||||||
|
|
||||||
* TLSV1_2 Cipher Suites:
|
* TLSV1_2 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||||
@@ -407,9 +378,6 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
|||||||
CAMELLIA128-SHA - 128 bits
|
CAMELLIA128-SHA - 128 bits
|
||||||
AES128-SHA - 128 bits
|
AES128-SHA - 128 bits
|
||||||
|
|
||||||
* SSLV3 Cipher Suites:
|
|
||||||
Server rejected all cipher suites.
|
|
||||||
|
|
||||||
* TLSV1 Cipher Suites:
|
* TLSV1 Cipher Suites:
|
||||||
Preferred:
|
Preferred:
|
||||||
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
ECDHE-RSA-AES256-SHA ECDH-384 bits 256 bits
|
||||||
@@ -423,9 +391,12 @@ _nassl.OpenSSLError - error:140940F5:SSL routines:ssl3_read_bytes:unexpected rec
|
|||||||
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
DHE-RSA-CAMELLIA128-SHA DH-1024 bits 128 bits
|
||||||
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
DHE-RSA-AES128-SHA DH-1024 bits 128 bits
|
||||||
CAMELLIA128-SHA - 128 bits
|
CAMELLIA128-SHA - 128 bits
|
||||||
AES128-SHA - 128 bits
|
AES128-SHA - 128 bits
|
||||||
|
|
||||||
|
* SSLV3 Cipher Suites:
|
||||||
|
Server rejected all cipher suites.
|
||||||
|
|
||||||
Should Not Offer: AES128-SHA, AES256-SHA, CAMELLIA128-SHA, CAMELLIA256-SHA, DHE-RSA-CAMELLIA128-SHA, DHE-RSA-CAMELLIA256-SHA
|
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
|
Could Also Offer: DHE-DSS-AES128-GCM-SHA256, DHE-DSS-AES128-SHA256, DHE-DSS-AES256-GCM-SHA384, DHE-DSS-AES256-SHA, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-SHA, ECDHE-ECDSA-AES128-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-SHA, ECDHE-ECDSA-AES256-SHA384, ECDHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-SHA384
|
||||||
Supported Clients: OpenSSL/1.0.2, Baidu/Jan 2015, Yahoo Slurp/Jan 2015, BingPreview/Jan 2015, OpenSSL/1.0.1l, Firefox/31.3.0 ESR/Win 7, Googlebot/Feb 2015, Android/4.2.2, Android/5.0.0, Android/4.0.4, Safari/8/iOS 8.1.2, Safari/7/OS X 10.9, YandexBot/Jan 2015, Safari/8/OS X 10.10, Safari/7/iOS 7.1, Chrome/42/OS X, Safari/5.1.9/OS X 10.6.8, Android/4.1.1, Firefox/37/OS X, Safari/6.0.4/OS X 10.8.4, Android/4.3, Safari/6/iOS 6.0.1, Android/4.4.2, OpenSSL/0.9.8y, IE Mobile/11/Win Phone 8.1, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, IE/8-10/Win 7, IE Mobile/10/Win Phone 8.0, Java/8u31, Java/7u25, Java/6u45, Android/2.3.7
|
Supported Clients: OpenSSL/1.0.2, Firefox/31.3.0 ESR/Win 7, OpenSSL/1.0.1l, BingPreview/Jan 2015, Yahoo Slurp/Jan 2015, Baidu/Jan 2015, Safari/7/iOS 7.1, Chrome/42/OS X, Googlebot/Feb 2015, Android/4.0.4, Safari/8/iOS 8.1.2, Android/4.1.1, Android/5.0.0, Safari/6/iOS 6.0.1, YandexBot/Jan 2015, Safari/6.0.4/OS X 10.8.4, Android/4.2.2, Safari/8/OS X 10.10, Firefox/37/OS X, Safari/7/OS X 10.9, Android/4.3, Safari/5.1.9/OS X 10.6.8, Android/4.4.2, IE/8-10/Win 7, IE/7/Vista, IE/11/Win 8.1, IE/11/Win 7, OpenSSL/0.9.8y, IE Mobile/10/Win Phone 8.0, IE Mobile/11/Win Phone 8.1, Java/7u25, Java/8u31, Java/6u45, Android/2.3.7
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
#
|
#
|
||||||
# This is a tool Josh uses on his box serving mailinabox.email to parse the nginx
|
# This is a tool Josh uses on his box serving mailinabox.email to parse the nginx
|
||||||
# access log to see how many people are installing Mail-in-a-Box each day, by
|
# access log to see how many people are installing Mail-in-a-Box each day, by
|
||||||
# looking at accesses to the bootstrap.sh script.
|
# looking at accesses to the bootstrap.sh script (which is currently at the URL
|
||||||
|
# .../setup.sh).
|
||||||
|
|
||||||
import re, glob, gzip, os.path, json
|
import re, glob, gzip, os.path, json
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
@@ -24,9 +25,10 @@ for fn in glob.glob("/var/log/nginx/access.log*"):
|
|||||||
# Loop through the lines in the access log.
|
# Loop through the lines in the access log.
|
||||||
with f:
|
with f:
|
||||||
for line in f:
|
for line in f:
|
||||||
# Find lines that are GETs on /bootstrap.sh by either curl or wget.
|
# Find lines that are GETs on the bootstrap script by either curl or wget.
|
||||||
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
|
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
|
||||||
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /bootstrap.sh HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
|
# (Also, the URL changed in January 2016, but we'll accept both.)
|
||||||
|
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /(bootstrap.sh|setup.sh) HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
|
||||||
if m:
|
if m:
|
||||||
date, time = m.group("date").decode("ascii").split(":", 1)
|
date, time = m.group("date").decode("ascii").split(":", 1)
|
||||||
date = dateutil.parser.parse(date).date().isoformat()
|
date = dateutil.parser.parse(date).date().isoformat()
|
||||||
|
|||||||
Reference in New Issue
Block a user