mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-04 15:54:48 +01:00
Merge branch 'master' into EHDD
This commit is contained in:
@@ -57,7 +57,7 @@ if [ ! -d $HOME/mailinabox ]; then
|
||||
echo Downloading Mail-in-a-Box $TAG. . .
|
||||
git clone \
|
||||
-b $TAG --depth 1 \
|
||||
https://github.com/mail-in-a-box/mailinabox \
|
||||
https://github.com/downtownallday/mailinabox-ldap.git \
|
||||
$HOME/mailinabox \
|
||||
< /dev/null 2> /dev/null
|
||||
|
||||
|
||||
121
setup/functions-ldap.sh
Normal file
121
setup/functions-ldap.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- indent-tabs-mode: t; tab-width: 4; -*-
|
||||
#
|
||||
# some helpful ldap function that are shared between setup/ldap.sh and
|
||||
# test suites in tests/suites/*
|
||||
#
|
||||
get_attribute_from_ldif() {
|
||||
local attr="$1"
|
||||
local ldif="$2"
|
||||
# Gather values - handle multivalued attributes and values that
|
||||
# contain whitespace
|
||||
ATTR_DN="$(awk "/^dn:/ { print substr(\$0, 4); exit }" <<< $ldif)"
|
||||
ATTR_VALUE=()
|
||||
local line
|
||||
while read line; do
|
||||
[ -z "$line" ] && break
|
||||
local v=$(awk "/^$attr:/ { print substr(\$0, length(\"$attr\")+3) }" <<<$line)
|
||||
[ ! -z "$v" ] && ATTR_VALUE+=( "$v" )
|
||||
done <<< "$ldif"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_attribute() {
|
||||
# Returns first matching dn in $ATTR_DN (empty if not found),
|
||||
# along with associated values of the specified attribute in
|
||||
# $ATTR_VALUE as an array
|
||||
local base="$1"
|
||||
local filter="$2"
|
||||
local attr="$3"
|
||||
local scope="${4:-sub}"
|
||||
local bind_dn="${5:-}"
|
||||
local bind_pw="${6:-}"
|
||||
local stderr_file="/tmp/ldap_search.$$.err"
|
||||
local code_file="$stderr_file.code"
|
||||
|
||||
# Issue the search
|
||||
local args=( "-Q" "-Y" "EXTERNAL" "-H" "ldapi:///" )
|
||||
if [ ! -z "$bind_dn" ]; then
|
||||
args=("-H" "$LDAP_URL" "-x" "-D" "$bind_dn" "-w" "$bind_pw" )
|
||||
fi
|
||||
args+=( "-LLL" "-s" "$scope" "-o" "ldif-wrap=no" "-b" "$base" )
|
||||
|
||||
local result
|
||||
result=$(ldapsearch ${args[@]} "$filter" "$attr" 2>$stderr_file; echo $? >$code_file)
|
||||
local exitcode=$(cat $code_file)
|
||||
local stderr=$(cat $stderr_file)
|
||||
rm -f "$stderr_file"
|
||||
rm -f "$code_file"
|
||||
if [ $exitcode -ne 0 -a $exitcode -ne 32 ]; then
|
||||
# 255 == unable to contact server
|
||||
# 32 == No such object
|
||||
die "$stderr"
|
||||
fi
|
||||
|
||||
get_attribute_from_ldif "$attr" "$result"
|
||||
}
|
||||
|
||||
|
||||
slappasswd_hash() {
|
||||
# hash the given password with our preferred algorithm and in a
|
||||
# format suitable for ldap. see crypt(3) for format
|
||||
slappasswd -h {CRYPT} -c \$6\$%.16s -s "$1"
|
||||
}
|
||||
|
||||
debug_search() {
|
||||
# perform a search and output the results
|
||||
# arg 1: the search criteria
|
||||
# arg 2: [optional] the base rdn
|
||||
# arg 3-: [optional] attributes to output, if not specified
|
||||
# all are output
|
||||
local base="$LDAP_BASE"
|
||||
local query="(objectClass=*)"
|
||||
local scope="sub"
|
||||
local attrs=( )
|
||||
case "$1" in
|
||||
\(* )
|
||||
# filters start with an open paren...
|
||||
query="$1"
|
||||
;;
|
||||
*@* )
|
||||
# looks like an email address
|
||||
query="(|(mail=$1)(maildrop=$1))"
|
||||
;;
|
||||
* )
|
||||
# default: it's a dn
|
||||
base="$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
base="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
attrs=( $@ )
|
||||
fi
|
||||
|
||||
local ldif=$(ldapsearch -H $LDAP_URL -o ldif-wrap=no -b "$base" -s $scope -LLL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" "$query" ${attrs[@]}; exit 0)
|
||||
|
||||
# expand 'member'
|
||||
local line
|
||||
while read line; do
|
||||
case "$line" in
|
||||
member:* )
|
||||
local member_dn=$(cut -c9- <<<"$line")
|
||||
get_attribute "$member_dn" "objectClass=*" mail base "$LDAP_ADMIN_DN" "$LDAP_ADMIN_PASSWORD"
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
echo "$line"
|
||||
echo "#^ member DOES NOT EXIST"
|
||||
else
|
||||
echo "member: ${ATTR_VALUE[@]}"
|
||||
echo "#^ $member_dn"
|
||||
fi
|
||||
;;
|
||||
* )
|
||||
echo "$line"
|
||||
;;
|
||||
esac
|
||||
done <<<"$ldif"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- indent-tabs-mode: t; tab-width: 4; -*-
|
||||
# Turn on "strict mode." See http://redsymbol.net/articles/unofficial-bash-strict-mode/.
|
||||
# -e: exit if any command unexpectedly fails.
|
||||
# -u: exit if we have a variable typo.
|
||||
@@ -214,3 +215,23 @@ function git_clone {
|
||||
mv $TMPPATH/$SUBDIR $TARGETPATH
|
||||
rm -rf $TMPPATH
|
||||
}
|
||||
|
||||
function generate_password() {
|
||||
# output a randomly generated password of the length specified as
|
||||
# the first argument. If no length is given, a password of 64
|
||||
# characters is generated.
|
||||
#
|
||||
# The actual returned password may be longer than requested to
|
||||
# avoid base64 padding characters
|
||||
#
|
||||
local input_len extra pw_length="${1:-64}"
|
||||
# choose a length (longer) that will avoid padding chars
|
||||
let extra="4 - $pw_length % 4"
|
||||
[ $extra -eq 4 ] && extra=0
|
||||
let input_len="($pw_length + $extra) / 4 * 3"
|
||||
# change forward slash to comma because forward slash causes problems
|
||||
# when used in regular expressions (for instance sed) or curl using
|
||||
# basic auth supplied in the url (https://user:pass@host)
|
||||
dd if=/dev/urandom bs=1 count=$input_len 2>/dev/null | base64 --wrap=0 | awk '{ gsub("/", ",", $0); print $0}'
|
||||
}
|
||||
|
||||
|
||||
884
setup/ldap.sh
Executable file
884
setup/ldap.sh
Executable file
@@ -0,0 +1,884 @@
|
||||
#!/bin/bash
|
||||
# -*- indent-tabs-mode: t; tab-width: 4; -*-
|
||||
|
||||
#
|
||||
# LDAP server (slapd) for user authentication and directory services
|
||||
#
|
||||
source setup/functions.sh # load our functions
|
||||
source setup/functions-ldap.sh # load our ldap-specific functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
|
||||
ORGANIZATION="Mail-In-A-Box"
|
||||
LDAP_DOMAIN="mailinabox"
|
||||
LDAP_BASE="dc=mailinabox"
|
||||
LDAP_SERVICES_BASE="ou=Services,$LDAP_BASE"
|
||||
LDAP_CONFIG_BASE="ou=Config,$LDAP_BASE"
|
||||
LDAP_DOMAINS_BASE="ou=domains,$LDAP_CONFIG_BASE"
|
||||
LDAP_PERMITTED_SENDERS_BASE="ou=permitted-senders,$LDAP_CONFIG_BASE"
|
||||
LDAP_USERS_BASE="ou=Users,${LDAP_BASE}"
|
||||
LDAP_ALIASES_BASE="ou=aliases,${LDAP_USERS_BASE}"
|
||||
LDAP_ADMIN_DN="cn=admin,dc=mailinabox"
|
||||
|
||||
STORAGE_LDAP_ROOT="$STORAGE_ROOT/ldap"
|
||||
MIAB_SLAPD_DB_DIR="$STORAGE_LDAP_ROOT/db"
|
||||
MIAB_SLAPD_CONF="$STORAGE_LDAP_ROOT/slapd.d"
|
||||
MIAB_INTERNAL_CONF_FILE="$STORAGE_LDAP_ROOT/miab_ldap.conf"
|
||||
|
||||
SERVICE_ACCOUNTS=(LDAP_DOVECOT LDAP_POSTFIX LDAP_WEBMAIL LDAP_MANAGEMENT LDAP_NEXTCLOUD)
|
||||
|
||||
declare -i verbose=0
|
||||
|
||||
|
||||
#
|
||||
# Helper functions
|
||||
#
|
||||
die() {
|
||||
local msg="$1"
|
||||
local rtn="${2:-1}"
|
||||
[ ! -z "$msg" ] && echo "FATAL: $msg" || echo "An unrecoverable error occurred, exiting"
|
||||
exit ${rtn}
|
||||
}
|
||||
|
||||
say_debug() {
|
||||
[ $verbose -gt 1 ] && echo $@
|
||||
return 0
|
||||
}
|
||||
|
||||
say_verbose() {
|
||||
[ $verbose -gt 0 ] && echo $@
|
||||
return 0
|
||||
}
|
||||
|
||||
say() {
|
||||
echo $@
|
||||
}
|
||||
|
||||
ldap_debug_flag() {
|
||||
[ $verbose -gt 1 ] && echo "-d 1"
|
||||
}
|
||||
|
||||
wait_slapd_start() {
|
||||
# Wait for slapd to start...
|
||||
say_verbose -n "Waiting for slapd to start"
|
||||
local let elapsed=0
|
||||
until nc -z -w 4 127.0.0.1 389
|
||||
do
|
||||
[ $elapsed -gt 30 ] && die "Giving up waiting for slapd to start!"
|
||||
[ $elapsed -gt 0 ] && say_verbose -n "...${elapsed}"
|
||||
sleep 2
|
||||
let elapsed+=2
|
||||
done
|
||||
say_verbose "...ok"
|
||||
}
|
||||
|
||||
create_miab_conf() {
|
||||
# create (if non-existing) or load (existing) ldap/miab_ldap.conf
|
||||
if [ ! -e "$MIAB_INTERNAL_CONF_FILE" ]; then
|
||||
say_verbose "Generating a new $MIAB_INTERNAL_CONF_FILE"
|
||||
mkdir -p "$(dirname $MIAB_INTERNAL_CONF_FILE)"
|
||||
|
||||
# Use 64-character secret keys of safe characters
|
||||
cat > "$MIAB_INTERNAL_CONF_FILE" <<EOF
|
||||
LDAP_SERVER=127.0.0.1
|
||||
LDAP_SERVER_PORT=389
|
||||
LDAP_SERVER_STARTTLS=no
|
||||
LDAP_SERVER_TLS=no
|
||||
LDAP_URL=ldap://127.0.0.1/
|
||||
LDAP_BASE="${LDAP_BASE}"
|
||||
LDAP_SERVICES_BASE="${LDAP_SERVICES_BASE}"
|
||||
LDAP_CONFIG_BASE="${LDAP_CONFIG_BASE}"
|
||||
LDAP_DOMAINS_BASE="${LDAP_DOMAINS_BASE}"
|
||||
LDAP_PERMITTED_SENDERS_BASE="${LDAP_PERMITTED_SENDERS_BASE}"
|
||||
LDAP_USERS_BASE="${LDAP_USERS_BASE}"
|
||||
LDAP_ALIASES_BASE="${LDAP_ALIASES_BASE}"
|
||||
LDAP_ADMIN_DN="${LDAP_ADMIN_DN}"
|
||||
LDAP_ADMIN_PASSWORD="$(generate_password 64)"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# add service account credentials
|
||||
local prefix
|
||||
for prefix in ${SERVICE_ACCOUNTS[*]}
|
||||
do
|
||||
if [ $(grep -c "^$prefix" "$MIAB_INTERNAL_CONF_FILE") -eq 0 ]; then
|
||||
local cn=$(awk -F_ '{print tolower($2)}' <<< $prefix)
|
||||
cat >>"$MIAB_INTERNAL_CONF_FILE" <<EOF
|
||||
${prefix}_DN="cn=$cn,$LDAP_SERVICES_BASE"
|
||||
${prefix}_PASSWORD="$(generate_password 64)"
|
||||
EOF
|
||||
fi
|
||||
done
|
||||
|
||||
chmod 0640 "$MIAB_INTERNAL_CONF_FILE"
|
||||
. "$MIAB_INTERNAL_CONF_FILE"
|
||||
}
|
||||
|
||||
|
||||
create_directory_containers() {
|
||||
# create organizationUnit containers
|
||||
local basedn
|
||||
for basedn in "$LDAP_SERVICES_BASE" "$LDAP_CONFIG_BASE" "$LDAP_DOMAINS_BASE" "$LDAP_PERMITTED_SENDERS_BASE" "$LDAP_USERS_BASE" "$LDAP_ALIASES_BASE"; do
|
||||
# add ou container
|
||||
get_attribute "$basedn" "objectClass=*" "ou" base
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
say_verbose "Adding $basedn"
|
||||
ldapadd -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null <<EOF
|
||||
dn: $basedn
|
||||
objectClass: organizationalUnit
|
||||
ou: $(awk -F'[=,]' '{print $2}' <<< $basedn)
|
||||
EOF
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
create_service_accounts() {
|
||||
# create service accounts. service accounts have special access
|
||||
# rights, generally read-only to users, aliases, and configuration
|
||||
# subtrees (see apply_access_control)
|
||||
|
||||
local prefix dn pass
|
||||
for prefix in ${SERVICE_ACCOUNTS[*]}
|
||||
do
|
||||
eval "dn=\$${prefix}_DN"
|
||||
eval "pass=\$${prefix}_PASSWORD"
|
||||
get_attribute "$dn" "objectClass=*" "cn" base
|
||||
say_debug "SERVICE_ACCOUNT $dn"
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
local cn=$(awk -F'[=,]' '{print $2}' <<< $dn)
|
||||
say_verbose "Adding service account: $dn"
|
||||
ldapadd -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null <<EOF
|
||||
dn: $dn
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: organizationalRole
|
||||
cn: $cn
|
||||
description: ${cn} service account
|
||||
userPassword: $(slappasswd_hash "$pass")
|
||||
EOF
|
||||
fi
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
|
||||
install_system_packages() {
|
||||
# install required deb packages, generate admin credentials
|
||||
# and apply them to the installation
|
||||
create_miab_conf
|
||||
|
||||
# Set installation defaults to avoid interactive dialogs. See
|
||||
# /var/lib/dpkg/info/slapd.templates for a list of what can be set
|
||||
debconf-set-selections <<EOF
|
||||
slapd shared/organization string ${ORGANIZATION}
|
||||
slapd slapd/domain string ${LDAP_DOMAIN}
|
||||
slapd slapd/password1 password ${LDAP_ADMIN_PASSWORD}
|
||||
slapd slapd/password2 password ${LDAP_ADMIN_PASSWORD}
|
||||
EOF
|
||||
|
||||
# Install packages
|
||||
say "Installing OpenLDAP server..."
|
||||
apt_install slapd ldap-utils python3-ldap3 python3-ldif3 ca-certificates xz-utils
|
||||
|
||||
# If slapd was not installed by us, the selections above did
|
||||
# nothing. To check this we see if SLAPD_CONF in
|
||||
# /etc/default/slapd is empty and that the olc does not have our
|
||||
# database. We could do 2 things in this situation:
|
||||
# 1. ask the user for the current admin password and add our domain
|
||||
# 2. reconfigure and wipe out the current database
|
||||
# we do #2 ....
|
||||
local SLAPD_CONF=""
|
||||
eval "$(grep ^SLAPD_CONF= /etc/default/slapd)"
|
||||
local cursuffix="$(slapcat -s "cn=config" | grep "^olcSuffix: ")"
|
||||
if [ -z "$SLAPD_CONF" ] &&
|
||||
! grep "$LDAP_DOMAIN" <<<"$cursuffix" >/dev/null
|
||||
then
|
||||
mkdir -p /var/backup
|
||||
local tgz="/var/backup/slapd-$(date +%Y%m%d-%H%M%S).tgz"
|
||||
(cd /var/lib/ldap; tar czf "$tgz" .)
|
||||
chmod 600 "$tgz"
|
||||
rm /var/lib/ldap/*
|
||||
say "Reininstalling slapd! - existing database saved in $tgz"
|
||||
dpkg-reconfigure --frontend=noninteractive slapd
|
||||
fi
|
||||
|
||||
# Clear passwords out of debconf
|
||||
debconf-set-selections <<EOF
|
||||
slapd slapd/password1 password
|
||||
slapd slapd/password2 password
|
||||
EOF
|
||||
|
||||
# Ensure slapd is running
|
||||
systemctl start slapd && wait_slapd_start
|
||||
|
||||
# Change the admin password hash format in the server from slapd's
|
||||
# default {SSHA} to SHA-512 {CRYPT} with 16 characters of salt
|
||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "olcRootPW"
|
||||
if [ ${#ATTR_VALUE[*]} -eq 1 -a $(grep -c "{SSHA}" <<< "$ATTR_VALUE") -eq 1 ]; then
|
||||
say_verbose "Updating root hash to SHA512-CRYPT"
|
||||
local hash=$(slappasswd_hash "$LDAP_ADMIN_PASSWORD")
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: $ATTR_DN
|
||||
replace: olcRootPW
|
||||
olcRootPW: $hash
|
||||
EOF
|
||||
say_verbose "Updating admin hash to SHA512-CRYPT"
|
||||
ldapmodify -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null <<EOF
|
||||
dn: $LDAP_ADMIN_DN
|
||||
replace: userPassword
|
||||
userPassword: $hash
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
relocate_slapd_data() {
|
||||
#
|
||||
# Move current ldap databases to user-data (eg. new install). A
|
||||
# new slapd installation places the ldap configuration database in
|
||||
# /etc/ldap/slapd.d and schema database in /var/lib/ldap. So that
|
||||
# backups include the ldap database, move everything to user-data.
|
||||
#
|
||||
# On entry:
|
||||
# SLAPD_CONF must point to the current slapd.d directory
|
||||
# (see /etc/default/slapd)
|
||||
# Global variables as defined above must be set
|
||||
# The slapd service must be running
|
||||
#
|
||||
# On success:
|
||||
# Config and data will be relocated to the new locations
|
||||
#
|
||||
say_verbose "Relocate ldap databases from current locations to user-data"
|
||||
|
||||
# Get the current database location from olc
|
||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "olcDbDirectory"
|
||||
local DN="$ATTR_DN"
|
||||
local DB_DIR="$ATTR_VALUE"
|
||||
if [ -z "$DN" ]; then
|
||||
say_verbose ""
|
||||
say_verbose "ACK! ${LDAP_BASE} does not exist in the LDAP server!!!"
|
||||
say_verbose "Something is amiss!!!!!"
|
||||
say_verbose "... to ensure no data is lost, please manually fix the problem"
|
||||
say_verbose " by running 'sudo dpkg-reconfigure slapd'"
|
||||
say_verbose ""
|
||||
say_verbose "CAUTION: running dbpg-reconfigure will remove ALL data"
|
||||
say_verbose "for the existing domain!"
|
||||
say_verbose ""
|
||||
die "Unable to continue!"
|
||||
fi
|
||||
|
||||
# Exit if destination directories are non-empty
|
||||
[ ! -z "$(ls -A $MIAB_SLAPD_CONF)" ] && die "Cannot relocate system LDAP because $MIAB_SLAPD_CONF is not empty!"
|
||||
[ ! -z "$(ls -A $MIAB_SLAPD_DB_DIR)" ] && die "Cannot relocate system LDAP because $MIAB_SLAPD_DB_DIR is not empty!"
|
||||
|
||||
# Stop slapd
|
||||
say_verbose ""
|
||||
say_verbose "Relocating ldap databases:"
|
||||
say_verbose " from: "
|
||||
say_verbose " CONF='${SLAPD_CONF}'"
|
||||
say_verbose " DB='${DB_DIR}'"
|
||||
say_verbose " to:"
|
||||
say_verbose " CONF=${MIAB_SLAPD_CONF}"
|
||||
say_verbose " DB=${MIAB_SLAPD_DB_DIR}"
|
||||
say_verbose ""
|
||||
say_verbose "Stopping slapd"
|
||||
systemctl stop slapd || die "Could not stop slapd"
|
||||
|
||||
# Modify the path to dc=mailinabox's database directory
|
||||
say_verbose "Dump config database"
|
||||
local TMP="/tmp/miab_relocate_ldap.ldif"
|
||||
slapcat -F "${SLAPD_CONF}" -l "$TMP" -n 0 || die "slapcat failed"
|
||||
awk -e "/olcDbDirectory:/ {print \$1 \"$MIAB_SLAPD_DB_DIR\"} !/^olcDbDirectory:/ { print \$0}" $TMP > $TMP.2
|
||||
rm -f "$TMP"
|
||||
|
||||
# Copy the existing database files
|
||||
say_verbose "Copy database files ($DB_DIR => $MIAB_SLAPD_DB_DIR)"
|
||||
cp -p "${DB_DIR}"/* "${MIAB_SLAPD_DB_DIR}" || die "Could not copy files '${DB_DIR}/*' to '${MIAB_SLAPD_DB_DIR}'"
|
||||
|
||||
# Re-create the config
|
||||
say_verbose "Create new slapd config"
|
||||
local xargs=()
|
||||
[ $verbose -gt 0 ] && xargs+=(-d 10 -v)
|
||||
slapadd -F "${MIAB_SLAPD_CONF}" ${xargs[@]} -n 0 -l "$TMP.2" 2>/dev/null || die "slapadd failed!"
|
||||
chown -R openldap:openldap "${MIAB_SLAPD_CONF}"
|
||||
rm -f "$TMP.2"
|
||||
|
||||
# Remove the old database files
|
||||
rm -f "${DB_DIR}/*"
|
||||
}
|
||||
|
||||
|
||||
schema_to_ldif() {
|
||||
# Convert a .schema file to ldif. This function follows the
|
||||
# conversion instructions found in /etc/ldap/schema/openldap.ldif
|
||||
local schema="$1" # path or url to schema
|
||||
local ldif="$2" # destination file - will be overwritten
|
||||
local cn="$3" # schema common name, eg "postfix"
|
||||
local cat='cat'
|
||||
if [ ! -e "$schema" ]; then
|
||||
if [ -e "conf/$(basename $schema)" ]; then
|
||||
schema="conf/$(basename $schema)"
|
||||
else
|
||||
cat="curl -s"
|
||||
fi
|
||||
fi
|
||||
|
||||
cat >"$ldif" <<EOF
|
||||
dn: cn=$cn,cn=schema,cn=config
|
||||
objectClass: olcSchemaConfig
|
||||
cn: $cn
|
||||
EOF
|
||||
|
||||
$cat "$schema" \
|
||||
| sed s/attributeType/olcAttributeTypes:/ig \
|
||||
| sed s/objectClass/olcObjectClasses:/ig \
|
||||
| sed s/objectIdentifier/olcObjectIdentifier:/ig \
|
||||
| sed 's/\t/ /g' \
|
||||
| sed 's/^\s*$/#/g' >> "$ldif"
|
||||
}
|
||||
|
||||
|
||||
add_schemas() {
|
||||
# Add necessary schema's for MiaB operaion
|
||||
#
|
||||
# First, apply rfc822MailMember from OpenLDAP's "misc"
|
||||
# schema. Don't apply the whole schema file because much is from
|
||||
# expired RFC's, and we just need rfc822MailMember
|
||||
local cn="misc"
|
||||
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
say_verbose "Adding '$cn' schema"
|
||||
cat "/etc/ldap/schema/misc.ldif" | awk 'BEGIN {C=0}
|
||||
/^(dn|objectClass|cn):/ { print $0; next }
|
||||
/^olcAttributeTypes:/ && /27\.2\.1\.15/ { print $0; C=1; next }
|
||||
/^(olcAttributeTypes|olcObjectClasses):/ { C=0; next }
|
||||
/^ / && C==1 { print $0 }' | ldapadd -Q -Y EXTERNAL -H ldapi:/// >/dev/null
|
||||
fi
|
||||
|
||||
# Next, apply the postfix schema from the ldapadmin project
|
||||
# (GPL)(*).
|
||||
# see: http://ldapadmin.org
|
||||
# http://ldapadmin.org/docs/postfix.schema
|
||||
# http://www.postfix.org/LDAP_README.html
|
||||
# (*) mailGroup modified to include rfc822MailMember
|
||||
local schema="http://ldapadmin.org/docs/postfix.schema"
|
||||
local cn="postfix"
|
||||
get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
local ldif="/tmp/$cn.$$.ldif"
|
||||
schema_to_ldif "$schema" "$ldif" "$cn"
|
||||
sed -i 's/\$ member \$/$ member $ rfc822MailMember $/' "$ldif"
|
||||
say_verbose "Adding '$cn' schema"
|
||||
[ $verbose -gt 1 ] && cat "$ldif"
|
||||
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
|
||||
rm -f "$ldif"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
modify_global_config() {
|
||||
#
|
||||
# Set ldap configuration attributes:
|
||||
# IdleTimeout: seconds to wait before forcibly closing idle connections
|
||||
# LogLevel: logging levels - see OpenLDAP docs
|
||||
# TLS configuration
|
||||
# Disable anonymous binds
|
||||
#
|
||||
say_verbose "Setting global ldap configuration"
|
||||
|
||||
# TLS requirements:
|
||||
#
|
||||
# The 'openldap' user must have read access to the TLS private key
|
||||
# and certificate (file system permissions and apparmor). If
|
||||
# access is not configured properly, slapd retuns error code 80
|
||||
# and won't apply the TLS configuration, or won't start.
|
||||
#
|
||||
# Openldap TLS will not operate with a self-signed server
|
||||
# certificate! The server will always log "unable to get TLS
|
||||
# client DN, error=49." Ensure the certificate is signed by
|
||||
# a certification authority.
|
||||
#
|
||||
# The list of trusted CA certificates must include the CA that
|
||||
# signed the server's certificate!
|
||||
#
|
||||
# For the olcCiperSuite setting, see:
|
||||
# https://www.gnutls.org/manual/gnutls.html#Priority-Strings
|
||||
#
|
||||
|
||||
ldapmodify $(ldap_debug_flag) -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: cn=config
|
||||
##
|
||||
## timeouts (1800=30 minutes) and logging
|
||||
##
|
||||
replace: olcIdleTimeout
|
||||
olcIdleTimeout: 1800
|
||||
-
|
||||
replace: olcLogLevel
|
||||
olcLogLevel: config stats shell
|
||||
#olcLogLevel: config stats shell filter ACL
|
||||
-
|
||||
##
|
||||
## TLS
|
||||
##
|
||||
replace: olcTLSCACertificateFile
|
||||
olcTLSCACertificateFile: /etc/ssl/certs/ca-certificates.crt
|
||||
-
|
||||
replace: olcTLSCertificateFile
|
||||
olcTLSCertificateFile: $STORAGE_ROOT/ssl/ssl_certificate.pem
|
||||
-
|
||||
replace: olcTLSCertificateKeyFile
|
||||
olcTLSCertificateKeyFile: $STORAGE_ROOT/ssl/ssl_private_key.pem
|
||||
-
|
||||
replace: olcTLSDHParamFile
|
||||
olcTLSDHParamFile: $STORAGE_ROOT/ssl/dh2048.pem
|
||||
-
|
||||
replace: olcTLSCipherSuite
|
||||
olcTLSCipherSuite: PFS
|
||||
-
|
||||
replace: olcTLSVerifyClient
|
||||
olcTLSVerifyClient: never
|
||||
-
|
||||
##
|
||||
## Password policies - use SHA512 with 16 characters of salt
|
||||
##
|
||||
replace: olcPasswordHash
|
||||
olcPasswordHash: {CRYPT}
|
||||
-
|
||||
replace: olcPasswordCryptSaltFormat
|
||||
olcPasswordCryptSaltFormat: \$6\$%.16s
|
||||
-
|
||||
##
|
||||
## Disable anonymous binds
|
||||
##
|
||||
replace: olcDisallows
|
||||
olcDisallows: bind_anon
|
||||
-
|
||||
replace: olcRequires
|
||||
olcRequires: authc
|
||||
|
||||
dn: olcDatabase={-1}frontend,cn=config
|
||||
replace: olcRequires
|
||||
olcRequires: authc
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
add_overlays() {
|
||||
# Apply slapd overlays - apply the commonly used member-of overlay
|
||||
# now because adding it later is harder.
|
||||
|
||||
# Get the config dn for the database
|
||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
||||
[ -z "$ATTR_DN" ] &&
|
||||
die "No config found for olcSuffix=$LDAP_BASE in cn=config!"
|
||||
local cdn="$ATTR_DN"
|
||||
|
||||
# Add member-of overlay (man 5 slapo-memberof)
|
||||
get_attribute "cn=module{0},cn=config" "(olcModuleLoad=memberof.la)" "dn" base
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
say_verbose "Adding memberof overlay module"
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: cn=module{0},cn=config
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: memberof.la
|
||||
EOF
|
||||
fi
|
||||
|
||||
get_attribute "$cdn" "(olcOverlay=memberof)" "olcOverlay"
|
||||
if [ -z "$ATTR_DN" ]; then
|
||||
say_verbose "Adding memberof overlay to $LDAP_BASE"
|
||||
ldapadd -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: olcOverlay=memberof,$cdn
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: olcMemberOf
|
||||
olcOverlay: memberof
|
||||
#olcMemberOfGroupOC: mailGroup
|
||||
olcMemberOfRefint: TRUE
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
add_indexes() {
|
||||
# Index mail-related attributes
|
||||
|
||||
# Get the config dn for the database
|
||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
||||
[ -z "$ATTR_DN" ] &&
|
||||
die "No config found for olcSuffix=$LDAP_BASE in cn=config!"
|
||||
local cdn="$ATTR_DN"
|
||||
|
||||
# Add the indexes
|
||||
get_attribute "$cdn" "(objectClass=*)" "olcDbIndex" base
|
||||
local attr
|
||||
for attr in mail maildrop mailaccess dc rfc822MailMember; do
|
||||
local type="eq" atype="" aindex=""
|
||||
[ "$attr" == "mail" ] && type="eq,sub"
|
||||
|
||||
# find the existing index for the attribute
|
||||
local item
|
||||
for item in "${ATTR_VALUE[@]}"; do
|
||||
local split=($item) # eg "mail eq"
|
||||
if [ "${split[0]}" == "$attr" ]; then
|
||||
aindex="$item"
|
||||
atype="${split[1]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# if desired index type (eg "eq") is equal to actual type,
|
||||
# continue, no change
|
||||
[ "$type" == "$atype" ] && continue
|
||||
|
||||
# replace it or add a new index if not present
|
||||
if [ ! -z "$atype" ]; then
|
||||
say_verbose "Replace index $attr ($atype -> $type)"
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: $cdn
|
||||
delete: olcDbIndex
|
||||
olcDbIndex: $aindex
|
||||
-
|
||||
add: olcDbIndex
|
||||
olcDbIndex: $attr $type
|
||||
EOF
|
||||
else
|
||||
say_verbose "Add index for attribute $attr ($type)"
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: $cdn
|
||||
add: olcDbIndex
|
||||
olcDbIndex: $attr $type
|
||||
EOF
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
apply_access_control() {
|
||||
# Apply access control to the mail-in-a-box databse.
|
||||
#
|
||||
# Permission restrictions:
|
||||
# service accounts (except management):
|
||||
# can bind but not change passwords, including their own
|
||||
# can read all attributes of all users but not userPassword
|
||||
# can read config subtree (permitted-senders, domains)
|
||||
# no access to services subtree, except their own dn
|
||||
# management service account:
|
||||
# can read and change password and shadowLastChange
|
||||
# all other service account permissions are the same
|
||||
# users:
|
||||
# can bind and change their own password
|
||||
# can read and change their own shadowLastChange
|
||||
# can read attributess of all users except mailaccess
|
||||
# no access to config subtree
|
||||
# no access to services subtree
|
||||
#
|
||||
|
||||
# Get the config dn for the database
|
||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
||||
[ -z "$ATTR_DN" ] &&
|
||||
die "No config found for olcSuffix=$LDAP_BASE in cn=config!"
|
||||
local cdn="$ATTR_DN"
|
||||
|
||||
say_verbose "Setting database permissions"
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
|
||||
dn: $cdn
|
||||
replace: olcAccess
|
||||
olcAccess: to attrs=userPassword
|
||||
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
|
||||
by dn.subtree="${LDAP_SERVICES_BASE}" none
|
||||
by self =wx
|
||||
by anonymous auth
|
||||
by * none
|
||||
olcAccess: to attrs=shadowLastChange
|
||||
by self write
|
||||
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
|
||||
by dn.subtree="${LDAP_SERVICES_BASE}" read
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
|
||||
by * none
|
||||
olcAccess: to attrs=mailaccess
|
||||
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
|
||||
by dn.subtree="${LDAP_SERVICES_BASE}" read
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
|
||||
by * none
|
||||
olcAccess: to dn.subtree="${LDAP_CONFIG_BASE}"
|
||||
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
|
||||
by dn.subtree="${LDAP_SERVICES_BASE}" read
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
|
||||
by * none
|
||||
olcAccess: to dn.subtree="${LDAP_SERVICES_BASE}"
|
||||
by self read
|
||||
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
|
||||
by * none
|
||||
olcAccess: to dn.subtree="${LDAP_USERS_BASE}"
|
||||
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
|
||||
by * read
|
||||
olcAccess: to *
|
||||
by * read
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
|
||||
update_apparmor() {
|
||||
# Update slapd's access rights under AppArmor so that it has
|
||||
# access to database files in the user-data location
|
||||
cat > /etc/apparmor.d/local/usr.sbin.slapd <<EOF
|
||||
# database directories
|
||||
$MIAB_SLAPD_CONF/** rw,
|
||||
$MIAB_SLAPD_DB_DIR/ r,
|
||||
$MIAB_SLAPD_DB_DIR/** rwk,
|
||||
$MIAB_SLAPD_DB_DIR/alock kw,
|
||||
|
||||
# certificates and keys
|
||||
$STORAGE_ROOT/ssl/* r,
|
||||
EOF
|
||||
chmod 0644 /etc/apparmor.d/local/usr.sbin.slapd
|
||||
|
||||
# Load settings into the kernel only if AppArmor is enabled
|
||||
if aa-status --enabled; then
|
||||
apparmor_parser -r /etc/apparmor.d/usr.sbin.slapd
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Process command line arguments -- these are here for debugging and
|
||||
# testing purposes
|
||||
#
|
||||
process_cmdline() {
|
||||
[ -e "$MIAB_INTERNAL_CONF_FILE" ] && . "$MIAB_INTERNAL_CONF_FILE"
|
||||
|
||||
if [ "$1" == "-d" ]; then
|
||||
# Start slapd in interactive/debug mode
|
||||
echo "!! SERVER DEBUG MODE !!"
|
||||
echo "Stopping slapd"
|
||||
systemctl stop slapd
|
||||
. /etc/default/slapd
|
||||
echo "Listening on $SLAPD_SERVICES..."
|
||||
/usr/sbin/slapd -h "$SLAPD_SERVICES" -g openldap -u openldap -F $MIAB_SLAPD_CONF -d ${2:-1}
|
||||
exit 0
|
||||
|
||||
elif [ "$1" == "-config" ]; then
|
||||
# Apply a certain configuration
|
||||
if [ "$2" == "server" ]; then
|
||||
modify_global_config
|
||||
add_overlays
|
||||
add_indexes
|
||||
apply_access_control
|
||||
elif [ "$2" == "apparmor" ]; then
|
||||
update_apparmor
|
||||
else
|
||||
echo "Invalid: '$2'. Only 'server' and 'apparmor' supported"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
|
||||
elif [ "$1" == "-search" ]; then
|
||||
# search for email addresses, distinguished names and general
|
||||
# ldap filters
|
||||
debug_search "$2"
|
||||
exit 0
|
||||
|
||||
elif [ "$1" == "-dumpdb" ]; then
|
||||
# Dump (to stdout) select ldap data and configuration
|
||||
local s=${2:-all}
|
||||
local hide_attrs="(structuralObjectClass|entryUUID|creatorsName|createTimestamp|entryCSN|modifiersName|modifyTimestamp)"
|
||||
local slapcat_args=(-F "$MIAB_SLAPD_CONF" -o ldif-wrap=no)
|
||||
[ $verbose -gt 0 ] && hide_attrs="(_____NEVERMATCHES)"
|
||||
|
||||
if [ "$s" == "all" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
slapcat ${slapcat_args[@]} -s "$LDAP_BASE" | grep -Ev "^$hide_attrs:"
|
||||
fi
|
||||
if [ "$s" == "all" -o "$s" == "config" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
cat "$MIAB_SLAPD_CONF/cn=config.ldif" | grep -Ev "^$hide_attrs:"
|
||||
get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
|
||||
echo ""
|
||||
slapcat ${slapcat_args[@]} -s "$ATTR_DN" | grep -Ev "^$hide_attrs:"
|
||||
fi
|
||||
if [ "$s" == "all" -o "$s" == "frontend" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
cat "$MIAB_SLAPD_CONF/cn=config/olcDatabase={-1}frontend.ldif" | grep -Ev "^$hide_attrs:"
|
||||
fi
|
||||
if [ "$s" == "all" -o "$s" == "module" ]; then
|
||||
echo ""
|
||||
cat "$MIAB_SLAPD_CONF/cn=config/cn=module{0}.ldif" | grep -Ev "^$hide_attrs:"
|
||||
fi
|
||||
if [ "$s" == "users" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
debug_search "(objectClass=mailUser)" "$LDAP_USERS_BASE"
|
||||
fi
|
||||
if [ "$s" == "aliases" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
local attrs=(mail member mailRoutingAddress rfc822MailMember)
|
||||
[ $verbose -gt 0 ] && attrs=()
|
||||
debug_search "(objectClass=mailGroup)" "$LDAP_ALIASES_BASE" ${attrs[@]}
|
||||
fi
|
||||
if [ "$s" == "permitted-senders" -o "$s" == "ps" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
local attrs=(mail member mailRoutingAddress rfc822MailMember)
|
||||
[ $verbose -gt 0 ] && attrs=()
|
||||
debug_search "(objectClass=mailGroup)" "$LDAP_PERMITTED_SENDERS_BASE" ${attrs[@]}
|
||||
fi
|
||||
if [ "$s" == "domains" ]; then
|
||||
echo ""
|
||||
echo '--------------------------------'
|
||||
debug_search "(objectClass=domain)" "$LDAP_DOMAINS_BASE"
|
||||
fi
|
||||
exit 0
|
||||
|
||||
elif [ "$1" == "-reset" ]; then
|
||||
#
|
||||
# Delete and remove OpenLDAP
|
||||
#
|
||||
echo ""
|
||||
echo "!!!!! WARNING! !!!!!"
|
||||
echo "!!!!! OPENLDAP WILL BE REMOVED !!!!!"
|
||||
echo "!!!!! ALL LDAP DATA WILL BE DESTROYED !!!!!"
|
||||
echo ""
|
||||
echo -n "Type 'YES' to continue: "
|
||||
read ans
|
||||
if [ "$ans" != "YES" ]; then
|
||||
echo "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
if [ -x /usr/sbin/slapd ]; then
|
||||
apt-get remove --purge -y slapd
|
||||
apt-get -y autoremove
|
||||
apt-get autoclean
|
||||
fi
|
||||
rm -rf "$STORAGE_LDAP_ROOT"
|
||||
rm -rf "/etc/ldap/slapd.d"
|
||||
rm -rf "/var/lib/ldap"
|
||||
rm -f "/etc/default/slapd"
|
||||
echo "Done"
|
||||
exit 0
|
||||
|
||||
elif [ ! -z "$1" ]; then
|
||||
echo "Invalid command line argument '$1'"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
if [ "$1" == "-verbose" -o "$1" == "-v" ]; then
|
||||
let verbose+=1
|
||||
shift
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[ $# -gt 0 ] && process_cmdline $@
|
||||
|
||||
|
||||
|
||||
####
|
||||
#### MAIN SCRIPT CODE STARTS HERE...
|
||||
####
|
||||
|
||||
# Run apt installs
|
||||
install_system_packages
|
||||
|
||||
# Update the ldap schema
|
||||
add_schemas
|
||||
|
||||
#
|
||||
# Create user-data/ldap directory structure:
|
||||
# db/ - holds slapd database for "dc=mailinabox"
|
||||
# slapd.d/ - holds slapd configuration
|
||||
# miab_ldap.conf - holds values for other subsystems like postfix, management, etc
|
||||
#
|
||||
for d in "$STORAGE_LDAP_ROOT" "$MIAB_SLAPD_DB_DIR" "$MIAB_SLAPD_CONF"; do
|
||||
mkdir -p "$d"
|
||||
chown openldap:openldap "$d"
|
||||
chmod 755 "$d"
|
||||
done
|
||||
|
||||
# Ensure openldap can access the tls/ssl private key file
|
||||
usermod -a -G ssl-cert openldap
|
||||
|
||||
# Ensure slapd can interact with the mailinabox database and config
|
||||
update_apparmor
|
||||
|
||||
# Load slapd's init script startup options
|
||||
. /etc/default/slapd
|
||||
if [ -z "$SLAPD_CONF" ]; then
|
||||
# when not defined, slapd uses its compiled-in default directory
|
||||
SLAPD_CONF="/etc/ldap/slapd.d"
|
||||
fi
|
||||
|
||||
|
||||
# Relocate slapd databases to user-data, which is needed after a new
|
||||
# installation, we're restoring from backup, or STORAGE_ROOT changes
|
||||
if [ "$SLAPD_CONF" != "$MIAB_SLAPD_CONF" ]; then
|
||||
if [ -z "$(ls -A $MIAB_SLAPD_CONF)" ]; then
|
||||
# Empty destination - relocate databases
|
||||
relocate_slapd_data
|
||||
else
|
||||
# Non-empty destination - use the backup data as-is
|
||||
systemctl stop slapd
|
||||
fi
|
||||
# Tell the system startup script to use our config database
|
||||
tools/editconf.py /etc/default/slapd \
|
||||
"SLAPD_CONF=$MIAB_SLAPD_CONF"
|
||||
systemctl start slapd || die "slapd woudn't start! try running $0 -d"
|
||||
wait_slapd_start
|
||||
fi
|
||||
|
||||
|
||||
# Configure syslog
|
||||
mkdir -p /var/log/ldap
|
||||
chmod 750 /var/log/ldap
|
||||
chown syslog:adm /var/log/ldap
|
||||
cp conf/slapd-logging.conf /etc/rsyslog.d/20-slapd.conf
|
||||
chmod 644 /etc/rsyslog.d/20-slapd.conf
|
||||
restart_service syslog
|
||||
|
||||
# Add log rotation
|
||||
cat > /etc/logrotate.d/slapd <<EOF;
|
||||
/var/log/ldap/slapd.log {
|
||||
weekly
|
||||
missingok
|
||||
rotate 52
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
}
|
||||
EOF
|
||||
|
||||
# Modify olc server config like TLS
|
||||
modify_global_config
|
||||
|
||||
# Add overlays and ensure mail-related attributes are indexed
|
||||
add_overlays
|
||||
add_indexes
|
||||
|
||||
# Lock down access
|
||||
apply_access_control
|
||||
|
||||
# Create general db structure
|
||||
create_directory_containers
|
||||
|
||||
# Create service accounts for dovecot, postfix, roundcube, etc
|
||||
create_service_accounts
|
||||
|
||||
# Update where slapd listens for incoming requests
|
||||
tools/editconf.py /etc/default/slapd \
|
||||
"SLAPD_SERVICES=\"ldap://127.0.0.1:389/ ldaps:/// ldapi:///\""
|
||||
|
||||
# Restart slapd
|
||||
restart_service slapd
|
||||
|
||||
# Dump the database daily, before backups run at 3
|
||||
# This is not required, but nice to have just in case.
|
||||
cat > /etc/cron.d/mailinabox-ldap << EOF
|
||||
# Mail-in-a-Box
|
||||
# Dump database to ldif
|
||||
30 2 * * * root /usr/sbin/slapcat -F "$MIAB_SLAPD_CONF" -o ldif-wrap=no -s "$LDAP_BASE" | /usr/bin/xz > "$STORAGE_LDAP_ROOT/db.ldif.xz"; chmod 600 "$STORAGE_LDAP_ROOT/db.ldif.xz"
|
||||
EOF
|
||||
@@ -26,7 +26,7 @@ source /etc/mailinabox.conf # load global vars
|
||||
echo "Installing Dovecot (IMAP server)..."
|
||||
apt_install \
|
||||
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \
|
||||
dovecot-sieve dovecot-managesieved
|
||||
dovecot-sieve dovecot-managesieved dovecot-ldap
|
||||
|
||||
# The `dovecot-imapd`, `dovecot-pop3d`, and `dovecot-lmtpd` packages automatically
|
||||
# enable IMAP, POP and LMTP protocols.
|
||||
@@ -84,6 +84,8 @@ tools/editconf.py /etc/dovecot/conf.d/10-ssl.conf \
|
||||
ssl=required \
|
||||
"ssl_cert=<$STORAGE_ROOT/ssl/ssl_certificate.pem" \
|
||||
"ssl_key=<$STORAGE_ROOT/ssl/ssl_private_key.pem" \
|
||||
"ssl_protocols=!SSLv3" \
|
||||
"ssl_prefer_server_ciphers = yes" \
|
||||
"ssl_protocols=TLSv1.2" \
|
||||
"ssl_cipher_list=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384" \
|
||||
"ssl_prefer_server_ciphers=no" \
|
||||
|
||||
@@ -42,7 +42,7 @@ source /etc/mailinabox.conf # load global vars
|
||||
# * `ca-certificates`: A trust store used to squelch postfix warnings about
|
||||
# untrusted opportunistically-encrypted connections.
|
||||
echo "Installing Postfix (SMTP server)..."
|
||||
apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates
|
||||
apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates postfix-ldap postfix-policyd-spf-python
|
||||
|
||||
# ### Basic Settings
|
||||
|
||||
@@ -53,6 +53,7 @@ apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates
|
||||
# * Set our name (the Debian default seems to be "localhost" but make it our hostname).
|
||||
# * Set the name of the local machine to localhost, which means xxx@localhost is delivered locally, although we don't use it.
|
||||
# * Set the SMTP banner (which must have the hostname first, then anything).
|
||||
# * Extend the SPF time limit to avoid timeouts chasing SPF records
|
||||
tools/editconf.py /etc/postfix/main.cf \
|
||||
inet_interfaces=all \
|
||||
smtp_bind_address=$PRIVATE_IP \
|
||||
@@ -67,7 +68,8 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||
tools/editconf.py /etc/postfix/main.cf \
|
||||
delay_warning_time=3h \
|
||||
maximal_queue_lifetime=2d \
|
||||
bounce_queue_lifetime=1d
|
||||
bounce_queue_lifetime=1d \
|
||||
policy-spf_time_limit=3600
|
||||
|
||||
# ### Outgoing Mail
|
||||
|
||||
@@ -97,6 +99,16 @@ tools/editconf.py /etc/postfix/master.cf -s -w \
|
||||
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters
|
||||
-o nested_header_checks="
|
||||
|
||||
# enable the SPF service
|
||||
tools/editconf.py /etc/postfix/master.cf -s -w \
|
||||
"policy-spf=unix y n n - 0 spawn user=policyd-spf argv=/usr/bin/policyd-spf"
|
||||
|
||||
# configure policyd-spf configuration
|
||||
# * reject SPF softfail (eg ~all) for some domains that are configured
|
||||
# not to reject
|
||||
tools/editconf.py /etc/postfix-policyd-spf-python/policyd-spf.conf \
|
||||
"Reject_Not_Pass_Domains=gmail.com,google.com"
|
||||
|
||||
# Install the `outgoing_mail_header_filters` file required by the new 'authclean' service.
|
||||
cp conf/postfix_outgoing_mail_header_filters /etc/postfix/outgoing_mail_header_filters
|
||||
|
||||
@@ -216,7 +228,7 @@ tools/editconf.py /etc/postfix/main.cf lmtp_destination_recipient_limit=1
|
||||
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC
|
||||
tools/editconf.py /etc/postfix/main.cf \
|
||||
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org" \
|
||||
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023"
|
||||
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service unix:private/policy-spf","check_policy_service inet:127.0.0.1:10023"
|
||||
|
||||
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
|
||||
# Postgrey listens on the same interface (and not IPv6, for instance).
|
||||
|
||||
@@ -5,54 +5,86 @@
|
||||
#
|
||||
# This script configures user authentication for Dovecot
|
||||
# and Postfix (which relies on Dovecot) and destination
|
||||
# validation by quering an Sqlite3 database of mail users.
|
||||
# validation by quering a ldap database of mail users.
|
||||
|
||||
# LDAP helpful links:
|
||||
# http://www.postfix.org/LDAP_README.html
|
||||
# http://www.postfix.org/postconf.5.html
|
||||
# http://www.postfix.org/ldap_table.5.html
|
||||
#
|
||||
|
||||
source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
source ${STORAGE_ROOT}/ldap/miab_ldap.conf # user-data specific vars
|
||||
|
||||
# ### User and Alias Database
|
||||
|
||||
# The database of mail users (i.e. authenticated users, who have mailboxes)
|
||||
# and aliases (forwarders).
|
||||
|
||||
db_path=$STORAGE_ROOT/mail/users.sqlite
|
||||
|
||||
# Create an empty database if it doesn't yet exist.
|
||||
if [ ! -f $db_path ]; then
|
||||
echo Creating new user database: $db_path;
|
||||
echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path;
|
||||
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
|
||||
fi
|
||||
|
||||
# ### User Authentication
|
||||
|
||||
# Have Dovecot query our database, and not system users, for authentication.
|
||||
sed -i "s/#*\(\!include auth-system.conf.ext\)/#\1/" /etc/dovecot/conf.d/10-auth.conf
|
||||
sed -i "s/#\(\!include auth-sql.conf.ext\)/\1/" /etc/dovecot/conf.d/10-auth.conf
|
||||
sed -i "s/#*\(\!include auth-sql.conf.ext\)/#\1/" /etc/dovecot/conf.d/10-auth.conf
|
||||
sed -i "s/#\(\!include auth-ldap.conf.ext\)/\1/" /etc/dovecot/conf.d/10-auth.conf
|
||||
|
||||
|
||||
# Specify how the database is to be queried for user authentication (passdb)
|
||||
# and where user mailboxes are stored (userdb).
|
||||
cat > /etc/dovecot/conf.d/auth-sql.conf.ext << EOF;
|
||||
cat > /etc/dovecot/conf.d/auth-ldap.conf.ext << EOF;
|
||||
passdb {
|
||||
driver = sql
|
||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||
driver = ldap
|
||||
args = /etc/dovecot/dovecot-ldap.conf.ext
|
||||
}
|
||||
userdb {
|
||||
driver = sql
|
||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||
driver = ldap
|
||||
args = /etc/dovecot/dovecot-userdb-ldap.conf.ext
|
||||
default_fields = uid=mail gid=mail home=$STORAGE_ROOT/mail/mailboxes/%d/%n
|
||||
}
|
||||
EOF
|
||||
|
||||
# Configure the SQL to query for a user's metadata and password.
|
||||
cat > /etc/dovecot/dovecot-sql.conf.ext << EOF;
|
||||
driver = sqlite
|
||||
connect = $db_path
|
||||
default_pass_scheme = SHA512-CRYPT
|
||||
password_query = SELECT email as user, password FROM users WHERE email='%u';
|
||||
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users WHERE email='%u';
|
||||
iterate_query = SELECT email AS user FROM users;
|
||||
# Dovecot ldap configuration
|
||||
cat > /etc/dovecot/dovecot-ldap.conf.ext << EOF;
|
||||
# LDAP server(s) to connect to
|
||||
uris = ${LDAP_URL}
|
||||
tls = ${LDAP_SERVER_TLS}
|
||||
|
||||
# Credentials dovecot uses to perform searches
|
||||
dn = ${LDAP_DOVECOT_DN}
|
||||
dnpass = ${LDAP_DOVECOT_PASSWORD}
|
||||
|
||||
# Use ldap authentication binding for verifying users' passwords
|
||||
# otherwise we have to give dovecot admin access to the database
|
||||
# so it can read userPassword, which is less secure
|
||||
auth_bind = yes
|
||||
# default_pass_scheme = SHA512-CRYPT
|
||||
|
||||
# Search base (subtree)
|
||||
base = ${LDAP_USERS_BASE}
|
||||
|
||||
# Find the user:
|
||||
# Dovecot uses its service account to search for the user using the
|
||||
# filter below. If found, the user is authenticated against this dn
|
||||
# (a bind is attempted as that user). The attribute 'mail' is
|
||||
# multi-valued and contains all the user's email addresses. We use
|
||||
# maildrop as the dovecot mailbox address and forbid then from using
|
||||
# it for authentication by excluding maildrop from the filter.
|
||||
pass_filter = (&(objectClass=mailUser)(mail=%u))
|
||||
pass_attrs = maildrop=user
|
||||
|
||||
# Apply per-user settings:
|
||||
# Post-login information specific to the user (eg. quotas). For
|
||||
# lmtp delivery, pass_filter is not used, and postfix has already
|
||||
# rewritten the envelope using the maildrop address.
|
||||
user_filter = (&(objectClass=mailUser)(|(mail=%u)(maildrop=%u)))
|
||||
user_attrs = maildrop=user
|
||||
|
||||
# Account iteration for various dovecot tools (doveadm)
|
||||
iterate_filter = (objectClass=mailUser)
|
||||
iterate_attrs = maildrop=user
|
||||
|
||||
EOF
|
||||
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
||||
chmod 0600 /etc/dovecot/dovecot-ldap.conf.ext # per Dovecot instructions
|
||||
|
||||
# symlink userdb ext file per dovecot instructions
|
||||
ln -sf /etc/dovecot/dovecot-ldap.conf.ext /etc/dovecot/dovecot-userdb-ldap.conf.ext
|
||||
|
||||
# Have Dovecot provide an authorization service that Postfix can access & use.
|
||||
cat > /etc/dovecot/conf.d/99-local-auth.conf << EOF;
|
||||
@@ -65,6 +97,7 @@ service auth {
|
||||
}
|
||||
EOF
|
||||
|
||||
#
|
||||
# And have Postfix use that service. We *disable* it here
|
||||
# so that authentication is not permitted on port 25 (which
|
||||
# does not run DKIM on relayed mail, so outbound mail isn't
|
||||
@@ -81,44 +114,97 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||
# prevent intra-domain spoofing by logged in but untrusted users in outbound
|
||||
# email. In all outbound mail (the sender has authenticated), the MAIL FROM
|
||||
# address (aka envelope or return path address) must be "owned" by the user
|
||||
# who authenticated. An SQL query will find who are the owners of any given
|
||||
# address.
|
||||
# who authenticated.
|
||||
#
|
||||
# sender-login-maps is given a FROM address (%s), which it uses to
|
||||
# obtain all the users that are permitted to MAIL FROM that address
|
||||
# (from the docs: "Optional lookup table with the SASL login names
|
||||
# that own the sender (MAIL FROM) addresses")
|
||||
# see: http://www.postfix.org/postconf.5.html
|
||||
#
|
||||
# With multiple lookup tables specified, the first matching lookup
|
||||
# ends the search. So, if there is a permitted-senders ldap group,
|
||||
# alias group memberships are not considered for inclusion that may
|
||||
# MAIL FROM the FROM address being searched for.
|
||||
tools/editconf.py /etc/postfix/main.cf \
|
||||
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
|
||||
smtpd_sender_login_maps="ldap:/etc/postfix/sender-login-maps-explicit.cf, ldap:/etc/postfix/sender-login-maps-aliases.cf"
|
||||
|
||||
# Postfix will query the exact address first, where the priority will be alias
|
||||
# records first, then user records. If there are no matches for the exact
|
||||
# address, then Postfix will query just the domain part, which we call
|
||||
# catch-alls and domain aliases. A NULL permitted_senders column means to
|
||||
# take the value from the destination column.
|
||||
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
||||
dbpath=$db_path
|
||||
query = SELECT permitted_senders FROM (SELECT permitted_senders, 0 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NOT NULL UNION SELECT destination AS permitted_senders, 1 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NULL UNION SELECT email as permitted_senders, 2 AS priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||
|
||||
# FROM addresses with an explicit list of "permitted senders"
|
||||
cat > /etc/postfix/sender-login-maps-explicit.cf <<EOF
|
||||
server_host = ${LDAP_URL}
|
||||
bind = yes
|
||||
bind_dn = ${LDAP_POSTFIX_DN}
|
||||
bind_pw = ${LDAP_POSTFIX_PASSWORD}
|
||||
version = 3
|
||||
search_base = ${LDAP_PERMITTED_SENDERS_BASE}
|
||||
query_filter = (mail=%s)
|
||||
result_attribute = maildrop
|
||||
special_result_attribute = member
|
||||
EOF
|
||||
# protect the password
|
||||
chgrp postfix /etc/postfix/sender-login-maps-explicit.cf
|
||||
chmod 0640 /etc/postfix/sender-login-maps-explicit.cf
|
||||
|
||||
# Users may MAIL FROM any of their own aliases
|
||||
cat > /etc/postfix/sender-login-maps-aliases.cf <<EOF
|
||||
server_host = ${LDAP_URL}
|
||||
bind = yes
|
||||
bind_dn = ${LDAP_POSTFIX_DN}
|
||||
bind_pw = ${LDAP_POSTFIX_PASSWORD}
|
||||
version = 3
|
||||
search_base = ${LDAP_USERS_BASE}
|
||||
query_filter = (mail=%s)
|
||||
result_attribute = maildrop
|
||||
special_result_attribute = member
|
||||
EOF
|
||||
chgrp postfix /etc/postfix/sender-login-maps-aliases.cf
|
||||
chmod 0640 /etc/postfix/sender-login-maps-aliases.cf
|
||||
|
||||
|
||||
# ### Destination Validation
|
||||
|
||||
# Use a Sqlite3 database to check whether a destination email address exists,
|
||||
# and to perform any email alias rewrites in Postfix.
|
||||
# Check whether a destination email address exists, and to perform any
|
||||
# email alias rewrites in Postfix.
|
||||
tools/editconf.py /etc/postfix/main.cf \
|
||||
virtual_mailbox_domains=sqlite:/etc/postfix/virtual-mailbox-domains.cf \
|
||||
virtual_mailbox_maps=sqlite:/etc/postfix/virtual-mailbox-maps.cf \
|
||||
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf \
|
||||
virtual_mailbox_domains=ldap:/etc/postfix/virtual-mailbox-domains.cf \
|
||||
virtual_mailbox_maps=ldap:/etc/postfix/virtual-mailbox-maps.cf \
|
||||
virtual_alias_maps=ldap:/etc/postfix/virtual-alias-maps.cf \
|
||||
local_recipient_maps=\$virtual_mailbox_maps
|
||||
|
||||
# SQL statement to check if we handle incoming mail for a domain, either for users or aliases.
|
||||
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF;
|
||||
dbpath=$db_path
|
||||
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s'
|
||||
EOF
|
||||
|
||||
# SQL statement to check if we handle incoming mail for a user.
|
||||
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF;
|
||||
dbpath=$db_path
|
||||
query = SELECT 1 FROM users WHERE email='%s'
|
||||
# the domains we handle mail for
|
||||
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF
|
||||
server_host = ${LDAP_URL}
|
||||
bind = yes
|
||||
bind_dn = ${LDAP_POSTFIX_DN}
|
||||
bind_pw = ${LDAP_POSTFIX_PASSWORD}
|
||||
version = 3
|
||||
search_base = ${LDAP_DOMAINS_BASE}
|
||||
query_filter = (&(dc=%s)(businessCategory=mail))
|
||||
result_attribute = dc
|
||||
EOF
|
||||
chgrp postfix /etc/postfix/virtual-mailbox-domains.cf
|
||||
chmod 0640 /etc/postfix/virtual-mailbox-domains.cf
|
||||
|
||||
# SQL statement to rewrite an email address if an alias is present.
|
||||
# check if we handle incoming mail for a user.
|
||||
# (this doesn't seem to ever be used by postfix)
|
||||
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF
|
||||
server_host = ${LDAP_URL}
|
||||
bind = yes
|
||||
bind_dn = ${LDAP_POSTFIX_DN}
|
||||
bind_pw = ${LDAP_POSTFIX_PASSWORD}
|
||||
version = 3
|
||||
search_base = ${LDAP_USERS_BASE}
|
||||
query_filter = (&(objectClass=mailUser)(mail=%s)(!(|(maildrop="*|*")(maildrop="*:*")(maildrop="*/*"))))
|
||||
result_attribute = maildrop
|
||||
EOF
|
||||
chgrp postfix /etc/postfix/virtual-mailbox-maps.cf
|
||||
chmod 0640 /etc/postfix/virtual-mailbox-maps.cf
|
||||
|
||||
|
||||
|
||||
# Rewrite an email address if an alias is present.
|
||||
#
|
||||
# Postfix makes multiple queries for each incoming mail. It first
|
||||
# queries the whole email address, then just the user part in certain
|
||||
@@ -142,10 +228,26 @@ EOF
|
||||
# Since we might have alias records with an empty destination because
|
||||
# it might have just permitted_senders, skip any records with an
|
||||
# empty destination here so that other lower priority rules might match.
|
||||
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
||||
dbpath=$db_path
|
||||
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND destination<>'' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||
|
||||
|
||||
#
|
||||
# This is the ldap version of aliases(5) but for virtual
|
||||
# addresses. Postfix queries this recursively to determine delivery
|
||||
# addresses. Aliases may be addresses, domains, and catch-alls.
|
||||
#
|
||||
cat > /etc/postfix/virtual-alias-maps.cf <<EOF
|
||||
server_host = ${LDAP_URL}
|
||||
bind = yes
|
||||
bind_dn = ${LDAP_POSTFIX_DN}
|
||||
bind_pw = ${LDAP_POSTFIX_PASSWORD}
|
||||
version = 3
|
||||
search_base = ${LDAP_USERS_BASE}
|
||||
query_filter = (mail=%s)
|
||||
result_attribute = maildrop, rfc822MailMember
|
||||
special_result_attribute = member
|
||||
EOF
|
||||
chgrp postfix /etc/postfix/virtual-alias-maps.cf
|
||||
chmod 0640 /etc/postfix/virtual-alias-maps.cf
|
||||
|
||||
# Restart Services
|
||||
##################
|
||||
@@ -153,4 +255,3 @@ EOF
|
||||
restart_service postfix
|
||||
restart_service dovecot
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ hide_output $venv/bin/pip install --upgrade pip
|
||||
hide_output $venv/bin/pip install --upgrade \
|
||||
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
|
||||
flask dnspython python-dateutil \
|
||||
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
|
||||
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver ldap3
|
||||
|
||||
# CONFIGURATION
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- indent-tabs-mode: t; tab-width: 8; python-indent-offset: 8; -*-
|
||||
|
||||
# Migrates any file structures, database schemas, etc. between versions of Mail-in-a-Box.
|
||||
|
||||
@@ -8,7 +9,7 @@
|
||||
import sys, os, os.path, glob, re, shutil
|
||||
|
||||
sys.path.insert(0, 'management')
|
||||
from utils import load_environment, save_environment, shell
|
||||
from utils import load_environment, load_env_vars_from_file, save_environment, shell
|
||||
|
||||
def migration_1(env):
|
||||
# Re-arrange where we store SSL certificates. There was a typo also.
|
||||
@@ -181,6 +182,65 @@ def migration_12(env):
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def migration_13(env):
|
||||
# This migration step moves users from sqlite3 to openldap
|
||||
|
||||
# users table:
|
||||
# for each row create an ldap entry of the form:
|
||||
# dn: uid=[uuid],ou=Users,dc=mailinabox
|
||||
# objectClass: inetOrgPerson, mailUser, shadowAccount
|
||||
# mail: [email]
|
||||
# maildrop: [email]
|
||||
# userPassword: [password]
|
||||
# mailaccess: [privilege] # multi-valued
|
||||
#
|
||||
# aliases table:
|
||||
# for each row create an ldap entry of the form:
|
||||
# dn: cn=[uuid],ou=aliases,ou=Users,dc=mailinabox
|
||||
# objectClass: mailGroup
|
||||
# mail: [source]
|
||||
# member: [destination-dn] # multi-valued
|
||||
# rfc822MailMember: [email] # multi-values
|
||||
#
|
||||
# if the alias has permitted_senders, create:
|
||||
# dn: cn=[uuid],ou=permitted-senders,ou=Config,dc=mailinabox
|
||||
# objectClass: mailGroup
|
||||
# mail: [source]
|
||||
# member: [user-dn] # multi-valued
|
||||
|
||||
print("Migrating users and aliases from sqlite to ldap")
|
||||
|
||||
# Get the ldap server up and running
|
||||
shell("check_call", ["setup/ldap.sh", "-v"])
|
||||
|
||||
import sqlite3, ldap3
|
||||
import migration_13 as m13
|
||||
|
||||
# 2. get ldap site details (miab_ldap.conf was created by ldap.sh)
|
||||
ldapvars = load_env_vars_from_file(os.path.join(env["STORAGE_ROOT"], "ldap/miab_ldap.conf"), strip_quotes=True)
|
||||
ldap_base = ldapvars.LDAP_BASE
|
||||
ldap_domains_base = ldapvars.LDAP_DOMAINS_BASE
|
||||
ldap_permitted_senders_base = ldapvars.LDAP_PERMITTED_SENDERS_BASE
|
||||
ldap_users_base = ldapvars.LDAP_USERS_BASE
|
||||
ldap_aliases_base = ldapvars.LDAP_ALIASES_BASE
|
||||
ldap_services_base = ldapvars.LDAP_SERVICES_BASE
|
||||
ldap_admin_dn = ldapvars.LDAP_ADMIN_DN
|
||||
ldap_admin_pass = ldapvars.LDAP_ADMIN_PASSWORD
|
||||
|
||||
# 3. connect
|
||||
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/users.sqlite"))
|
||||
ldap = ldap3.Connection('127.0.0.1', ldap_admin_dn, ldap_admin_pass, raise_exceptions=True)
|
||||
ldap.bind()
|
||||
|
||||
# 4. perform the migration
|
||||
users=m13.create_users(env, conn, ldap, ldap_base, ldap_users_base, ldap_domains_base)
|
||||
aliases=m13.create_aliases(conn, ldap, ldap_aliases_base)
|
||||
permitted=m13.create_permitted_senders(conn, ldap, ldap_users_base, ldap_permitted_senders_base)
|
||||
m13.populate_aliases(conn, ldap, users, aliases)
|
||||
|
||||
ldap.unbind()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_current_migration():
|
||||
ver = 0
|
||||
|
||||
220
setup/migration_13.py
Normal file
220
setup/migration_13.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
|
||||
|
||||
#
|
||||
# helper functions for migration #13
|
||||
#
|
||||
|
||||
import uuid, os, sqlite3, ldap3
|
||||
|
||||
|
||||
def add_user(env, ldapconn, search_base, users_base, domains_base, email, password, privs, cn=None):
|
||||
# Add a sqlite user to ldap
|
||||
# env are the environment variables
|
||||
# ldapconn is the bound ldap connection
|
||||
# search_base is for finding a user with the same email
|
||||
# users_base is the rdn where the user will be added
|
||||
# domains_base is the rdn for 'domain' entries
|
||||
# email is the user's email
|
||||
# password is the user's current sqlite password hash
|
||||
# privs is an array of privilege names for the user
|
||||
# cn is the user's common name [optional]
|
||||
#
|
||||
# the email address should be as-is from sqlite (encoded as
|
||||
# ascii using IDNA rules)
|
||||
|
||||
# If the email address exists, return and do nothing
|
||||
ldapconn.search(search_base, "(mail=%s)" % email)
|
||||
if len(ldapconn.entries) > 0:
|
||||
print("user already exists: %s" % email)
|
||||
return ldapconn.response[0]['dn']
|
||||
|
||||
# Generate a unique id for uid
|
||||
uid = '%s' % uuid.uuid4()
|
||||
|
||||
# Attributes to apply to the new ldap entry
|
||||
attrs = {
|
||||
"mail" : email,
|
||||
"maildrop" : email,
|
||||
"uid" : uid,
|
||||
# Openldap uses prefix {CRYPT} for all crypt(3) formats
|
||||
"userPassword" : password.replace('{SHA512-CRYPT}','{CRYPT}')
|
||||
}
|
||||
|
||||
# Add privileges ('mailaccess' attribute)
|
||||
privs_uniq = {}
|
||||
for priv in privs:
|
||||
if priv.strip() != '': privs_uniq[priv] = True
|
||||
if len(privs_uniq) > 0:
|
||||
attrs['mailaccess'] = privs_uniq.keys()
|
||||
|
||||
# Get a common name
|
||||
localpart, domainpart = email.split("@")
|
||||
|
||||
if cn is None:
|
||||
# Get the name for the email address from Roundcube and
|
||||
# use that or `localpart` if no name
|
||||
rconn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
|
||||
rc = rconn.cursor()
|
||||
rc.execute("SELECT name FROM identities WHERE email = ? AND standard = 1 AND del = 0 AND name <> ''", (email,))
|
||||
rc_all = rc.fetchall()
|
||||
if len(rc_all)>0:
|
||||
cn = rc_all[0][0]
|
||||
attrs["displayName"] = cn
|
||||
else:
|
||||
cn = localpart.replace('.',' ').replace('_',' ')
|
||||
rconn.close()
|
||||
attrs["cn"] = cn
|
||||
|
||||
# Choose a surname for the user (required attribute)
|
||||
attrs["sn"] = cn[cn.find(' ')+1:]
|
||||
|
||||
# Add user
|
||||
dn = "uid=%s,%s" % (uid, users_base)
|
||||
print("adding user %s" % email)
|
||||
ldapconn.add(dn,
|
||||
[ 'inetOrgPerson','mailUser','shadowAccount' ],
|
||||
attrs);
|
||||
|
||||
# Create domain entry indicating that we are handling
|
||||
# mail for that domain
|
||||
domain_dn = 'dc=%s,%s' % (domainpart, domains_base)
|
||||
try:
|
||||
ldapconn.add(domain_dn, [ 'domain' ], {
|
||||
"businessCategory": "mail"
|
||||
})
|
||||
except ldap3.core.exceptions.LDAPEntryAlreadyExistsResult:
|
||||
pass
|
||||
return dn
|
||||
|
||||
|
||||
def create_users(env, conn, ldapconn, ldap_base, ldap_users_base, ldap_domains_base):
|
||||
# iterate through sqlite 'users' table and create each user in
|
||||
# ldap. returns a map of email->dn
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT email,password,privileges from users")
|
||||
users = {}
|
||||
for row in c:
|
||||
email=row[0]
|
||||
password=row[1]
|
||||
privs=row[2]
|
||||
dn = add_user(env, ldapconn, ldap_base, ldap_users_base, ldap_domains_base, email, password, privs.split("\n"))
|
||||
users[email] = dn
|
||||
return users
|
||||
|
||||
|
||||
def create_aliases(conn, ldapconn, aliases_base):
|
||||
# iterate through sqlite 'aliases' table and create ldap
|
||||
# aliases but without members. returns a map of alias->dn
|
||||
aliases={}
|
||||
c = conn.cursor()
|
||||
for row in c.execute("SELECT source FROM aliases WHERE destination<>''"):
|
||||
alias=row[0]
|
||||
ldapconn.search(aliases_base, "(mail=%s)" % alias)
|
||||
if len(ldapconn.entries) > 0:
|
||||
# Already present
|
||||
print("alias already exists %s" % alias)
|
||||
aliases[alias] = ldapconn.response[0]['dn']
|
||||
else:
|
||||
cn="%s" % uuid.uuid4()
|
||||
dn="cn=%s,%s" % (cn, aliases_base)
|
||||
print("adding alias %s" % alias)
|
||||
ldapconn.add(dn, ['mailGroup'], {
|
||||
"mail": alias,
|
||||
"description": "Mail group %s" % alias
|
||||
})
|
||||
aliases[alias] = dn
|
||||
return aliases
|
||||
|
||||
|
||||
def populate_aliases(conn, ldapconn, users_map, aliases_map):
|
||||
# populate alias with members.
|
||||
# conn is a connection to the users sqlite database
|
||||
# ldapconn is a connecton to the ldap database
|
||||
# users_map is a map of email -> dn for every user on the system
|
||||
# aliases_map is a map of email -> dn for every pre-created alias
|
||||
#
|
||||
# email addresses should be encoded as-is from sqlite (IDNA
|
||||
# domains)
|
||||
c = conn.cursor()
|
||||
for row in c.execute("SELECT source,destination FROM aliases where destination<>''"):
|
||||
alias=row[0]
|
||||
alias_dn=aliases_map[alias]
|
||||
members = []
|
||||
mailMembers = []
|
||||
|
||||
for email in row[1].split(','):
|
||||
email=email.strip()
|
||||
if email=="":
|
||||
continue
|
||||
elif email in users_map:
|
||||
members.append(users_map[email])
|
||||
elif email in aliases_map:
|
||||
members.append(aliases_map[email])
|
||||
else:
|
||||
mailMembers.append(email)
|
||||
|
||||
print("populate alias group %s" % alias)
|
||||
changes = {}
|
||||
if len(members)>0:
|
||||
changes["member"]=[(ldap3.MODIFY_REPLACE, members)]
|
||||
if len(mailMembers)>0:
|
||||
changes["rfc822MailMember"]=[(ldap3.MODIFY_REPLACE, mailMembers)]
|
||||
ldapconn.modify(alias_dn, changes)
|
||||
|
||||
|
||||
def add_permitted_senders_group(ldapconn, users_base, group_base, source, permitted_senders):
|
||||
# creates a single permitted_senders ldap group
|
||||
#
|
||||
# email addresses should be encoded as-is from sqlite (IDNA
|
||||
# domains)
|
||||
|
||||
# If the group already exists, return and do nothing
|
||||
ldapconn.search(group_base, "(&(objectClass=mailGroup)(mail=%s))" % source)
|
||||
if len(ldapconn.entries) > 0:
|
||||
return ldapconn.response[0]['dn']
|
||||
|
||||
# get a dn for every permitted sender
|
||||
permitted_dn = {}
|
||||
for email in permitted_senders:
|
||||
email = email.strip()
|
||||
if email == "": continue
|
||||
ldapconn.search(users_base, "(mail=%s)" % email)
|
||||
for result in ldapconn.response:
|
||||
permitted_dn[result["dn"]] = True
|
||||
if len(permitted_dn) == 0:
|
||||
return None
|
||||
|
||||
# add permitted senders group for the 'source' email
|
||||
gid = '%s' % uuid.uuid4()
|
||||
group_dn = "cn=%s,%s" % (gid, group_base)
|
||||
print("adding permitted senders group for %s" % source)
|
||||
try:
|
||||
ldapconn.add(group_dn, [ "mailGroup" ], {
|
||||
"cn" : gid,
|
||||
"mail" : source,
|
||||
"member" : permitted_dn.keys(),
|
||||
"description": "Permitted to MAIL FROM this address"
|
||||
})
|
||||
except ldap3.core.exceptions.LDAPEntryAlreadyExistsResult:
|
||||
pass
|
||||
return group_dn
|
||||
|
||||
|
||||
def create_permitted_senders(conn, ldapconn, users_base, group_base):
|
||||
# iterate through the 'aliases' table and create all
|
||||
# permitted-senders groups
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT source, permitted_senders from aliases WHERE permitted_senders is not null")
|
||||
groups={}
|
||||
for row in c:
|
||||
source=row[0]
|
||||
senders=[]
|
||||
for line in row[1].split("\n"):
|
||||
for sender in line.split(","):
|
||||
if sender.strip() != "":
|
||||
senders.append(sender.strip())
|
||||
dn=add_permitted_senders_group(ldapconn, users_base, group_base, source, senders)
|
||||
if dn is not None:
|
||||
groups[source] = dn
|
||||
return groups
|
||||
@@ -9,7 +9,7 @@ if [ -z "${NONINTERACTIVE:-}" ]; then
|
||||
if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then
|
||||
echo Installing packages needed for setup...
|
||||
apt-get -q -q update
|
||||
apt_get_quiet install dialog python3 python3-pip || exit 1
|
||||
apt_get_quiet install dialog python3 python3-pip python3-ldap3 || exit 1
|
||||
fi
|
||||
|
||||
# Installing email_validator is repeated in setup/management.sh, but in setup/management.sh
|
||||
|
||||
148
setup/ssl.sh
148
setup/ssl.sh
@@ -3,8 +3,9 @@
|
||||
# RSA private key, SSL certificate, Diffie-Hellman bits files
|
||||
# -------------------------------------------
|
||||
|
||||
# Create an RSA private key, a self-signed SSL certificate, and some
|
||||
# Diffie-Hellman cipher bits, if they have not yet been created.
|
||||
# Create an RSA private key, a SSL certificate signed by a generated
|
||||
# CA, and some Diffie-Hellman cipher bits, if they have not yet been
|
||||
# created.
|
||||
#
|
||||
# The RSA private key and certificate are used for:
|
||||
#
|
||||
@@ -12,6 +13,7 @@
|
||||
# * IMAP
|
||||
# * SMTP (opportunistic TLS for port 25 and submission on port 587)
|
||||
# * HTTPS
|
||||
# * SLAPD (OpenLDAP server)
|
||||
#
|
||||
# The certificate is created with its CN set to the PRIMARY_HOSTNAME. It is
|
||||
# also used for other domains served over HTTPS until the user installs a
|
||||
@@ -25,8 +27,10 @@ source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
|
||||
# Show a status line if we are going to take any action in this file.
|
||||
if [ ! -f /usr/bin/openssl ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ] \
|
||||
if [ ! -f /usr/bin/openssl ] \
|
||||
|| [ ! -s $STORAGE_ROOT/ssl/ca_private_key.pem ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/ca_certificate.pem ] \
|
||||
|| [ ! -s $STORAGE_ROOT/ssl/ssl_private_key.pem ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ] \
|
||||
|| [ ! -f $STORAGE_ROOT/ssl/dh2048.pem ]; then
|
||||
echo "Creating initial SSL certificate and perfect forward secrecy Diffie-Hellman parameters..."
|
||||
@@ -40,9 +44,9 @@ apt_install openssl
|
||||
|
||||
mkdir -p $STORAGE_ROOT/ssl
|
||||
|
||||
# Generate a new private key.
|
||||
# Generate new private keys.
|
||||
#
|
||||
# The key is only as good as the entropy available to openssl so that it
|
||||
# Keys are only as good as the entropy available to openssl so that it
|
||||
# can generate a random key. "OpenSSL’s built-in RSA key generator ....
|
||||
# is seeded on first use with (on Linux) 32 bytes read from /dev/urandom,
|
||||
# the process ID, user ID, and the current time in seconds. [During key
|
||||
@@ -52,40 +56,144 @@ mkdir -p $STORAGE_ROOT/ssl
|
||||
#
|
||||
# A perfect storm of issues can cause the generated key to be not very random:
|
||||
#
|
||||
# * improperly seeded /dev/urandom, but see system.sh for how we mitigate this
|
||||
# * the user ID of this process is always the same (we're root), so that seed is useless
|
||||
# * zero'd memory (plausible on embedded systems, cloud VMs?)
|
||||
# * a predictable process ID (likely on an embedded/virtualized system)
|
||||
# * a system clock reset to a fixed time on boot
|
||||
# * improperly seeded /dev/urandom, but see system.sh for how we mitigate this
|
||||
# * the user ID of this process is always the same (we're root), so that seed is useless
|
||||
# * zero'd memory (plausible on embedded systems, cloud VMs?)
|
||||
# * a predictable process ID (likely on an embedded/virtualized system)
|
||||
# * a system clock reset to a fixed time on boot
|
||||
#
|
||||
# Since we properly seed /dev/urandom in system.sh we should be fine, but I leave
|
||||
# in the rest of the notes in case that ever changes.
|
||||
if [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then
|
||||
if [ ! -s $STORAGE_ROOT/ssl/ca_private_key.pem ]; then
|
||||
# Set the umask so the key file is never world-readable.
|
||||
(umask 077; hide_output \
|
||||
openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048)
|
||||
openssl genrsa -aes256 -passout 'pass:SECRET-PASSWORD' \
|
||||
-out $STORAGE_ROOT/ssl/ca_private_key.pem 4096)
|
||||
|
||||
# remove the existing ca-certificate, it must be regenerated
|
||||
rm -f $STORAGE_ROOT/ssl/ca_certificate.pem
|
||||
|
||||
# Remove the ssl_certificate.pem symbolic link to force a
|
||||
# regeneration of a self-signed server certificate. Old certs need
|
||||
# to be signed by the new ca.
|
||||
if [ -L $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
||||
# Get the name of the certificate issuer
|
||||
issuer="$(openssl x509 -issuer -nocert -in $STORAGE_ROOT/ssl/ssl_certificate.pem)"
|
||||
|
||||
# Determine if the ssl cert if self-signed. If unique hashes is 1,
|
||||
# the cert is self-signed (pior versions of MiaB used self-signed
|
||||
# certs).
|
||||
uniq_hashes="$(openssl x509 -subject_hash -issuer_hash -nocert -in $STORAGE_ROOT/ssl/ssl_certificate.pem | uniq | wc -l)"
|
||||
|
||||
if [ "$uniq_hashes" == "1" ] || grep "Temporary-Mail-In-A-Box-CA" <<<"$issuer" >/dev/null
|
||||
then
|
||||
rm -f $STORAGE_ROOT/ssl/ssl_certificate.pem
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate a self-signed SSL certificate because things like nginx, dovecot,
|
||||
if [ ! -s $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then
|
||||
# Set the umask so the key file is never world-readable.
|
||||
(umask 037; hide_output \
|
||||
openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048)
|
||||
|
||||
# Give the group 'ssl-cert' read access so slapd can read it
|
||||
groupadd -fr ssl-cert
|
||||
chgrp ssl-cert $STORAGE_ROOT/ssl/ssl_private_key.pem
|
||||
chmod g+r $STORAGE_ROOT/ssl/ssl_private_key.pem
|
||||
|
||||
# Remove the ssl_certificate.pem symbolic link to force a
|
||||
# regeneration of the server certificate. It needs to be
|
||||
# signed by the new ca.
|
||||
if [ -L $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
||||
rm -f $STORAGE_ROOT/ssl/ssl_certificate.pem
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Generate a root CA certificate
|
||||
#
|
||||
if [ ! -f $STORAGE_ROOT/ssl/ca_certificate.pem ]; then
|
||||
# Generate the self-signed certificate.
|
||||
CERT=$STORAGE_ROOT/ssl/ca_certificate.pem
|
||||
hide_output \
|
||||
openssl req -new -x509 \
|
||||
-days 3650 -sha256 \
|
||||
-key $STORAGE_ROOT/ssl/ca_private_key.pem \
|
||||
-passin 'pass:SECRET-PASSWORD' \
|
||||
-out $CERT \
|
||||
-subj '/CN=Temporary-Mail-In-A-Box-CA'
|
||||
|
||||
# add the certificate to the system's trusted root ca list
|
||||
# this is required for openldap's TLS implementation
|
||||
hide_output \
|
||||
cp $CERT /usr/local/share/ca-certificates/mailinabox.crt
|
||||
hide_output \
|
||||
update-ca-certificates
|
||||
fi
|
||||
|
||||
# Generate a signed SSL certificate because things like nginx, dovecot,
|
||||
# etc. won't even start without some certificate in place, and we need nginx
|
||||
# so we can offer the user a control panel to install a better certificate.
|
||||
if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
||||
# Generate a certificate signing request.
|
||||
# # Generate a certificate signing request.
|
||||
CSR=/tmp/ssl_cert_sign_req-$$.csr
|
||||
hide_output \
|
||||
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CSR \
|
||||
-sha256 -subj "/CN=$PRIMARY_HOSTNAME"
|
||||
|
||||
# Generate the self-signed certificate.
|
||||
CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem
|
||||
# create a ca database (directory) for openssl
|
||||
CADIR=$STORAGE_ROOT/ssl/ca
|
||||
mkdir -p $CADIR/newcerts
|
||||
touch $CADIR/index.txt $CADIR/index.txt.attr
|
||||
[ ! -e $CADIR/serial ] && date +%s > $CADIR/serial
|
||||
|
||||
# Generate the signed certificate.
|
||||
CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-cert-$(date --rfc-3339=date | sed s/-//g).pem
|
||||
hide_output \
|
||||
openssl x509 -req -days 365 \
|
||||
-in $CSR -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CERT
|
||||
openssl ca -batch \
|
||||
-keyfile $STORAGE_ROOT/ssl/ca_private_key.pem \
|
||||
-cert $STORAGE_ROOT/ssl/ca_certificate.pem \
|
||||
-passin 'pass:SECRET-PASSWORD' \
|
||||
-in $CSR \
|
||||
-out $CERT \
|
||||
-days 365 \
|
||||
-name miab_ca \
|
||||
-config - <<< "
|
||||
[ miab_ca ]
|
||||
dir = $CADIR
|
||||
certs = \$dir
|
||||
database = \$dir/index.txt
|
||||
unique_subject = no
|
||||
new_certs_dir = \$dir/newcerts # default place for new certs.
|
||||
serial = \$dir/serial # The current serial number
|
||||
x509_extensions = server_cert # The extensions to add to the cert
|
||||
name_opt = ca_default # Subject Name options
|
||||
cert_opt = ca_default # Certificate field options
|
||||
policy = policy_anything
|
||||
default_md = default # use public key default MD
|
||||
|
||||
[ policy_anything ]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
localityName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[ server_cert ]
|
||||
basicConstraints = CA:FALSE
|
||||
nsCertType = server
|
||||
nsComment = \"Mail-In-A-Box Generated Certificate\"
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid,issuer
|
||||
"
|
||||
|
||||
# Delete the certificate signing request because it has no other purpose.
|
||||
rm -f $CSR
|
||||
|
||||
# Symlink the certificate into the system certificate path, so system services
|
||||
# Symlink the certificates into the system certificate path, so system services
|
||||
# can find it.
|
||||
ln -s $CERT $STORAGE_ROOT/ssl/ssl_certificate.pem
|
||||
fi
|
||||
|
||||
@@ -101,6 +101,7 @@ EOF
|
||||
source setup/system.sh
|
||||
source setup/ssl.sh
|
||||
source setup/dns.sh
|
||||
source setup/ldap.sh
|
||||
source setup/mail-postfix.sh
|
||||
source setup/mail-dovecot.sh
|
||||
source setup/mail-users.sh
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
source ${STORAGE_ROOT}/ldap/miab_ldap.conf
|
||||
|
||||
# ### Installing Roundcube
|
||||
|
||||
@@ -131,6 +132,33 @@ cat > $RCM_CONFIG <<EOF;
|
||||
\$config['login_autocomplete'] = 2;
|
||||
\$config['password_charset'] = 'UTF-8';
|
||||
\$config['junk_mbox'] = 'Spam';
|
||||
\$config['ldap_public']['public'] = array(
|
||||
'name' => 'Directory',
|
||||
'hosts' => array('${LDAP_SERVER}'),
|
||||
'port' => ${LDAP_SERVER_PORT},
|
||||
'user_specific' => false,
|
||||
'scope' => 'sub',
|
||||
'base_dn' => '${LDAP_USERS_BASE}',
|
||||
'bind_dn' => '${LDAP_WEBMAIL_DN}',
|
||||
'bind_pass' => '${LDAP_WEBMAIL_PASSWORD}',
|
||||
'writable' => false,
|
||||
'ldap_version' => 3,
|
||||
'search_fields' => array( 'mail' ),
|
||||
'name_field' => 'mail',
|
||||
'email_field' => 'mail',
|
||||
'sort' => 'mail',
|
||||
'filter' => '(objectClass=mailUser)',
|
||||
'fuzzy_search' => false,
|
||||
'global_search' => true,
|
||||
# 'groups' => array(
|
||||
# 'base_dn' => '${LDAP_ALIASES_BASE}',
|
||||
# 'filter' => '(objectClass=mailGroup)',
|
||||
# 'member_attr' => 'member',
|
||||
# 'scope' => 'sub',
|
||||
# 'name_attr' => 'mail',
|
||||
# 'member_filter' => '(|(objectClass=mailGroup)(objectClass=mailUser))',
|
||||
# )
|
||||
);
|
||||
?>
|
||||
EOF
|
||||
|
||||
@@ -160,7 +188,7 @@ mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundc
|
||||
chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
||||
|
||||
# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
|
||||
sudo -u www-data touch /var/log/roundcubemail/errors
|
||||
sudo -u www-data touch /var/log/roundcubemail/errors.log
|
||||
|
||||
# Password changing plugin settings
|
||||
# The config comes empty by default, so we need the settings
|
||||
@@ -169,22 +197,21 @@ cp ${RCM_PLUGIN_DIR}/password/config.inc.php.dist \
|
||||
${RCM_PLUGIN_DIR}/password/config.inc.php
|
||||
|
||||
tools/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php \
|
||||
"\$config['password_minimum_length']=8;" \
|
||||
"\$config['password_db_dsn']='sqlite:///$STORAGE_ROOT/mail/users.sqlite';" \
|
||||
"\$config['password_query']='UPDATE users SET password=%D WHERE email=%u';" \
|
||||
"\$config['password_dovecotpw']='/usr/bin/doveadm pw';" \
|
||||
"\$config['password_dovecotpw_method']='SHA512-CRYPT';" \
|
||||
"\$config['password_dovecotpw_with_method']=true;"
|
||||
|
||||
# so PHP can use doveadm, for the password changing plugin
|
||||
usermod -a -G dovecot www-data
|
||||
|
||||
# set permissions so that PHP can use users.sqlite
|
||||
# could use dovecot instead of www-data, but not sure it matters
|
||||
chown root.www-data $STORAGE_ROOT/mail
|
||||
chmod 775 $STORAGE_ROOT/mail
|
||||
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
|
||||
chmod 664 $STORAGE_ROOT/mail/users.sqlite
|
||||
"\$config['password_driver']='ldap';" \
|
||||
"\$config['password_ldap_host']='${LDAP_SERVER}';" \
|
||||
"\$config['password_ldap_port']=${LDAP_SERVER_PORT};" \
|
||||
"\$config['password_ldap_starttls']=$([ ${LDAP_SERVER_STARTTLS} == yes ] && echo true || echo false);" \
|
||||
"\$config['password_ldap_basedn']='${LDAP_BASE}';" \
|
||||
"\$config['password_ldap_userDN_mask']=null;" \
|
||||
"\$config['password_ldap_searchDN']='${LDAP_WEBMAIL_DN}';" \
|
||||
"\$config['password_ldap_searchPW']='${LDAP_WEBMAIL_PASSWORD}';" \
|
||||
"\$config['password_ldap_search_base']='${LDAP_USERS_BASE}';" \
|
||||
"\$config['password_ldap_search_filter']='(&(objectClass=mailUser)(mail=%login))';" \
|
||||
"\$config['password_ldap_encodage']='default';" \
|
||||
"\$config['password_ldap_lchattr']='shadowLastChange';" \
|
||||
"\$config['password_algorithm']='sha512-crypt';" \
|
||||
"\$config['password_algorithm_prefix']='{CRYPT}';" \
|
||||
"\$config['password_minimum_length']=8;"
|
||||
|
||||
# Fix Carddav permissions:
|
||||
chown -f -R root.www-data ${RCM_PLUGIN_DIR}/carddav
|
||||
@@ -197,5 +224,5 @@ chown www-data:www-data $STORAGE_ROOT/mail/roundcube/roundcube.sqlite
|
||||
chmod 664 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite
|
||||
|
||||
# Enable PHP modules.
|
||||
phpenmod -v php mcrypt imap
|
||||
phpenmod -v php mcrypt imap ldap
|
||||
restart_service php7.2-fpm
|
||||
|
||||
Reference in New Issue
Block a user