mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-04 15:54:48 +01:00
Issue #1340 - LDAP backend for accounts
This commit will: 1. Change the user account database from sqlite to OpenLDAP 2. Add policyd-spf to postfix for SPF validation 3. Add a test runner with some automated test suites Notes: User account password hashes are preserved. There is a new Roundcube contact list called "Directory" that lists the users in LDAP (MiaB users), similar to what Google Suite does. Users can still change their password in Roundcube. OpenLDAP is configured with TLS, but all remote access is blocked by firewall rules. Manual changes are required to open it for remote access (eg. "ufw allow proto tcp from <HOST> to any port ldaps"). The test runner is started by executing tests/runner.sh. Be aware that it will make changes to your system, including adding new users, domains, mailboxes, start/stop services, etc. It is highly unadvised to run it on a production system! The LDAP schema that supports mail delivery with postfix and dovecot is located in conf/postfix.schema. This file is copied verbatim from the LdapAdmin project (GPL, ldapadmin.org). Instead of including the file in git, it could be referenced by URL and downloaded by the setup script if GPL is an issue or apply for a PEN from IANA. Mangement console and other services should not appear or behave any differently than before.
This commit is contained in:
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.
|
||||
@@ -223,3 +224,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}'
|
||||
}
|
||||
|
||||
|
||||
882
setup/ldap.sh
Executable file
882
setup/ldap.sh
Executable file
@@ -0,0 +1,882 @@
|
||||
#!/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
|
||||
apparmor_parser -r /etc/apparmor.d/usr.sbin.slapd
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
|
||||
@@ -208,7 +220,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
|
||||
"idna>=2.0.0" "cryptography==2.2.2" boto psutil 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
|
||||
|
||||
@@ -99,6 +99,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
|
||||
|
||||
@@ -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