1
0
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:
downtownallday
2020-06-05 12:03:58 -04:00
45 changed files with 5701 additions and 432 deletions

View File

@@ -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
View 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"
}

View File

@@ -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
View 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

View File

@@ -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" \

View File

@@ -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).

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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. "OpenSSLs 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

View File

@@ -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

View File

@@ -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