mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-21 03:02:09 +00:00
Merge branch 'master' into owncloud9
This commit is contained in:
commit
294f13c630
11
CHANGELOG.md
11
CHANGELOG.md
@ -9,6 +9,17 @@ Mail:
|
|||||||
* Roundcube is updated to version 1.2.0.
|
* Roundcube is updated to version 1.2.0.
|
||||||
* SSLv3 and RC4 are now no longer supported in incoming and outgoing mail (SMTP port 25).
|
* 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.
|
||||||
|
|
||||||
|
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)
|
v0.18c (June 2, 2016)
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
12
conf/fail2ban/filter.d/miab-management-daemon.conf
Normal file
12
conf/fail2ban/filter.d/miab-management-daemon.conf
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Fail2Ban filter Mail-in-a-Box management daemon
|
||||||
|
|
||||||
|
[INCLUDES]
|
||||||
|
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
|
||||||
|
_daemon = mailinabox
|
||||||
|
|
||||||
|
failregex = Mail-in-a-Box Management Daemon: Failed login attempt from ip <HOST> - timestamp .*
|
||||||
|
ignoreregex =
|
7
conf/fail2ban/filter.d/miab-munin.conf
Normal file
7
conf/fail2ban/filter.d/miab-munin.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[INCLUDES]
|
||||||
|
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
failregex=<HOST> - .*GET /admin/munin/.* HTTP/1.1\" 401.*
|
||||||
|
ignoreregex =
|
7
conf/fail2ban/filter.d/miab-owncloud.conf
Normal file
7
conf/fail2ban/filter.d/miab-owncloud.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[INCLUDES]
|
||||||
|
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
failregex=Login failed: .*Remote IP: '<HOST>[\)']
|
||||||
|
ignoreregex =
|
7
conf/fail2ban/filter.d/miab-postfix-submission.conf
Normal file
7
conf/fail2ban/filter.d/miab-postfix-submission.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[INCLUDES]
|
||||||
|
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
failregex=postfix/submission/smtpd.*warning.*\[<HOST>\]: .* authentication (failed|aborted)
|
||||||
|
ignoreregex =
|
9
conf/fail2ban/filter.d/miab-roundcube.conf
Normal file
9
conf/fail2ban/filter.d/miab-roundcube.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[INCLUDES]
|
||||||
|
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
|
||||||
|
failregex = IMAP Error: Login failed for .*? from <HOST>\. AUTHENTICATE.*
|
||||||
|
|
||||||
|
ignoreregex =
|
@ -1,4 +1,5 @@
|
|||||||
# Fail2Ban configuration file for Mail-in-a-Box
|
# Fail2Ban configuration file for Mail-in-a-Box. Do not edit.
|
||||||
|
# This file is re-generated on updates.
|
||||||
|
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
||||||
@ -6,24 +7,52 @@
|
|||||||
# ours too. The string is substituted during installation.
|
# ours too. The string is substituted during installation.
|
||||||
ignoreip = 127.0.0.1/8 PUBLIC_IP
|
ignoreip = 127.0.0.1/8 PUBLIC_IP
|
||||||
|
|
||||||
# JAILS
|
|
||||||
|
|
||||||
[ssh]
|
|
||||||
maxretry = 7
|
|
||||||
bantime = 3600
|
|
||||||
|
|
||||||
[ssh-ddos]
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[sasl]
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[dovecot]
|
[dovecot]
|
||||||
enabled = true
|
enabled = true
|
||||||
filter = dovecotimap
|
filter = dovecotimap
|
||||||
|
logpath = /var/log/mail.log
|
||||||
findtime = 30
|
findtime = 30
|
||||||
maxretry = 20
|
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
|
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]
|
[recidive]
|
||||||
enabled = true
|
enabled = true
|
||||||
@ -39,3 +68,13 @@ action = iptables-allports[name=recidive]
|
|||||||
# By default we don't configure this address and no action is required from the admin anyway.
|
# 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
|
# So the notification is ommited. This will prevent message appearing in the mail.log that mail
|
||||||
# can't be delivered to fail2ban@$HOSTNAME.
|
# can't be delivered to fail2ban@$HOSTNAME.
|
||||||
|
|
||||||
|
[sasl]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[ssh]
|
||||||
|
maxretry = 7
|
||||||
|
bantime = 3600
|
||||||
|
|
||||||
|
[ssh-ddos]
|
||||||
|
enabled = true
|
@ -9,6 +9,7 @@
|
|||||||
add_header X-Frame-Options "DENY";
|
add_header X-Frame-Options "DENY";
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header Content-Security-Policy "frame-ancestors 'none';";
|
add_header Content-Security-Policy "frame-ancestors 'none';";
|
||||||
|
add_header Strict-Transport-Security max-age=31536000;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ownCloud configuration.
|
# ownCloud configuration.
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os, os.path, re, json
|
import os, os.path, re, json, time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
|
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
|
||||||
@ -45,6 +46,9 @@ def authorized_personnel_only(viewfunc):
|
|||||||
privs = []
|
privs = []
|
||||||
error = "Incorrect username or password"
|
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:
|
||||||
# Call view func.
|
# Call view func.
|
||||||
@ -117,6 +121,9 @@ 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": "Incorrect username or password",
|
"reason": "Incorrect username or password",
|
||||||
@ -583,6 +590,22 @@ def munin_cgi(filename):
|
|||||||
app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO'])
|
app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO'])
|
||||||
return response
|
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__':
|
||||||
|
@ -18,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")
|
||||||
@ -61,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": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, },
|
|
||||||
|
|
||||||
{ "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
|
|
||||||
{ "name": "Public DNS (nsd4)", "port": 53, "public": True, },
|
|
||||||
{ "name": "Incoming Mail (SMTP/postfix)", "port": 25, "public": True, },
|
|
||||||
{ "name": "Outgoing Mail (SMTP 587/postfix)", "port": 587, "public": True, },
|
|
||||||
#{ "name": "Postfix/master", "port": 10587, "public": True, },
|
|
||||||
{ "name": "IMAPS (dovecot)", "port": 993, "public": True, },
|
|
||||||
{ "name": "Mail Filters (Sieve/dovecot)", "port": 4190, "public": True, },
|
|
||||||
{ "name": "HTTP Web (nginx)", "port": 80, "public": True, },
|
|
||||||
{ "name": "HTTPS Web (nginx)", "port": 443, "public": True, },
|
|
||||||
]
|
|
||||||
|
|
||||||
all_running = True
|
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
|
||||||
@ -169,6 +168,26 @@ def run_system_checks(rounded_values, 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)
|
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
|
||||||
# package may not be installed so check that before trying to access
|
# package may not be installed so check that before trying to access
|
||||||
@ -240,6 +259,8 @@ def run_network_checks(env, output):
|
|||||||
|
|
||||||
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.
|
||||||
|
@ -106,6 +106,40 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Mail alias API</h3>
|
||||||
|
|
||||||
|
<p>Use your box’s Mail alias API to add/remove aliases.</p>
|
||||||
|
|
||||||
|
<p>Usage:</p>
|
||||||
|
|
||||||
|
<pre>curl -X <b>VERB</b> [-d "<b>value</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/aliases[<b>action</b>]</pre>
|
||||||
|
|
||||||
|
<p>(Brackets denote an optional argument.)</p>
|
||||||
|
<p>(Adding <code>?format=json</code> will give json encoded results)</p>
|
||||||
|
|
||||||
|
<h4>Verbs</h4>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead><th>Verb</th> <th>Action</th><th></th></thead>
|
||||||
|
<tr><td>GET</td><td></td> <td>Returns a list of existing mail aliases.</td></tr>
|
||||||
|
<tr><td>POST</td><td>/add</td> <td>Adds a new mail alias. Required 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 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 users
|
||||||
|
curl -X GET https://{{hostname}}/admin/mail/users?format=json
|
||||||
|
|
||||||
|
# adds a new email alias
|
||||||
|
curl -X POST -d "address=new_alias@mydomail.com" -d "forward_to=my_email@mydomain.com" https://{{hostname}}/admin/mail/aliases/add
|
||||||
|
|
||||||
|
# removes a email 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() {
|
||||||
|
@ -84,6 +84,47 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Mail user API</h3>
|
||||||
|
|
||||||
|
<p>Use your box’s Mail user API to add/change/remove users.</p>
|
||||||
|
|
||||||
|
<p>Usage:</p>
|
||||||
|
|
||||||
|
<pre>curl -X <b>VERB</b> [-d "<b>value</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/users[<b>action</b>]</pre>
|
||||||
|
|
||||||
|
<p>(Brackets denote an optional argument.)</p>
|
||||||
|
<p>(Adding <code>?format=json</code> will give json encoded results)</p>
|
||||||
|
|
||||||
|
<h4>Verbs</h4>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead><th>Verb</th> <th>Action</th><th></th></thead>
|
||||||
|
<tr><td>GET</td><td></td> <td>Returns a list of existing mail users.</td></tr>
|
||||||
|
<tr><td>POST</td><td>/add</td> <td>Adds a new mail user. Required parameters are <code>email</code> and <code>password</code>.</td></tr>
|
||||||
|
<tr><td>POST</td><td>/remove</td> <td>Removes a mail user. Required 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 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 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 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() {
|
||||||
|
@ -153,7 +153,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
|
||||||
@ -183,7 +182,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
|
||||||
@ -221,7 +219,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
|
||||||
@ -233,6 +235,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 ";";
|
||||||
|
@ -47,15 +47,15 @@ if [ -e ~/.wgetrc ]; then
|
|||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check that we are running on x86_64, any other architecture is unsupported and
|
# Check that we are running on x86_64 or i686, any other architecture is unsupported and
|
||||||
# will fail later in the setup when we try to install the custom build lucene packages.
|
# 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
|
# Set ARM=1 to ignore this check if you have built the packages yourself. If you do this
|
||||||
# you are on your own!
|
# you are on your own!
|
||||||
ARCHITECTURE=$(uname -m)
|
ARCHITECTURE=$(uname -m)
|
||||||
if [ "$ARCHITECTURE" != "x86_64" ]; then
|
if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "i686" ]; then
|
||||||
if [ -z "$ARM" ]; then
|
if [ -z "$ARM" ]; then
|
||||||
echo "Mail-in-a-Box only supports x86_64 and will not work on any other architecture, like ARM."
|
echo "Mail-in-a-Box only supports x86_64 or i686 and will not work on any other architecture, like ARM."
|
||||||
echo "Your architecture is $ARCHITECTURE"
|
echo "Your architecture is $ARCHITECTURE"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
@ -291,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
|
||||||
|
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)
|
Loading…
Reference in New Issue
Block a user