EC2 Guide: Software Configuration (6a / 7)

Configure ClamAV

ClamAV will be our anti-virus system and is very easy to set-up! We just need to update some permissions:

sudo adduser clamav amavis 
sudo adduser amavis clamav 

Configure SpamAssassin

SpamAssassin's function is given away by it's name. We will use this for spam filtering our e-mails.

sudo nano /etc/default/spamassassin 
[...]
ENABLED=1
[...]
CRON=1
[...]

We need to create a razor config file for it to work:

sudo razor-admin -create 

Start the service:

sudo service spamassassin start 

Configure Amavisd-New

Amavisd-New will be our interface between our mail transport program (Postfix) and our spam and virus filters (SpamAssassin / ClamAV).

sudo nano /etc/amavis/conf.d/15-content_filter_mode 

Uncomment the spam and anti-virus lines.

use strict;

# You can modify this file to re-enable SPAM checking through spamassassin
# and to re-enable antivirus checking.

#
# Default antivirus checking mode
# Uncomment the two lines below to enable it
#

@bypass_virus_checks_maps = (
   \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);

#
# Default SPAM checking mode
# Uncomment the two lines below to enable it
#

@bypass_spam_checks_maps = (
   \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);

1;  # insure a defined return

Some fairly key definitions are missing because they are not "free" - thanks Ubuntu. Enable / uncomment them!

sudo nano /etc/amavis/conf.d/01-debian 
[...]
$unfreeze   = ['unfreeze', 'freeze -d', 'melt', 'fcat'];
[...]
$lha    = 'lha';
[...]

Amavis needs permissions here:

sudo chmod -R 775 /var/lib/amavis/tmp 
sudo service amavis restart 

Configure Mail Logging

Because we mounted parts of our file system through EBS volumes we need to update the mail log locations.

sudo nano /etc/rsyslog.d/50-default.conf 
[...]
mail.*                          -/var/log/postfix/mail.log
[...]
mail.info                       -/var/log/postfix/mail.info
mail.warn                       -/var/log/postfix/mail.warn
mail.err                        /var/log/postfix/mail.err
[..]
sudo service rsyslog restart 

Configure MySQL

MySQL set-up largely depends upon your use case and virtual hardware configuration. Shown here is an example, working config.

sudo nano /etc/mysql/my.cnf 
[client]
port                  = 3306
socket                = /var/run/mysqld/mysqld.sock
default-character-set = utf8

[mysqld_safe]
socket                = /var/run/mysqld/mysqld.sock
nice                  = 0

[mysqld]
user                  = mysql
socket                = /var/run/mysqld/mysqld.sock
port                  = 3306
basedir               = /usr
datadir               = /var/lib/mysql
tmpdir                = /tmp

skip-external-locking
default-character-set = utf8
collation_server      = utf8_general_ci
character_set_server  = utf8
bind-address          = 127.0.0.1

key_buffer            = 512M
max_allowed_packet    = 16M
thread_stack          = 192K
thread_cache_size     = 8
myisam-recover        = BACKUP
max_connections       = 200
table_cache           = 1024
thread_concurrency    = 10

query-cache-type      = 1 
query_cache_limit     = 8M
query_cache_size      = 128M

log_error             = /var/log/mysql/error.log

server-id             = 1
expire_logs_days      = 10
max_binlog_size       = 100M

[mysqldump]
quick
quote-names
max_allowed_packet    = 16M

[mysql]
no-auto-rehash
default-character-set = utf8

[isamchk]
key_buffer            = 64M
sort_buffer           = 64M
read_buffer           = 16M
write_buffer          = 16M

[myisamchk]
key_buffer            = 64M
sort_buffer           = 64M
read_buffer           = 16M
write_buffer          = 16M

!includedir /etc/mysql/conf.d/

If you require MySQL to be available from other locations you will need to comment out the "bind-address = 127.0.0.1" line.

sudo service mysql restart 

Configure PHP

The default installation of PHP on Ubuntu 10.04 is good. There isn't much wrong with it, but there are always changes that need making. Most notably is the session clean up (which should be exclusively by cron jobs due to potential security risks of giving the web user access) and the configuration of either iconv or mb_string. There are 3 php.ini files, one for Apache (which is the only one I will show here), one for CLI and one for CGI. You should probably make pretty similar changes to all of them.

sudo nano /etc/php5/apache2/php.ini 
engine					= On
short_open_tag				= Off
asp_tags				= Off
precision				= 14
y2k_compliance				= On
output_buffering			= 4096
zlib.output_compression			= Off
implicit_flush				= Off
unserialize_callback_func		=
serialize_precision			= 100
allow_call_time_pass_reference		= Off

safe_mode				= Off
safe_mode_gid				= Off
safe_mode_include_dir			=
safe_mode_exec_dir			=
safe_mode_allowed_env_vars		= PHP_
safe_mode_protected_env_vars		= LD_LIBRARY_PATH

