Complete Guide To Setup Email Server On Debian

I’ve published simple php script for managing email accounts to the Github.
https://github.com/habibzadeh/php-mailadmin

We’re going to setup a secure mail server with Postfix, Dovecot, and MySQL on Debian or Ubuntu. Specifically, create new user mailboxes and send or receive email for configured domains.

Prerequisites

  • Debian based system with stable and fast internet connection.
  • Ensure that the iptables firewall is not blocking any of the standard mail ports (25, 465, 587, 110, 995, 143, and 993). If using a different form of firewall, confirm that it is not blocking any of the needed ports either.
  • Setup DNS and MX records for your domains, you need to ensure your domains MX records pointed to your email server public IP address.

Installing an SSL Certificate

Dovecot offers a default self-signed certificate for free. This certificate encrypts the mail connections similar to a purchased certificate. However, the email users receive warnings about the certificate when they attempt to set up their email accounts. Optionally, purchase and configure a commercial SSL certificate to avoid the warnings.

As of version 2.2.13-7, Dovecot no longer provides a default SSL certificate. This affects Debian 8 users, and means that if you wish to use SSL encryption (reccomended), you must generate your own self-signed certificate or use a trusted certificate from a Certificate Authority.
Many email service providers such as Gmail will only accept commercial SSL certificates for secure IMAP/POP3 connections.

Installing Packages

  1. Log in as the root user:
    su
  2. Install the required packages:
    apt-get install postfix postfix-mysql dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql mysql-server

Follow the prompt to type in a secure MySQL password and to select the type of mail server you wish to configure. Select Internet Site. The System Mail Name should be the FQDN.

