LINUXOR.SK ... open source notes ...

Email 13 - Webmail

category: solutionz · date: 2019-12-31 · updated: 2026-10-02 · author: LALA

Email Solution · Previous: Mailbox server · Next: Accounts, clients and operations

The mail system exists to send alerts, but some of those alerts go to mailboxes of its own domain, and somebody has to read them. The requirements asked for a web interface (FR5) reachable only over a secure channel (FR6). The answer is Roundcube on the mailbox server DC2-A-VCMSX002: one Apache with PHP, one MariaDB, and the Dovecot and Postfix that are on that server anyway. This Article is about the web side; the mailboxes themselves are in Mailbox server.

Who may use it, and from where

The design splits the users of the system in two groups, senders and readers, and says of the readers that they "are authorized only to access webmail client and are not authorized to send emails". Reading is a group membership in Active Directory.

AD groupMembers
IMAP_PARTNER1user accounts of the first partner that may use the webmail
IMAP_ORGuser accounts of the organisation that may use the webmail
IMAP_PARTNER2user accounts of the second partner that may use the webmail
IMAP_ACCESScontains the three groups above; this is the group Dovecot asks for

Dovecot's user lookup accepts an account only when it is a member of IMAP_ACCESS, directly or through a nested group. A device account can be let in the same way, and the design adds that this happens "only for troubleshooting or service purposes" and on request. How the lookup works is in Dovecot authentication and Active Directory integration.

From where is a firewall rule: TCP 443 from the admin VPN 10.11.20.0/25 to 10.12.19.43, and nothing else. Site 1 has the same rule towards 10.11.19.43 in the design. The conceptual diagram shows who sits behind that VPN: administrators and operators. There is no HTTP listener to redirect from; the server has no Listen 80 at all.

The pieces

mermaid
flowchart LR
  b["Browser on the admin VPN"] -- "HTTPS 443, TLS 1.2" --> a
  subgraph x2["DC2-A-VCMSX002 10.12.19.43"]
    a["Apache httpd with mod_ssl"]
    p["PHP 5 module running Roundcube"]
    d["Dovecot IMAP"]
    m["MariaDB, database roundcube"]
    s["Postfix smtpd on 465"]
    md["Maildir under /data/vmail"]
    a --> p
    p -- "IMAP 127.0.0.1 port 143" --> d
    p -- "Unix socket mysql.sock" --> m
    p -- "SMTPS 127.0.0.1 port 465, the user's login" --> s
    d --> md
  end
  d -- "LDAPS, group IMAP_ACCESS" --> ad["Active Directory"]
  s -- "relayhost" --> x1["Internal pair"]
PieceService accountListens onPackage source
Apache HTTP Server 2.4 with mod_sslapacheTCP 443RHEL 7
PHP 5 as an Apache moduleapachenothingRHEL 7
MariaDB 5.5mysql/var/lib/mysql/mysql.sock onlyRHEL 7
Roundcuberuns inside ApachenothingEPEL

Apache from the unified template

The organisation had one Apache configuration template for all its web servers, kept in a GitLab repository, and the design says the webmail server "follows the unified apache configuration". The Source material holds the template as it was applied to this server, not the repository. It consists of six files plus the module lists.

FilePurposeConfig document
/etc/httpd/conf/httpd.confglobal settings and hardeninghttpd.conf
/etc/httpd/conf.d/ssl.confglobal TLS settings, the only Listenssl.conf
/etc/httpd/conf.d/<FQDN>.conf4virtual host on the IPv4 addressvirtual host, IPv4
/etc/httpd/conf.d/<FQDN>.conf6virtual host on the IPv6 addressdifferences in the same document
/etc/httpd/conf.d/<FQDN>.conf4.app and .conf6.appdirectives of the application, included into the virtual hostapplication include
/etc/httpd/conf.modules.d/00-base.conf, 00-dav.conf, 00-lua.conf, 00-proxy.confmodules switched off00-base.conf

