1
0
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:
downtownallday
2020-01-17 17:03:21 -05:00
parent a67f90593d
commit 1f0d2ddb92
41 changed files with 5509 additions and 339 deletions

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

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

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
"idna>=2.0.0" "cryptography==2.2.2" boto psutil 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

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

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