MySQL Database Setup

  1. Create a new database:
    mysqladmin -p create mailserver
  2. Enter the MySQL root password.
  3. Log in to MySQL:
    mysql -p mailserver
  4. Create the MySQL user and grant the new user permissions over the database. Replace mailuserpass with a secure password:
    GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1' IDENTIFIED BY 'mailuserpass';
  5. Flush the MySQL privileges to apply the change:
    FLUSH PRIVILEGES;
  6. Create a table for the domains:
        
        CREATE TABLE `virtual_domains` (
            `id` int(11) NOT NULL auto_increment,
            `name` varchar(50) NOT NULL,
            PRIMARY KEY (`id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
        
        
  7. Create a table for all of the email addresses and passwords:
        
        CREATE TABLE `virtual_users` (
            `id` int(11) NOT NULL auto_increment,
            `domain_id` int(11) NOT NULL,
            `password` varchar(106) NOT NULL,
            `email` varchar(100) NOT NULL,
            PRIMARY KEY (`id`),
            UNIQUE KEY `email` (`email`),
            FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
        
        
  8. Create a table for the email aliases:
        
        CREATE TABLE `virtual_aliases` (
            `id` int(11) NOT NULL auto_increment,
            `domain_id` int(11) NOT NULL,
            `source` varchar(100) NOT NULL,
            `destination` varchar(100) NOT NULL,
            PRIMARY KEY (`id`),
            FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
        
        

Adding Data

Now that the database and tables have been created, add some data to MySQL.

  1. Add the domains to the virtual_domains table. Replace the values for example.com and hostname with your own settings.
        
        INSERT INTO `mailserver`.`virtual_domains`
            (`id` ,`name`)
        VALUES
            ('1', 'example.com'),
            ('2', 'hostname.example.com'),
            ('3', 'hostname'),
            ('4', 'localhost.example.com');
        
        

    Note which id goes with which domain, the id is necessary for the next two steps.

  2. Add email addresses to the virtual_users table. Replace the email address values with the addresses that you wish to configure on the mailserver. Replace the password values with strong passwords.
        
        INSERT INTO `mailserver`.`virtual_users`
            (`id`, `domain_id`, `password` , `email`)
        VALUES
            ('1', '1', ENCRYPT('password', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '[email protected]'),
            ('2', '1', ENCRYPT('password', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '[email protected]');
        
        
  3. To set up an email alias, add it to the virtual_aliases table.
        
        INSERT INTO `mailserver`.`virtual_aliases`
            (`id`, `domain_id`, `source`, `destination`)
        VALUES
            ('1', '1', '[email protected]', '[email protected]');
        
        

That’s it! Now you’re ready to verify that the data was successfully added to MySQL.

Testing

Since all of the information has been entered into MySQL, check that the data is there.

  1. Check the contents of the virtual_domains table:
    SELECT * FROM mailserver.virtual_domains;
  2. Verify that you see the following output:
        
        +----+-----------------------+
        | id | name                  |
        +----+-----------------------+
        |  1 | example.com           |
        |  2 | hostname.example.com  |
        |  3 | hostname              |
        |  4 | localhost.example.com |
        +----+-----------------------+
        4 rows in set (0.00 sec)
        
        
  3. Check the virtual_users table:
    SELECT * FROM mailserver.virtual_users;
  4. Verify the following output, the hashed passwords are longer than they appear below:
        
        +----+-----------+-------------------------------------+--------------------+
        | id | domain_id | password                            | email              |
        +----+-----------+-------------------------------------+--------------------+
        |  1 |         1 | $6$574ef443973a5529c20616ab7c6828f7 | [email protected] |
        |  2 |         1 | $6$030fa94bcfc6554023a9aad90a8c9ca1 | [email protected] |
        +----+-----------+-------------------------------------+--------------------+
        2 rows in set (0.01 sec)
        
        
  5. Check the virtual_users table:
    SELECT * FROM mailserver.virtual_aliases;
  6. Verify the following output:
        
        +----+-----------+-------------------+--------------------+
        | id | domain_id | source            | destination        |
        +----+-----------+-------------------+--------------------+
        |  1 |         1 | [email protected] | [email protected] |
        +----+-----------+-------------------+--------------------+
        1 row in set (0.00 sec)
        
        
  7. If everything outputs correctly, you’re done with MySQL! Exit MySQL:
    exit

Postfix

Next, set up Postfix so the server can accept incoming messages for the domains.

  1. Immediately make a copy of the default Postfix configuration file in case you need to revert to the default configuration:
    cp /etc/postfix/main.cf /etc/postfix/main.cf.orig
  2. Edit the /etc/postfix/main.cf file to match the following. Ensure that occurrences of example.com are replaced with the domain name. Also, replace hostname with the system’s hostname on line 44.

    File : /etc/postfix/main.cf

        
        # See /usr/share/postfix/main.cf.dist for a commented, more complete version
    
        # Debian specific:  Specifying a file name will cause the first
        # line of that file to be used as the name.  The Debian default
        # is /etc/mailname.
        #myorigin = /etc/mailname
    
        smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
        biff = no
    
        # appending .domain is the MUA's job.
        append_dot_mydomain = no
    
        # Uncomment the next line to generate "delayed mail" warnings
        #delay_warning_time = 4h
    
        readme_directory = no
    
        # TLS parameters
        #smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
        #smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
        #smtpd_use_tls=yes
        #smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
        #smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
    
        smtpd_tls_cert_file=/etc/dovecot/dovecot.pem
        smtpd_tls_key_file=/etc/dovecot/private/dovecot.pem
        smtpd_use_tls=yes
        smtpd_tls_auth_only = yes
    
        #Enabling SMTP for authenticated users, and handing off authentication to Dovecot
        smtpd_sasl_type = dovecot
        smtpd_sasl_path = private/auth
        smtpd_sasl_auth_enable = yes
    
        smtpd_recipient_restrictions =
        permit_sasl_authenticated,
        permit_mynetworks,
        reject_unauth_destination
    
        # See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
        # information on enabling SSL in the smtp client.
    
        myhostname = hostname.example.com
        alias_maps = hash:/etc/aliases
        alias_database = hash:/etc/aliases
        myorigin = /etc/mailname
        #mydestination = example.com, hostname.example.com, localhost.example.com, localhost
        mydestination = localhost
        relayhost =
        mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
        mailbox_size_limit = 0
        recipient_delimiter = +
        inet_interfaces = all
    
        #Handing off local delivery to Dovecot's LMTP, and telling it where to store mail
        virtual_transport = lmtp:unix:private/dovecot-lmtp
    
        #Virtual domains, users, and aliases
        virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
        virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
        virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf,
        mysql:/etc/postfix/mysql-virtual-email2email.cf
        
        
  3. Create the file for virtual domains. Ensure that you change the password for the mailuser account. If you used a different user, database name, or table name, customize those settings as well.

    File : /etc/postfix/mysql-virtual-mailbox-domains.cf

        
        user = mailuser
        password = mailuserpass
        hosts = 127.0.0.1
        dbname = mailserver
        query = SELECT 1 FROM virtual_domains WHERE name='%s'
        
        
  4. Create the /etc/postfix/mysql-virtual-mailbox-maps.cf file, and enter the following values. Make sure you use the mailuser’s password and make any other changes as needed.

    File : /etc/postfix/mysql-virtual-alias-maps.cf

        
        user = mailuser
        password = mailuserpass
        hosts = 127.0.0.1
        dbname = mailserver
        query = SELECT destination FROM virtual_aliases WHERE source='%s'
        
        
  5. Create the /etc/postfix/mysql-virtual-email2email.cf file and enter the following values. Again, make sure you use the mailuser’s password, and make any other changes as necessary.

    File : /etc/postfix/mysql-virtual-email2email.cf

        
        user = mailuser
        password = mailuserpass
        hosts = 127.0.0.1
        dbname = mailserver
        query = SELECT email FROM virtual_users WHERE email='%s'
        
        
  6. Save the changes you’ve made to the /etc/postfix/mysql-virtual-email2email.cf file, and restart Postfix:
    service postfix restart
  7. Enter the following command to ensure that Postfix can find the first domain. Be sure to replace example.com with the first virtual domain. The command should return 1 if it is successful.
    postmap -q example.com mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
  8. Test Postfix to verify that it can find the first email address in the MySQL table. Enter the following command, replacing [email protected] with the first email address in the MySQL table. You should again receive 1 as the output:
    postmap -q [email protected] mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
  9. Test Postfix to verify that it can find the aliases by entering the following command. Be sure to replace [email protected] with the actual alias you entered:
    postmap -q [email protected] mysql:/etc/postfix/mysql-virtual-alias-maps.cf

    This should return the email address to which the alias forwards, which is [email protected] in this example.

  10. Make a copy of the /etc/postfix/master.cf file:
    cp /etc/postfix/master.cf /etc/postfix/master.cf.orig
  11. Open the configuration file for editing and uncomment the two lines starting with submission and smtps and the block of lines starting with -o after each. The first section of the /etc/postfix/master.cf file should resemble the following:

    File : /etc/postfix/master.cf

        
        # Postfix master process configuration file.  For details on the format
        # of the file, see the master(5) manual page (command: "man 5 master").
        #
        # Do not forget to execute "postfix reload" after editing this file.
        #
        # ==========================================================================
        # service type  private unpriv  chroot  wakeup  maxproc command + args
        #               (yes)   (yes)   (yes)   (never) (100)
        # ==========================================================================
        smtp      inet  n       -       -       -       -       smtpd
        #smtp      inet  n       -       -       -       1       postscreen
        #smtpd     pass  -       -       -       -       -       smtpd
        #dnsblog   unix  -       -       -       -       0       dnsblog
        #tlsproxy  unix  -       -       -       -       0       tlsproxy
        submission inet n       -       -       -       -       smtpd
        -o syslog_name=postfix/submission
        -o smtpd_tls_security_level=encrypt
        -o smtpd_sasl_auth_enable=yes
        -o smtpd_client_restrictions=permit_sasl_authenticated,reject
        -o milter_macro_daemon_name=ORIGINATING
        smtps     inet  n       -       -       -       -       smtpd
        -o syslog_name=postfix/smtps
        -o smtpd_tls_wrappermode=yes
        -o smtpd_sasl_auth_enable=yes
        -o smtpd_client_restrictions=permit_sasl_authenticated,reject
        -o milter_macro_daemon_name=ORIGINATING
        
        
  12. Restart Postfix by entering the following command:
    service postfix restart

Congratulations! You have successfully configured Postfix.

Dovecot

Dovecot allows users to log in and check their email using POP3 and IMAP. In this section, configure Dovecot to force users to use SSL when they connect so that their passwords are never sent to the server in plain text.

  1. Copy all of the configuration files so that you can easily revert back to them if needed:
        
        cp /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.orig
        cp /etc/dovecot/conf.d/10-mail.conf /etc/dovecot/conf.d/10-mail.conf.orig
        cp /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.orig
        cp /etc/dovecot/dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext.orig
        cp /etc/dovecot/conf.d/10-master.conf /etc/dovecot/conf.d/10-master.conf.orig
        cp /etc/dovecot/conf.d/10-ssl.conf /etc/dovecot/conf.d/10-ssl.conf.orig
        
        
  2. Open the main configuration file and edit the contents to match the following:

    File : /etc/dovecot/dovecot.conf

        
        ## Dovecot configuration file
    
        # If you're in a hurry, see http://wiki2.dovecot.org/QuickConfiguration
    
        # "doveconf -n" command gives a clean output of the changed settings. Use it
        # instead of copy&pasting files when posting to the Dovecot mailing list.
    
        # '#' character and everything after it is treated as comments. Extra spaces
        # and tabs are ignored. If you want to use either of these explicitly, put the
        # value inside quotes, eg.: key = "# char and trailing whitespace  "
    
        # Default values are shown for each setting, it's not required to uncomment
        # those. These are exceptions to this though: No sections (e.g. namespace {})
        # or plugin settings are added by default, they're listed only as examples.
        # Paths are also just examples with the real defaults being based on configure
        # options. The paths listed here are for configure --prefix=/usr
        # --sysconfdir=/etc --localstatedir=/var
    
        # Enable installed protocols
        !include_try /usr/share/dovecot/protocols.d/*.protocol
        protocols = imap pop3 lmtp
    
        # A comma separated list of IPs or hosts where to listen in for connections.
        # "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.
        # If you want to specify non-default ports or anything more complex,
        # edit conf.d/master.conf.
        #listen = *, ::
    
        # Base directory where to store runtime data.
        #base_dir = /var/run/dovecot/
    
        # Name of this instance. Used to prefix all Dovecot processes in ps output.
        #instance_name = dovecot
    
        # Greeting message for clients.
        #login_greeting = Dovecot ready.
    
        # Space separated list of trusted network ranges. Connections from these
        # IPs are allowed to override their IP addresses and ports (for logging and
        # for authentication checks). disable_plaintext_auth is also ignored for
        # these networks. Typically you'd specify the IMAP proxy servers here.
        #login_trusted_networks =
    
        # Sepace separated list of login access check sockets (e.g. tcpwrap)
        #login_access_sockets =
    
        # Show more verbose process titles (in ps). Currently shows user name and
        # IP address. Useful for seeing who are actually using the IMAP processes
        # (eg. shared mailboxes or if same uid is used for multiple accounts).
        #verbose_proctitle = no
    
        # Should all processes be killed when Dovecot master process shuts down.
        # Setting this to "no" means that Dovecot can be upgraded without
        # forcing existing client connections to close (although that could also be
        # a problem if the upgrade is e.g. because of a security fix).
        #shutdown_clients = yes
    
        # If non-zero, run mail commands via this many connections to doveadm server,
        # instead of running them directly in the same process.
        #doveadm_worker_count = 0
        # UNIX socket or host:port used for connecting to doveadm server
        #doveadm_socket_path = doveadm-server
    
        # Space separated list of environment variables that are preserved on Dovecot
        # startup and passed down to all of its child processes. You can also give
        # key=value pairs to always set specific settings.
        #import_environment = TZ
    
        ##
        ## Dictionary server settings
        ##
    
        # Dictionary can be used to store key=value lists. This is used by several
        # plugins. The dictionary can be accessed either directly or though a
        # dictionary server. The following dict block maps dictionary names to URIs
        # when the server is used. These can then be referenced using URIs in format
        # "proxy::<name>".
    
        dict {
        #quota = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
        #expire = sqlite:/etc/dovecot/dovecot-dict-sql.conf.ext
        }
    
        # Most of the actual configuration gets included below. The filenames are
        # first sorted by their ASCII value and parsed in that order. The 00-prefixes
        # in filenames are intended to make it easier to understand the ordering.
        !include conf.d/*.conf
    
        # A config file can also tried to be included without giving an error if
        # it's not found:
        !include_try local.conf
        
        
  3. Save the changes to the /etc/dovecot/dovecot.conf file.
  4. Open the /etc/dovecot/conf.d/10-mail.conf file. This file controls how Dovecot interacts with the server’s file system to store and retrieve messages.

    Modify the following variables within the configuration file.

    File : /etc/dovecot/conf.d/10-mail.conf

        
        mail_location = maildir:/var/mail/vhosts/%d/%n
        ...
        mail_privileged_group = mail
        
        

    Save the changes.

  5. Enter the following command to verify the permissions for /var/mail:
    ls -ld /var/mail
  6. Verify that the permissions for /var/mail are as follows:
    drwxrwsr-x 2 root mail 4096 Mar  6 15:08 /var/mail
  7. Create the /var/mail/vhosts/ folder and the folder for the domain:
    mkdir -p /var/mail/vhosts/example.com
  8. Create the vmail user with a user and group id of 5000 by entering the following commands, one by one. This user will be in charge of reading mail from the server.
        
        groupadd -g 5000 vmail
        useradd -g vmail -u 5000 vmail -d /var/mail
        
        
  9. Change the owner of the /var/mail/ folder and its contents to belong to vmail:
    chown -R vmail:vmail /var/mail
  10. Open the user authentication file, located in /etc/dovecot/conf.d/10-auth.conf and disable plain-text authentication by uncommenting this line:
    disable_plaintext_auth = yes

    Set the auth_mechanisms by modifying the following line:

    auth_mechanisms = plain login

    Comment out the system user login line:

    #!include auth-system.conf.ext

    Enable MySQL authentication by uncommenting the auth-sql.conf.ext line:

        
        #!include auth-system.conf.ext
        !include auth-sql.conf.ext
        #!include auth-ldap.conf.ext
        #!include auth-passwdfile.conf.ext
        #!include auth-checkpassword.conf.ext
        #!include auth-vpopmail.conf.ext
        #!include auth-static.conf.ext
        
        

    Save the changes to the /etc/dovecot/conf.d/10-auth.conf file.

    File : /etc/dovecot/conf.d/10-auth.conf

        
        ##
        ## Authentication processes
        ##
    
        # Disable LOGIN command and all other plaintext authentications unless
        # SSL/TLS is used (LOGINDISABLED capability). Note that if the remote IP
        # matches the local IP (ie. you're connecting from the same computer), the
        # connection is considered secure and plaintext authentication is allowed.
        disable_plaintext_auth = yes
    
        # Authentication cache size (e.g. 10M). 0 means it's disabled. Note that
        # bsdauth, PAM and vpopmail require cache_key to be set for caching to be used.
        #auth_cache_size = 0
        # Time to live for cached data. After TTL expires the cached record is no
        # longer used, *except* if the main database lookup returns internal failure.
        # We also try to handle password changes automatically: If user's previous
        # authentication was successful, but this one wasn't, the cache isn't used.
        # For now this works only with plaintext authentication.
        #auth_cache_ttl = 1 hour
        # TTL for negative hits (user not found, password mismatch).
        # 0 disables caching them completely.
        #auth_cache_negative_ttl = 1 hour
    
        # Space separated list of realms for SASL authentication mechanisms that need
        # them. You can leave it empty if you don't want to support multiple realms.
        # Many clients simply use the first one listed here, so keep the default realm
        # first.
        #auth_realms =
    
        # Default realm/domain to use if none was specified. This is used for both
        # SASL realms and appending @domain to username in plaintext logins.
        #auth_default_realm =
    
        # List of allowed characters in username. If the user-given username contains
        # a character not listed in here, the login automatically fails. This is just
        # an extra check to make sure user can't exploit any potential quote escaping
        # vulnerabilities with SQL/LDAP databases. If you want to allow all characters,
        # set this value to empty.
        #auth_username_chars = abc[email protected]
    
        # Username character translations before it's looked up from databases. The
        # value contains series of from -> to characters. For example "#@/@" means
        # that '#' and '/' characters are translated to '@'.
        #auth_username_translation =
    
        # Username formatting before it's looked up from databases. You can use
        # the standard variables here, eg. %Lu would lowercase the username, %n would
        # drop away the domain if it was given, or "%n-AT-%d" would change the '@' into
        # "-AT-". This translation is done after auth_username_translation changes.
        #auth_username_format =
    
        # If you want to allow master users to log in by specifying the master
        # username within the normal username string (ie. not using SASL mechanism's
        # support for it), you can specify the separator character here. The format
        # is then <username><separator><master username>. UW-IMAP uses "*" as the
        # separator, so that could be a good choice.
        #auth_master_user_separator =
    
        # Username to use for users logging in with ANONYMOUS SASL mechanism
        #auth_anonymous_username = anonymous
    
        # Maximum number of dovecot-auth worker processes. They're used to execute
        # blocking passdb and userdb queries (eg. MySQL and PAM). They're
        # automatically created and destroyed as needed.
        #auth_worker_max_count = 30
    
        # Host name to use in GSSAPI principal names. The default is to use the
        # name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab
        # entries.
        #auth_gssapi_hostname =
    
        # Kerberos keytab to use for the GSSAPI mechanism. Will use the system
        # default (usually /etc/krb5.keytab) if not specified. You may need to change
        # the auth service to run as root to be able to read this file.
        #auth_krb5_keytab =
    
        # Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and
        # ntlm_auth helper. <doc/wiki/Authentication/Mechanisms/Winbind.txt>
        #auth_use_winbind = no
    
        # Path for Samba's ntlm_auth helper binary.
        #auth_winbind_helper_path = /usr/bin/ntlm_auth
    
        # Time to delay before replying to failed authentications.
        #auth_failure_delay = 2 secs
    
        # Require a valid SSL client certificate or the authentication fails.
        #auth_ssl_require_client_cert = no
    
        # Take the username from client's SSL certificate, using
        # X509_NAME_get_text_by_NID() which returns the subject's DN's
        # CommonName.
        #auth_ssl_username_from_cert = no
    
        # Space separated list of wanted authentication mechanisms:
        #   plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey
        #   gss-spnego
        # NOTE: See also disable_plaintext_auth setting.
        auth_mechanisms = plain login
    
        ##
        ## Password and user databases
        ##
    
        #
        # Password database is used to verify user's password (and nothing more).
        # You can have multiple passdbs and userdbs. This is useful if you want to
        # allow both system users (/etc/passwd) and virtual users to login without
        # duplicating the system users into virtual database.
        #
        # <doc/wiki/PasswordDatabase.txt>
        #
        # User database specifies where mails are located and what user/group IDs
        # own them. For single-UID configuration use "static" userdb.
        #
        # <doc/wiki/UserDatabase.txt>
    
        #!include auth-deny.conf.ext
        #!include auth-master.conf.ext
    
        #!include auth-system.conf.ext
        !include auth-sql.conf.ext
        #!include auth-ldap.conf.ext
        #!include auth-passwdfile.conf.ext
        #!include auth-checkpassword.conf.ext
        #!include auth-vpopmail.conf.ext
        #!include auth-static.conf.ext
        
        
  11. Edit the /etc/dovecot/conf.d/auth-sql.conf.ext file with the authentication information. Paste the following lines into in the file:
        
        passdb {
            driver = sql
            args = /etc/dovecot/dovecot-sql.conf.ext
        }
        userdb {
            driver = static
            args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n
        }
        
        

    Save the changes to the /etc/dovecot/conf.d/auth-sql.conf.ext file.

  12. Update the /etc/dovecot/dovecot-sql.conf.ext file with our custom MySQL connection information.

    Uncomment and set the driver line as shown below:

    driver = mysql

    Uncomment the connect line and set the MySQL connection information. Use the mailuser’s password and any other custom settings:

    connect = host=127.0.0.1 dbname=mailserver user=mailuser password=mailuserpass

    Uncomment the default_pass_scheme line and set it to SHA512-CRYPT:

    default_pass_scheme = SHA512-CRYPT
    password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';

    This password query lets you use an email address listed in the virtual_users table as the username credential for an email account. If you want to be able to use the alias as the username instead (listed in the virtual_aliases table), first add every primary email address to the virtual_aliases table (directing to themselves) and then use the following line in /etc/dovecot/dovecot-sql.conf.ext instead:

    password_query = SELECT email as user, password FROM virtual_users WHERE email=(SELECT destination FROM virtual_aliases WHERE source = '%u');

    Save the changes to the /etc/dovecot/dovecot-sql.conf.ext file.

    File : /etc/dovecot/dovecot-sql.conf.ext

        
        # This file is opened as root, so it should be owned by root and mode 0600.
        #
        # http://wiki2.dovecot.org/AuthDatabase/SQL
        #
        # For the sql passdb module, you'll need a database with a table that
        # contains fields for at least the username and password. If you want to
        # use the [email protected] syntax, you might want to have a separate domain
        # field as well.
        #
        # If your users all have the same uig/gid, and have predictable home
        # directories, you can use the static userdb module to generate the home
        # dir based on the username and domain. In this case, you won't need fields
        # for home, uid, or gid in the database.
        #
        # If you prefer to use the sql userdb module, you'll want to add fields
        # for home, uid, and gid. Here is an example table:
        #
        # CREATE TABLE users (
        #     username VARCHAR(128) NOT NULL,
        #     domain VARCHAR(128) NOT NULL,
        #     password VARCHAR(64) NOT NULL,
        #     home VARCHAR(255) NOT NULL,
        #     uid INTEGER NOT NULL,
        #     gid INTEGER NOT NULL,
        #     active CHAR(1) DEFAULT 'Y' NOT NULL
        # );
    
        # Database driver: mysql, pgsql, sqlite
        driver = mysql
    
        # Database connection string. This is driver-specific setting.
        #
        # HA / round-robin load-balancing is supported by giving multiple host
        # settings, like: host=sql1.host.org host=sql2.host.org
        #
        # pgsql:
        #   For available options, see the PostgreSQL documention for the
        #   PQconnectdb function of libpq.
        #   Use maxconns=n (default 5) to change how many connections Dovecot can
        #   create to pgsql.
        #
        # mysql:
        #   Basic options emulate PostgreSQL option names:
        #     host, port, user, password, dbname
        #
        #   But also adds some new settings:
        #     client_flags        - See MySQL manual
        #     ssl_ca, ssl_ca_path - Set either one or both to enable SSL
        #     ssl_cert, ssl_key   - For sending client-side certificates to server
        #     ssl_cipher          - Set minimum allowed cipher security (default: HIGH)
        #     option_file         - Read options from the given file instead of
        #                           the default my.cnf location
        #     option_group        - Read options from the given group (default: client)
        #
        #   You can connect to UNIX sockets by using host: host=/var/run/mysql.sock
        #   Note that currently you can't use spaces in parameters.
        #
        # sqlite:
        #   The path to the database file.
        #
        # Examples:
        #   connect = host=192.168.1.1 dbname=users
        #   connect = host=sql.example.com dbname=virtual user=virtual password=blarg
        #   connect = /etc/dovecot/authdb.sqlite
        #
        connect = host=127.0.0.1 dbname=mailserver user=mailuser password=mailuserpass
    
        # Default password scheme.
        #
        # List of supported schemes is in
        # http://wiki2.dovecot.org/Authentication/PasswordSchemes
        #
        default_pass_scheme = SHA512-CRYPT
    
        # passdb query to retrieve the password. It can return fields:
        #   password - The user's password. This field must be returned.
        #   user - [email protected] from the database. Needed with case-insensitive lookups.
        #   username and domain - An alternative way to represent the "user" field.
        #
        # The "user" field is often necessary with case-insensitive lookups to avoid
        # e.g. "name" and "nAme" logins creating two different mail directories. If
        # your user and domain names are in separate fields, you can return "username"
        # and "domain" fields instead of "user".
        #
        # The query can also return other fields which have a special meaning, see
        # http://wiki2.dovecot.org/PasswordDatabase/ExtraFields
        #
        # Commonly used available substitutions (see http://wiki2.dovecot.org/Variables
        # for full list):
        #   %u = entire [email protected]
        #   %n = user part of [email protected]
        #   %d = domain part of [email protected]
        #
        # Note that these can be used only as input to SQL query. If the query outputs
        # any of these substitutions, they're not touched. Otherwise it would be
        # difficult to have eg. usernames containing '%' characters.
        #
        # Example:
        #   password_query = SELECT userid AS user, pw AS password \
        #     FROM users WHERE userid = '%u' AND active = 'Y'
        #
        #password_query = \
        #  SELECT username, domain, password \
        #  FROM users WHERE username = '%n' AND domain = '%d'
        password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';
    
        # userdb query to retrieve the user information. It can return fields:
        #   uid - System UID (overrides mail_uid setting)
        #   gid - System GID (overrides mail_gid setting)
        #   home - Home directory
        #   mail - Mail location (overrides mail_location setting)
        #
        # None of these are strictly required. If you use a single UID and GID, and
        # home or mail directory fits to a template string, you could use userdb static
        # instead. For a list of all fields that can be returned, see
        # http://wiki2.dovecot.org/UserDatabase/ExtraFields
        #
        # Examples:
        #   user_query = SELECT home, uid, gid FROM users WHERE userid = '%u'
        #   user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%u'
        #   user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%u'
        #
        #user_query = \
        #  SELECT home, uid, gid \
        #  FROM users WHERE username = '%n' AND domain = '%d'
    
        # If you wish to avoid two SQL lookups (passdb + userdb), you can use
        # userdb prefetch instead of userdb sql in dovecot.conf. In that case you'll
        # also have to return userdb fields in password_query prefixed with "userdb_"
        # string. For example:
        #password_query = \
        #  SELECT userid AS user, password, \
        #    home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \
        #  FROM users WHERE userid = '%u'
    
        # Query to get a list of all usernames.
        #iterate_query = SELECT username AS user FROM users
        
        
  13. Change the owner and group of the /etc/dovecot/ directory to vmail and dovecot:
    chown -R vmail:dovecot /etc/dovecot
  14. Change the permissions on the /etc/dovecot/ directory:
    chmod -R o-rwx /etc/dovecot
  15. Open the sockets configuration file, located at /etc/dovecot/conf.d/10-master.conf
  16. Disable unencrypted IMAP and POP3 by setting the protocols’ ports to 0, as shown below. Ensure that the entries for port and ssl below the IMAPS and pop3s entries are uncommented:
        
        service imap-login {
            inet_listener imap {
                #port = 0
            }
            inet_listener imaps {
                port = 993
                ssl = yes
            }
        }
        service pop3-login {
            inet_listener pop3 {
                port = 0
            }
            inet_listener pop3s {
                port = 995
                ssl = yes
            }
        }
        
        

    Leave the secure versions unedited, specifically the imaps and pop3s, so that their ports still work. The default settings for imaps and pop3s are fine. Optionally, leave the port lines commented out, as the default ports are the standard 993 and 995.

    Find the service lmtp section and use the configuration shown below:

        
        service lmtp {
            unix_listener /var/spool/postfix/private/dovecot-lmtp {
                mode = 0600
                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 =
            #}
        }
        
        

    Locate the service auth section and configure it as shown below:

        
        service auth {
            # auth_socket_path points to this userdb socket by default. It's typically
            # used by dovecot-lda, doveadm, possibly imap process, etc. Its default
            # permissions make it readable only by root, but you may need to relax these
            # permissions. Users that have access to this socket are able to get a list
            # of all usernames and get results of everyone's userdb lookups.
            unix_listener /var/spool/postfix/private/auth {
                mode = 0666
                user = postfix
                group = postfix
            }
    
            unix_listener auth-userdb {
                mode = 0600
                user = vmail
                #group =
            }
    
            # Postfix smtp-auth
            #unix_listener /var/spool/postfix/private/auth {
                #  mode = 0666
            #}
    
            # Auth process is run as this user.
            user = dovecot
        }
        
        

    In the service auth-worker section, uncomment the user line and set it to vmail as shown below:

        
        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 = vmail
        }
        
        

    Save the changes to the /etc/dovecot/conf.d/10-master.conf file.

  17. Verify that the default Dovecot SSL certificate and key exist:
        
            ls /etc/dovecot/dovecot.pem
            ls /etc/dovecot/private/dovecot.pem
        
        

    As noted above, these files are not provided in Dovecot 2.2.13-7 and above, and will not be present on Debian 8 systems.
    If using a different SSL certificate, upload the certificate to the server and make a note of its location and the key’s location.

  18. Open /etc/dovecot/conf.d/10-ssl.conf
  19. Verify that the ssl_cert setting has the correct path to the certificate, and that the ssl_key setting has the correct path to the key. The default setting displayed uses Dovecot’s built-in certificate, so you can leave this as-is if using the Dovecot certificate. Update the paths accordingly if you are using a different certificate and key.
        
        ssl_cert = </etc/dovecot/dovecot.pem
        ssl_key = </etc/dovecot/private/dovecot.pem
        
        

    Force the clients to use SSL encryption by uncommenting the ssl line and setting it to required:

    ssl = required

    Save the changes to the /etc/dovecot/conf.d/10-ssl.conf file.

  20. Finally, restart Dovecot:
    service dovecot restart

You now have a functioning mail server that can securely send and receive email. To setup email account with email client use Port 993 for secure IMAP, Port 995 for secure POP3, and Port 25 with SSL for SMTP.

You can check log file /var/log/mail.log for any errors. At this point, consider adding spam and virus filtering and a webmail client. If DNS records have not been created for the mail server yet, do so now. Once the DNS records have propagated, email will be delivered via the new mail server.

If errors are encountered in the /var/log/syslog stating “Invalid settings: postmaster_address setting not given”, you may need to append the following line to the /etc/dovecot/dovecot.conf file, replacing domain with the domain name.

postmaster_address=postmaster at DOMAIN

Build A Read-Only Linux System

There are a couple of different approaches to making a Linux system read-only.Unfortunately, it is usually not as simple as using a conventional filesystem mounted with the read-only option. Many programs assume that at least some parts of the system are writable. In some cases, these programs will fail to run correctly if this turns out not to be the case.

I’ll outline here what I think is the best approach for most applications. It is similar to that taken by the current generation of live CD distributions.

Live CDs typically have read-only access to a root filesystem, which is often compressed into a single file to be mounted later using a loopback device. Knoppix broke new ground with its use of the cloop filesystem for this purpose. More recent live distributions take this a step further by using a union filesystem to make the root filesystem writable. This approach is quite useful for our purposes, as well.

Union Filesystems

Generally speaking, a union filesystem combines multiple filesystems into a single virtual filesystem. There are two popular union fileystems that I’m aware of: unionfs and aufs. Both have the same basic model. The following is a dramatic simplification:

  • Filesystems are stacked vertically.
  • Read accesses are attempted on each filesystem in turn from top to bottom.The first filesystem that contains the file being read is used for the read operation.
  • Write accesses are performed similarly, but files that are written to are stored in the top-most writable filesystem. This usually means there is a single writable layer in the union. If files that exist in a read-only layer are written to,they are first copied to the next highest writable layer.

Obviously, there are a lot of subtleties and corner cases that I am not presenting here. What’s important is that we can use a read-only filesystem (which may be a compressed filesystem image or a flash device containing a more conventional filesystem like ext3) and build a writable system on top of it.All we need is a writable filesystem to union with the read-only layer.

The Writable Layer

What kind of writable filesystem you use depends on what you are trying to achieve. If you don’t need any persistence between boots, it is pretty easy to use tmpfs. Writes to the system will be preserved in RAM while the system is up, but will disappear when the system is shutdown or rebooted.

If you want persistence over the whole system directory structure, you’ll need to use a persistent writable layer. This is likely a conventional filesystem on some other media (a second disk, perhaps). This is probably most useful for live systems or thin clients where using a read-only base is not done so much for longevity as it is to minimize local storage requirements.

In many cases, when you do need persistence, you only need it for specific files. For instance, if you have a kiosk that stores user input in a local database, the database must persist on disk, but you probably don’t want to persist temporary files or other transient runtime data. The best approach for dealing with this common use case is to have a tmpfs read/write layer and then mount some writable media on an arbitrary mount point like /var/local/data (for example).

Implementation

Implementing a read-only system requires hooking into the boot process. How this is done varies from distribution to distribution, and can probably be done in a variety of ways on a single distribution. In this article, I’ll demonstrate an approach that works with Ubuntu 8.04.

By default, Ubuntu 8.04 uses an initramfs. This is the best place to make our modification, as we can make sure that the union filesystem is mounted early on in the boot process.

initramfs-tools

Ubuntu has an extensible system for building the initramfs called “initramfs-tools”. We can use this to plug some scripts into the initramfs.There are a few different ways that initramfs-tools can be extended: “hooks” and “scripts.”

Hooks are run when the initramfs is being built, and are useful for adding kernel modules or executables to the initramfs image. Hooks that are distributed with packages are usually installed in /usr/share/initramfs-tools/hooks, and they make use of the functions defined in /usr/share/initramfs-tools/hook-functions. Local hooks should be placed in /etc/initramfs-tools/hooks.

Scripts are run within the initramfs environment at boot time. These can be used to modify the early boot process. As with hooks, scripts that are distributed with packages are usually installed into /usr/share/initramfs-tools/scripts. Local scripts should go into /etc/initramfs-tools/scripts.

initramfs generation is controlled by the configuration files found in /etc/initramfs-tools. /etc/initramfs-tools/initramfs.conf is the primary configuration file, but files can also be placed in /etc/initramfs-tools/conf.d. The primary boot method can be configured in initramfs.conf by changing the value of variable “BOOT.” By default, it is “local,” a boot method that mounts the root filesystem on local media like a hard disk.

For each boot method, there is a script with that name that controls how that boot method works. For instance, there is a script called “local” that defines how a local boot is performed. Many such scripts also provide hooks for other shell scripts to be executed at certain points during the boot process. For instance, any scripts placed in /usr/share/initramfs-tools/local-premount will be executed by the “local” script just prior to mounting the root filesystem. The init script itself (which acts as process #1 up until the point where the real init daemon is launched, after mounting the root filesystem) provides similar hooks. See the contents of /usr/share/initramfs-tools/scripts to get an idea of what other hooks are available.

Finally, both hooks and scripts must be written such that, if they are run with a single argument “prereqs,” they print a space-separated list of the names of other scripts or hooks that should be run before running this particular script or hook. This provides a simple system of dependencies between hooks and scripts. I find that I very rarely make use of this feature, but it is available should your application require it.

Hooks and Scripts

We’ll implement our read-only system by introducing one hook and one script. Our script will actually be an init-bottom script, run after the real root device is already mounted. Our goal will be to take the already-mounted root filesystem and shuffle it around as the base for an aufs union with a tmpfs writable layer. This allows us to continue to use the standard Ubuntu configuration mechanisms for specifying the device that contains the real root filesystem.

We need a hook to tell initramfs-tools that we need a few kernel modules (aufs and tmpfs, both of which are included with Ubuntu 8.04) and an executable (chmod). We’ll see why we need chmod shortly. Our hook is quite simple (as most of them are).

We’ll call this hooks/ro_root:


#!/bin/sh

PREREQ=''

prereqs() {
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /usr/share/initramfs-tools/hook-functions
manual_add_modules aufs
manual_add_modules tmpfs
copy_exec /bin/chmod /bin

The script does the real work of making sure the filesystems are all mounted in the right places. At this point in the boot process, the real root device has been mounted on $rootmnt, and /sbin/init on that mount point is about to be executed. At this point, we’ll be looking to move the root device mount to a different mount point, and build our union mount in its place.

Here’s how we’ll do this:


    Move $rootmnt to /${rootmnt}.ro (this is the read-only layer).
    Mount our writable layer as tmpfs on /${rootmnt}.rw.
    Mount the union on ${rootmnt}.

Additionally, we may want to have access to the read-only and read/write layers independently from the union. In order to maintain access to these mounts, we’ll have to bind them to a new mount point under ${rootmnt}. We’ll do this with “mount –bind”.

The union is still able to access the original read-only and read/write mounts even after the root is rotated and init is launched, causing those mount points to fall outside of the new root filesystem. I assume that aufs opens these directories at mount time and the filesystems continue to be accessible as long as processes have open file handles. The kernel seems to be pretty smart about dealing with these kinds of interesting situations.

Getting back to things, here is the init-bottom script we’ll be using (scripts/init-bottom/ro_root):


#!/bin/sh

PREREQ=''

prereqs() {
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

ro_mount_point="${rootmnt%/}.ro"
rw_mount_point="${rootmnt%/}.rw"

# Create mount points for the read-only and read/write layers:
mkdir "${ro_mount_point}" "${rw_mount_point}"

# Move the already-mounted root filesystem to the ro mount point:
mount --move "${rootmnt}" "${ro_mount_point}"

# Mount the read/write filesystem:
mount -t tmpfs root.rw "${rw_mount_point}"

# Mount the union:
mount -t aufs -o "dirs=${rw_mount_point}=rw:${ro_mount_point}=ro" root.union "${rootmnt}"

# Correct the permissions of /:
chmod 755 "${rootmnt}"

# Make sure the individual ro and rw mounts are accessible from within the root
# once the union is assumed as /.  This makes it possible to access the
# component filesystems individually.
mkdir "${rootmnt}/ro" "${rootmnt}/rw"
mount --bind "${ro_mount_point}" "${rootmnt}/ro"
mount --bind "${rw_mount_point}" "${rootmnt}/rw"

Rebuilding The initramfs

The hook and init-bottom script that we wrote above can be installed in the following locations, respectively:

  • /etc/initramfs-tools/hooks/ro_root
  • /etc/initramfs-tools/scripts/init-bottom/ro_root

They should both have the execute permission bit set.

After copying the files into place, regenerate your initramfs with update-initramfs:


    update-initramfs -u

The -u switch tells update-initramfs to update the initramfs for the most recent kernel on the system. I assume that that is the kernel that you are running. For most embedded or other single-purpose machines, there is typically only one kernel installed.

Booting

The system should appear to boot as it would without the changes we made. However, once it is finished booting, you can confirm that:

  • /ro contains the read-only base filesystem.
  • /rw contains the read/write layer, and will usually have some new files there immediately following boot (/var/run, etc.).
  • If you create a file and then reboot, the file will be gone.

Of course, a system like this has a few caveats:

  • The contents of /etc/mtab are likely not correct, so the output of the mount command is probably missing some information. There are steps we can take to correct /etc/mtab, but I won’t cover those in detail here.
  • No runtime state is preserved. Don’t forget that and save a file, expecting it to be around after a reboot!
  • Subtle semantic differences between aufs, tmpfs, and traditional filesystems

may cause problems with some applications. Most applications won’t notice, but those that leverage more advanced filesystem features or rely on filesystem implementation details might run into errors or, worse, fail subtley. I believe most of these kinds of issues are now a thing of the past, but if you find yourself troubleshooting mysterious failures, keep it in mind.

This kind of system customization really demonstrates the power and flexibility of the initramfs-tools configuration infrastructure. This architectural style is common in Debian and Ubuntu, making these distributions ideal choices for embedded and applied computing projects.

Improvements

The following updated script incorporates some improvements :

  • Boot with normally-mounted read/write root filesystem when the user requests single user mode (a.k.a. recovery mode).
  • Prevent /etc/init.d/checkroot.sh from running when booting into the read-only system.
  • Use mount –move instead of mount –bind when moving the ro and rw mount points into the new root.

#!/bin/sh

PREREQ=''

prereqs() {
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

# Boot normally when the user selects single user mode.
if grep single /proc/cmdline >/dev/null; then
    exit 0
fi

ro_mount_point="${rootmnt%/}.ro"
rw_mount_point="${rootmnt%/}.rw"

# Create mount points for the read-only and read/write layers:
mkdir "${ro_mount_point}" "${rw_mount_point}"

# Move the already-mounted root filesystem to the ro mount point:
mount --move "${rootmnt}" "${ro_mount_point}"

# Mount the read/write filesystem:
mount -t tmpfs root.rw "${rw_mount_point}"

# Mount the union:
mount -t aufs -o "dirs=${rw_mount_point}=rw:${ro_mount_point}=ro" root.union "${rootmnt}"

# Correct the permissions of /:
chmod 755 "${rootmnt}"

# Make sure the individual ro and rw mounts are accessible from within the root
# once the union is assumed as /.  This makes it possible to access the
# component filesystems individually.
mkdir "${rootmnt}/ro" "${rootmnt}/rw"
mount --move "${ro_mount_point}" "${rootmnt}/ro"
mount --move "${rw_mount_point}" "${rootmnt}/rw"

# Make sure checkroot.sh doesn't run.  It might fail or erroneously remount /.
rm -f "${rootmnt}/etc/rcS.d"/S[0-9][0-9]checkroot.sh