disable_functions			=
disable_classes				=
expose_php				= On
max_execution_time			= 330
max_input_time				= 360
memory_limit				= 128M
error_reporting				= E_ALL & ~E_DEPRECATED
display_errors				= Off
display_startup_errors			= Off
log_errors				= On
log_errors_max_len			= 1024
ignore_repeated_errors			= Off
ignore_repeated_source			= Off
report_memleaks				= On
track_errors				= Off
html_errors				= Off
variables_order				= "GPCS"
request_order				= "GP"
register_globals			= Off
register_long_arrays			= Off
register_argc_argv			= Off
auto_globals_jit			= On
post_max_size				= 18M
magic_quotes_gpc			= Off
magic_quotes_runtime			= Off
magic_quotes_sybase			= Off
auto_prepend_file			=
auto_append_file			=
default_mimetype			= "text/html"
default_charset				= UTF-8
doc_root				=
user_dir				=
enable_dl				= Off
file_uploads				= On
upload_max_filesize			= 12M
max_file_uploads			= 20
allow_url_fopen				= On
allow_url_include			= Off
default_socket_timeout			= 60

pdo_mysql.cache_size			= 2000
pdo_mysql.default_socket		=

define_syslog_variables			= Off

SMTP					= localhost
smtp_port				= 25
mail.add_x_header			= On

sql.safe_mode				= Off

odbc.allow_persistent			= On
odbc.check_persistent			= On
odbc.max_persistent			= -1
odbc.max_links				= -1
odbc.defaultlrl				= 4096
odbc.defaultbinmode			= 1

ibase.allow_persistent			= 1
ibase.max_persistent			= -1
ibase.max_links				= -1
ibase.timestampformat			= "%Y-%m-%d %H:%M:%S"
ibase.dateformat			= "%Y-%m-%d"
ibase.timeformat			= "%H:%M:%S"

mysql.allow_local_infile		= On
mysql.allow_persistent			= On
mysql.cache_size			= 2000
mysql.max_persistent			= -1

mysql.max_links				= -1
mysql.default_port			=
mysql.default_socket			=
mysql.default_host			=
mysql.default_user			=
mysql.default_password			=
mysql.connect_timeout			= 60
mysql.trace_mode			= Off

mysqli.max_persistent			= -1
mysqli.allow_persistent			= On
mysqli.max_links			= -1
mysqli.cache_size			= 2000
mysqli.default_port			= 3306
mysqli.default_socket			=
mysqli.default_host			=
mysqli.default_user			=
mysqli.default_pw			=
mysqli.reconnect			= Off

mysqlnd.collect_statistics		= On
mysqlnd.collect_memory_statistics	= Off

pgsql.allow_persistent			= On
pgsql.auto_reset_persistent		= Off
pgsql.max_persistent			= -1
pgsql.max_links				= -1
pgsql.ignore_notice			= 0
pgsql.log_notice			= 0

sybct.allow_persistent			= On
sybct.max_persistent			= -1
sybct.max_links				= -1
sybct.min_server_severity		= 10
sybct.min_client_severity		= 10

bcmath.scale				= 0

session.save_handler			= files
session.use_cookies			= 1
session.use_only_cookies		= 1
session.name				= PHPSESSID
session.auto_start			= 0
session.cookie_lifetime			= 0
session.cookie_path			= /
session.cookie_domain			=
session.cookie_httponly			=
session.serialize_handler		= php
session.gc_probability			= 0
session.gc_divisor			= 1000
session.gc_maxlifetime			= 2700
session.bug_compat_42			= Off
session.bug_compat_warn			= Off
session.referer_check			=
session.entropy_length			= 0
session.entropy_file			=
session.cache_limiter			= nocache
session.cache_expire			= 180
session.use_trans_sid			= 0
session.hash_function			= 1
session.hash_bits_per_character		= 5

url_rewriter.tags			= "a=href,area=href,frame=src,input=src,form=fakeentry"

mssql.allow_persistent			= On
mssql.max_persistent			= -1
mssql.max_links				= -1
mssql.min_error_severity		= 10
mssql.min_message_severity		= 10
mssql.secure_connection			= Off

mbstring.language			= Neutral
mbstring.internal_encoding		= UTF-8
mbstring.http_input			= auto
mbstring.http_output			= UTF-8
mbstring.encoding_translation		= On
mbstring.detect_order			= auto
mbstring.substitute_character		= none;
mbstring.func_overload			= 0

tidy.clean_output			= Off

soap.wsdl_cache_enabled			= 1
soap.wsdl_cache_dir			= "/tmp"
soap.wsdl_cache_ttl			= 86400
soap.wsdl_cache_limit			= 5

ldap.max_links				= -1
sudo service apache2 graceful  

Configure Apache and a test site

Apache / PHP configuration again largely depends upon your use case. The following will set up a working system and configure a site at "http://www.domain.com" for you.

Create some directories for vhosts and sites first:

sudo mkdir /etc/apache2/vhosts/db-maintenance/ /etc/apache2/vhosts/catch-all/ /etc/apache2/vhosts/redirects/ /var/www/default /var/www/www.domain.com /var/www/db-maintenance.domain.com 

Technically you are not supposed to edit the main apache2.conf file file, but I found some parts of it not to my liking.

sudo nano /etc/apache2/apache2.conf 
ServerRoot "/etc/apache2"
LockFile /var/lock/apache2/accept.lock
PidFile ${APACHE_PID_FILE}
Timeout 300
KeepAlive Off
MaxKeepAliveRequests 100
KeepAliveTimeout 15

<IfModule mpm_prefork_module>
    StartServers          8
    MinSpareServers       5
    MaxSpareServers      20
    MaxClients          256
    MaxRequestsPerChild 4000
</IfModule>

<IfModule mpm_worker_module>
    StartServers          2
    MinSpareThreads      25
    MaxSpareThreads      75
    ThreadLimit          64
    ThreadsPerChild      25
    MaxClients          150
    MaxRequestsPerChild   0
