Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save stewartadam/efe1531c6463ba66259e to your computer and use it in GitHub Desktop.
Save stewartadam/efe1531c6463ba66259e to your computer and use it in GitHub Desktop.
CentOS 7 shared hosting server setup script based on the SCS Shared Linux Hosting Server Security tutorial at Concordia University (2016) - discussion about security principles in the context of a shared hosting server.
#!/bin/sh
#
# Usage:
# This script will establish a basic shared hosting server running DNS, Web (HTTP), DB (SQL) and e-mail (SMTP/IMAP) services.
#
# This script doesn't include usage information on adding new user accounts and setting up mail inboxes; for that, please see my
# CentOS 5 server setup tutorial: http://www.firewing1.com/howtos/servers/centos5/getting_started
#
# This configuration is very similar to my previously documented CentOS 5 server setup, but has been ported to take advantage
# of the new features in CentOS 7.
#
generate_password() {
len=18
if [ -n "$1" ] && [ "$1" -eq "$1" ];then
len="$1"
fi
tr -cd '[:alnum:]' < /dev/urandom | fold -w$len | head -n1;
}
#
## Configure these variables
#
DOMAIN="example.com"
FQDN="server1.example.com"
#
## Basic system setup
#
# Get the server IPs
IPv4="$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1 | head -n1)"
IPv6="$(ip addr show eth0 | grep "inet6\b" | awk '{print $2}' | cut -d/ -f1 | head -n1)"
# Set the hostname
hostnamectl set-hostname $FQDN
# Automate system time adjustments
yum install -y chrony
timedatectl set-timezone America/Montreal
timedatectl set-ntp true
# Disable SELinux
sed -i -e 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
setenforce 0
# Install a few utilities
yum install -y epel-release
yum install -y git nano wget net-tools deltarpm bind-utils rsync pv telnet
# Enable firewall
systemctl enable firewalld
systemctl start firewalld
# Ensure mail for root@localhost goes somewhere
cat << EOF >> /etc/aliases
root: sysadmin@${DOMAIN}
EOF
newaliases
# Archive all logs in /root/logarchive
mkdir /root/logarchive
cat << EOF > /etc/cron.daily/logrotate_archivelogs
#!/bin/sh
for file in /var/log/*.gz;do
mv "\$file" /root/logarchive
done 2>/dev/null
find /var/log/ -type f -name '*.gz' | while read line;do
base=\$(basename "\$(dirname "\$line")")
mkdir -p /root/logarchive/\$base
mv "\$line" /root/logarchive/\$base
done 2>/dev/null
for home in /home/*;do
if [ -d "\$home/web/logs" ];then
if [ ! -d /root/logarchive/\${home##/home/} ];then
mkdir /root/logarchive/\${home##/home/}
fi
for file in "\$home/web/logs"/*.gz;do
mv "\$file" /root/logarchive/\${home##/home/}
done 2>/dev/null
fi
done 2>/dev/null
EOF
chmod +x /etc/cron.daily/logrotate_archivelogs
#
## Install DNS server
#
yum install -y bind-chroot
firewall-cmd --permanent --add-service dns
systemctl enable named-chroot
systemctl start named-chroot
# Allow recursion (DNS server serves zone records to outside world, but also does lookusp for localhost)
sed -i -e "s/listen-on-v6 port 53 { ::1; };/listen-on-v6 port 53 { none; };/" /etc/named.conf
sed -i -e "s/recursion yes;/recursion yes;\n\tallow-recursion { localhost; };/" /etc/named.conf
#
## Apache (HTTP) Webserver
#
yum install -y httpd httpd-itk php php-{gd,mbstring,mcrypt,dom,pdo,process,mysqlnd,soap,xml} mod_ssl
# Run each site as its own user
sed -i -e 's/^#LoadModule/LoadModule/' /etc/httpd/conf.modules.d/00-mpm-itk.conf
# Enable server status
cat << EOF > /etc/httpd/conf.d/server-status.conf
<Location /server-status>
Require local
SetHandler server-status
</Location>
EOF
# Rotate user logs regularly
sed -i -e 's/#compress/compress/' /etc/logrotate.conf
cat << EOF > /etc/logrotate.d/httpd-vhosts
/home/*/web/logs/*log {
missingok
notifempty
sharedscripts
postrotate
/sbin/service httpd reload > /dev/null 2>/dev/null || true
endscript
}
EOF
# Since each site runs as its own user, we need to have a PHP session folder per account
chmod 771 /var/lib/php/session
chown root:apache /var/lib/php/session
# Configure default PHP settings
sed -i 's|;date.timezone =|date.timezone = America/New_York|' /etc/php.ini
sed -i 's|memory_limit = 128M|memory_limit = 256M|' /etc/php.ini
sed -i 's|upload_max_filesize = 2M|upload_max_filesize = 10M|' /etc/php.ini
sed -i 's|post_max_size = 2M|post_max_size = 12M|' /etc/php.ini
# Setup the default/fallback Virtual Host
cat << EOF > /etc/httpd/conf.d/00-default-vhost.conf
<VirtualHost *:80>
ServerName $FQDN
DocumentRoot /var/www/html
<IfModule mod_ruid2.c>
RUidGid apache apache
</IfModule>
<IfModule mpm_itk.c>
AssignUserId apache apache
</IfModule>
</VirtualHost>
EOF
cat << EOF > /etc/httpd/conf.d/z_custom.conf
# named with a z_ prefix to ensure this is parsed last.
# Restrict access to file extensions that shouldn't be read via a browser:
# *~ for temp/backup files, *.sql, *.inc as it isn't registered as a php file
<FilesMatch "\.(inc|.*sql|.*~)$">
Order allow,deny
Deny from all
</FilesMatch>
EOF
firewall-cmd --permanent --add-service http
firewall-cmd --permanent --add-service https
systemctl enable httpd
systemctl start httpd
#
## Database server
#
yum install -y mariadb-server
firewall-cmd --permanent --add-service mysql
systemctl enable mariadb
systemctl start mariadb
# Increase max packet size for better compatibility with WordPress, Drupal and other CMSs
# InnoDB table-per-file allows us to optimize and compact databases more effectively
cat << EOF > /etc/my.cnf.d/custom.cnf
[server]
max_allowed_packet=64M
innodb_file_per_table=1
EOF
# Increase the open file limit since we have lots of databases and innodb_file_per_table=1
mkdir /etc/systemd/system/mariadb.service.d/
cat << EOF > /etc/systemd/system/mariadb.service.d/limit.conf
[Service]
LimitNOFILE=65535
EOF
# Reset root password & configure what mysql_secure_installation does
MYSQL_ROOT_PW="$(generate_password)"
cat << EOF | mysql -u root
UPDATE mysql.user SET Password=PASSWORD('$MYSQL_ROOT_PW') WHERE User='root';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
EOF
systemctl daemon-reload
systemctl restart mariadb
# SSL w/ Lets Encrypt - remember to setup your FQDN's DNS A record first
yum install -y letsencrypt
firewall-cmd --add-service https
cat << EOF > /etc/cron.monthly/letsencrypt-renew
letsencrypt renew
EOF
chmod +x /etc/cron.monthly/letsencrypt-renew
letsencrypt certonly -d "$FQDN" --standalone-supported-challenges tls-sni-01 --email "admin@${DOMAIN}" --agree-tos
#
## Dovecot (POP/IMAP)
#
# Setup a database table for mail user authentication
# We use this database to validate which recipients are valid/exist, as well as
# for which users are allowed to relay (send outgoing mail).
MAIL_DB_NAME="mailauth"
MAIL_DB_USER="mailauth"
MAIL_DB_PASSWORD=$(generate_password)
cat << EOF | mysql -u root -p"$MYSQL_ROOT_PW"
CREATE DATABASE $MAIL_DB_NAME;
USE $MAIL_DB_USER;
CREATE TABLE mail_aliases (
source varchar(128) NOT NULL COMMENT 'The first field in the Postfix virtual table.',
destination varchar(128) NOT NULL COMMENT 'The rest of the Postfix virtual table.',
PRIMARY KEY (source)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Virtual mail aliases';
CREATE TABLE mail_accounts (
local varchar(128) NOT NULL COMMENT 'The local part of the email in local@domain.tld',
domain varchar(255) NOT NULL DEFAULT '' COMMENT 'The domain.tld part in local@domain.tld',
password varchar(255) DEFAULT NULL COMMENT 'The mail user password as output by dovecotpw',
PRIMARY KEY (local,domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Virtual mail users';
CREATE TABLE domains (
domain varchar(255) NOT NULL DEFAULT '' COMMENT 'The domain name, including TLD.',
sys_uid int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'The system UID of the user that owns this website.',
sys_gid int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'The system GID of the user that owns this website.',
dochome varchar(255) NOT NULL COMMENT 'Directory to places user documents (mail, webroot, etc).',
PRIMARY KEY (domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Stores client domain information.';
GRANT ALL ON $MAIL_DB_NAME.* TO '$MAIL_DB_NAME'@'localhost' IDENTIFIED BY '$MAIL_DB_PASSWORD'
EOF
# Configure Dovecot w/ its access to the authentication table
yum install -y dovecot dovecot-mysql
cat << EOF > /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=/var/lib/mysql/mysql.sock dbname=$MAIL_DB_NAME user=$MAIL_DB_USER password=$MAIL_DB_PASSWORD
default_pass_scheme = CRAM-MD5
user_query = \\
SELECT \\
d.dochome as home, \\
d.sys_uid AS uid, \\
d.sys_gid AS gid \\
FROM mail_accounts a \\
INNER JOIN domains d ON a.domain=d.domain \\
WHERE local = '%Ln' AND d.domain = '%Ld'
password_query = \\
SELECT \\
concat(a.local, '@', a.domain) AS user, \\
a.password, d.dochome AS userdb_home, \\
d.sys_uid AS userdb_uid, \\
d.sys_gid AS userdb_gid \\
FROM mail_accounts a \\
INNER JOIN domains d ON a.domain=d.domain \\
WHERE a.local = '%Ln' AND d.domain = '%Ld'
iterate_query = SELECT local AS username, domain FROM mail_accounts
EOF
chmod 600 /etc/dovecot/dovecot-sql.conf.ext
sed -i -e 's/auth_mechanisms = plain/auth_mechanisms = plain login cram-md5/' /etc/dovecot/conf.d/10-auth.conf
sed -i -e 's/#auth_verbose = no/auth_verbose = yes/' /etc/dovecot/conf.d/10-logging.conf
sed -i -e 's/#imap_client_workarounds = /imap_client_workarounds = delay-newmail/' /etc/dovecot/conf.d/20-imap.conf
sed -i -e 's/#pop3_client_workarounds = /pop3_client_workarounds = outlook-no-nuls oe-ns-eoh/' /etc/dovecot/conf.d/20-pop3.conf
sed -i -e 's|#mail_location = |mail_location = maildir:%h/mail/%Ld/%Ln/mail|' /etc/dovecot/conf.d/10-mail.conf
sed -i -e 's/!include auth-system.conf.ext/#!include auth-system.conf.ext/' /etc/dovecot/conf.d/10-auth.conf
sed -i -e 's/#!include auth-sql.conf.ext/!include auth-sql.conf.ext/' /etc/dovecot/conf.d/10-auth.conf
sed -i -e 's/#mail_max_userip_connections = 10/mail_max_userip_connections = 15/' /etc/dovecot/conf.d/20-imap.conf
sed -i -e 's/#mail_max_userip_connections = 10/mail_max_userip_connections = 15/' /etc/dovecot/conf.d/20-pop3.conf
sed -i -e 's/#ssl_protocols = !SSLv2/ssl_protocols = !SSLv2 !SSLv3/' /etc/dovecot/conf.d/10-ssl.conf
sed -i -e 's/#ssl_protocols = !SSLv2/ssl_protocols = !SSLv2 !SSLv3/' /etc/dovecot/conf.d/10-ssl.conf
sed -i -e "s|ssl_cert = </etc/pki/dovecot/certs/dovecot.pem|ssl_cert = </etc/pki/tls/certs/$FQDN.crt|" /etc/dovecot/conf.d/10-ssl.conf
sed -i -e "s|ssl_key = </etc/pki/dovecot/private/dovecot.pem|ssl_key = </etc/pki/tls/private/$FQDN.key|" /etc/dovecot/conf.d/10-ssl.conf
cat << EOF > /etc/dovecot/conf.d/auth-sql.conf.ext
# Authentication for SQL users. Included from 10-auth.conf.
#
# <doc/wiki/AuthDatabase.SQL.txt>
passdb {
driver = sql
# Path for SQL configuration file, see example-config/dovecot-sql.conf.ext
args = /etc/dovecot/dovecot-sql.conf.ext
}
# "prefetch" user database means that the passdb already provided the
# needed information and there's no need to do a separate userdb lookup.
# <doc/wiki/UserDatabase.Prefetch.txt>
userdb {
driver = prefetch
}
userdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
# If you don't have any user-specific settings, you can avoid the user_query
# by using userdb static instead of userdb sql, for example:
# <doc/wiki/UserDatabase.Static.txt>
#userdb {
#driver = static
#args = uid=vmail gid=vmail home=/var/vmail/%u
#}
EOF
cat << EOF > /etc/dovecot/conf.d/10-master.conf
#default_process_limit = 100
#default_client_limit = 1000
# Default VSZ (virtual memory size) limit for service processes. This is mainly
# intended to catch and kill processes that leak memory before they eat up
# everything.
#default_vsz_limit = 256M
# Login user is internally used by login processes. This is the most untrusted
# user in Dovecot system. It shouldn't have access to anything at all.
#default_login_user = dovenull
# Internal user is used by unprivileged processes. It should be separate from
# login user, so that login processes can't disturb other processes.
#default_internal_user = dovecot
service imap-login {
inet_listener imap {
#port = 143
}
inet_listener imaps {
#port = 993
#ssl = yes
}
# Number of connections to handle before starting a new process. Typically
# the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
# is faster. <doc/wiki/LoginProcess.txt>
#service_count = 1
# Number of processes to always keep waiting for more connections.
#process_min_avail = 0
# If you set service_count=0, you probably need to grow this.
#vsz_limit = $default_vsz_limit
}
service pop3-login {
inet_listener pop3 {
#port = 110
}
inet_listener pop3s {
#port = 995
#ssl = yes
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0666
user = postfix
group = postfix
}
# Create inet listener only if you can't use the above UNIX socket
#inet_listener lmtp {
# Avoid making LMTP visible for the entire internet
#address =
#port =
#}
}
service imap {
# Most of the memory goes to mmap()ing files. You may need to increase this
# limit if you have huge mailboxes.
#vsz_limit = $default_vsz_limit
# Max. number of IMAP processes (connections)
#process_limit = 1024
}
service pop3 {
# Max. number of POP3 processes (connections)
#process_limit = 1024
}
service auth {
# auth_socket_path points to this userdb socket by default. It's typically
# used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
# full permissions to this socket are able to get a list of all usernames and
# get the results of everyone's userdb lookups.
#
# The default 0666 mode allows anyone to connect to the socket, but the
# userdb lookups will succeed only if the userdb returns an "uid" field that
# matches the caller process's UID. Also if caller's uid or gid matches the
# socket's uid or gid the lookup succeeds. Anything else causes a failure.
#
# To give the caller full permissions to lookup all users, set the mode to
# something else than 0666 and Dovecot lets the kernel enforce the
# permissions (e.g. 0777 allows everyone full permissions).
unix_listener auth-userdb {
#mode = 0666
#user =
#group =
}
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
# Auth process is run as this user.
user = \$default_internal_user
}
service auth-worker {
# Auth worker process is run as root by default, so that it can access
# /etc/shadow. If this isn't necessary, the user should be changed to
# $default_internal_user.
user = \$default_internal_user
}
service dict {
# If dict proxy is used, mail processes should have access to its socket.
# For example: mode=0660, group=vmail and global mail_access_groups=vmail
unix_listener dict {
#mode = 0600
#user =
#group =
}
}
EOF
# Drop in a config file for systemd to ensure dovecot starts after MySQL is available
mkdir /etc/systemd/dovecot.service.d
cat << EOF > /etc/systemd/dovecot.service.d/afterdb.conf
[Unit]
After=mariadb.service
EOF
systemctl daemon-reload
systemctl enable dovecot
systemctl restart dovecot
#
## Postfix (SMTP) mail server
#
# Install Postfix and configure access to the authentication tables
POSTMASTER_EMAIL="postmaster@${DOMAIN}"
yum install -y postfix
sed -i -e "s/#myhostname = virtual.domain.tld/#myhostname = virtual.domain.tld\nmyhostname = $FQDN/" /etc/postfix/main.cf
cat << EOF > /etc/postfix/mysql-virtual-aliases.cf
# This file contains the SQL query for looking up virtual users aliases.
user = $MAIL_DB_USER
password = $MAIL_DB_PASSWORD
hosts = 127.0.0.1
dbname = $MAIL_DB_NAME
table = mail_aliases
select_field = destination
where_field = source
EOF
cat << EOF > /etc/postfix/mysql-virtual-domains.cf
# Generates a list of distinct domain names that postfix should accept mail for
user = $MAIL_DB_USER
password = $MAIL_DB_PASSWORD
hosts = 127.0.0.1
dbname = $MAIL_DB_NAME
table = domains
select_field = domain
where_field = domain
#additional_conditions = AND domain <> 'example.com'
EOF
cat << EOF > /etc/postfix/mysql-virtual-recipients.cf
# Queries the list of virtual email addresses to determine if there is a match
user = $MAIL_DB_USER
password = $MAIL_DB_PASSWORD
hosts = 127.0.0.1
dbname = $MAIL_DB_NAME
query = SELECT 1 FROM mail_accounts WHERE concat(local, '@', domain)='%s'
EOF
# Tweak settings to disable relay except for authenticated users
cat << EOF >> /etc/postfix/main.cf
# Adopt future behavior now
parent_domain_matches_subdomains = no
# dovecot local delivery agent (lda)
dovecot_destination_recipient_limit = 1
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-recipients.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf
virtual_transport = lmtp:unix:private/dovecot-lmtp
# SASL authentication via dovecot
smtpd_sasl_type = dovecot
smtpd_sasl_path = /var/spool/postfix/private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_recipient_restrictions =
permit_mynetworks, permit_sasl_authenticated,
reject_unauth_pipelining,
reject_invalid_hostname,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_unauth_destination,
reject_non_fqdn_recipient,
reject_unknown_recipient_domain,
reject_rbl_client zen.spamhaus.org,
reject_rbl_client bl.spamcop.net,
permit
smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_sender, reject_unknown_sender_domain
smtpd_data_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, check_client_access regexp:/etc/postfix/add_auth_header.regexp
broken_sasl_auth_clients = yes
# Do not discard messages at HELO until RCPT TO command is given
smtpd_delay_reject = yes
smtpd_helo_required = yes
smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_non_fqdn_helo_hostname, reject_invalid_hostname
# TLS config
smtpd_tls_security_level = may
smtpd_tls_key_file = /etc/pki/tls/private/$FQDN.key
smtpd_tls_cert_file = /etc/pki/tls/certs/$FQDN.pem
# send session info to log
smtpd_tls_loglevel = 1
# don't renegotiate new TLS sessions for an hour
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom
# Force STARTTLS before auth
smtpd_tls_auth_only = no
# Limit how fast we can accept mail so that is is processed correctly
anvil_rate_time_unit = 60
default_process_limit = 15
# Limit to 20 connections or 25 messages per client per unit time (anvil_rate_time_unit)
smtpd_client_connection_count_limit = 20
smtpd_client_message_rate_limit = 25
# Clients can connect once/6 seconds
smtpd_client_connection_rate_limit = 10
# 30MB message size limit, and must have 5x as much free space to accept mail
message_size_limit = 31457280
queue_minfree = 157286400
header_size_limit = 51200
# Limits on bulk mail
default_destination_recipient_limit = 25
smtpd_recipient_limit = 25
smtpd_recipient_overshoot_limit = 25
# Mail goes out from this IP
smtp_bind_address=$IPv4
smtp_bind_address6=$IPv6
EOF
# Allow communication on port 26 (since many ISPs block 25), 465 (SSL/TLS) and 587.
cat << EOF >> /etc/postfix/master.cf
26 inet n - n - - smtpd
587 inet n - n - - smtpd
smtps inet n - n - - smtpd
-o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes
# Dovecot LDA - ignores extensions - user+extension@domain.com --> user@domain.com
dovecot unix - n n - - pipe
flags=DRhu user=mail:mail argv=/usr/libexec/dovecot/deliver -f \${sender} -d \${recipient}
EOF
# Spam filtering with amavisd and virus scanning with clamav
yum install -y clamav amavisd-new spamassassin swaks perl-Mail-SPF clamav-update
systemctl enable amavisd
# Open two internal ports, 10024 and 10025. All incoming mail is forwarded to port 10024
# and fed into amavisd for spam filtering. If it checks out, amavisd returns it to Postfix
# on port 10025 so it can be further processed.
cat << EOF > /etc/postfix/master.cf
# Spam filtering
amavisfeed unix - - n - 2 smtp
-o smtp_data_done_timeout=1200
-o smtp_send_xforward_command=yes
-o smtp_bind_address=127.0.0.1
-o smtpd_data_restrictions=
-o disable_dns_lookups=yes
-o max_use=5
127.0.0.1:10025 inet n - - - 0 smtpd
-o smtpd_sasl_auth_enable=no
-o content_filter=
-o smtpd_delay_reject=no
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_helo_restrictions=
-o smtpd_sender_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o smtpd_data_restrictions=reject_unauth_pipelining
-o smtpd_end_of_data_restrictions=
-o smtpd_restriction_classes=
-o mynetworks=127.0.0.0/8
-o smtpd_error_sleep_time=0
-o smtpd_soft_error_limit=1001
-o smtpd_hard_error_limit=1000
-o smtpd_client_connection_count_limit=0
-o smtpd_client_connection_rate_limit=0
-o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters,no_address_mappings
-o local_header_rewrite_clients=
-o smtpd_milters=
-o local_recipient_maps=
-o relay_recipient_maps=
EOF
# Tell Postfix to add a 'X-SMTP-ValidUser' header to all outgoing mail for authenticated users
cat << EOF > /etc/postfix/add_auth_header.regexp
/^/ PREPEND X-SMTP-ValidUser: no
EOF
# amavisd uses spamassassin for its spam filtering. We are going to tweak the settings
# so that and mail flagged with our 'X-SMTP-ValidUser' header set by Postfix has a -10
# spam score, preventing authenticated users from having their outgoing mail categorized
# as spam.
cat << EOF >> /etc/mail/spamassassin/local.cf
# These values can be overridden by editing ~/.spamassassin/user_prefs.cf
# (see spamassassin(1) for details)
# These should be safe assumptions and allow for simple visual sifting
# without risking lost emails.
required_hits 5
report_safe 0
rewrite_header Subject [SPAM]
# Don't mark SMTP authenticated sessions as spam
header __NO_SMTP_AUTH X-SMTP-ValidUser =~ /^no$/m
meta SMTP_AUTH !__NO_SMTP_AUTH
describe SMTP_AUTH Message sent using SMTP Authentication
tflags SMTP_AUTH nice
score SMTP_AUTH -10
EOF
sa-update
# Set the virus signature database to auto-update
sed -i -e 's/Example/#Example/' /etc/freshclam.conf
sed -i -e 's/FRESHCLAM_DELAY=disabled-warn/#FRESHCLAM_DELAY=disabled-warn/' /etc/sysconfig/freshclam
freshclam
# Set the spam rules database to auto-update
# See https://bugs.centos.org/view.php?id=8102
sed -i -e 's/#SAUPDATE=yes/SAUPDATE=yes/' /etc/sysconfig/sa-update
cat << EOF > /etc/cron.d/amavisd-reload
# See https://bugzilla.redhat.com/show_bug.cgi?id=1145652
0 6 * * * root /bin/systemctl reload amavisd
EOF
# Configure the amavisd spam scanner
sed -i -e 's|$max_servers =.*|$max_servers = 5;|' /etc/amavisd/amavisd.conf
sed -i -e "s|\$mydomain = 'example.com';|\$mydomain = '$DOMAIN';|" /etc/amavisd/amavisd.conf
sed -i -e 's|# $helpers_home = "$MYHOME/var"|$helpers_home = "$MYHOME/var"|' /etc/amavisd/amavisd.conf
sed -i -e "s|\$virus_admin = .*|\$virus_admin = \"postmaster\\\\@\$mydomain\";|" /etc/amavisd/amavisd.conf
sed -i -e "s|\$mailfrom_notify_admin = .*|\$mailfrom_notify_admin = \"postmaster\\\\@\$mydomain\";|" /etc/amavisd/amavisd.conf
sed -i -e "s|\$mailfrom_notify_recip = .*|\$mailfrom_notify_recip = \"postmaster\\\\@\$mydomain\";|" /etc/amavisd/amavisd.conf
sed -i -e "s|\$mailfrom_notify_spamadmin = .*|\$mailfrom_notify_spamadmin = \"postmaster\\\\@\$mydomain\";|" /etc/amavisd/amavisd.conf
sed -i -e "s|# \$myhostname = 'host.example.com';|\$myhostname = \"$FQDN\";|" /etc/amavisd/amavisd.conf
sed -i -e 's|$final_banned_destiny = D_BOUNCE;|$final_banned_destiny = D_DISCARD;|' /etc/amavisd/amavisd.conf
# We were getting bad headers from automated messages being sent to Beltronic accounts
sed -i -e 's|$final_bad_header_destiny = D_BOUNCE;|$final_bad_header_destiny = D_PASS;|' /etc/amavisd/amavisd.conf
sed -i -e 's|# $defang_bad_header, $defang_undecipherable, $defang_spam|# $defang_bad_header, $defang_undecipherable, $defang_spam\n$spam_quarantine_to = "spam\@$mydomain";|' /etc/amavisd/amavisd.conf
systemctl start amavisd
cat << EOF >> /etc/postfix/main.cf
# Spam filtering
content_filter = amavisfeed:[127.0.0.1]:10024
EOF
# Like Dovecot, because we have users in MySQL we ensure MySQL is available before starting Postfix.
mkdir /etc/systemd/postfix.service.d
cat << EOF > /etc/systemd/postfix.service.d/afterdb.conf
[Unit]
After=mariadb.service
EOF
systemctl daemon-reload
systemctl reload postfix
# Support DKIM signing
yum install -y opendkim
sed -i -e 's/Mode.*/Mode sv/' /etc/opendkim.conf
sed -i -e 's/^KeyFile/#KeyFile/' /etc/opendkim.conf
sed -i -e 's/^# KeyTable/KeyTable/' /etc/opendkim.conf
sed -i -e 's/^# SigningTable/SigningTable/' /etc/opendkim.conf
sed -i -e 's/^# ExternalIgnoreList/ExternalIgnoreList/' /etc/opendkim.conf
sed -i -e 's/^# InternalHosts/InternalHosts/' /etc/opendkim.conf
cat << EOF >> /etc/opendkim/TrustedHosts
$FQDN
EOF
cat << EOF >> /etc/opendkim.conf
# Custom options
AutoRestart Yes
AutoRestartRate 10/1h
SignatureAlgorithm rsa-sha256
DNSTimeout 10
EOF
systemctl enable opendkim
systemctl start opendkim
# Ensure Postfix forwards mail to OpenDKIM for signing before sending mail
cat << EOF >> /etc/postfix/main.cf
# DKIM
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = $smtpd_milters
milter_default_action = accept
milter_protocol = 2
EOF
systemctl reload postfix
# Add a webmail portal (at '/webmail' on any domain) thanks to RoundCube Mail
yum install -y roundcubemail
cat << EOF >> /etc/httpd/conf.d/roundcubemail-custom.conf
Alias /webmail /usr/share/roundcubemail
<Directory /usr/share/roundcubemail/>
Options none
AllowOverride Limit
Require all granted
</Directory>
<Directory /usr/share/roundcubemail/installer>
Options none
AllowOverride Limit
Require all granted
</Directory>
EOF
systemctl enable postfix
systemctl enable dovecot
# Open all mail ports in the firewall
firewall-cmd --permanent --add-service smtp
firewall-cmd --permanent --add-port 26/tcp
firewall-cmd --permanent --add-port 587/tcp
firewall-cmd --permanent --add-port 465/tcp
firewall-cmd --permanent --add-service imaps
firewall-cmd --permanent --add-service pop3s
firewall-cmd --permanent --add-port 110/tcp
firewall-cmd --permanent --add-port 143/tcp
#
## Hardening SSH/SFTP
#
# Make a jail for our users - they are locked down /srv/sftp/<username> when connecting over SFTP
# Use bind mounts to bind their web root to /srv/sftp/<username>/web_files, granting them access
# only to their web files and nothing else.
# See https://utcc.utoronto.ca/~cks/space/blog/linux/SystemdBindMountUnits
mkdir /srv/sftp
# Enable SSH's internal SFTP server, disable password authentication
sed -i -e 's|Subsystem\tsftp\t/usr/libexec/openssh/sftp-server|#Subsystem\tsftp\t/usr/libexec/openssh/sftp-server|' /etc/ssh/sshd_config
sed -i -e 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
# Lock down SSH and enable permissions selectively based on user groups
cat << EOF >> /etc/ssh/sshd_config
#
## Customizations ##
# Some of the settings here duplicate defaults, however this is to ensure that
# if for some reason the defaults change in the future, your server's
# configuration will not be affected.
# Do not allow root to login over SSH. If you need to become root, login as your
# regular use and use su - instead.
PermitRootLogin no
# Do not allow TCP or X11 forwarding by default.
AllowTcpForwarding no
X11Forwarding no
# Why give such a large window? If the user has not provided credentials in 30
# seconds, disconnect the user.
LoginGraceTime 30s
#
## Access control ##
# We need to use the internal sftp subsystem
Subsystem sftp internal-sftp
# Allow access if user is in these groups
AllowGroups serv_sftponly serv_sshall
# Give tunnelling + X11 access to users who are members of group "serv_sshall"
Match group serv_sshall
X11Forwarding yes
AllowTcpForwarding yes
# Restrict users who are members of group "serv_sftponly"
Match group serv_sftponly
# We can't use a path relative to ~ (or %h) because we make the user homes
# /public_html in order to get the chroot above working properly. As a
# result, we need to set an absolute path that will make SSH look in the
# usual place for authorized keys.
AuthorizedKeysFile /home/%u/.ssh/authorized_keys
X11Forwarding no
AllowTcpForwarding no
# Force the internal SFTP subsystem and jailroot the user in their home.
# %u gets substituted with the user name, %h with home
ForceCommand internal-sftp
ChrootDirectory /srv/sftp/%u
Match group serv_pwauth
PasswordAuthentication yes
EOF
systemctl reload sshd
# Users can be members of these group to unlock extra capabilities
groupadd serv_sftponly
groupadd serv_sshall
groupadd serv_pwauth
#
## fail2ban
#
yum install -y fail2ban fail2ban-systemd
cat << EOF > /etc/fail2ban/jail.d/monit.conf
[monit]
enabled = true
EOF
# Ban repeat offenders
cat << EOF > /etc/fail2ban/jail.d/recidive.conf
[recidive]
enabled = true
backend = auto
logpath = /var/log/fail2ban.log
protocol = tcp
port = ssh,smtp,26,465,submission,imap2,imap3,imaps,pop3,pop3s,http,https,ftp,ftps,mysql
EOF
touch /var/log/fail2ban.log
# Jail brute force attacks against SSH
cat << EOF > /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
action = %(action_)s
EOF
# Jail brute force attacks against roundcubemail
cat << EOF > /etc/fail2ban/jail.d/roundcube-auth.conf
[roundcube-auth]
backend = auto
enabled = true
logpath = /var/log/roundcubemail/errors
failregex = IMAP Error: (FAILED login|Login failed) for .*? from <HOST>\.
maxretry = 20
findtime = 1200
bantime = 1200
EOF
touch /var/log/roundcubemail/errors
# Jail brute force attacks against Dovecot
cat << EOF > /etc/fail2ban/jail.d/dovecot.conf
[dovecot]
enabled = true
maxretry = 20
findtime = 1200
bantime = 1200
EOF
# Jail brute force attacks against Postfix
cat << EOF > /etc/fail2ban/jail.d/postfix.conf
[postfix]
enabled = true
maxretry = 20
findtime = 1200
bantime = 1200
EOF
cat << EOF > /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = $IGNORE_IP_SUBNETS $DYNAMIC_HOSTNAME
action = %(action_mwl)s
destemail = root
EOF
# Disable start/stop emails
cat << EOF >> /etc/fail2ban/action.d/sendmail-common.local
# Override the Fail2Ban defaults in sendmail-common.conf with these entries
[Definition]
# Disable email notifications of jails stopping or starting
actionstart =
actionstop =
EOF
systemctl enable fail2ban
systemctl start fail2ban
#
## rootkit hunter
#
yum install -y rkhunter
sed -i -e 's/ALLOW_SSH_ROOT_USER=unset/ALLOW_SSH_ROOT_USER=no/' /etc/rkhunter.conf
sed -i -e 's/DISABLE_TESTS=suspscan/DISABLE_TESTS=os_specific suspscan/' /etc/rkhunter.conf
sed -i -e "s|ALLOWHIDDENFILE=/etc/.bzrignore|# Linode\nALLOWHIDDENFILE=/etc/.resolv.conf.linode-last\nALLOWHIDDENFILE=/etc/.resolv.conf.linode-orig\n|" /etc/rkhunter.conf
rkhunter --propupd
#
## Service monitoring
#
yum install -y monit
cat << EOF > /etc/monit.d/notifications
# Set alert email
set mailserver localhost
set alert root@localhost
EOF
# Automatically restart Apache
cat << EOF > /etc/monit.d/httpd
check process httpd with pidfile /var/run/httpd/httpd.pid
group web
start program = "/bin/systemctl start httpd"
stop program = "/bin/systemctl stop httpd"
if failed port 80 protocol http then restart
if 3 restarts within 5 cycles then timeout
EOF
# Automatically restart MySQL
cat << EOF > /etc/monit.d/mysql
check process mysql with pidfile /var/run/mariadb/mariadb.pid
group database
start program = "/bin/systemctl start mariadb"
stop program = "/bin/systemctl stop mariadb"
if failed port 3306 protocol mysql then restart
if 5 restarts within 5 cycles then timeout
EOF
# Automatically set PHP session folder permissions
# (they get reset on php package updates)
cat << EOF > /etc/monit.d/php
check directory session with path /var/lib/php/session
if failed permission 0771 then exec "/bin/chmod 771 /var/lib/php/session"
if failed uid root then exec "/bin/chmod 771 /var/lib/php/session"
if failed gid apache then exec "/bin/chmod 771 /var/lib/php/session"
EOF
# Automatically restart Postfix
cat << EOF > /etc/monit.d/postfix
check process postfix with pidfile /var/spool/postfix/pid/master.pid
group mail
start program = "/bin/systemctl start postfix"
stop program = "/bin/systemctl stop postfix"
if cpu > 60% for 2 cycles then alert
if totalmem > 200.0 MB for 5 cycles then alert
if children > 250 then alert
if failed port 25 protocol smtp
with timeout 15 seconds
then alert
if 3 restarts within 5 cycles then timeout
EOF
# Automatically restart Dovecot
cat << EOF > /etc/monit.d/dovecot
check process dovecot with pidfile /var/run/dovecot/master.pid
group mail
if cpu > 60% for 2 cycles then alert
start program = "/bin/systemctl start dovecot"
stop program = "/bin/systemctl stop dovecot"
if failed port 993 protocol imap for 5 cycles then restart
if 3 restarts within 5 cycles then timeout
EOF
# Automatically restart Amavisd
cat << EOF > /etc/monit.d/amavisd
check process amavisd with pidfile /var/run/amavisd/amavisd.pid
group mail
start program = "/bin/systemctl start amavisd"
stop program = "/bin/systemctl stop amavisd"
if failed port 10024 protocol smtp then restart
if 5 restarts within 5 cycles then timeout
EOF
# Automatically restart SSH
cat << EOF > /etc/monit.d/sshd
check process sshd with pidfile /var/run/sshd.pid
start program "/bin/systemctl start sshd"
stop program "/bin/systemctl stop sshd"
if failed port 22 protocol ssh then restart
if 5 restarts within 5 cycles then timeout
EOF
systemctl enable monit
systemctl start monit
# Reload firewall rules
firewall-cmd --reload
# Commit our etc configuration
yum -y install etckeeper
cd /etc
etckeeper init
git add -A .
git commit -am "Initial commit."
# Print setup info
echo "IPv4: $IPv4"
echo "IPv6: $IPv6"
echo "DOMAIN: $DOMAIN"
echo "FQDN: $FQDN"
echo "MAIL_DB_NAME: $MAIL_DB_NAME"
echo "MAIL_DB_USER: $MAIL_DB_USER"
echo "MAIL_DB_PASSWORD: $MAIL_DB_PASSWORD"
echo "MYSQL_ROOT_PW: $MYSQL_ROOT_PW"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment