eGroupware with Virtualmin and CentOS 6


First, you will need the php-mcrypt module which is no longer provided in RHEL6 or Centos 6. For that, you must install the EPEL (Extra Packages for Enterprise Linux) library. As root, follow the instructions here as follows:

rpm -Uvh

(Do not use that command verbatim without double-checking the link above!). Then --

Following the instructions here

but changing the wget to:


then, noting capitazliation:

yum install eGroupware

which should say (amongst much other):

Installed: eGroupware.noarch 0:

The software installs into /usr/share/groupware

I had to edit two parameters in my php.ini file.

Note that with Virtualmin, it is not /etc/php.ini to modify but rather the one for the domain in which you are running eGroupware. If your domain is a Virtualmin subdomain, the actual file might be:


You can find the exact location by creating a file, let's say foo.php, within your public_html directory, and having the following contents:

<?php phpinfo(); ?>

Then look for the value of Loaded Configuration File... that is the php.ini file to edit.

Once you have located the right php.ini file, change:

upload_max_filesize = 16M

and un-commenting and modifying the line:

date.timezone = "America/Phoenix"

...and restart Apache:

sudo service httpd restart

Now, rather than write an Apache alias (which caused me problems with PHP files being returned as plaintext instead of executable code), I set a symlink:

ln -s /usr/share/egroupware public_html/egroupware

The RPM install of eGroupware sets a link from /usr/share/egroupware/ to ../../../var/lib/egroupware/ ... which should be actually in /var itself... thus, presumably, avoiding an RPM update from overwriting your config file. However this may cause problems with permissions. For me, I created the file but found that the directory /var/lib/egroupware was set to owner apache with no read or execute (i.e., list-directory) permissions for anyone else. This did the trick:

sudo chmod 755 /var/lib/egroupware sudo chmod 644 /var/lib/egroupware/

With that done, direct your web browser to and you should see the setup screen.

Note that you can have multiple domains with the single instance of egroupware. We could be fancy and call this a "multi-tenant" install. To do this, remove the "default" domain and add,, and so on.

Copy the text of the created and paste it into /usr/share/egroupware/ (which is actually /var/lib/egroupweare/

In your domain's root directory (the one above public_html), create a directory egw that will contain the files and backup directories:

mkdir egw mkdir egw/files mkdir egw/backup chown youruser:apache -R egw/

and configure eGroupware to use, for example, /home/maindomain/domains/ and /home/maindomain/domains/ as its directories.

At that point, you should be up and running!

Listing email accounts hosted on a Webmin / Virtualmin server


Here's a little script that I found handy to scan Postfix's virtual address table, compare the domains with the ones actually hosted by the system, and tell me what's really going on.

This works great for servers setup with Webmin and Virtualmin, or with plain postfix installs.

See the comments about how the script determines who "we" really are.


# Looks at /etc/postfix/virtual and tells us which of those emails
# are _actually_ hosted by this system, based on whether DNS lookups of
# the domains seem to point to "us"... where "us" is defined as any
# of the IP addresses on any of localhost's interfaces.
# Naturally, this will fail if your system is behind a gateway/firewall,
# because we have no way of probing that gadget to see how connections
# are routed from "The Internet" to us.

# Copyright (c) 2012, William Lindley bill -at- saltriversystems -dot- com
# 2012-06-06

# This script is free software, you may distribute it and/or modify it
# under the same terms as Perl itself.

use Net::DNS;
use Socket qw/inet_aton/;

use IO::Socket;
use IO::Interface qw(:flags);

my $s = IO::Socket::INET->new(Proto => 'udp');
my @interfaces = $s->if_list;
my %local_interfaces;

for my $if (@interfaces) {
    my $flags = $s->if_flags($if);

    if ( ( $flags & IFF_RUNNING ) && 
     !( $flags & IFF_LOOPBACK ) &&
     !( $flags & IFF_NOARP )) {
    $local_interfaces{$if}{address} = $s->if_addr($if);
    $local_interfaces{$s->if_addr($if)}{interface} = $if;


my $r = Net::DNS::Resolver->new;

open VIRTUAL, '<', '/etc/postfix/virtual';

my %domains_hosted;

while (<VIRTUAL>) {
    s/#.*$//;  # Remove after comment
    my ($address, $alias) = split;
    if ($address) {
    if ($alias !~ /@/) { # Only for local addresses (not forwarded)
        my ($name, $domain) = ($address =~ /^([^@]+)@(.+)$/);
        next unless $name;
#        print "[$name]@[$domain] -> [$alias]n";
        $domains_hosted{$domain}{hosted} = 1;

use Data::Dumper;

foreach my $domain (keys %domains_hosted) {

# Liberally borrowed from David Landgren (grinder)'s code at
    my %res;
    my $rr = $r->query( $domain, 'MX' );
    if ($rr) {
    for my $mx( $rr->answer ) {
            if( $mx->type eq 'CNAME' ) {
                my $a_rr = $r->query( $mx->cname, 'A' );
                if( !$a_rr ) {
                    push @{$res{-1}}, { ip => $mx->cname, forw => $r->
                        errorstring, back => 'CNAME' };
                } else {
                    $_->type eq "A"
                        and push @{$res{-1}}, { ip => $mx->cname, forw => $_->address, back => 'CNAME' }
            for( $a_rr->answer );

            next unless $mx->type eq 'MX';

            my $a_rr = $r->query( $mx->exchange, 'A' );

            if( !$a_rr ) {
                push @{$res{$mx->preference ? $mx->preference : 0}}, {
                    ip   => $mx->exchange,
                    forw => $r->errorstring,
                    back => $r->errorstring,

            my @a;
            for my $a( $a_rr->answer ) {
                next unless $a->type eq "A";

                my $ptr_rr = $r->query( join( '.', reverse( split /./ , $a->address )) . '', 'PTR' );
        if ($local_interfaces{$a->address}{interface}) {
                if( !$ptr_rr ) {
                    push @{$res{$mx->preference}}, {
                        ip => $a->address,
                        forw => $mx->exchange,
                        back => $r->errorstring,
                } else {
                    foreach ( $ptr_rr->answer ) {
            if ( $_->type eq 'PTR' ) {
                push @{$res{$mx->preference}}, {
                ip => $a->address,
                forw => lc $mx->exchange,
                back => lc $_->ptrdname,

    $domains_hosted{$domain}{mx} = %res;

# This could be greatly expanded by doing more with the data herein:
# print Dumper(%domains_hosted);

print "These email accounts are actually hosted here:n";

foreach my $domain (sort keys %domains_hosted) {
    next unless $domains_hosted{$domain}{local};
    print $domain . "n";
    foreach my $email (sort keys %{$domains_hosted{$domain}{address}}) {
    print "   ${email}@${domain}n";


Virtualmin Configuration and Tuning


Upon first install Virtualmin, you will need to set a few configuration parameters:

We will be disabling root login to webmin / virtualmin. Start by creating an administration group:

The default should be to use Unix authentication for the Webmin users. That means, resetting your Unix password will update your Webmin login as well.

Logout of Webmin and then back in with your username. Go back to Webmin Users and click on the 'root' user in the list of users. Set the password to "No password accepted" and voilá, your Webmin is now a little more secure.

Disabling Unused Cronjobs

CentOS in particular puts unwanted tasks in the cron entries. They do not appear in the 'crontab' proper but in /etc/cron.daily and /etc/cron.hourly. Rename these files to be their hidden dotfile equivalents:


Otherwise, awstats will run every hour for every domain, regardless of the settings you make in Virtualmin. Also I disabled 'makewhatis' 'webalizer' and 'freshclam' as I am not using them.

Virtualmin and Centos 6 on a Linode


Ready to move your hosted websites to a new a Virtualmin + LAMP (Linux/Apache/MySQL/Perl-PHP) server on a Linode? Here's the steps to get everything working smoothly with a minimum of hassle:

Initial Preparation

First, starting with a freshly provisioned CentOS install, ssh into the root account. Let's see what we actually have for a distribution:

$ cat /etc/issue
CentOS Linux release 6.0 (Final)
Kernel r on an m

First, create regular user accounts for the administrators, and disable direct root login. We will place these administrators into the 'wheel' group so they can sudo, and also create a developer group:

# groupadd devel
# useradd -c "Winston Smith" -g devel --groups wheel -m -s /bin/bash wsmith

Use the visudo* *command to edit the /etc/sudoers file, removing the leading # to un-comment this line:

## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)       ALL

Now, set a password for yourself:

# passwd wsmith

The logout, and ssh back in as wsmith

If you haven't read the Linode Security Basics article, now's a good time. At the very least, disable root logins by editing the /etc/ssh/sshd_config file to modify the PermitRootLogin option as follows:

PermitRootLogin no

Then restart ssh:

$ sudo service sshd restart

I also recommend fail2ban, which should reduce the clutter and risk of repeated ssh attacks:

$ sudo yum install fail2ban
$ sudo chkconfig --level 23 fail2ban on
$ sudo service fail2ban start

Next, edit /etc/sysconfig/network and change the hostname. If there is no HOSTNAME line, add one with your desired machine name in your domain; it should look like:

That file is read at boot, so you might also want to set the hostname for the current session:


$ sudo hostname `

Be sure that your DNS server has an A Record with that exact name, pointing to your new server.

Install Updates before Virtualmin

Now we will update the base system --


$ sudo yum update `

We haven't installed PHP and MySQL yet, but let's see what versions will be installed from our currently selected repositories:

$ yum search php
Loaded plugins: fastestmirror
============================================ Matched: php ============================================
php.i686 : PHP scripting language for creating dynamic web sites
php-bcmath.i686 : A module for PHP applications for using the bcmath library
php-cli.i686 : Command-line interface for PHP
php-common.i686 : Common files for PHP
$ yum list php
Loaded plugins: fastestmirror
Available Packages
mysql.i686             5.1.52-1.el6_0.1    updates
php.i686               5.3.2-6.el6_0.1     updates

OK, well that's not the latest, but Centos assures us (as of mid-October 2011) that Centos 6.1 will have updates. Meantime, the folks at Centos recommend the use of the Continuous Release (CR) repository, see this article.

$ sudo yum install centos-release-cr
$ sudo yum update

That installed, on my system, 144 new and updated packages. Now let's see what that will do for us in terms of PHP and MySQL:

$ yum list php mysql
Loaded plugins: fastestmirror
Available Packages
mysql.i686              5.1.52-1.el6_0.1   updates
php.i686                5.3.3-3.el6        cr

Ah, a nice fresh version of PHP. Before going much further, I find it convenient to also install the console version of emacs for editing, and the 'screen' package for multi-virtual-screen remote administration:


$ sudo yum install emacs-nox screen `

Load Virtualmin

Download the script from into /usr/src ... and then execute it:


$ sudo bash # cd /usr/src # wget



That may take some time indeed! Eventually you should see:

INFO - Updating SpamAssassin rules...
INFO - Rule updates done
[root@linode1 src]#

Once it completes, login, as root, to your new virtualmin configuration at your linode's address: .. and then read the next part of this article.

Useful Tools

sudo yum install emacs-nox screen

For PHP with Graphics support

sudo yum install gd php-gd ImageMagick ImageMagick-devel php-devel httpd-devel
sudo pecl install imagick
<em>(and, as root) </em>echo "" > /etc/php.d/imagick.ini
sudo apachectl graceful

The php-devel module is required for loading ImageMagick into PHP; without it, you will get phpize: command not found. Note that the pecl command above actually compiles some code, so you need the gcc compilers and the various development modules.

Webmin and fail2ban

If you ever use Webmin to edit your firewall rules, you should be sure to do this last step. It's also handy if you lock yourself out of your secure shell, to be able to do a one-time reset of fail2ban's rules from the webmin interface.

Login to Webmin. Under Networking, on the “Linux Firewall” main page, click Module Config in the upper-left of the pane. On the configuration page, in the “Configurable options” section, look for the line “Command to run after applying configuration.” Click the button next to the text box on that line, and in the text box enter service fail2ban restart and then click the Save button at the bottom of the page. Now, whenever you click “Apply Configuration” on the Firewall rule page, it will automatically restart fail2ban.

Controlling backscatter spam in Mailman


By default, Mailman -- which is installed by default in Virtualmin as your mailing list manager, exhibits some nasty behavior, being open to sending "backscatter" spam. This means that the Bad Guys send fraudulent messages "from" the email address they actually want to send spam to; Mailman rejects those messages, basically sending a bounce message to the victim.

The mechanism the spammers use is left over from the days before web interfaces. Nowadays, your subscribers interact with Mailman almost exclusively via its HTTP interface. Other than actual postings from subscribed members, and messages to the list owner, there is little or no reason to keep the vestigial email aliases.

To disable the vulnerable aliases:

With Virtualmin, go into the Webmin interface, under Servers; Postfix Mail Server; and click on the Aliases icon. You should see, for example, defined among the email addresses:    Program /usr/lib/mailman/mail/mailman post yourlist
<strong>      Program /usr/lib/mailman/mail/mailman admin yourlist</strong>    Program /usr/lib/mailman/mail/mailman bounces yourlist
<strong>    Program /usr/lib/mailman/mail/mailman confirm yourlist       Program /usr/lib/mailman/mail/mailman join yourlist      Program /usr/lib/mailman/mail/mailman leave yourlist
</strong>     Program /usr/lib/mailman/mail/mailman owner yourlist
<strong>    Program /usr/lib/mailman/mail/mailman request yourlist  Program /usr/lib/mailman/mail/mailman subscribe yourlist        Program /usr/lib/mailman/mail/mailman unsubscribe yourlist</strong>

for "" ... I recommend you disable these aliases shown in bold above:

-admin-, -confirm-, -join-, -leave-, -request-, -subscribe-, -unsubscribe-.

You also need to edit /etc/postfix/virtual -- or in Webmin, open "Servers" and click on "Postfix Mail Server" then click on the "Virtual Domains" icon:

and delete the virtual mappings for all the addresses you just removed.

You will want to do this for each mailing list on your system.