</IfModule>

<IfModule mpm_event_module>
    StartServers          2
    MaxClients          150
    MinSpareThreads      25
    MaxSpareThreads      75
    ThreadLimit          64
    ThreadsPerChild      25
    MaxRequestsPerChild   0
</IfModule>

User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}

AccessFileName .htaccess
<Files ~ "^\.ht">
    Order allow,deny
    Deny from all
    Satisfy all
</Files>

DefaultType text/plain
HostnameLookups Off
ErrorLog /var/log/apache2/error.log
LogLevel warn

Include /etc/apache2/mods-enabled/*.load
Include /etc/apache2/mods-enabled/*.conf
Include /etc/apache2/httpd.conf
Include /etc/apache2/ports.conf

LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
CustomLog /var/log/apache2/access.log vhost_combined

Include /etc/apache2/conf.d/

#Include /etc/apache2/vhosts/db-maintenance/*.conf
Include /etc/apache2/vhosts/*.conf
Include /etc/apache2/vhosts/redirects/*.conf
Include /etc/apache2/vhosts/catch-all/*.conf

This is the user editable file which we will use to set a couple of options and define a default directory.

sudo nano /etc/apache2/httpd.conf 
DocumentRoot "/var/www/default"

<Directory />
    Options FollowSymLinks
    AllowOverride None
</Directory>

<Directory "/var/www/default">
            Options Includes FollowSymLinks MultiViews -Indexes
            AllowOverride AuthConfig
            Order allow,deny
            Allow from all
</Directory>

DirectoryIndex index.php index.html index.htm
AddDefaultCharset UTF-8

Next we will create some default site pages so you can check everything is working. The main (www) site first:

sudo nano /var/www/www.domain.com/index.php 
<?php phpinfo();

Then the database maintenance site:

sudo nano /var/www/db-maintenance.domain.com/index.php
<?php 
echo 'Sorry, this site is currently undergoing scheduled maintenance. 
Please check back soon!';

And a default page to catch incorrect sub-domains:

sudo nano /var/www/default/index.php
<?php 
echo 'We think that you may have mis-typed! 
Were you looking for: <a href="http://www.domain.com">www.domain.com</a>?';

The premise of these files will be that if you visit "http://www.domain.com" you will receive a PHP info screen, but if you visit a none-existent sub-domain (e.g. "http://totally-random.domain.com") you will end up at the default location (assuming you have set your DNS to a catch all, *, setting in your zone files). If you were to enable the db-maintenance vhosts in the apache2.conf file the idea will be your users will go there instead of your main site. None of this will work yet because we have not set up any vhost files.

The first vhost will be the default (catch-all):

sudo nano /etc/apache2/vhosts/default.conf  
<VirtualHost *:80>
        DocumentRoot /var/www/default/
        ServerName localhost
        <Directory "/var/www/default/">
                Options -Indexes
                AllowOverride AuthConfig
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>

The only key points to note are the choice of document root (you may want the trailing slash, you may not), the exclusion of indexing (i.e. you don't want anyone able to view all folder contents) and the ability to over-ride the settings using an .htaccess file should you wish to (I prefer to make those changes at this level). The next vhost will be the main site:

sudo nano /etc/apache2/vhosts/www.domain.com.conf  
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        DocumentRoot /var/www/www.domain.com
        ServerName www.domain.com
        <Directory "/var/www/www.domain.com/">
                Options -Indexes FollowSymLinks
                AllowOverride AuthConfig
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>  

Hopefully everything should be fairly obvious and you will note we have used a unique "ServerName" value. Lastly we need the database maintenance vhost:

sudo nano /etc/apache2/vhosts/db-maintenance/www.domain.com.conf  
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        DocumentRoot /var/www/db-maintenance.domain.com
        ServerName www.domain.com
        <Directory "/var/www/db-maintenance.domain.com/">
                Options -Indexes FollowSymLinks
                AllowOverride AuthConfig
                Order allow,deny
                Allow from all
        </Directory>
        <IfModule mod_rewrite.c>
                RewriteEngine On
                RewriteCond %{REQUEST_FILENAME} !-d
                RewriteCond %{REQUEST_FILENAME} !-f
                RewriteRule ^(.*)$ /index.php [L,QSA]
        </IfModule>
</VirtualHost>  

This time we have also included some re-write rules which mean that every address a user tries when maintenance is being carried out (e.g. http://www.domain.com/one-of-your-pages.php) will be handled by just one file, index.php, so you only need to create a single maintenance file. This technique is commonly used in MVC patterns.

Apache needs a restart / reload before these changes are applied:

sudo service apache2 graceful 

Now if you test your site using "http://www.domain.com" you should receive a PHP info screen, but if you visit a none-existent sub-domain (e.g. "http://totally-random.domain.com) you should receive the default message. If you enable database maintenance (by un-commenting the relevant line in the apache2.conf and restarting Apache) and visit your site using "http://www.domain.com/anything.php" you should see the maintenance message. If at this point you don't see these pages you should check your zone settings with your domain registrar or DNS manager.

Obviously it would be better that all incorrectly typed (non-existing) domains were forwarded on to an actual domain of your choosing. This isn't hard to achieve with a catch-all vhost:

sudo nano /etc/apache2/vhosts/catch-all/domain.com.conf  
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        ServerName unknown.domain.com
        ServerAlias *.domain.com
        KeepAlive Off
        RewriteEngine On
        RewriteRule ^/(.*)$ http://www.domain.com/$1 [R=301,L]
</VirtualHost>

This time we are using a "ServerAlias" in addition to an arbitrary "ServerName" with a re-write rule to forward everything (including a page name and GET data) to the correct location on the actual site.

It would also be better if the domain without a sub-domain (i.e. http://domain.com) redirected to your main site. Again this isn't hard to achieve with another vhost:

sudo nano /etc/apache2/vhosts/redirects/domain.com.conf  
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        ServerName domain.com
        KeepAlive Off
        RewriteEngine On
        RewriteRule ^/(.*)$ http://www.domain.com/$1 [R=301,L]
</VirtualHost>

Nothing new in this one, just another re-write rule. Once again we need to reload our Apche settings:

sudo service apache2 graceful 

Now if you test your site using a none-existent sub-domain (e.g. "http://totally-random.domain.com) or the raw domain "http://domain.com" you should be 301 redirected to "http://www.domain.com."

Install PhpMyAdmin (optional)

While we are configuring sites it is a good idea to install this very convenient tool to manage MySQL databases. It will need its own vhost:

sudo nano /etc/apache2/vhosts/database.domain.com.conf 
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        DocumentRoot /var/www/database.domain.com
        ServerName database.domain.com
        <Directory "/var/www/database.domain.com/">
                Options -Indexes FollowSymLinks
                AllowOverride AuthConfig
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>

Get the actual program, extract it and edit its settings. You will probably need to customise this link I'm afraid because it changes often!

cd /var/www 
sudo wget "http://downloads.sourceforge.net/project/phpmyadmin/phpMyAdmin/4.6.5.2/phpMyAdmin-4.6.5.2-english.tar.gz" -O /var/www/phpmyadmin.tar.gz 
sudo tar xvfz /var/www/phpmyadmin.tar.gz 
sudo rm /var/www/phpmyadmin.tar.gz 
sudo mv /var/www/phpMyAdmin-4.6.5.2-english /var/www/database.domain.com 
sudo mv /var/www/database.domain.com/config.sample.inc.php /var/www/database.domain.com/config.inc.php 
sudo nano /var/www/database.domain.com/config.inc.php 
[...]
$cfg['blowfish_secret'] = 'choose a suitably random string here';
[...]
$cfg['Servers'][$i]['extension'] = 'mysqli';
[...]

Clean up any temporary files left by our editor:

sudo rm /var/www/database.domain.com/config.inc.php~ 

It's also a good idea to password this directory so we'll create a general password file for use with our restricted sites:

sudo htpasswd -c /var/www/.htpasswd user 

And if you want to add another user:

sudo htpasswd /var/www/.htpasswd another_user 

Now we need to tell the phpMyAdmin directory to use this extra authentication:

sudo nano /var/www/database.domain.com/.htaccess 
AuthUserFile /var/www/.htpasswd
AuthName "Database Management"
AuthType Basic
require valid-user

Restart Apache to check that everything is working:

sudo service apache2 graceful 

If you visit "http://database.domain.com" you should be able to log in and then use your MySQL username (root) and password (root_db_password) to manage your MySQL databases.

Install PostfixAdmin

This is the tool that we will use to manage our e-mail domains / users and it must be installed at this stage because it dictates the structure of the database that Postfix and Dovecot will use for e-mail delivery.

We need to create a MySQL database and user for this. This can either be done through phpMyAdmin or via the MySQL command prompt, either way the 4 commands are the same.

mysql -uroot -proot_db_password
CREATE USER 'vmail'@'%' IDENTIFIED BY 'vmail_db_password'; 
GRANT USAGE ON * . * TO 'vmail'@'%' IDENTIFIED BY 'vmail_db_password' WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0; 
CREATE DATABASE IF NOT EXISTS `vmail` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 
GRANT ALL PRIVILEGES ON `vmail` . * TO 'vmail'@'%'; 
exit;

This new site needs a vhost too:

sudo nano /etc/apache2/vhosts/mailadmin.domain.com.conf 
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        DocumentRoot /var/www/mailadmin.domain.com
        ServerName mailadmin.domain.com
        <Directory "/var/www/mailadmin.domain.com/">
                Options -Indexes FollowSymLinks
                AllowOverride AuthConfig
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>

Get the actual program, extract it and edit its settings. You may need to customise this link I'm afraid because it changes occasionally!

cd /var/www 
sudo wget "http://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-3.0/postfixadmin-3.0.tar.gz" -O /var/www/postfixadmin.tar.gz 
sudo tar xvfz /var/www/postfixadmin.tar.gz 
sudo rm /var/www/postfixadmin.tar.gz 
sudo mv /var/www/postfixadmin-3.0 /var/www/mailadmin.domain.com 
sudo nano /var/www/mailadmin.domain.com/config.inc.php 
[...]
$CONF['configured'] = true;
[...]
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'vmail';
$CONF['database_password'] = 'vmail_db_password';
$CONF['database_name'] = 'vmail';
$CONF['database_prefix'] = '';
[...]
$CONF['admin_email'] = 'postmaster@domain.com';
[...]
$CONF['default_aliases'] = array (
    'abuse' => 'abuse@domain.com',
    'hostmaster' => 'hostmaster@domain.com',
    'postmaster' => 'postmaster@domain.com',
    'webmaster' => 'webmaster@domain.com'
);
[...]

Now we need to tell the PostfixAdmin directory to use our general .htpasswd file:

sudo nano /var/www/mailadmin.domain.com/.htaccess 
AuthUserFile /var/www/.htpasswd
AuthName "E-mail Management"
AuthType Basic
require valid-user

Restart Apache to apply the changes:

sudo service apache2 graceful 

You now need to visit "http://mailadmin.domain.com/setup.php" to install the database tables, then enter a password to generate a hash. Please copy this value, but do not proceed with any further prompts. Instead return to the terminal window:

sudo nano /var/www/mailadmin.domain.com/config.inc.php 
[...]
$CONF['setup_password'] = 'the password hash';
[...]

Clean up any temporary files left by our editor:

sudo rm /var/www/database.domain.com/config.inc.php~ 

Now return to "http://mailadmin.domain.com/setup.php", but don't refresh, and configure a system admin user using the un-hashed set-up password and whatever details you wish for the account. Then visit "http://mailadmin.domain.com/" and login as your admin user.

Visit the domain list, add domain.com as a new domain and create any e-mail addresses that you want here (e.g. info@domain.com, your_name@domain.com etc). You should also create the four default accounts (webmaster@domain.com, postmaster@domain.com, abuse@domain.com and hostmaster@domain.com) for it too. These can just be aliases to another account if you wish, but it's a good idea to have them set up.

Create SSL certificates (for Postfix / Dovecot)

In order to accept secure connections we need to generate some self signed certificates for e-mail use. If you wish you can use official certificates, but that is outside the scope of this guide.

cd /home/ubuntu/smtp-certificates 
sudo openssl req -new -outform PEM -out smtpd.cert -newkey rsa:2048 -nodes -keyout smtpd.key -keyform PEM -days 3650 -x509 
Country Name (2 letter code) [AU]: UK
State or Province Name (full name) [Some-State]: Greater London
Locality Name (eg, city) []: London
Organization Name (eg, company) [Internet Widgits Pty Ltd]: Company Name
Organizational Unit Name (eg, section) []: Trading Name
Common Name (eg, YOUR name) []: subdomain.domain.com
Email Address []: postmaster@domain.com

Create a self-singed root certificate.

sudo openssl req -new -x509 -extensions v3_ca -keyout cakey.pem -out cacert.pem -days 3650 
sudo chmod 0640 /home/ubuntu/smtp-certificates/smtpd.key 
sudo chmod 0640 /home/ubuntu/smtp-certificates/cakey.pem 

For far more detail on this process please visit Ubuntu's own guide on the subject.

Configure Postfix

Postfix will be our mail server, replacing the ancient Sendmail. Configuration is fairly short, but getting it right is a bit of a black art. To keep our database configuration files contained we'll put them in a single folder that Postfix owns:

sudo mkdir /etc/postfix/sql 
sudo chown postfix:postfix /etc/postfix/sql 
sudo chmod 0750 /etc/postfix/sql 

What we are essentially doing is using MySQL to determine what happens to the mail. There are 8 different possibilities for this. First up is the relay domains rule, which tells Postfix which domains are to have their mail passed on to another server.

sudo nano /etc/postfix/sql/relay_domains.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT domain FROM domain WHERE domain = '%s' AND backupmx = '1'

We then define a rule for domain forwarding catch-all, e.g. anything@my-domain.com -> anything@mydomain.com:

sudo nano /etc/postfix/sql/virtual_alias_domain_catchall_maps.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT goto FROM alias, alias_domain WHERE alias_domain = '%d' AND address = CONCAT('@', target_domain) AND alias.active = '1' AND alias_domain.active = '1'

We can also define a rule for account forwarding for certain addresses within a domain, e.g. userX@my-domain.com may be forwarded to userX@mydomain.com, but userY@my-domain.com may or may not be forwarded. This query selects the correct mailbox directory for that scenario:

sudo nano /etc/postfix/sql/virtual_alias_domain_mailbox_maps.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT CONCAT(domain, '/', maildir) FROM mailbox, alias_domain WHERE alias_domain = '%d' AND username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = '1' AND alias_domain.active = '1'

Continuing from the previous rule we also need a query to select the account that a user on an aliased domain will be sent to:

sudo nano /etc/postfix/sql/virtual_alias_domain_maps.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT goto FROM alias, alias_domain WHERE alias_domain = '%d' AND address = CONCAT('%u', '@', target_domain) AND alias.active = '1' AND alias_domain.active = '1'

We can also just define simple aliases on a per address basis, e.g. userX@my-domain.com could be forwarded to anything@yahoo.com, or in fact multiple recipients. This query handles those.

sudo nano /etc/postfix/sql/virtual_alias_maps.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT goto FROM alias WHERE address = '%s' AND active = '1'

We need a simple query to select the mailbox for a certain domain:

sudo nano /etc/postfix/sql/virtual_mailbox_domains.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT domain FROM domain WHERE domain = '%s' AND active = '1'

If the mailboxes are defined with disk space quotas we need to be able to access them:

sudo nano /etc/postfix/sql/virtual_mailbox_limit_maps.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT quota FROM mailbox WHERE username = '%s' AND active = '1'

And finally just get a mailbox location for a certain user on a domain:

sudo nano /etc/postfix/sql/virtual_mailbox_maps.cf 
user		= vmail
password	= vmail_db_password
hosts		= localhost
dbname		= vmail
query		= SELECT CONCAT(domain, '/', maildir) FROM mailbox WHERE username = '%s' AND active = '1'

We can then configure the two main Postfix files:

sudo nano /etc/postfix/main.cf 
smtpd_banner				= $myhostname ESMTP $mail_name
biff					= no
append_dot_mydomain			= no
myhostname				= subdomain.domain.com
alias_maps				= hash:/etc/aliases
alias_database				= hash:/etc/aliases
myorigin				= /etc/mailname
mydestination				= subdomain.domain.com, localhost.domain.com, localhost
relayhost				=
mynetworks				= 127.0.0.0/8
mailbox_size_limit			= 0
recipient_delimiter			= +
inet_interfaces				= all
message_size_limit			= 50000000

# Virtual mail set-up
virtual_mailbox_base			= /home/vmail
virtual_uid_maps			= static:5000
virtual_gid_maps			= static:5000
virtual_transport			= dovecot
dovecot_destination_recipient_limit	= 1
virtual_create_maildirsize		= yes
virtual_mailbox_extended		= yes
relay_domains				= proxy:mysql:/etc/postfix/sql/relay_domains.cf
virtual_mailbox_domains			= proxy:mysql:/etc/postfix/sql/virtual_mailbox_domains.cf
virtual_alias_maps			= proxy:mysql:/etc/postfix/sql/virtual_alias_maps.cf,
					  proxy:mysql:/etc/postfix/sql/virtual_alias_domain_maps.cf,
					  proxy:mysql:/etc/postfix/sql/virtual_alias_domain_catchall_maps.cf
virtual_mailbox_maps			= proxy:mysql:/etc/postfix/sql/virtual_mailbox_maps.cf,
					  proxy:mysql:/etc/postfix/sql/virtual_alias_domain_mailbox_maps.cf
virtual_mailbox_limit_maps		= proxy:mysql:/etc/postfix/sql/virtual_mailbox_limit_maps.cf
virtual_mailbox_limit_override		= yes
virtual_maildir_limit_message		= Sorry, that user is over their e-mail quota. Please try again later.
virtual_overquota_bounce		= yes

# SASL Related
smtpd_sasl_type				= dovecot
smtpd_sasl_path				= private/auth
smtpd_sasl_local_domain			=
smtpd_sasl_auth_enable			= yes
smtpd_sasl_security_options		= noanonymous
broken_sasl_auth_clients		= yes
smtpd_sasl2_auth_enable			= yes
smtpd_sasl_authenticated_header		= yes
smtpd_sasl_tls_security_options		= noanonymous
#smtpd_sasl_exceptions_networks		= $mynetworks

# TLS Commands
smtpd_tls_auth_only			= no
smtp_tls_security_level			= may
smtpd_tls_security_level		= may
smtp_tls_note_starttls_offer		= yes
smtpd_tls_cert_file			= /home/ubuntu/smtp-certificates/smtpd.cert
smtpd_tls_key_file			= /home/ubuntu/smtp-certificates/smtpd.key
smtpd_tls_CAfile			= /home/ubuntu/smtp-certificates/cacert.pem
smtpd_tls_loglevel			= 1
smtpd_tls_received_header		= yes
smtpd_tls_session_cache_timeout		= 3600s
tls_random_source			= dev:/dev/urandom

# Include Spam settings
disable_vrfy_command			= yes
smtpd_delay_reject			= yes
smtpd_helo_required			= yes
smtpd_helo_restrictions			= permit_mynetworks,
					  reject_non_fqdn_hostname,
					  reject_invalid_hostname,
					  permit

smtpd_recipient_restrictions		= reject_unauth_pipelining,
					  permit_mynetworks,
					  permit_sasl_authenticated,
					  reject_unauth_destination,
					  reject_invalid_hostname,
					  reject_non_fqdn_hostname,
					  reject_non_fqdn_sender,
					  reject_non_fqdn_recipient,
					  reject_unknown_sender_domain,
					  reject_unknown_recipient_domain,
					  reject_rbl_client zen.spamhaus.org,
					  reject_rbl_client dnsbl.sorbs.net,
					  reject_rbl_client dsn.rfc-ignorant.org,
					  reject_rbl_client bl.spamcop.net,
					  permit

smtpd_error_sleep_time			= 1s
smtpd_soft_error_limit			= 10
smtpd_hard_error_limit			= 20

content_filter				= smtp-amavis:[127.0.0.1]:10024
sudo nano /etc/postfix/master.cf 
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
smtp      inet  n       -       n       -       -       smtpd
smtps     inet  n       -       -       -       -       smtpd
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o broken_sasl_auth_clients=yes
pickup    fifo  n       -       -       60      1       pickup
  -o content_filter=
  -o receive_override_options=no_header_body_checks
cleanup   unix  n       -       -       -       0       cleanup
qmgr      fifo  n       -       n       300     1       qmgr
tlsmgr    unix  -       -       n       1000?   1       tlsmgr
rewrite   unix  -       -       -       -       -       trivial-rewrite
bounce    unix  -       -       -       -       0       bounce
defer     unix  -       -       -       -       0       bounce
trace     unix  -       -       -       -       0       bounce
verify    unix  -       -       -       -       1       verify
flush     unix  n       -       -       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       -       -       -       smtp
relay     unix  -       -       -       -       -       smtp
	-o smtp_fallback_relay=
showq     unix  n       -       -       -       -       showq
error     unix  -       -       -       -       -       error
retry     unix  -       -       -       -       -       error
discard   unix  -       -       -       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       -       -       -       lmtp
anvil     unix  -       -       -       -       1       anvil
scache    unix  -       -       -       -       1       scache
maildrop  unix  -       n       n       -       -       pipe
  flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
uucp      unix  -       n       n       -       -       pipe
  flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
ifmail    unix  -       n       n       -       -       pipe
  flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
bsmtp     unix  -       n       n       -       -       pipe
  flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
scalemail-backend unix	-	n	n	-	2	pipe
  flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
mailman   unix  -       n       n       -       -       pipe
  flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
  ${nexthop} ${user}
dovecot   unix  -       n       n       -       -       pipe
  flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -d ${recipient}
smtp-amavis     unix    -       -       -       -       2       smtp
        -o smtp_data_done_timeout=1200
        -o smtp_send_xforward_command=yes
        -o disable_dns_lookups=yes
        -o max_use=20
127.0.0.1:10025 inet    n       -       -       -       -       smtpd
        -o content_filter=
        -o local_recipient_maps=
        -o relay_recipient_maps=
        -o smtpd_restriction_classes=
        -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 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

That is all for Postfix, but before we can use the mail system we need to tweak Dovecot.

Configure Dovecot

Dovecot will be our POP3(S) / IMAP(S) server and handle our SASL authentication. Again, there's not too much to set up, but if you get a setting or two wrong, it simply won't work properly!

We are using Dovecot to handle our authentication, and again we will do this from our MySQL tables to ease management:

sudo nano /etc/dovecot/dovecot-sql.conf 
driver			= mysql
connect			= host=127.0.0.1 dbname=vmail user=vmail password=vmail_db_password
user_query		= SELECT concat('/home/vmail/', domain, '/', maildir) as home, concat('maildir:/home/vmail/', domain, '/', maildir) as mail, 5000 AS uid, 5000 AS gid, concat('maildir:storage=', quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1'
default_pass_scheme	= MD5
password_query		= SELECT username as user, password FROM mailbox WHERE username = '%u' AND active = '1'

And then we need to configure Dovecot's main configuration file:

sudo nano /etc/dovecot/dovecot.conf 
protocols		= pop3 pop3s imap imaps
log_path		= /var/log/dovecot/log
info_log_path		= /var/log/dovecot/info
log_timestamp		= "%Y-%m-%d %H:%M:%S "
ssl			= yes
ssl_cert_file		= /home/ubuntu/smtp-certificates/smtpd.cert
ssl_key_file		= /home/ubuntu/smtp-certificates/smtpd.key
disable_plaintext_auth	= no
login_dir		= /var/run/dovecot/login
login_chroot		= yes
login_user		= dovecot
login_greeting		= Dovecot ready.
mail_location		= maildir:/home/vmail/%d/%u
mail_debug		= no

auth_executable		= /usr/lib/dovecot/dovecot-auth
auth_verbose		= no
auth_debug		= no
auth_debug_passwords	= no

protocol imap {
  login_executable	= /usr/lib/dovecot/imap-login
  mail_executable	= /usr/lib/dovecot/imap
  imap_max_line_length	= 65536
}

protocol pop3 {
  login_executable	= /usr/lib/dovecot/pop3-login
  mail_executable	= /usr/lib/dovecot/pop3
  pop3_uidl_format	= %08Xu%08Xv
}

protocol lda {
  log_path		= /var/log/dovecot/lda
  auth_socket_path	= /var/run/dovecot/auth-master
  postmaster_address	= postmaster@domain.com
}

auth default {
  mechanisms		= plain login

  passdb sql {
    args		= /etc/dovecot/dovecot-sql.conf
  }

  userdb sql {
    args		= /etc/dovecot/dovecot-sql.conf
  }

  socket listen {
    master {
      path		= /var/run/dovecot/auth-master
      mode		= 0600
      user		= vmail
      group		= vmail
   }

    client {
       path		= /var/spool/postfix/private/auth
       mode		= 0660
       user		= postfix
       group		= postfix
    }
  }
}

There are some files designed to be automatically included. They shouldn't be, but just in case:

sudo mv /etc/dovecot/conf.d/01-dovecot-postfix.conf 01-dovecot-postfix.conf.bak 
sudo mv /etc/dovecot/auth.d/01-dovecot-postfix.auth 01-dovecot-postfix.auth.bak 

Before we can use the mail system the new settings need to be loaded in.

sudo service dovecot restart 
sudo service postfix restart 

Configure Squirrelmail (optional)

Squirrelmail is a convenient web-mail client. It will need a vhost file:

sudo nano /etc/apache2/vhosts/mail.domain.com.conf 
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        DocumentRoot /var/www/mail.domain.com
        ServerName mail.domain.com
        <Directory "/var/www/mail.domain.com/">
                Options -Indexes FollowSymLinks
                AllowOverride AuthConfig
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>

Get the actual program, extract it and edit its settings. You may need to check this link I'm afraid because it may change.

cd /var/www 
sudo wget "http://downloads.sourceforge.net/project/squirrelmail/stable/1.4.22/squirrelmail-webmail-1.4.22.tar.gz" -O /var/www/squirrelmail.tar.gz 
sudo tar xvfz /var/www/squirrelmail.tar.gz 
sudo rm /var/www/squirrelmail.tar.gz 
sudo mv /var/www/squirrelmail-1.4.22 /var/www/mail.domain.com 

Squirrelmail needs a private folder, so we need to create and secure it:

sudo mkdir /var/www/private 
sudo mkdir /var/www/private/mail.domain.com 
sudo mkdir /var/www/private/mail.domain.com/data 
sudo mkdir /var/www/private/mail.domain.com/attachments 
sudo chown -R root:www-data /var/www/private/mail.domain.com  
sudo chmod -R 0730 /var/www/private/mail.domain.com 

We can now configure it. There is a Perl script to do this, but I find it faster just to edit the settings manually.

sudo mv /var/www/mail.domain.com/config/config_default.php /var/www/mail.domain.com/config/config.php 
sudo nano /var/www/mail.domain.com/config/config.php 
[...]
$org_name = 'Your Website Name';
[...]
$domain = 'domain.com';
[...]
$smtp_auth_mech = 'plain';
[...]
$data_dir = '/var/www/private/mail.domain.com/data/';
[...]
$attachment_dir = '/var/www/private/mail.domain.com/attachments/';
[...]
$default_charset = 'utf-8';
[...]

Clean up any temporary files left by our editor:

sudo rm /var/www/mail.domain.com/config/config.php~ 

To apply the changes Apache needs to have the data reloaded:

sudo service apache2 graceful 

If you visit "http://mail.domain.com" you will be able to send and receive e-mails. Of course this is no replacement for a mail client, but it's useful to have on occasion!

Configure Subversion (optional)

We will bring your website (www.domain.com) under version control. This step will create the repositories and set their permissions to allow the web user access.

It's unlikely that you'll want anyone to be able to access your files so we can generate a password file for SVN access:

sudo htpasswd -c /svn-repositories/passwd svn-username 

We can then create a folder, a repository within the folder and import our files:

sudo mkdir /svn-repositories/www.domain.com 
sudo svnadmin create /svn-repositories/www.domain.com 
sudo svn import /var/www/www.domain.com file:////svn-repositories/www.domain.com 

This repository needs to be owned by the web user and be granted certain permissions:

sudo chown -R www-data:www-data /svn-repositories/www.domain.com 
sudo chmod -R g+rws /svn-repositories/www.domain.com 

We can then remove our existing site directory and recreate it from our repository:

cd /var/www 
sudo rm -rf /var/www/www.domain.com/ 
sudo svn co file:///svn-repositories/www.domain.com 
sudo chown -R www-data:www-data /var/www/www.domain.com/ 

We can also create a trigger so that every time you commit a change it goes live. This probably wants some modification for production use, i.e. Local -> Staging -> Production with the later step conducted manually.

sudo nano /svn-repositories/www.domain.com/hooks/post-commit 
#!/bin/bash

cd /var/www/www.domain.com
/usr/bin/svn update

This also needs the correct permissions to run:

sudo chown www-data:www-data /svn-repositories/www.domain.com/hooks/post-commit 
sudo chmod +x /svn-repositories/www.domain.com/hooks/post-commit 

We also need to add this repository to a vhost file so that you can check it out. I would suggest a new one for this purpose. Multiple locations can be added, but for now we just have one.

sudo nano /etc/apache2/vhosts/svn.domain.com.conf 
<VirtualHost *:80>
        ServerAdmin webmaster@domain.com
        DocumentRoot /var/www/default/
        ServerName svn.domain.com
        <Directory "/var/www/default/">
                Options -Indexes FollowSymLinks
                AllowOverride FileInfo
                Order allow,deny
                Allow from all
        </Directory>
        <Location /svn/www.domain.com>
        	DAV svn
        	SVNPath /svn-repositories/www.domain.com
        	AuthType Basic
        	AuthName "www.domain.com subversion repository"
        	AuthUserFile /svn-repositories/passwd
        	Require valid-user
        </Location>
</VirtualHost>

Before this can be tested Apache needs to be reloaded:

sudo service apache2 graceful 

You can now check out the repository from "http://svn.domain.com/svn/www.domain.com" using the command line or client (Linux / OSX) or a client such as Tortoise SVN on Windows. You will need to validate using the username / password that you created in this step. If you would rather use the .htpasswd file used for the database and mail admin just update the vhost file with that location.

Download this guide

To make this guide more useful I've added a feature to allow you to save it offline in a simple HTML format. If you have not customised this guide to your own values you may wish to do so here before you download it. There are a few options here:

Guide contents

  1. Hosting a website on Amazon EC2 - The goals and assumptions of this guide
  2. Preparing required tools - Create an AWS account, configure Elastic Fox and add an SSH tool
  3. Customise this guide - Allow all commands to be tailored to you (optional)
  4. Core software installation - Install some common software to the server image
  5. Depending upon your chosen configuration there is a choice here:
    1. Create and attach new EBS volumes - New server that you may want to split in future
    2. Attach existing EBS volumes - If you have used this guide before and have EBS volumes
    3. No attached EBS volumes - If you are not using the cloud or don't want to use them
  6. Depending upon your chosen configuration there is another choice here:
    1. Software Configuration - Set up the system to work as a multi-function server (from 5a or 5c)
    2. Software Configuration from existing EBS volumes - Use settings from EBS volumes (from 5b)
  7. Backing up and clean up - Configure Crons, log rotation etc