Category: System Administration

  • Setup Postfix Email Server

    Setup Postfix Email Server

    For saving money and taking the full control of the service, I decided to setup my own postfix email server on a Ubuntu virtual server. And this article recorded the process.

    1. Prerequisites

    There are a set of tasks that should be finished before we even get to Postfix.

    1.1. Setup DNS

    The DNS records should look like this:

    HOSTNAME                    TTL    TYPE  PRIO  VALUE
    
    # Where to find the server
    mail.[DOMAIN_NAME]          57600  A           [IP-ADDRESS]
    
    # If the request is related to email, then forward to mail sub-domain
    [DOMAIN_NAME]               57600  MX    10    mail.[DOMAIN_NAME]
    
    # Optional, only used for setting up MUA (mail user agent)
    autoconfig.[DOMAIN_NAME]    57600  CNAME       mail.[DOMAIN_NAME]
    
    # Optional, only used for setting up MUA
    autodiscover.[DOMAIN_NAME]  57600  CNAME       mail.[DOMAIN_NAME]
    

    The core of the DNS records is the A and the MX records. The A record defines the mail sub-domain pointing to the actual mail server, and the MX record redirects the mail request from the main domain to the sub-domain, so that we could use the domain name like user@[DOMAIN_NAME] instead of user@mail.[DOMAIN_NAME].

    If there is an IPv6 address, we should also set up the AAAA record for the mail sub-domain.

    1.2. Setup the Reverse DNS

    After DNS records have been updated, go to the hosting server provider, and set the reverse DNS from the IP-Adress back to the domain name (mail.[DOMAIN_NAME]).

    1.3. Firewall Setup

    Enable the port 25 and 465.

    This has to be done for both inbound and also outbound, otherwise either sending or recieving mails won’t work. Note that the outbound rules sometimes are disabled by hosting service providers, so we have to create a service ticket for them to enable the 2 ports for us.

    1.4. Change the Hostname

    Since the Postfix reads the hostname of the server to identify itself while communicating with other MTA (message transfer agent), it is important to change the hostname to match the hostname in the DNS record:

    Use following command to see what the current hostname is:

    hostname -f
    

    If it is not the one configured in the DNS record, change it using the following command:

    hostnamectl set-hostname mail.[DOMAIN_NAME]
    

    2. Installation

    Finally all the preparation work is done, we can install the Postfix now.

    2.1. Basic Installation

    Update the package manager and the installed packages first:

    apt upgrade
    apt update
    reboot
    

    And then install the postfix package:

    apt install postfix
    

    Then a dialog will be displayed asking us how we want to configure Postfix. In my case, I should set it to Internal Site, this is useful for most of the cases.

    In the next step, I should enter my “System mail name”. Here I should make it [DOMAIN_NAME] instead of mail.[DOMAIN_NAME], because if I set it to the latter, I won’t be able to send/receive Emails from [DOMAIN_NAME] domain.

    To verify the mail service, use following command:

    postconf mail_version
    

    If everything works fine, the version number will be printed to the standard output.

    We could also check who is now listening to port 25:

    ss -lnpt | grep master
    

    The output should look like this:

    LISTEN 0   100   0.0.0.0:25   0.0.0.0:*   users:(("master",pid=2633,fd=13))                     
    LISTEN 0   100      [::]:25      [::]:*   users:(("master",pid=2633,fd=14))
    

    If we compare the PID with the result from ps, we should find that it is the Postfix process.

    2.2. Send/Receive Test Emails

    To send Email, I can use the mail command in the mailutils package in Ubuntu:

    apt install mailutils
    

    Then write a short mail:

    mail -a FROM:[USER]@[DOMAIN_NAME] user@example.com
    Cc:
    Subject: Test
    This Email is sent from a newly setup mail server.
    

    To send the Email, press Ctrl + D, and after a few seconds, the mail should be found in the mailbox.

    To receive Email, use mail command:

    mail
    

    It will start an interactive CLI environment. Use ? and Enter for a short command instructions. Some useful commands are:

    • 1: Read the first mail
    • h: List the head of the mails
    • d 1: Delete first mail
    • q: Quit program

    3. Setup TLS Encryption and IMAP

    There are still 2 problems after all the setup:

    1. All the Emails are transmitted in Internet in clear text.
    2. We can’t use any mail client to connect to the server.

    So this paragraph will provide solutions for these two problems.

    3.1. Enable More Ports in Firewall

    To setup TLS and IMAP, I have to enable more ports:

    • 80 and 443 – to sign the TLS signature using certbot
    • 587 – for encrypted SMTP
    • 465 – for encrypted SMTP (deprecated)
    • 143 – for non-encrypted IMAP
    • 993 – for encrypted IMAP

    3.2. Install Nginx and Certbot and Request for SSL Certificate

    apt install nginx
    snap install --classic cerbot
    

    As an alternative to install certbot from snap, we could also use:

    pip install certbot certbot-nginx
    

    Provide an Nginx site config like this and put it into /etc/nginx/sites-enabled, name it like mail.[DOMAIN_NAME]:

    server {
        listen  80;
        listen  [::]:80;
    
        server_name  mail.[DOMAIN_NAME];
    
        root  /usr/share/nginx/html/;
    
        location ~/.well-known/acme-challenge {
            allow  all;
        }
    }
    

    Request a new certificate using certbot:

    certbot certonly --nginx -d mail.[DOMAIN_NAME]
    

    In this case, certbot won’t change the settings in the nginx, it just tries to get a new certificate.

    After the interactive script, the certificate files will be saved:

    • Certificate file: /etc/letsencrypt/live/mail.[DOMAIN_NAME]/fullchain.pem
    • Key file: /etc/letsencrypt/live/mail.[DOMAIN_NAME]/privkey.pem

    3.3. Setup SMTP

    Edit the /etc/postfix/master.cf file, and add following two blocks into the file:

    submission inet n       -       y       -       -       smtpd
      -o syslog_name=postfix/submission
      -o smtpd_tls_security_level=encrypt
      -o smtpd_tls_wrappermode=no
      -o smtpd_sasl_auth_enable=yes
      -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
      -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
      -o smtpd_sasl_type=dovecot
      -o smtpd_sasl_path=private/auth
    smtps     inet  n       -       y       -       -       smtpd
      -o syslog_name=postfix/smtps
      -o smtpd_tls_wrappermode=yes
      -o smtpd_sasl_auth_enable=yes
      -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
      -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
      -o smtpd_sasl_type=dovecot
      -o smtpd_sasl_path=private/auth
    

    Edit the /etc/postfix/main.cf file, modify the # TLS parameters section:

    # TLS parameters
    smtpd_tls_cert_file = /etc/letsencrypt/live/mail.[DOMAIN_NAME]/fullchain.pem
    smtpd_tls_key_file = /etc/letsencrypt/live/mail.[DOMAIN_NAME]/privkey.pem
    smtpd_tls_security_level = may
    smtpd_tls_loglevel = 1
    smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
    
    smtp_tls_security_level = may
    smtp_tls_loglevel = 1
    smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
    
    smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
    smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
    smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
    smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
    

    And then restart Postfix service:

    systemctl restart postfix
    

    After that, if I check the port which is listened by master, I can see there are not only port 25, but also 465 and 587 opened:

    ss -lnpt | grep master
    

    The result should be like:

    LISTEN 0  100   0.0.0.0:25  0.0.0.0:*  users:(("master",pid=12881,fd=13))
    LISTEN 0  100  0.0.0.0:465  0.0.0.0:*  users:(("master",pid=12881,fd=22))
    LISTEN 0  100  0.0.0.0:587  0.0.0.0:*  users:(("master",pid=12881,fd=18))
    LISTEN 0  100      [::]:25     [::]:*  users:(("master",pid=12881,fd=14))
    LISTEN 0  100     [::]:465     [::]:*  users:(("master",pid=12881,fd=23))
    LISTEN 0  100     [::]:587     [::]:*  users:(("master",pid=12881,fd=19))
    

    3.4. Install and Setup Dovecot to Enable IMAP

    To install Dovecot, simply run:

    apt install dovecot-core dovecot-imapd dovecot-lmtpd
    

    After that, edit Dovecot configruation file /etc/dovecot/dovecot.conf to enable IMAP. Add one line:

    # Enable installed protocols
    protocols = imap lmtp   # Add this line
    !include_try /usr/share/dovecot/protocols.d/*.protocol
    

    Edit /etc/dovecot/conf.d/10-mail.conf to config it using the Maildir instead of mbox directory structure:

    mail_location = maildir:~/Maildir   # Modify this line
    mail_privileged_group = mail        # This line should be already there, just check it
    

    After that we want to add the user dovecot to the mail group:

    adduser dovecot mail
    

    Open the /etc/dovecot/conf.d/10-master.conf file and edit the service lmtp and service auth section:

    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 = 
      #}
    }
    
    service auth {
      # auth_socket_path points to this userdb socket by default. It's typically
      # used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
      # full permissions to this socket are able to get a list of all usernames and
      # get the results of everyone's userdb lookups.
      #
      # The default 0666 mode allows anyone to connect to the socket, but the
      # userdb lookups will succeed only if the userdb returns an "uid" field that
      # matches the caller process's UID. Also if caller's uid or gid matches the
      # socket's uid or gid the lookup succeeds. Anything else causes a failure.
      #
      # To give the caller full permissions to lookup all users, set the mode to
      # something else than 0666 and Dovecot lets the kernel enforce the
      # permissions (e.g. 0777 allows everyone full permissions).
      unix_listener /var/spool/postfix/private/auth {
        mode = 0660
        user = postfix
        group = postfix
      }
    
      # Postfix smtp-auth
      #unix_listener /var/spool/postfix/private/auth {
      #  mode = 0666
      #}
    
      # Auth process is run as this user.
      #user = $default_internal_user
    }
    

    Edit /etc/postfix/main.cf file, and add following 2 lines at the bottom of the file:

    mailbox_transport = lmtp:unix:private/dovecot-lmtp
    smtputf8_enable = no
    

    Edit /etc/dovecot/conf.d/10-auth.conf file, edit multiple places:

    disable_plaintext_auth = yes    # Uncomment this line
    auth_username_format = %n       # Uncomment and modify this line
    auth_mechanisms = plain login   # Modify this line
    

    Edit /etc/dovecot/conf.d/10-ssl.conf, edit multiple places:

    ssl = required                    # Modify this line
    ssl_cert = </etc/letsencrypt/live/mail.[DOMAIN_NAME]/fullchain.pem
                                      # Modify this line
    ssl_key = </etc/letsencrypt/live/mail.[DOMAIN_NAME]/privkey.pem
                                      # Modify this line
    ssl_prefer_server_ciphers = yes   # Uncomment and modify this line
    ssl_min_protocol = TLSv1.2        # Uncomment and modify this line
    

    Edit /etc/ssl/openssl.cnf file, and comment one line to disable FIPS:

    #providers = provider_sect   # Comment this line
    

    Edit /etc/dovecot/conf.d/15-mailboxes.conf file, and edit it. This tells dovecot to create default folders automatically. Besides, it will also avoid to create 2 different folders for the \Sent usage (previously there are “Sent” and “Sent Messages”):

      mailbox Drafts {
        auto = create
        special_use = \Drafts
      }
      mailbox Junk {
        auto = create
        special_use = \Junk
      }
      mailbox Trash {
        auto = create
        special_use = \Trash
      }
      mailbox Sent { 
        auto = create
        special_use = \Sent
      }
    

    And finally restart everything that I have configured:

    systemctl restart postfix dovecot
    

    And check whether dovecot is up and running:

    ss -lnpt | grep dovecot
    

    The result should be like:

    LISTEN 0   100   0.0.0.0:143   0.0.0.0:*   users:(("dovecot",pid=18756,fd=36))
    LISTEN 0   100   0.0.0.0:993   0.0.0.0:*   users:(("dovecot",pid=18756,fd=38))
    LISTEN 0   100      [::]:143      [::]:*   users:(("dovecot",pid=18756,fd=37))
    LISTEN 0   100      [::]:993      [::]:*   users:(("dovecot",pid=18756,fd=39))
    

    4. Use MUA (Mail User Agent) to Connect to the Server

    4.1. Create User for Mail

    Postfix will use the system users as Email users. So create a user like this:

    useradd -d /home/[UNIX_USERNAME] -s /bin/false [UNIX_USERNAME]
    mkdir /home/[UNIX_USERNAME]
    chown [UNIX_USERNAME]:[UNIX_USERGRP] /home/[UNIX_USERNAME]
    passwd [UNIX_USERNAME]
    

    4.2. Setup MUA

    Using following data to setup the Mail Client:

    Name: [MAIL_NAME]
    EMail Address: [UNIX_USERNAME]@[DOMAIN_NAME]
    
    # IMAP Mail Server
    Host: mail.[DOMAIN_NAME]
    Port: 143
    User Name: [UNIX_USERNAME]
    Security: STARTTLS
    Authentication Method: Normal password
    
    # SMTP Server
    Host: mail.[DOMAIN_NAME]
    Port: 587
    User Name: [UNIX_USERNAME]
    Security: STARTTLS
    Authentication Method: Normal password
    

    5. Setup SPF

    SPF or DKIM is a way to verify the ownership of the mail server, so that some famous mail service provider can ensure that our mail server is not a spammer. Since SPF is easier than DKIM, I have only setup SPF.

    To do this, simply add a DNS record:

    HOSTNAME        TTL   TYPE   PRIO   VALUE
    [DOMAIN_NAME]   A                   v=spf1 mx ~all
    

    And it is done.

  • WordPress Deployment with Nginx Reverse-Proxy and HTTPS for Multiple Domains

    WordPress Deployment with Nginx Reverse-Proxy and HTTPS for Multiple Domains

    About 10 years ago, a bunch of Frameworks was gaining their popularities, while PHP was considered dying. However, up to 2025, WordPress is still the technology behind over 40% of the websites. Powered by PHP 8.x and the Gutenberg block system. WordPress is now a full featured modern CMS. This also became the motivation for me to get into the world of WordPress once again.

    For saving server costs, it might be a good idea to deploy different WordPress instances on the same physical machine. With the Nginx domain name configuration, it is quite easy to serve WordPress for multiple domains.

    Prerequisites

    Before starting, some preparations on the server should be done. More precisely, the following services should be properly setup and configured:

    • Docker
    • Certbot
    • Nginx

    Overview

    The whole system can be devided into 3 layers:

    1. Database: in my case, I’d like to use MariaDB. One can choose any Database supported by WordPress.
    2. Application: of course it is WordPress, and multiple instances can be deployed.
    3. Reverse-proxy: in my case, I’m using Nginx.

    In my setup, database and WordPress are being containerized. Only Nginx is running on the server as a classic daemon. There is no specific reason why I am doing this. The server was there for a while already, and the Nginx has already been installed. If next time I have to do everything once again, I will probably also containerize Nginx.

    Setup Network

    Since database and WordPress instances are all containerized, it would be great if we have a private network to connect both.

    Use the following command to create a network:

    docker network create [NETWORK_NAME]
    docker network ls
    

    On success, the newly created network shall be listed:

    NETWORK ID   NAME             DRIVER    SCOPE
    ...          ...              ...       ...
    ...          [NETWORK_NAME]   bridge    local
    ...          ...              ...       ...
    

    Note that the name [NETWORK_NAME] is only a placeholder, it should be named properly.

    If later we want to remove the network, we could simply do:

    docker network rm [NETWORK_NAME]
    

    Setup Database

    This can be easily done by writing a compose file like this:

    services:
    
      database:
        image: mariadb:latest
        restart: always
        environment:
          - MARIADB_ROOT_PASSWORD_HASH=[PASSWORD_HASH]
        volumes:
          - ./database_data:/var/lib/mysql:Z
        networks:
          - [NETWORK_NAME]
    
    networks:
      [NETWORK_NAME]:
        external: true
    

    Here, [PASSWORD_HASH] is a placeholder, the hash can be generated by PASSWORD() function in SQL. Or one could also find a generator online.

    The path /var/lib/mysql is the path to the database content. To create a volume mapped to the local storage will probably simplify the backup process.

    Since we have created a network in the previous step, and the WordPress will also be setup in the same network. So the port mapping is not necessary.

    To start the database application, use docker compose command:

    docker compose up -d
    

    To login to the database, we have to login to a temporary container first, and run mariadb command connecting with the hostname database:

    docker run -it --network [NETWORK_NAME] --rm mariadb mariadb -h database -u [USERNAME] -p
    

    As usual [USERNAME] here is the placeholder.

    If in the future, we need to stop the container, use following command:

    docker compose down
    

    And then we can start with creating database for the WordPress instances:

    CREATE DATABASE [DB_NAME];
    CREATE USER '[DB_USER]'@'%' IDENTIFIED BY '[DB_PASSWD]';
    GRANT ALL PRIVILEGES ON [DB_NAME].* TO '[DB_USER]'@'%';
    

    Here, [DB_NAME], [DB_USER] and [DB_PASSWD] are placeholders.

    Setup WordPress Instances

    Similar to the database setpu, WordPress setup can also be done with docker compose:

    services:
      wordpress:
        image: wordpress
        restart: always
        ports:
          - [WP_PORT]:80
        environment:
          WORDPRESS_DB_HOST: database
          WORDPRESS_DB_USER: [DB_USER]
          WORDPRESS_DB_PASSWORD: [DB_PASSWD]
          WORDPRESS_DB_NAME: [DB_NAME]
        volumes:
          - ./wordpress_data:/var/www/html
          - ./wordpress_config/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
        networks:
          - [NETWORK_NAME]
    
    networks:
      [NETWORK_NAME]:
        external: true
    

    Since my Nginx is a classic service daemon, so we have to map the container port 80 to host port [WP_PORT] so that the Nginx can access the WordPress instance.

    Two volumes are mapped to the local storage. The first one is generally for the WordPress files; the second one is for setting up the file upload limit. By editing ./wordpress_config/uploads.ini like this:

    upload_max_filesize = 100M
    post_max_size = 100M
    

    One can increase the max size of uploaded files.

    Just like all the other docker compose applications, using following command to start the WordPress instance:

    docker compose up -d
    

    And use the following command to stop it:

    docker compose down
    

    Setup Nginx

    First of all, we need an HTTPS certificate. We can create one using certbot easily:

    certbot --nginx -d [DOMAIN_NAME] -d www.[DOMAIN_NAME]
    

    And then create the site configuration files for Nginx under /etc/nginx/sites-available:

    server {
        listen  [::]:443 ssl;
        listen  443 ssl;
    
        ssl_certificate      /etc/letsencrypt/live/[DOMAIN_NAME]/fullchain.pem;
        ssl_certificate_key  /etc/letsencrypt/live/[DOMAIN_NAME]/privkey.pem;
        include              /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam          /etc/letsencrypt/ssl-dhparams.pem;
    
        server_name  [DOMAIN_NAME] www.[DOMAIN_NAME];
    
        client_max_body_size  100M;
    
        location / {
            proxy_pass          http://localhost:[WP_PORT]/;
            proxy_http_version  1.1;
            proxy_set_header    Host $host;
            proxy_set_header    X-Real-IP $remote_addr;
            proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header    X-Forwarded-Host $server_name;
            proxy_set_header    X-Forwarded-Proto $scheme;
            proxy_redirect      off;
        }
    }
    
    server {
        listen  80;
        listen  [::]:80;
    
        server_name  [DOMAIN_NAME] www.[DOMAIN_NAME];
    
        if ($host = www.[DOMAIN_NAME]) {
            return  301 https://$host$request_uri;
        }
    
        if ($host = [DOMAIN_NAME]) {
            return  301 https://$host$request_uri;
        }
    
        return  404;
    }
    

    This setup will use the certificate created by certbot, also redirect all the HTTP requests to HTTPS, and redirect all the HTTPS requests as HTTP request to the WordPress instance.

    And since we have the server_name configuration on line 10, it will only accept queries which match the configured domain. In this way, we can serve multiple domains on a single physical server.

    Then we create a symbol link in the /etc/nginx/sites-enabled folder:

    ln -s /etc/nginx/sites-available/[SITE_CONF_NAME] /etc/nginx/sites-enabled/[SITE_CONF_NAME]
    

    And finally restart the Nginx service:

    systemctl restart nginx
    

    If we visit website now, the WordPress setup page will be displayed.

    Some Discussions

    • I have used separated docker compose file instead of using a single one, because I’d like to have the ability to group the containers according to their usage so that they can be started and stopped separately. In this case, all docker compose file contains exactly one service, which is a coincidence.
    • In the database, we can probably restrict the user scope to database, for this is the only source where the database queries could come from. This might increase the security.