Email 13 - Webmail
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 group | Members |
|---|---|
IMAP_PARTNER1 | user accounts of the first partner that may use the webmail |
IMAP_ORG | user accounts of the organisation that may use the webmail |
IMAP_PARTNER2 | user accounts of the second partner that may use the webmail |
IMAP_ACCESS | contains 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
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"]
| Piece | Service account | Listens on | Package source |
|---|---|---|---|
Apache HTTP Server 2.4 with mod_ssl | apache | TCP 443 | RHEL 7 |
| PHP 5 as an Apache module | apache | nothing | RHEL 7 |
| MariaDB 5.5 | mysql | /var/lib/mysql/mysql.sock only | RHEL 7 |
| Roundcube | runs inside Apache | nothing | EPEL |
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.
| File | Purpose | Config document |
|---|---|---|
/etc/httpd/conf/httpd.conf | global settings and hardening | httpd.conf |
/etc/httpd/conf.d/ssl.conf | global TLS settings, the only Listen | ssl.conf |
/etc/httpd/conf.d/<FQDN>.conf4 | virtual host on the IPv4 address | virtual host, IPv4 |
/etc/httpd/conf.d/<FQDN>.conf6 | virtual host on the IPv6 address | differences in the same document |
/etc/httpd/conf.d/<FQDN>.conf4.app and .conf6.app | directives of the application, included into the virtual host | application include |
/etc/httpd/conf.modules.d/00-base.conf, 00-dav.conf, 00-lua.conf, 00-proxy.conf | modules switched off | 00-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.
| Directive | Value | Why, in the template's comments |
|---|---|---|
ServerTokens, ServerSignature | Prod, Off | no version banner; the comment notes that the Server header cannot be removed entirely without mod_security |
FileETag | none | an ETag can give away inode numbers |
LimitExcept GET POST HEAD | deny from all | no other request method |
TraceEnable | off | no cross-site tracing |
Timeout | 30 | "lower the timeout value" |
X-XSS-Protection | 1; mode=block | switch the browser's XSS filter on |
X-Content-Type-Options | nosniff | no content-type sniffing |
Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | HTTPS only, for two years |
X-Frame-Options | SAMEORIGIN | DENY is not possible for applications that use frames, "for example RoundCube" |
Header edit Set-Cookie | appends ;HttpOnly;Secure | every cookie gets both flags, whatever the application sets |
Header unset | Server, X-Powered-By | less 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.
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.
$ 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.
$ yum install mysql mariadb-server $ systemctl start mariadb $ systemctl enable mariadb $ mysql_secure_installation
Question of mysql_secure_installation | Answer |
|---|---|
| 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.
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.
$ 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 section | Answers recorded |
|---|---|
| General | product name with the site code, des_key generated by the installer, ip_check off, spell check on with Pspell, identities fully editable |
| Logging | log errors, do not print them to the browser; log_driver syslog, syslog_id roundcube, facility mail |
| Database | MySQL on localhost, database roundcube, user roundcube, table prefix rc_ |
| IMAP | default_host localhost, port 143, username_domain ad-dc2.example.net, create the user on first login |
| SMTP | ssl://127.0.0.1, port 465, "use the current IMAP username and password", log sent messages |
| Display | English, skin larry, 50 rows per page, HTML display preferred, compose in plain text, autosave every 5 minutes |
| Plugins | archive |
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.
$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.
$ setsebool -P httpd_can_network_connect onWithout 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
| Log | Written by | Rotated by |
|---|---|---|
/var/log/httpd/<FQDN>-IPv4-access.log, -IPv4-error.log and the IPv6 pair | the virtual hosts | the stock /etc/logrotate.d/httpd, whose pattern /var/log/httpd/*log matches them |
/var/log/httpd/access_log, error_log | the main server | the same |
| Roundcube errors and the log of sent messages | syslog, facility mail, tag roundcube | with the mail log |
/var/log/roundcubemail/*.log | nothing, 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
- How Roundcube got under the document root. The design says the application files are in
/var/www/<FQDN>; the notes only install the package, and the archived tree has the document root as an empty entry. The step in between, a link or a copy, is not recorded. The archivedconf.dalso holds no Roundcube snippet from the package. - The Roundcube version. Neither the notes nor the design record it.
- Site 1. The design gives the FQDN
dc1-a-vcmsx002.adm.example.netand the firewall rule; I have no configuration tree of that server.
What I would do differently
- Send the Content Security Policy. The example for Roundcube was written and left in a comment.
- Narrow the SELinux boolean. Two loopback ports do not need
httpd_can_network_connect; a small local module for exactly those ports would have matched how Postfix and Dovecot were handled. - Keep the notes and the file in step. The installer answers and the archived configuration disagree in a dozen options, and only the file says what ran.
- Not copy the template today. Checked against current software:
X-XSS-Protectionis a deprecated header,SSLProtocol -all +TLSv1.2switches TLS 1.3 off,SSLCertificateChainFileis deprecated since Apache 2.4.8,mod_phpis gone from RHEL 9, and Roundcube 1.6 renamed the host options (imap_host,smtp_host). The Config documents carry the tables. - Decide the upload limit in one place. Three limits apply to an attachment, in Apache and twice in PHP, and the smallest one is in the file where nobody looks for it.