The virtual host files end in .conf4 and .conf6 so that the stock IncludeOptional conf.d/*.conf does not pick them up; httpd.conf includes them with two lines of its own. One virtual host per address family keeps the logs apart: the IPv4 host writes …-IPv4-access.log, the IPv6 host …-IPv6-access.log. diff between the two files shows the address, the log names and the include, nothing else.

What left the stock httpd.conf. Listen 80, the global DocumentRoot with its /var/www/html directory block, the ScriptAlias for /cgi-bin/ and its directory block. The stock file is kept beside the new one as httpd.conf.original. DirectoryIndex got index.php.

What was added. A hardening block.

DirectiveValueWhy, in the template's comments
ServerTokens, ServerSignatureProd, Offno version banner; the comment notes that the Server header cannot be removed entirely without mod_security
FileETagnonean ETag can give away inode numbers
LimitExcept GET POST HEADdeny from allno other request method
TraceEnableoffno cross-site tracing
Timeout30"lower the timeout value"
X-XSS-Protection1; mode=blockswitch the browser's XSS filter on
X-Content-Type-Optionsnosniffno content-type sniffing
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadHTTPS only, for two years
X-Frame-OptionsSAMEORIGINDENY is not possible for applications that use frames, "for example RoundCube"
Header edit Set-Cookieappends ;HttpOnly;Secureevery cookie gets both flags, whatever the application sets
Header unsetServer, X-Powered-Byless to read for a scanner

A Content Security Policy is the one header the template does not set. Its comment explains that the policy depends on the application and has to go into the .app file, and it carries a ready example for Roundcube. On this server both .app files hold only their header comment, so no policy was sent.

Modules. In 00-base.conf 32 LoadModule lines are commented out with my CFG-OFF marker: basic and digest authentication, all authn_ and most authz_ modules, include, info, userdir, negotiation, env, expires, filter and others. WebDAV, Lua and every proxy module are off in their own files. 25 lines of 00-base.conf stay. They hold what the configuration uses, headers, access_compat for the deny from all, authz_core for Require, dir, mime and the logging modules, but also modules that nothing in it uses: cache, cache_disk, suexec, status, vhost_alias, rewrite, deflate, remoteip, data. ssl, the prefork MPM and PHP are loaded from files of their own. The CGI module file was not touched and still loads mod_cgi.

TLS in the virtual host.

apache
SSLEngine on
SSLCompression off
SSLUseStapling off
SSLProtocol -all +TLSv1.2
SSLCipherSuite EECDH+AESGCM
SSLHonorCipherOrder on

TLS 1.2 only, and only ECDHE key exchange with AES-GCM: a short list that every browser of 2019 could meet and that needs no Diffie-Hellman parameter file. OCSP stapling is prepared (ssl.conf defines the stapling cache) and switched off; my comment says Firefox could not open the site with stapling on and ends with "Need more testing". It stayed off. The certificate, key and chain are the files of the server's FQDN under /etc/pki/tls, issued by the internal CA; see TLS and certificates.

The document root is /var/www/dc2-a-vcmsx002.adm.example.net with Options -Indexes -Includes -ExecCGI -FollowSymlinks, AllowOverride None and LimitRequestBody 1048576. The last one is the real upload limit of the webmail: Apache refuses a request over 1 MiB, long before PHP's own limits of 2 MB per file and 8 MB per request.

PHP

The commands ran as root.

bash
$ yum install httpd mod_ssl openssl php php-common php-xml php-mbstring php-imap php-pear php-pear-DB php-mysql
$ vi /etc/php.ini
$ systemctl start httpd
$ systemctl enable httpd

The install notes change one directive in php.ini, date.timezone. Everything else is the package default: php.ini. expose_php stayed On, and the header it produces is removed by Apache.

MariaDB

Roundcube needs a database for user preferences, identities, address books and its cache. It does not hold mail. The commands ran as root.

bash
$ yum install mysql mariadb-server
$ systemctl start mariadb
$ systemctl enable mariadb
$ mysql_secure_installation
Question of mysql_secure_installationAnswer
Set root password?Y, <MARIADB_ROOT_PASSWORD>
Remove anonymous users?Y
Disallow root login remotely?Y
Remove test database and access to it?Y
Reload privilege tables now?Y

Then the server was taken off the network. skip-networking went into /etc/my.cnf.d/server.cnf, in the groups [mariadb] and [mariadb-5.5]; the main file stayed stock: my.cnf. With that option MariaDB opens no TCP port and the Unix socket is the only way in, which is also all Roundcube needs. The notes do not record the restart that makes the option effective.

The database and its user were created in the mysql client, started as root with mysql -u root -p.

sql
create database roundcube;
grant all on roundcube.* to roundcube identified by '<ROUNDCUBE_DB_PASSWORD>';
flush privileges;
quit

Roundcube

The design decides on the version in one sentence: the one "which is in the EPEL repository". The commands ran as root.

bash
$ yum install roundcubemail
$ cp /etc/roundcubemail/defaults.inc.php /etc/roundcubemail/config.inc.php
$ chown apache:apache /etc/roundcubemail/config.inc.php

After that the notes open the web installer at https://dc2-a-vcmsx002.adm.example.net/installer/ and list the answers.

Installer sectionAnswers recorded
Generalproduct name with the site code, des_key generated by the installer, ip_check off, spell check on with Pspell, identities fully editable
Logginglog errors, do not print them to the browser; log_driver syslog, syslog_id roundcube, facility mail
DatabaseMySQL on localhost, database roundcube, user roundcube, table prefix rc_
IMAPdefault_host localhost, port 143, username_domain ad-dc2.example.net, create the user on first login
SMTPssl://127.0.0.1, port 465, "use the current IMAP username and password", log sent messages
DisplayEnglish, skin larry, 50 rows per page, HTML display preferred, compose in plain text, autosave every 5 minutes
Pluginsarchive

The archived config.inc.php is not what these two steps would leave behind. It is a short, tidy file with 22 options, default_host is 127.0.0.1, there is no table prefix, and the plugin list has zipdownload beside archive. I must have rewritten it by hand after the installer, and the notes were not updated. The Config document lists the differences. The installer itself is off in the archived state: enable_installer is not set, and the default is false.

Four groups of settings carry the integration.

php
$config['username_domain'] = 'ad-dc2.example.net';
$config['username_domain_forced'] = true;
$config['default_host'] = '127.0.0.1';
$config['default_port'] = 143;
$config['smtp_server'] = 'ssl://127.0.0.1';
$config['smtp_port'] = 465;
$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';
$config['db_dsnw'] = 'mysql://roundcube:<ROUNDCUBE_DB_PASSWORD>@localhost/roundcube';
$config['session_storage'] = 'php';

Login. Whatever the user types, the domain part is replaced with ad-dc2.example.net. The result is the user principal name Dovecot looks up in Active Directory, so a user types the short name and cannot try another domain.

IMAP. Plain IMAP on the loopback address, port 143. The design wanted the IMAP service "reachable only from localhost", and Roundcube is its only client.

SMTP. Roundcube does not use PHP's mail() and does not relay through the internal pair itself. It opens SMTPS to the Postfix on the same server and logs in with the name and password the user gave at the Roundcube login (%u, %p). So a message written in the webmail enters the system like a message of any authenticated client: under the user's own identity, through the sender restrictions of the mailbox server (Postfix main.cf, mailbox server), and from there to the internal pair and its encryption filter. Whether a reader may send at all is therefore not decided in Roundcube but by the AD groups Postfix checks.

Database and sessions. In a mysql:// DSN the host localhost means the Unix socket, which fits skip-networking. Sessions are PHP's own files and not rows in the database. des_key encrypts the IMAP password that Roundcube keeps in the session for the duration of a login; it is a placeholder in the Config document.

SELinux and the host firewall

The server runs SELinux in enforcing mode. The local policy modules for Postfix and Dovecot are in SELinux module postfix-local and in the mailbox Article. For Apache the install notes have one command, as root, and no file context changes.

bash
$ setsebool -P httpd_can_network_connect on

Without the boolean, a PHP script inside httpd may not open TCP connections, and Roundcube opens two: to port 143 and to port 465 on the loopback address. The boolean allows connections to any port, which is more than needed. The database needs nothing, because it is reached through the socket.

The host firewall got the https service with firewall-cmd --permanent --add-service=https. The notes contain the firewall block twice, and the second copy also adds the imap service, which contradicts the localhost-only IMAP of the design; the network firewall table has no IMAP rule, so the port was not reachable from other segments either way.

Logs and rotation

LogWritten byRotated by
/var/log/httpd/<FQDN>-IPv4-access.log, -IPv4-error.log and the IPv6 pairthe virtual hoststhe stock /etc/logrotate.d/httpd, whose pattern /var/log/httpd/*log matches them
/var/log/httpd/access_log, error_logthe main serverthe same
Roundcube errors and the log of sent messagessyslog, facility mail, tag roundcubewith the mail log
/var/log/roundcubemail/*.lognothing, as long as log_driver is syslog/etc/logrotate.d/roundcubemail

The archive holds /etc/logrotate.d/roundcubemail (rotate at 30 kB, su root apache, nocreate). I cannot tell from the Source material whether it is the file of the EPEL package or was edited, so it is not published as a Config document. With the syslog driver it has nothing to rotate anyway. Sending Roundcube's messages to the mail facility puts logins and sent mail into the same log as Postfix and Dovecot.

What the archive does not tell

What I would do differently

← solutionz