Merge, conflict fixed

master
andryyy 2017-07-02 11:22:35 +02:00
commit cf902854d7
15 changed files with 431 additions and 316 deletions

View File

@ -8,6 +8,7 @@ RUN apk add --update --no-cache \
curl \ curl \
openssl \ openssl \
bind-tools \ bind-tools \
jq \
mariadb-client mariadb-client
COPY docker-entrypoint.sh /srv/docker-entrypoint.sh COPY docker-entrypoint.sh /srv/docker-entrypoint.sh

View File

@ -2,10 +2,12 @@
ACME_BASE=/var/lib/acme ACME_BASE=/var/lib/acme
SSL_EXAMPLE=/var/lib/ssl-example SSL_EXAMPLE=/var/lib/ssl-example
mkdir -p ${ACME_BASE}/acme/private mkdir -p ${ACME_BASE}/acme/private
restart_containers(){ restart_containers(){
for container in $*; do for container in $*; do
echo "Restarting ${container}..."
curl -X POST \ curl -X POST \
--unix-socket /var/run/docker.sock \ --unix-socket /var/run/docker.sock \
"http/containers/${container}/restart" "http/containers/${container}/restart"
@ -45,14 +47,14 @@ else
echo "Restoring previous acme certificate and restarting script..." echo "Restoring previous acme certificate and restarting script..."
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
exec $(readlink -f "$0") exec env TRIGGER_RESTART=1 $(readlink -f "$0")
fi fi
ISSUER="mailcow" ISSUER="mailcow"
else else
echo "Restoring mailcow snake-oil certificates and restarting script..." echo "Restoring mailcow snake-oil certificates and restarting script..."
cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem
cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
exec $(readlink -f "$0") exec env TRIGGER_RESTART=1 $(readlink -f "$0")
fi fi
fi fi
@ -66,6 +68,8 @@ while true; do
declare -a ADDITIONAL_VALIDATED_SAN declare -a ADDITIONAL_VALIDATED_SAN
IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}" IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}"
IPV4=$(curl -4s https://mailcow.email/ip.php) IPV4=$(curl -4s https://mailcow.email/ip.php)
# Container ids may have changed
CONTAINERS_RESTART=($(curl --silent --unix-socket /var/run/docker.sock http/containers/json | jq -rc 'map(select(.Names[] | contains ("nginx-mailcow") or contains ("postfix-mailcow") or contains ("dovecot-mailcow"))) | .[] .Id' | tr "\n" " "))
while read line; do while read line; do
SQL_DOMAIN_ARR+=("${line}") SQL_DOMAIN_ARR+=("${line}")
@ -75,7 +79,7 @@ while true; do
A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1) A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1)
if [[ ! -z ${A_CONFIG} ]]; then if [[ ! -z ${A_CONFIG} ]]; then
echo "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}" echo "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}"
if [[ ${IPV4} == ${A_CONFIG} ]]; then if [[ ${IPV4:-ERR} == ${A_CONFIG} ]]; then
echo "Confirmed A record autoconfig.${SQL_DOMAIN}" echo "Confirmed A record autoconfig.${SQL_DOMAIN}"
VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}") VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}")
else else
@ -88,7 +92,7 @@ while true; do
A_DISCOVER=$(dig A autodiscover.${SQL_DOMAIN} +short | tail -n 1) A_DISCOVER=$(dig A autodiscover.${SQL_DOMAIN} +short | tail -n 1)
if [[ ! -z ${A_DISCOVER} ]]; then if [[ ! -z ${A_DISCOVER} ]]; then
echo "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}" echo "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}"
if [[ ${IPV4} == ${A_DISCOVER} ]]; then if [[ ${IPV4:-ERR} == ${A_DISCOVER} ]]; then
echo "Confirmed A record autodiscover.${SQL_DOMAIN}" echo "Confirmed A record autodiscover.${SQL_DOMAIN}"
VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}") VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}")
else else
@ -102,7 +106,7 @@ while true; do
A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1) A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1)
if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
echo "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}" echo "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
if [[ ${IPV4} == ${A_MAILCOW_HOSTNAME} ]]; then if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]]; then
echo "Confirmed A record ${MAILCOW_HOSTNAME}" echo "Confirmed A record ${MAILCOW_HOSTNAME}"
VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
else else
@ -116,7 +120,7 @@ while true; do
A_SAN=$(dig A ${SAN} +short | tail -n 1) A_SAN=$(dig A ${SAN} +short | tail -n 1)
if [[ ! -z ${A_SAN} ]]; then if [[ ! -z ${A_SAN} ]]; then
echo "Found A record for ${SAN}: ${A_SAN}" echo "Found A record for ${SAN}: ${A_SAN}"
if [[ ${IPV4} == ${A_SAN} ]]; then if [[ ${IPV4:-ERR} == ${A_SAN} ]]; then
echo "Confirmed A record ${SAN}" echo "Confirmed A record ${SAN}"
ADDITIONAL_VALIDATED_SAN+=("${SAN}") ADDITIONAL_VALIDATED_SAN+=("${SAN}")
else else
@ -127,7 +131,7 @@ while true; do
fi fi
done done
ALL_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${VALIDATED_MAILCOW_HOSTNAME})) ALL_VALIDATED="$(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${VALIDATED_MAILCOW_HOSTNAME})"
if [[ -z ${ALL_VALIDATED[*]} ]]; then if [[ -z ${ALL_VALIDATED[*]} ]]; then
echo "Cannot validate hostnames, skipping Let's Encrypt..." echo "Cannot validate hostnames, skipping Let's Encrypt..."
echo 0 echo 0
@ -136,7 +140,7 @@ while true; do
ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${MAILCOW_HOSTNAME} | tr ' ' '\n' | sort | uniq -u )) ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${MAILCOW_HOSTNAME} | tr ' ' '\n' | sort | uniq -u ))
if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then
DATE=$(date +%Y-%m-%d_%H_%M_%S) DATE=$(date +%Y-%m-%d_%H_%M_%S)
echo "Found orphaned SAN(s) ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/" echo "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..."
mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/ mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/
[[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/ [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/
mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/
@ -159,11 +163,11 @@ while true; do
# restart docker containers # restart docker containers
if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then
echo "Certificate was successfully request, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..." echo "Certificate was successfully requested, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..."
cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem
cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
fi fi
restart_containers ${CONTAINERS_RESTART} restart_containers ${CONTAINERS_RESTART[*]}
;; ;;
1) # failure 1) # failure
if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
@ -171,7 +175,7 @@ while true; do
cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem
TRIGGER_RESTART=1 TRIGGER_RESTART=1
elif [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then
echo "Error requesting certificate, restoring from previous acme request and restarting containers..." echo "Error requesting certificate, restoring from previous acme request and restarting containers..."
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
@ -183,20 +187,20 @@ while true; do
cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
TRIGGER_RESTART=1 TRIGGER_RESTART=1
fi fi
[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART} [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
exit 1;; exit 1;;
2) # no change 2) # no change
if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then
echo "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..." echo "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..."
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
restart_containers ${CONTAINERS_RESTART} restart_containers ${CONTAINERS_RESTART[*]}
fi fi
if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then
echo "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..." echo "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..."
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
restart_containers ${CONTAINERS_RESTART} restart_containers ${CONTAINERS_RESTART[*]}
fi fi
;; ;;
*) # unspecified *) # unspecified
@ -205,7 +209,7 @@ while true; do
cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem
TRIGGER_RESTART=1 TRIGGER_RESTART=1
elif [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then
echo "Error requesting certificate, restoring from previous acme request and restarting containers..." echo "Error requesting certificate, restoring from previous acme request and restarting containers..."
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
@ -217,7 +221,7 @@ while true; do
cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
TRIGGER_RESTART=1 TRIGGER_RESTART=1
fi fi
[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART} [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
exit 1;; exit 1;;
esac esac

View File

@ -3,8 +3,8 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C ENV LC_ALL C
ENV DOVECOT_VERSION 2.2.30.2 ENV DOVECOT_VERSION 2.2.31
ENV PIGEONHOLE_VERSION 0.4.18 ENV PIGEONHOLE_VERSION 0.4.19
RUN apt-get update && apt-get -y install \ RUN apt-get update && apt-get -y install \
automake \ automake \

View File

@ -19,12 +19,33 @@ if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
raise SystemExit raise SystemExit
r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0) r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
RULES = { client = docker.from_env()
'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),', for container in client.containers.list():
'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked', if "postfix-mailcow" in container.name:
'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)', postfix_container = container.name
} elif "dovecot-mailcow" in container.name:
dovecot_container = container.name
elif "sogo-mailcow" in container.name:
sogo_container = container.name
elif "php-fpm-mailcow" in container.name:
php_fpm_container = container.name
RULES = {}
RULES[postfix_container] = {}
RULES[dovecot_container] = {}
RULES[sogo_container] = {}
RULES[php_fpm_container] = {}
RULES[postfix_container][1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed'
RULES[dovecot_container][1] = '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),'
RULES[dovecot_container][2] = '-login: Disconnected \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
RULES[dovecot_container][3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
RULES[dovecot_container][4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
RULES[sogo_container][1] = 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked'
RULES[php_fpm_container][1] = 'mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)'
r.setnx("F2B_BAN_TIME", "1800") r.setnx("F2B_BAN_TIME", "1800")
r.setnx("F2B_MAX_ATTEMPTS", "10") r.setnx("F2B_MAX_ATTEMPTS", "10")
@ -135,12 +156,17 @@ def watch(container):
log['message'] = "Watching %s" % container log['message'] = "Watching %s" % container
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False)) r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
print "Watching", container print "Watching", container
client = docker.from_env()
for msg in client.containers.get(container).attach(stream=True, logs=False): for msg in client.containers.get(container).attach(stream=True, logs=False):
result = re.search(RULES[container], msg) for rule_id, rule_regex in RULES[container].iteritems():
if result: result = re.search(rule_regex, msg)
addr = result.group(1) if result:
ban(addr) addr = result.group(1)
print "%s matched rule id %d in %s" % (addr, rule_id, container)
log['time'] = int(round(time.time()))
log['priority'] = "warn"
log['message'] = "%s matched rule id %d in %s" % (addr, rule_id, container)
r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
ban(addr)
def autopurge(): def autopurge():
while not quit_now: while not quit_now:

View File

@ -4,15 +4,10 @@ The match section performs AND operation on different matches: for example, if y
then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches, then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
then any of these will trigger the rule. If a rule is triggered then no more rules are matched. then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
*/ */
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
}
header('Content-Type: text/plain'); header('Content-Type: text/plain');
require_once "vars.inc.php"; require_once "vars.inc.php";
ini_set('error_reporting', 0); ini_set('error_reporting', 1);
$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name; $dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$opt = [ $opt = [
@ -29,6 +24,77 @@ catch (PDOException $e) {
exit; exit;
} }
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
}
function ucl_rcpts($object, $type) {
global $pdo;
if ($type == 'mailbox') {
// Standard aliases
$stmt = $pdo->prepare("SELECT `address` FROM `alias`
WHERE `goto` LIKE :object_goto
AND `address` NOT LIKE '@%'
AND `address` != :object_address");
$stmt->execute(array(
':object_goto' => '%' . $object . '%',
':object_address' => $object
));
$standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($standard_aliases)) {
$local = parse_email($row['address'])['local'];
$domain = parse_email($row['address'])['domain'];
if (!empty($local) && !empty($domain)) {
$rcpt[] = '/' . $local . '\+.*' . $domain . '/';
}
$rcpt[] = $row['address'];
}
// Aliases by alias domains
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object");
$stmt->execute(array(
':object' => $object
));
$by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($by_domain_aliases);
while ($row = array_shift($by_domain_aliases)) {
if (!empty($row['alias'])) {
$local = parse_email($row['alias'])['local'];
$domain = parse_email($row['alias'])['domain'];
if (!empty($local) && !empty($domain)) {
$rcpt[] = '/' . $local . '\+.*' . $domain . '/';
}
$rcpt[] = $row['alias'];
}
}
// Mailbox self
$local = parse_email($row['object'])['local'];
$domain = parse_email($row['object'])['domain'];
if (!empty($local) && !empty($domain)) {
$rcpt[] = '/' . $local . '\+.*' . $domain . '/';
}
$rcpt[] = $object;
}
elseif ($type == 'domain') {
// Domain self
$rcpt[] = '/.*@' . $object . '/';
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
WHERE `target_domain` = :object");
$stmt->execute(array(':object' => $row['object']));
$alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($alias_domains);
while ($row = array_shift($alias_domains)) {
$rcpt[] = '/.*@' . $row['alias_domain'] . '/';
}
}
if (!empty($rcpt)) {
return $rcpt;
}
return false;
}
?> ?>
settings { settings {
<?php <?php
@ -44,73 +110,18 @@ while ($row = array_shift($rows)) {
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
?> ?>
score_<?=$username_sane;?> { score_<?=$username_sane;?> {
priority = low; priority = 4;
<?php <?php
foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
?>
rcpt = "<?=$rcpt;?>";
<?php
}
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` $stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel') WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
AND `object`= :object"); AND `object`= :object");
$stmt->execute(array(':object' => $row['object'])); $stmt->execute(array(':object' => $row['object']));
$spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP); $spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
WHERE (`object`= :object OR `object`= :object_domain)
AND (`option` = 'blacklist_from' OR `option` = 'whitelist_from')");
$stmt->execute(array(':object' => $row['object'], ':object_domain' => substr(strrchr($row['object'], "@"), 1)));
$grouped_lists = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($grouped_lists);
while ($grouped_list = array_shift($grouped_lists)) {
$value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_list['value'])));
if (!empty($value_sane)) {
?>
from = "/^((?!<?=$value_sane;?>).)*$/";
<?php
}
}
$local = parse_email($row['object'])['local'];
$domain = parse_email($row['object'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row['object'];?>";
<?php
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address");
$stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object']));
$rows_aliases_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row_aliases_1 = array_shift($rows_aliases_1)) {
$local = parse_email($row_aliases_1['address'])['local'];
$domain = parse_email($row_aliases_1['address'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row_aliases_1['address'];?>";
<?php
}
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object");
$stmt->execute(array(':object' => $row['object']));
$rows_aliases_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_aliases_2);
while ($row_aliases_2 = array_shift($rows_aliases_2)) {
if (!empty($row_aliases_2['aliases'])) {
$local = parse_email($row_aliases_2['aliases'])['local'];
$domain = parse_email($row_aliases_2['aliases'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row_aliases_2['aliases'];?>";
<?php
}
}
?> ?>
apply "default" { apply "default" {
actions { actions {
@ -145,70 +156,23 @@ while ($row = array_shift($rows)) {
<?php <?php
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) { if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
?> ?>
priority = medium; priority = 5;
rcpt = "/.*@<?=$row['object'];?>/";
<?php <?php
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
WHERE `target_domain` = :object");
$stmt->execute(array(':object' => $row['object']));
$rows_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_domain_aliases);
while ($row_domain_aliases = array_shift($rows_domain_aliases)) {
?> ?>
rcpt = "/.*@<?=$row_domain_aliases['alias_domain'];?>/"; rcpt = "<?=$rcpt;?>";
<?php <?php
} }
} }
else { else {
?> ?>
priority = high; priority = 6;
<?php <?php
$local = parse_email($row['object'])['local']; foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
$domain = parse_email($row['object'])['domain'];
if (!empty($local) && !empty($local)) {
?> ?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/"; rcpt = "<?=$rcpt;?>";
<?php <?php
} }
?>
rcpt = "<?=$row['object'];?>";
<?php
}
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address");
$stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object']));
$rows_aliases_wl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_aliases_wl_1);
while ($row_aliases_wl_1 = array_shift($rows_aliases_wl_1)) {
$local = parse_email($row_aliases_wl_1['address'])['local'];
$domain = parse_email($row_aliases_wl_1['address'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row_aliases_wl_1['address'];?>";
<?php
}
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object");
$stmt->execute(array(':object' => $row['object']));
$rows_aliases_wl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_aliases_wl_2);
while ($row_aliases_wl_2 = array_shift($rows_aliases_wl_2)) {
if (!empty($row_aliases_wl_2['aliases'])) {
$local = parse_email($row_aliases_wl_2['aliases'])['local'];
$domain = parse_email($row_aliases_wl_2['aliases'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row_aliases_wl_2['aliases'];?>";
<?php
}
} }
?> ?>
apply "default" { apply "default" {
@ -243,70 +207,23 @@ while ($row = array_shift($rows)) {
<?php <?php
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) { if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
?> ?>
priority = medium; priority = 5;
rcpt = "/.*@<?=$row['object'];?>/";
<?php <?php
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
WHERE `target_domain` = :object");
$stmt->execute(array(':object' => $row['object']));
$rows_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_domain_aliases);
while ($row_domain_aliases = array_shift($rows_domain_aliases)) {
?> ?>
rcpt = "/.*@<?=$row_domain_aliases['alias_domain'];?>/"; rcpt = "<?=$rcpt;?>";
<?php <?php
} }
} }
else { else {
?> ?>
priority = high; priority = 6;
<?php <?php
$local = parse_email($row['object'])['local']; foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
$domain = parse_email($row['object'])['domain'];
if (!empty($local) && !empty($local)) {
?> ?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/"; rcpt = "<?=$rcpt;?>";
<?php <?php
} }
?>
rcpt = "<?=$row['object'];?>";
<?php
}
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address");
$stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object']));
$rows_aliases_bl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_aliases_bl_1);
while ($row_aliases_bl_1 = array_shift($rows_aliases_bl_1)) {
$local = parse_email($row_aliases_bl_1['address'])['local'];
$domain = parse_email($row_aliases_bl_1['address'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row_aliases_bl_1['address'];?>";
<?php
}
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object");
$stmt->execute(array(':object' => $row['object']));
$rows_aliases_bl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($rows_aliases_bl_2);
while ($row_aliases_bl_2 = array_shift($rows_aliases_bl_2)) {
if (!empty($row_aliases_bl_2['aliases'])) {
$local = parse_email($row_aliases_bl_2['aliases'])['local'];
$domain = parse_email($row_aliases_bl_2['aliases'])['domain'];
if (!empty($local) && !empty($local)) {
?>
rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
<?php
}
?>
rcpt = "<?=$row_aliases_bl_2['aliases'];?>";
<?php
}
} }
?> ?>
apply "default" { apply "default" {
@ -319,4 +236,4 @@ while ($row = array_shift($rows)) {
<?php <?php
} }
?> ?>
} }

View File

@ -230,6 +230,27 @@ $tfa_data = get_tfa();
</div> </div>
<button class="btn btn-default" id="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button> <button class="btn btn-default" id="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
</form> </form>
<legend data-target="#import_dkim" style="margin-top:40px;cursor:pointer" data-toggle="collapse"> <?=$lang['admin']['import_private_key'];?></legend>
<div id="import_dkim" class="collapse">
<form class="form" data-id="dkim_import" role="form" method="post">
<div class="form-group">
<label for="domain">Domain:</label>
<input class="form-control" id="domain" name="domain" placeholder="example.org" required>
</div>
<div class="form-group">
<label for="domain">Selector:</label>
<input class="form-control" id="dkim_selector" name="dkim_selector" value="dkim" required>
</div>
<div class="form-group">
<label for="private_key_file"><?=$lang['admin']['private_key'];?>:</label>
<textarea class="form-control" rows="5" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA PRIVATE KEY-----
XYZ
-----END RSA PRIVATE KEY-----"></textarea>
</div>
<button class="btn btn-default" id="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button>
</form>
</div>
</div> </div>
</div> </div>
@ -271,26 +292,26 @@ $tfa_data = get_tfa();
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Fail2Ban parameters</div> <div class="panel-heading"><?=$lang['admin']['f2b_parameters'];?></div>
<div class="panel-body"> <div class="panel-body">
<?php <?php
$f2b_data = get_f2b_parameters(); $f2b_data = fail2ban('get');
?> ?>
<form class="form" data-id="f2b" role="form" method="post"> <form class="form" data-id="f2b" role="form" method="post">
<div class="form-group"> <div class="form-group">
<label for="ban_time">Ban time (s):</label> <label for="ban_time"><?=$lang['admin']['f2b_ban_time'];?>:</label>
<input type="number" class="form-control" id="ban_time" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required> <input type="number" class="form-control" id="ban_time" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="max_attempts">Max. attempts:</label> <label for="max_attempts"><?=$lang['admin']['f2b_max_attempts'];?>:</label>
<input type="number" class="form-control" id="max_attempts" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required> <input type="number" class="form-control" id="max_attempts" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="retry_window">Retry window (s) for max. attempts:</label> <label for="retry_window"><?=$lang['admin']['f2b_retry_window'];?>:</label>
<input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required> <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="retry_window">Whitelisted networks/hosts</label> <label for="retry_window"><?=$lang['admin']['f2b_whitelist'];?>:</label>
<textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea> <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
</div> </div>
<button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button> <button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>

View File

@ -88,6 +88,84 @@ function dkim($_action, $_data = null) {
return false; return false;
} }
break; break;
case 'import':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
$private_key_input = trim($_data['private_key_file']);
$private_key_normalized = preg_replace('~\r\n?~', "\n", $private_key_input);
$private_key = openssl_pkey_get_private($private_key_normalized);
if ($ssl_error = openssl_error_string()) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Private key error: ' . $ssl_error
);
return false;
}
// Explode by nl
$pem_public_key_array = explode(PHP_EOL, trim(openssl_pkey_get_details($private_key)['key']));
// Remove first and last line/item
array_shift($pem_public_key_array);
array_pop($pem_public_key_array);
// Implode as single string
$pem_public_key = implode('', $pem_public_key_array);
$dkim_selector = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : 'dkim';
$domain = $_data['domain'];
if (!is_valid_domain_name($domain)) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
);
return false;
}
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
);
return false;
}
if (!ctype_alnum($dkim_selector)) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
);
return false;
}
try {
$redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
unset($private_key_normalized);
unset($private_key);
unset($private_key_input);
try {
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
$_SESSION['return'] = array(
'type' => 'success',
'msg' => sprintf($lang['success']['dkim_added'])
);
return true;
break;
case 'details': case 'details':
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false; return false;
@ -95,7 +173,18 @@ function dkim($_action, $_data = null) {
$dkimdata = array(); $dkimdata = array();
if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) { if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) {
$dkimdata['pubkey'] = $redis_dkim_key_data; $dkimdata['pubkey'] = $redis_dkim_key_data;
$dkimdata['length'] = (strlen($dkimdata['pubkey']) < 391) ? 1024 : 2048; if (strlen($dkimdata['pubkey']) < 391) {
$dkimdata['length'] = "1024";
}
elseif (strlen($dkimdata['pubkey']) < 736) {
$dkimdata['length'] = "2048";
}
elseif (strlen($dkimdata['pubkey']) < 1416) {
$dkimdata['length'] = "4096";
}
else {
$dkimdata['length'] = ">= 8192";
}
$dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data; $dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
$dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data); $dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
} }

View File

@ -0,0 +1,93 @@
<?php
function fail2ban($_action, $_data = null) {
global $redis;
global $lang;
switch ($_action) {
case 'get':
$data = array();
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
try {
$data['ban_time'] = $redis->Get('F2B_BAN_TIME');
$data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
$data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
$wl = $redis->hGetAll('F2B_WHITELIST');
if (is_array($wl)) {
foreach ($wl as $key => $value) {
$tmp_data[] = $key;
}
$data['whitelist'] = implode(PHP_EOL, $tmp_data);
}
else {
$data['whitelist'] = "";
}
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
return $data;
break;
case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
$is_now = fail2ban('get');
if (!empty($is_now)) {
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
$max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['active_int']);
$retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
}
else {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
$wl = $_data['whitelist'];
$ban_time = ($ban_time < 60) ? 60 : $ban_time;
$max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
$retry_window = ($retry_window < 1) ? 1 : $retry_window;
try {
$redis->Set('F2B_BAN_TIME', $ban_time);
$redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
$redis->Set('F2B_RETRY_WINDOW', $retry_window);
$redis->Del('F2B_WHITELIST');
if(!empty($wl)) {
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
if (is_array($wl_array)) {
foreach ($wl_array as $wl_item) {
$cidr = explode('/', $wl_item);
if (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 32))) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
}
elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 128))) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
}
}
}
}
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
$_SESSION['return'] = array(
'type' => 'success',
'msg' => sprintf($lang['success']['f2b_modified'])
);
break;
}
}

View File

@ -229,11 +229,11 @@ function check_login($user, $pass) {
} }
if (!isset($_SESSION['ldelay'])) { if (!isset($_SESSION['ldelay'])) {
$_SESSION['ldelay'] = "0"; $_SESSION['ldelay'] = "0";
error_log("Mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
elseif (!isset($_SESSION['mailcow_cc_username'])) { elseif (!isset($_SESSION['mailcow_cc_username'])) {
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5; $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
error_log("Mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
sleep($_SESSION['ldelay']); sleep($_SESSION['ldelay']);
} }
@ -1435,94 +1435,4 @@ function get_logs($container, $lines = 100) {
} }
return false; return false;
} }
function get_f2b_parameters() {
global $lang;
global $redis;
$data = array();
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
try {
$data['ban_time'] = $redis->Get('F2B_BAN_TIME');
$data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
$data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
$wl = $redis->hGetAll('F2B_WHITELIST');
if (is_array($wl)) {
foreach ($wl as $key => $value) {
$tmp_data[] = $key;
}
$data['whitelist'] = implode(PHP_EOL, $tmp_data);
}
else {
$data['whitelist'] = "";
}
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
return $data;
}
function edit_f2b_parameters($postarray) {
global $lang;
global $redis;
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
$is_now = get_f2b_parameters();
if (!empty($is_now)) {
$ban_time = intval((isset($postarray['ban_time'])) ? $postarray['ban_time'] : $is_now['ban_time']);
$max_attempts = intval((isset($postarray['max_attempts'])) ? $postarray['max_attempts'] : $is_now['active_int']);
$retry_window = intval((isset($postarray['retry_window'])) ? $postarray['retry_window'] : $is_now['retry_window']);
}
else {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
$wl = $postarray['whitelist'];
$ban_time = ($ban_time < 60) ? 60 : $ban_time;
$max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
$retry_window = ($retry_window < 1) ? 1 : $retry_window;
try {
$redis->Set('F2B_BAN_TIME', $ban_time);
$redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
$redis->Set('F2B_RETRY_WINDOW', $retry_window);
$redis->Del('F2B_WHITELIST');
if(!empty($wl)) {
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
if (is_array($wl_array)) {
foreach ($wl_array as $wl_item) {
$cidr = explode('/', $wl_item);
if (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 32))) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
}
elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 128))) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
}
}
}
}
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
$_SESSION['return'] = array(
'type' => 'success',
'msg' => 'Saved changes to Fail2ban configuration'
);
}
?> ?>

View File

@ -64,6 +64,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
init_db_schema(); init_db_schema();

View File

@ -390,6 +390,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
)); ));
} }
break; break;
case "dkim_import":
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (dkim('import', $attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Cannot add item'
));
}
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Cannot find attributes in post data'
));
}
break;
case "domain-admin": case "domain-admin":
if (isset($_POST['attr'])) { if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true); $attr = (array)json_decode($_POST['attr'], true);
@ -1925,7 +1958,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
// No items // No items
if (isset($_POST['attr'])) { if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true); $attr = (array)json_decode($_POST['attr'], true);
if (edit_f2b_parameters($attr) === false) { if (fail2ban('edit', $attr) === false) {
if (isset($_SESSION['return'])) { if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']); echo json_encode($_SESSION['return']);
} }

View File

@ -49,6 +49,7 @@ $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden ges
$lang['success']['mailbox_modified'] = 'Änderungen an Mailbox %s wurden gespeichert'; $lang['success']['mailbox_modified'] = 'Änderungen an Mailbox %s wurden gespeichert';
$lang['success']['resource_modified'] = "Änderungen an Ressource %s wurden gespeichert"; $lang['success']['resource_modified'] = "Änderungen an Ressource %s wurden gespeichert";
$lang['success']['object_modified'] = "Änderungen an Objekt %s wurden gespeichert"; $lang['success']['object_modified'] = "Änderungen an Objekt %s wurden gespeichert";
$lang['success']['f2b_modified'] = "Änderungen an Fail2ban Parametern wurden gespeichert";
$lang['success']['msg_size_saved'] = 'Limit wurde gesetzt'; $lang['success']['msg_size_saved'] = 'Limit wurde gesetzt';
$lang['danger']['aliasd_not_found'] = 'Alias-Domain nicht gefunden'; $lang['danger']['aliasd_not_found'] = 'Alias-Domain nicht gefunden';
$lang['danger']['targetd_not_found'] = 'Ziel-Domain nicht gefunden'; $lang['danger']['targetd_not_found'] = 'Ziel-Domain nicht gefunden';
@ -416,7 +417,14 @@ $lang['tfa']['scan_qr_code'] = "Bitte scannen Sie jetzt den angezeigten QR-Code:
$lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel"; $lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel";
$lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens"; $lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens";
$lang['admin']['search_domain_da'] = 'Domains durchsuchen'; $lang['admin']['private_key'] = 'Private Key';
$lang['admin']['import'] = 'Importieren';
$lang['admin']['import_private_key'] = 'Private Key importieren';
$lang['admin']['f2b_parameters'] = 'Fail2ban Parameter';
$lang['admin']['f2b_ban_time'] = 'Banzeit (s)';
$lang['admin']['f2b_max_attempts'] = 'Max. Versuche';
$lang['admin']['f2b_retry_window'] = 'Wiederholungen im Zeitraum von (s)';
$lang['admin']['f2b_whitelist'] = 'Whitelist für Netzwerke und Hosts';
$lang['admin']['restrictions'] = 'Postfix Restriktionen'; $lang['admin']['restrictions'] = 'Postfix Restriktionen';
$lang['admin']['rr'] = 'Postfix Empfänger Restriktionen'; $lang['admin']['rr'] = 'Postfix Empfänger Restriktionen';
$lang['admin']['sr'] = 'Postfix Sender Restriktionen'; $lang['admin']['sr'] = 'Postfix Sender Restriktionen';

View File

@ -51,6 +51,7 @@ $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved";
$lang['success']['mailbox_modified'] = "Changes to mailbox %s have been saved"; $lang['success']['mailbox_modified'] = "Changes to mailbox %s have been saved";
$lang['success']['resource_modified'] = "Changes to mailbox %s have been saved"; $lang['success']['resource_modified'] = "Changes to mailbox %s have been saved";
$lang['success']['object_modified'] = "Changes to object %s have been saved"; $lang['success']['object_modified'] = "Changes to object %s have been saved";
$lang['success']['f2b_modified'] = "Changes to Fail2ban parameters have been saved";
$lang['success']['msg_size_saved'] = "Message size limit has been set"; $lang['success']['msg_size_saved'] = "Message size limit has been set";
$lang['danger']['aliasd_not_found'] = "Alias domain not found"; $lang['danger']['aliasd_not_found'] = "Alias domain not found";
$lang['danger']['targetd_not_found'] = "Target domain not found"; $lang['danger']['targetd_not_found'] = "Target domain not found";
@ -421,6 +422,14 @@ $lang['tfa']['scan_qr_code'] = "Please scan the following code with your authent
$lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes"; $lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes";
$lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token"; $lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token";
$lang['admin']['private_key'] = 'Private key';
$lang['admin']['import'] = 'Import';
$lang['admin']['import_private_key'] = 'Import private key';
$lang['admin']['f2b_parameters'] = 'Fail2ban parameters';
$lang['admin']['f2b_ban_time'] = 'Ban time (s)';
$lang['admin']['f2b_max_attempts'] = 'Max. attempts';
$lang['admin']['f2b_retry_window'] = 'Retry window (s) for max. attempts';
$lang['admin']['f2b_whitelist'] = 'Whitelisted networks/hosts';
$lang['admin']['search_domain_da'] = 'Search domains'; $lang['admin']['search_domain_da'] = 'Search domains';
$lang['admin']['restrictions'] = 'Postfix Restrictions'; $lang['admin']['restrictions'] = 'Postfix Restrictions';
$lang['admin']['rr'] = 'Postfix Recipient Restrictions'; $lang['admin']['rr'] = 'Postfix Recipient Restrictions';

View File

@ -10,9 +10,9 @@ services:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"] test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"]
interval: 3s interval: 30s
timeout: 3s timeout: 7s
retries: 5 retries: 10
volumes: volumes:
- ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
restart: always restart: always
@ -28,8 +28,8 @@ services:
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "--host", "localhost", "--silent"] test: ["CMD", "mysqladmin", "ping", "--host", "localhost", "--silent"]
interval: 10s interval: 10s
timeout: 30s timeout: 7s
retries: 5 retries: 10
volumes: volumes:
- mysql-vol-1:/var/lib/mysql/ - mysql-vol-1:/var/lib/mysql/
- ./data/conf/mysql/:/etc/mysql/conf.d/:ro - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
@ -171,7 +171,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: mailcow/dovecot:1.0 image: mailcow/dovecot:1.1
build: ./data/Dockerfiles/dovecot build: ./data/Dockerfiles/dovecot
depends_on: depends_on:
unbound-mailcow: unbound-mailcow:
@ -293,19 +293,19 @@ services:
acme-mailcow: acme-mailcow:
depends_on: depends_on:
- nginx-mailcow - nginx-mailcow
image: mailcow/acme:1.8 image: mailcow/acme:1.9
build: ./data/Dockerfiles/acme build: ./data/Dockerfiles/acme
dns: dns:
- 172.22.1.254 - 172.22.1.254
dns_search: mailcow-network dns_search: mailcow-network
environment: environment:
- CONTAINERS_RESTART=mailcowdockerized_postfix-mailcow_1 mailcowdockerized_dovecot-mailcow_1 mailcowdockerized_nginx-mailcow_1 - ADDITIONAL_SAN=${ADDITIONAL_SAN:- }
- ADDITIONAL_SAN=${ADDITIONAL_SAN}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- DBNAME=${DBNAME} - DBNAME=${DBNAME}
- DBUSER=${DBUSER} - DBUSER=${DBUSER}
- DBPASS=${DBPASS} - DBPASS=${DBPASS}
- SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
- SKIP_IP_CHECK=${SKIP_IP_CHECK:-n}
volumes: volumes:
- ./data/web/.well-known/acme-challenge:/var/www/acme:rw - ./data/web/.well-known/acme-challenge:/var/www/acme:rw
- ./data/assets/ssl:/var/lib/acme/:rw - ./data/assets/ssl:/var/lib/acme/:rw
@ -319,7 +319,7 @@ services:
- acme - acme
fail2ban-mailcow: fail2ban-mailcow:
image: mailcow/fail2ban:1.3 image: mailcow/fail2ban:1.4
build: ./data/Dockerfiles/fail2ban build: ./data/Dockerfiles/fail2ban
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow

View File

@ -81,6 +81,9 @@ ADDITIONAL_SAN=
# To never run acme-mailcow for Let's Encrypt, set this to y # To never run acme-mailcow for Let's Encrypt, set this to y
SKIP_LETS_ENCRYPT=n SKIP_LETS_ENCRYPT=n
# Skip IPv4 check in ACME container
SKIP_IP_CHECK=n
# To never run fail2ban-mailcow # To never run fail2ban-mailcow
SKIP_FAIL2BAN=n SKIP_FAIL2BAN=n