Instantly share code, notes, and snippets.
Created
March 10, 2016 04:48
-
Star
4
(4)
You must be signed in to star a gist -
Fork
4
(4)
You must be signed in to fork a gist
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 [email protected]', | |
domain varchar(255) NOT NULL DEFAULT '' COMMENT 'The domain.tld part in [email protected]', | |
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 - [email protected] --> [email protected] | |
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