|

Roundcube Webmail on a Separate Incus Container

Part 6 of the Building a Modern Mail Server on Debian 13 series

Series Navigation:

Introduction

In this final chapter of our mail server series, we’ll set up Roundcube webmail on a separate Incus container. This architecture provides several advantages:

  • Security isolation: Webmail runs in its own container, separate from mail services
  • Scalability: Each component can be scaled independently
  • Maintenance: Updates to webmail don’t affect mail delivery
  • Flexibility: Can run on a different host server entirely

Our setup assumes:

  • An existing mail server with Postfix/Dovecot (from Parts 1-5)
  • Nginx reverse proxy handling SSL termination (on a separate host/container)
  • Incus containers with bridged networking
  • MariaDB database on the mail server (we’ll connect remotely)

Architecture Overview

                    ┌──────────────────────────────────────────────────────┐
                    │                    HOST SERVER 1                     │
                    │  ┌─────────────────────────────────────────────────┐ │
                    │  │              Nginx Proxy Container              │ │
 Internet ─────────────▶  SSL Termination (443)                          │ │
                    │  │  - webmail.example.com → Roundcube (80)         │ │
                    │  │  - mail.example.com → PostfixAdmin (80)         │ │
                    │  └─────────────────────────────────────────────────┘ │
                    └──────────────────────────────────────────────────────┘
                                              │
                                              │ Internal Network
                                              │
        ┌─────────────────────────────────────┼─────────────────────────────────────┐
        │                                     │                                     │
        ▼                                     ▼                                     │
┌───────────────────┐               ┌───────────────────┐                           │
│   HOST SERVER 2   │               │   HOST SERVER 1   │                           │
│ ┌───────────────┐ │               │ ┌───────────────┐ │                           │
│ │   Roundcube   │ │               │ │  Mail Server  │ │                           │
│ │   Container   │ │◀────────────▶│ │   Container   │ │                           │
│ │               │ │   IMAP/SMTP   │ │               │ │                           │
│ │  - PHP-FPM    │ │   MariaDB     │ │  - Postfix    │ │                           │
│ │  - Nginx      │ │               │ │  - Dovecot    │ │                           │
│ │               │ │               │ │  - MariaDB    │ │                           │
│ └───────────────┘ │               │ │  - Rspamd     │ │                           │
└───────────────────┘               │ └───────────────┘ │                           │
                                    └───────────────────┘                           │

In this guide, the Roundcube container can be on:

  1. The same host as your mail server (different container)
  2. A completely different host server (our scenario)

Prerequisites

Before starting, ensure you have:

  • Working mail server from Parts 1-5 (Postfix/Dovecot/MariaDB)
  • Nginx reverse proxy configured for SSL termination
  • DNS record for webmail subdomain (e.g., webmail.example.com)
  • Fresh Debian 13 container for Roundcube
  • Network connectivity between containers/hosts

DNS Record

Add an A record for your webmail subdomain:

webmail.example.com.    IN    A    YOUR_PROXY_IP

If using IPv6:

webmail.example.com.    IN    AAAA    YOUR_PROXY_IPv6

Section 1: Create the Roundcube Container

1.1 Launch the Container

On your Incus host (can be different from mail server host):

# Create a new Debian 13 container
incus launch images:debian/13 roundcube -d eth0,ipv4.address=10.0.10.50

# Verify it's running
incus list

1.2 Enter the Container

incus shell roundcube

1.3 Set Up Variables

Before starting, create a variables file that will be used throughout this guide. This keeps all the configuration in one place, making the setup reproducible.

cat > /root/roundcube-vars.sh << 'EOF'
# Roundcube Installation Variables
# Source this file before running commands: source /root/roundcube-vars.sh

# ===========================================
# DOMAIN AND SERVER SETTINGS
# ===========================================

# Your webmail domain (must have DNS pointing to your proxy)
SERVER_NAME="webmail.example.com"

# Your mail server FQDN (from Parts 1-5)
MAIL_SERVER="mail.example.com"

# ===========================================
# NETWORK SETTINGS
# ===========================================

# Nginx reverse proxy IP (for real_ip_from directive)
PROXY_IP="10.0.10.100"

# Roundcube container IP (for reverse proxy config)
ROUNDCUBE_IP="10.0.10.50"

# CrowdSec LAPI server IP (usually same as proxy)
LAPI_SERVER_IP="10.0.10.100"

# ===========================================
# CROWDSEC SETTINGS
# ===========================================

# Agent name (must match what's registered on LAPI server)
CROWDSEC_AGENT_NAME="roundcube-agent"

# Agent password (get this from: cscli machines add roundcube-agent --auto -f -)
CROWDSEC_AGENT_PASSWORD="YOUR_AGENT_PASSWORD"

# ===========================================
# ROUNDCUBE CONTAINER IPS FOR MAIL SERVER WHITELIST
# ===========================================

# Roundcube container IPv4 (for mail server whitelist)
RC_IPV4="10.0.10.50"

# Roundcube container IPv6 (for mail server whitelist)
RC_IPV6="2001:db8::50"

# ===========================================
# AUTO-GENERATED (do not edit below this line)
# ===========================================

# Database password (generated during setup)
# DB_PW will be set during Section 5

# Encryption key (generated during setup)
# DES_KEY will be set during Section 6
EOF

# Secure the file (contains sensitive credentials)
chmod 600 /root/roundcube-vars.sh

Edit the file with your actual values:

vi /root/roundcube-vars.sh

Source the variables before running any commands:

source /root/roundcube-vars.sh

Tip: Source this file at the beginning of each SSH session, or add it to your .bashrc for persistence.

1.4 Initial System Setup

# Update system
apt update && apt upgrade -y

# Set timezone
timedatectl set-timezone Europe/Brussels

# Set hostname
hostnamectl set-hostname roundcube

# Install essential tools (including host for DNS lookups)
apt install -y curl wget gnupg2 ca-certificates lsb-release apt-transport-https bind9-dnsutils

1.5 Verify DNS Resolution

Since your Roundcube container is on a different host, it will connect to the mail server via DNS. Verify that DNS resolution works:

# Source variables
source /root/roundcube-vars.sh

# Test DNS resolution to your mail server
host $MAIL_SERVER

# Test connectivity
ping -c 3 $MAIL_SERVER

Note: If you have split-horizon DNS (internal vs external resolution), ensure the Roundcube container resolves to the correct IP for your mail server.

Section 2: Install Required Packages

2.1 Install Nginx

apt install -y nginx
systemctl enable nginx

2.2 Install PHP 8.4 and Required Extensions

Debian 13 ships with PHP 8.4:

apt install -y \
    php8.4-fpm \
    php8.4-mysql \
    php8.4-gd \
    php8.4-imagick \
    php8.4-curl \
    php8.4-intl \
    php8.4-zip \
    php8.4-xml \
    php8.4-mbstring \
    php8.4-ldap \
    php8.4-common

systemctl enable php8.4-fpm

Note: The php8.4-xml package includes DOM, SimpleXML, and XMLReader/Writer. The php-imap extension is not needed, as Roundcube uses its own native IMAP implementation.

2.3 Install Additional Dependencies

apt install -y \
    mariadb-server \
    unzip

systemctl enable mariadb

Section 3: Configure PHP

3.1 Adjust PHP Settings for Roundcube

Edit the default PHP-FPM pool to accommodate Roundcube’s needs (file uploads, timeouts):

cat > /etc/php/8.4/fpm/conf.d/99-roundcube.ini << 'EOF'
; Roundcube optimizations
upload_max_filesize = 25M
post_max_size = 25M
max_execution_time = 300
memory_limit = 128M
date.timezone = Europe/Brussels
EOF

3.2 Create Log Directory

mkdir -p /var/log/roundcube
chown www-data:www-data /var/log/roundcube

3.3 Restart PHP-FPM

systemctl restart php8.4-fpm

Section 4: Install Roundcube

4.1 Download Latest Roundcube

Check the latest version at https://roundcube.net/download/

cd /tmp

# Download Roundcube (check for latest version)
ROUNDCUBE_VERSION="1.6.10"
wget https://github.com/roundcube/roundcubemail/releases/download/${ROUNDCUBE_VERSION}/roundcubemail-${ROUNDCUBE_VERSION}-complete.tar.gz

# Extract
tar xzf roundcubemail-${ROUNDCUBE_VERSION}-complete.tar.gz

# Move to web directory
mv roundcubemail-${ROUNDCUBE_VERSION} /var/www/roundcube

# Set ownership
chown -R www-data:www-data /var/www/roundcube

4.2 Create Required Directories

# Temp and log directories
mkdir -p /var/www/roundcube/temp
mkdir -p /var/www/roundcube/logs

# Set permissions
chown -R www-data:www-data /var/www/roundcube/temp
chown -R www-data:www-data /var/www/roundcube/logs
chmod 750 /var/www/roundcube/temp
chmod 750 /var/www/roundcube/logs

Section 5: Database Setup

Roundcube needs its own database for storing contacts, identities, session data, and cache. Since the Roundcube container is on a separate server from your mail infrastructure, we’ll run MariaDB locally.

5.1 Secure MariaDB Installation

mariadb-secure-installation

Answer the prompts:

  • Switch to unix_socket authentication? — Yes (already default on Debian 13)
  • Change the root password? — No (not needed with unix_socket)
  • Remove anonymous users? — Yes
  • Disallow root login remotely? — Yes
  • Remove test database? — Yes
  • Reload privilege tables? — Yes

5.2 Create Roundcube Database and User

Generate a secure random password and create the database:

# Source variables
source /root/roundcube-vars.sh

# Generate random password and store it
DB_PW=$(openssl rand -base64 24)
echo "Roundcube DB password: $DB_PW"

# Append to variables file for future reference
echo "" >> /root/roundcube-vars.sh
echo "# Database password (generated)" >> /root/roundcube-vars.sh
echo "DB_PW=\"$DB_PW\"" >> /root/roundcube-vars.sh

# Create database and user
mariadb << EOF
CREATE DATABASE roundcubemail CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'roundcube'@'localhost' IDENTIFIED BY '$DB_PW';
GRANT ALL PRIVILEGES ON roundcubemail.* TO 'roundcube'@'localhost';
FLUSH PRIVILEGES;
EOF

5.3 Initialize Database Schema

Import the Roundcube database schema:

mariadb roundcubemail < /var/www/roundcube/SQL/mysql.initial.sql

5.4 Verify Database Setup

mariadb roundcubemail -e "SHOW TABLES;"

You should see tables like cache, contacts, identities, session, users, etc.

Section 6: Configure Roundcube

6.1 Source Variables and Generate Encryption Key

# Source variables (includes DB_PW from Section 5)
source /root/roundcube-vars.sh

# Generate 24-character encryption key and store it
DES_KEY=$(openssl rand -hex 24)
echo "Roundcube DES key: $DES_KEY"

# Append to variables file
echo "# Encryption key (generated)" >> /root/roundcube-vars.sh
echo "DES_KEY=\"$DES_KEY\"" >> /root/roundcube-vars.sh

6.2 Create Main Configuration

The variables $DB_PW, $MAIL_SERVER, and $DES_KEY are now set from the variables file:

cat > /var/www/roundcube/config/config.inc.php << EOF
<?php

/*
 * Roundcube Configuration
 * Part 6 of Mail Server Tutorial Series
 */

// ----------------------------------
// DATABASE CONFIGURATION
// ----------------------------------

// Local database on Roundcube container
\$config['db_dsnw'] = 'mysql://roundcube:${DB_PW}@localhost/roundcubemail';

// ----------------------------------
// IMAP CONFIGURATION
// ----------------------------------

// Connect to mail server via IMAPS (SSL)
\$config['imap_host'] = 'ssl://${MAIL_SERVER}:993';

// IMAP authentication type
\$config['imap_auth_type'] = 'PLAIN';

// IMAP connection options
\$config['imap_conn_options'] = array(
    'ssl' => array(
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'allow_self_signed' => false,
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
    ),
);

// ----------------------------------
// SMTP CONFIGURATION
// ----------------------------------

// Submit mail via SMTPS (SSL)
\$config['smtp_host'] = 'ssl://${MAIL_SERVER}:465';

// Use current user's credentials for SMTP
\$config['smtp_user'] = '%u';
\$config['smtp_pass'] = '%p';

// SMTP authentication type
\$config['smtp_auth_type'] = 'PLAIN';

// SMTP connection options
\$config['smtp_conn_options'] = array(
    'ssl' => array(
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'allow_self_signed' => false,
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
    ),
);

// ----------------------------------
// SYSTEM CONFIGURATION
// ----------------------------------

// Encryption key
\$config['des_key'] = '${DES_KEY}';

// Support URL (optional)
\$config['support_url'] = '';

// Product name shown in interface
\$config['product_name'] = 'Webmail';

// User agent string
\$config['useragent'] = 'Roundcube Webmail';

// Default language
\$config['language'] = 'en_US';

// Enable caching
\$config['enable_caching'] = true;

// Message cache lifetime
\$config['message_cache_lifetime'] = '10d';

// ----------------------------------
// LOGGING
// ----------------------------------

// Log to files
\$config['log_driver'] = 'file';

// Log directory
\$config['log_dir'] = '/var/www/roundcube/logs/';

// Log IMAP conversations (required for brute force detection)
\$config['imap_log'] = true;

// Log SMTP conversations (debug)
\$config['smtp_log'] = true;

// Log logins
\$config['log_logins'] = true;

// Per-user logging
\$config['per_user_logging'] = false;

// ----------------------------------
// USER INTERFACE
// ----------------------------------

// Skin (default: elastic)
\$config['skin'] = 'elastic';

// Default timezone (empty = auto-detect)
\$config['timezone'] = 'auto';

// Date format
\$config['date_format'] = 'd-m-Y';

// Time format
\$config['time_format'] = 'H:i';

// Show images from known senders
\$config['show_images'] = 1;

// Preview pane (right side)
\$config['preview_pane'] = true;

// Default view (list or thread)
\$config['default_list_mode'] = 'list';

// Messages per page
\$config['mail_pagesize'] = 50;

// ----------------------------------
// MESSAGE COMPOSITION
// ----------------------------------

// Default HTML editor
\$config['htmleditor'] = 1;

// Reply mode (bottom, top)
\$config['reply_mode'] = 0;

// Reply all mode (list, all)
\$config['reply_all_mode'] = 1;

// Forward mode
\$config['forward_attachment'] = false;

// Draft auto-save interval (seconds, 0 = disabled)
\$config['draft_autosave'] = 60;

// Upload max file size (matches PHP config)
\$config['max_message_size'] = '25M';

// ----------------------------------
// ADDRESS BOOK
// ----------------------------------

// Default address book
\$config['default_addressbook'] = 'sql';

// Autocomplete from address book
\$config['autocomplete_addressbooks'] = array('sql');

// Minimum characters for autocomplete
\$config['autocomplete_min_length'] = 2;

// ----------------------------------
// SPECIAL FOLDERS
// ----------------------------------

// Automatically create special folders
\$config['create_default_folders'] = true;

// Special folder names (match Dovecot namespace)
\$config['drafts_mbox'] = 'Drafts';
\$config['junk_mbox'] = 'Junk';
\$config['sent_mbox'] = 'Sent';
\$config['trash_mbox'] = 'Trash';

// ----------------------------------
// SECURITY
// ----------------------------------

// IP check on session (may cause issues behind load balancer)
\$config['ip_check'] = false;

// Session lifetime (minutes)
\$config['session_lifetime'] = 60;

// Login rate limit (shows friendly message for existing users only)
// Non-existing usernames are handled by CrowdSec after 10 attempts
\$config['login_rate_limit'] = 3;

// Session name
\$config['session_name'] = 'roundcube_sessid';

// Force HTTPS (handled by reverse proxy)
\$config['force_https'] = false;

// Trust X-Forwarded headers (Nginx handles real IP translation)
\$config['use_https'] = true;

// CSRF protection
\$config['request_path'] = '/';

// ----------------------------------
// PLUGINS
// ----------------------------------

// Active plugins
\$config['plugins'] = array(
    'archive',
    'zipdownload',
    'emoticons',
    'vcard_attachments',
);

EOF

Note: The \$ escaping is required inside the heredoc to produce literal $config in the PHP file. We’ve set $config['imap_log'] = true; which is required for CrowdSec brute force detection.

6.3 Set Permissions

chown www-data:www-data /var/www/roundcube/config/config.inc.php
chmod 640 /var/www/roundcube/config/config.inc.php

Section 7: Configure Nginx (Roundcube Container)

7.1 Create Virtual Host

Source the variables file (includes SERVER_NAME and PROXY_IP):

source /root/roundcube-vars.sh

This is the Nginx configuration inside the Roundcube container (not the reverse proxy):

cat > /etc/nginx/sites-available/roundcube << EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${SERVER_NAME};

    root /var/www/roundcube/public_html;
    index index.php;

    # Logging
    access_log /var/log/nginx/roundcube_access.log;
    error_log /var/log/nginx/roundcube_error.log;

    # Security headers (some handled by reverse proxy)
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "same-origin" always;

    # Real IP from reverse proxy
    set_real_ip_from ${PROXY_IP};
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    # Max upload size (match PHP)
    client_max_body_size 25M;

    # Deny access to sensitive files
    location ~ /\. {
        deny all;
    }

    location ~ ^/(config|temp|logs)/ {
        deny all;
    }

    location ~ /README|INSTALL|LICENSE|CHANGELOG|UPGRADING {
        deny all;
    }

    location ~ /(bin|SQL|vendor)/ {
        deny all;
    }

    # Main location
    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }

    # PHP handling
    location ~ \.php\$ {
        try_files \$uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)\$;
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        fastcgi_param PATH_INFO \$fastcgi_path_info;
        
        # Security
        fastcgi_param HTTPS on;
        
        # Timeouts for slow operations
        fastcgi_read_timeout 300;
        fastcgi_send_timeout 300;
    }

    # Static files caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)\$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
EOF

7.2 Enable Site and Test

# Enable site
ln -s /etc/nginx/sites-available/roundcube /etc/nginx/sites-enabled/

# Remove default site
rm -f /etc/nginx/sites-enabled/default

# Test configuration
nginx -t

# Restart Nginx
systemctl restart nginx

Section 8: Configure Nginx Reverse Proxy

On your Nginx reverse proxy container/server, add the Roundcube backend.

8.1 Set Variables

On the Nginx proxy, set the variables needed for this configuration:

# Set your webmail domain and Roundcube container IP
SERVER_NAME="webmail.example.com"
ROUNDCUBE_IP="10.0.10.50"

Alternatively, create a variables file for consistency:

cat > /root/roundcube-vars.sh << 'EOF'
# Roundcube Reverse Proxy Variables
SERVER_NAME="webmail.example.com"
ROUNDCUBE_IP="10.0.10.50"
EOF

chmod 600 /root/roundcube-vars.sh
source /root/roundcube-vars.sh

8.2 Create HTTP Server Block for Certificate Issuance

First, create a simple HTTP-only server block to obtain the SSL certificate:

cat > /etc/nginx/sites-available/${SERVER_NAME} << EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${SERVER_NAME};

    # ACME challenge for certificate issuance/renewal
    include snippets/acme.conf;

    # Temporary: return 503 until HTTPS is configured
    location / {
        return 503 "Site under construction";
        add_header Content-Type text/plain;
    }
}
EOF

Enable the site and reload Nginx:

ln -s /etc/nginx/sites-available/${SERVER_NAME} /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

8.3 Issue SSL Certificate

Using acme.sh with webroot validation:

acme.sh --issue -d ${SERVER_NAME} -w /var/www/acme

Create the certificate directory and deploy:

mkdir -p /etc/acme/live/${SERVER_NAME}

acme.sh --install-cert -d ${SERVER_NAME} \
    --key-file /etc/acme/live/${SERVER_NAME}/privkey.pem \
    --fullchain-file /etc/acme/live/${SERVER_NAME}/fullchain.pem \
    --reloadcmd "systemctl reload nginx"

8.4 Update to Full HTTPS Configuration

Now replace the temporary config with the full reverse proxy configuration:

cat > /etc/nginx/sites-available/${SERVER_NAME} << EOF
# HTTP - ACME challenge and redirect
server {
    listen 80;
    listen [::]:80;
    server_name ${SERVER_NAME};

    # ACME challenge for certificate renewal
    include snippets/acme.conf;

    # Redirect all other requests to HTTPS
    location / {
        return 301 https://\$server_name\$request_uri;
    }
}

# HTTPS server
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name ${SERVER_NAME};

    # SSL certificates (managed by acme.sh)
    ssl_certificate /etc/acme/live/${SERVER_NAME}/fullchain.pem;
    ssl_certificate_key /etc/acme/live/${SERVER_NAME}/privkey.pem;

    # SSL configuration (from your ssl.conf snippet)
    include snippets/ssl.conf;

    # HSTS (uncomment after testing)
    # add_header Strict-Transport-Security "max-age=63072000" always;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Logging
    access_log /var/log/nginx/${SERVER_NAME}_access.log;
    error_log /var/log/nginx/${SERVER_NAME}_error.log;

    # Max upload size (match Roundcube)
    client_max_body_size 25M;

    # Reverse proxy to Roundcube container
    location / {
        proxy_pass http://${ROUNDCUBE_IP}:80;
        proxy_http_version 1.1;

        # Headers
        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-Proto \$scheme;
        proxy_set_header X-Forwarded-Host \$host;
        proxy_set_header X-Forwarded-Server \$host;

        # WebSocket support (for future features)
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;

        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 16k;
    }
}
EOF

Note: This configuration uses the modern HTTP/2 syntax (http2 on; as a separate directive) and includes your SSL snippet (include snippets/ssl.conf;). Make sure the certificate paths are correct: fullchain.pem for the certificate and privkey.pem for the key.

8.5 Enable and Test

nginx -t
systemctl reload nginx

The certificate will auto-renew via the webroot method — acme.sh uses /var/www/acme/.well-known/acme-challenge/ which is served by the include snippets/acme.conf; directive.

Section 9: CrowdSec Integration

Protecting Roundcube login against brute force attacks is essential. We’ll install the CrowdSec agent to detect attacks and report them to your central LAPI server (from Part 3). The bouncers on your Nginx proxy will handle the blocking; no bouncer is needed on the Roundcube container itself.

Architecture:

Roundcube Container          Nginx Proxy (ct1)
┌─────────────────┐         ┌─────────────────────┐
│ CrowdSec Agent  │────────▶│ Central LAPI        │
│ (detects)       │ alerts  │                     │
└─────────────────┘         │ Firewall Bouncer    │──▶ Blocks IPs
                            │ Nginx Bouncer       │──▶ Blocks IPs
                            └─────────────────────┘

9.1 On Your CrowdSec LAPI Server

Register the Roundcube container as a machine (agent):

# Register the agent (output to screen, not file)
cscli machines add roundcube-agent --auto -f -

Note the generated password; you’ll need it for the agent configuration.

9.2 On the Roundcube Container

Install CrowdSec Repository

# Install prerequisites
apt install -y curl gnupg

# Create keyrings directory
mkdir -p /etc/apt/keyrings

# Add CrowdSec GPG key
curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey | gpg --dearmor -o /etc/apt/keyrings/crowdsec.gpg

# Add CrowdSec repository (using 'any any' for latest packages)
cat > /etc/apt/sources.list.d/crowdsec.list << 'EOF'
deb [signed-by=/etc/apt/keyrings/crowdsec.gpg] https://packagecloud.io/crowdsec/crowdsec/any any main
EOF

# Update package list
apt update

Install CrowdSec Agent Only

apt install -y crowdsec

Note: We only install the agent, not the bouncer. The Nginx proxy handles blocking.

Remove Default Bouncer (if installed)

CrowdSec installation may have installed a default bouncer. Remove it since blocking is handled by the Nginx proxy:

# Check if bouncer is installed
dpkg -l | grep crowdsec-firewall-bouncer

# If installed, remove it
apt remove -y crowdsec-firewall-bouncer-nftables 2>/dev/null || true

Configure Agent to Connect to Central LAPI

Source variables and create the configuration:

source /root/roundcube-vars.sh

cat > /etc/crowdsec/local_api_credentials.yaml << EOF
url: http://${LAPI_SERVER_IP}:8080
login: ${CROWDSEC_AGENT_NAME}
password: ${CROWDSEC_AGENT_PASSWORD}
EOF

Restart and Verify Connection

systemctl restart crowdsec
cscli lapi status

You should see: You can successfully interact with Local API (LAPI)

If the connection fails, check:

  • Is the LAPI server IP correct?
  • Is the agent registered on the LAPI server?
  • Is port 8080 accessible from this container?

Configure Log Acquisition

cat > /etc/crowdsec/acquis.d/roundcube.yaml << 'EOF'
filenames:
  - /var/log/nginx/roundcube_access.log
labels:
  type: nginx
---
filenames:
  - /var/www/roundcube/logs/errors.log
labels:
  type: roundcube
EOF

Fix Log Directory Permissions

CrowdSec needs to read the Roundcube logs directory. By default, it may be too restrictive:

chmod 755 /var/www/roundcube/logs

9.3 Create Custom Roundcube Parser and Scenario

CrowdSec doesn’t include a Roundcube parser, so we create custom ones.

Create the Parser

cat > /etc/crowdsec/parsers/s01-parse/roundcube-logs.yaml << 'EOF'
name: custom/roundcube-logs
description: "Parse Roundcube authentication failures"
filter: evt.Parsed.program == 'roundcube'
onsuccess: next_stage
nodes:
  - grok:
      pattern: '\[%{DATA:timestamp}\]: <%{DATA:session}> IMAP Error: Login failed for %{DATA:user} against %{DATA:server} from %{IPORHOST:src_ip} \(X-Forwarded-For: %{IPORHOST:xff_ip}\)\. %{GREEDYDATA:error_msg}'
      apply_on: message
    statics:
      - meta: log_type
        value: roundcube_auth_fail
      - meta: service
        value: roundcube
      - meta: source_ip
        expression: evt.Parsed.xff_ip
EOF

Create the Brute Force Scenario

cat > /etc/crowdsec/scenarios/roundcube-bf.yaml << 'EOF'
type: leaky
name: custom/roundcube-bf
description: "Detect Roundcube brute force"
filter: evt.Meta.log_type == 'roundcube_auth_fail'
groupby: evt.Meta.source_ip
capacity: 9
leakspeed: 2m
blackhole: 5m
labels:
  service: roundcube
  type: bruteforce
  remediation: true
EOF

This triggers a ban after 10 failed login attempts within 2 minutes.

Two-layer protection:

  • Existing users (wrong password): Roundcube shows “Too many login attempts” after 3 failures
  • Non-existing users (attackers): CrowdSec blocks IP after 10 attempts with “Access Forbidden”

9.4 Verify Services

# Check CrowdSec is running
systemctl status crowdsec

# Verify connection to LAPI
cscli lapi status

# Check scenarios are loaded
cscli scenarios list | grep roundcube

# Check logs are being acquired
cscli metrics | grep "file:"

9.5 Verify Connection

On the Roundcube container:

# Check agent connection
cscli lapi status

# Check scenario is loaded
cscli scenarios list | grep roundcube

# Check logs are being acquired
cscli metrics | grep "file:"

Should show:

  • Successful LAPI authentication with username “roundcube-agent”
  • custom/roundcube-bf scenario enabled
  • Both log files being read

On the LAPI server:

cscli machines list

You should see roundcube-agent in machines with a recent heartbeat.

9.6 Whitelist Roundcube Container IPs on Mail Server

Important: On your mail server’s CrowdSec, whitelist the Roundcube container IPs (both IPv4 and IPv6) to prevent them from being banned when users have login issues.

Note: If your Roundcube container is on the same host as the mail server, use the internal container IP (e.g., 10.0.10.50). If it’s on a different VPS/host, use the public IP of that VPS.

# Set the Roundcube container IPs
RC_IPV4="10.0.10.50"
RC_IPV6="2001:db8::50"

cat > /etc/crowdsec/parsers/s02-enrich/roundcube-whitelist.yaml << EOF
name: custom/roundcube-whitelist
description: "Whitelist Roundcube webmail servers"
whitelist:
  reason: "Roundcube webmail servers"
  ip:
    - "${RC_IPV4}"
    - "${RC_IPV6}"
EOF

systemctl restart crowdsec

If you have multiple Roundcube instances, add all their IPs to the whitelist.

9.7 Add Dovecot Rate Limiting on Mail Server

Important: Since Roundcube IPs are whitelisted on the mail server, they bypass CrowdSec’s brute force protection. Add Dovecot-level rate limiting as defense-in-depth:

cat > /etc/dovecot/conf.d/10-ratelimit.conf << 'EOF'
# Rate limiting - defense in depth for whitelisted IPs (e.g., Roundcube)
# Adds delay after each failed authentication attempt
auth_failure_delay = 3 secs
EOF

systemctl restart dovecot

This adds a 3-second delay after each failed authentication, slowing down brute force attempts even from whitelisted sources.

9.8 Test Brute Force Protection

Try 4+ failed logins quickly on Roundcube from a test IP. Check on the LAPI server:

cscli alerts list | head -5
cscli decisions list

You should see the test IP banned with reason custom/roundcube-bf.

The ban will be enforced by the bouncers on your Nginx proxy, blocking access to all services behind the proxy.

To unblock an IP:

cscli decisions delete --ip THE_BLOCKED_IP

Section 10: Testing

10.1 Test Database Connection

From the Roundcube container:

mariadb roundcubemail -e "SHOW TABLES;"

10.2 Test IMAP Connection

openssl s_client -connect ${MAIL_SERVER}:993

10.3 Test SMTP Connection

openssl s_client -connect ${MAIL_SERVER}:465

10.4 Test Web Access

  1. Open https://webmail.example.com in your browser
  2. Log in with an existing mail account
  3. Test sending an email to yourself
  4. Test receiving the email
  5. Test archiving a message
  6. Test downloading attachments

10.5 Check Logs

# Roundcube logs
tail -f /var/www/roundcube/logs/errors.log
tail -f /var/www/roundcube/logs/sendmail.log

# Nginx logs
tail -f /var/log/nginx/roundcube_error.log

Section 11: Post-Installation Cleanup

11.1 Remove Installer

Important: Delete the installer directory after setup:

rm -rf /var/www/roundcube/installer

11.2 Verify Permissions

# Ensure correct ownership
chown -R www-data:www-data /var/www/roundcube

# Secure config file
chmod 640 /var/www/roundcube/config/config.inc.php

# Secure logs and temp
chmod 750 /var/www/roundcube/logs
chmod 750 /var/www/roundcube/temp

11.3 Set Up Log Rotation

cat > /etc/logrotate.d/roundcube << 'EOF'
/var/www/roundcube/logs/*.log {
    weekly
    rotate 8
    compress
    delaycompress
    missingok
    notifempty
    create 640 www-data www-data
}

/var/log/roundcube/*.log {
    weekly
    rotate 8
    compress
    delaycompress
    missingok
    notifempty
    create 640 www-data www-data
}
EOF

Section 12: Updating Roundcube

To update Roundcube in the future:

# Backup current installation
cp -a /var/www/roundcube /var/www/roundcube.bak
cp /var/www/roundcube/config/config.inc.php /root/roundcube-config-backup.php

# Download new version
cd /tmp
wget https://github.com/roundcube/roundcubemail/releases/download/VERSION/roundcubemail-VERSION-complete.tar.gz
tar xzf roundcubemail-VERSION-complete.tar.gz

# Run update script
cd roundcubemail-VERSION
./bin/update.sh --version=OLD_VERSION

# Or manual update
rsync -av --exclude 'config/config.inc.php' --exclude 'logs/*' --exclude 'temp/*' \
    roundcubemail-VERSION/ /var/www/roundcube/

# Update database if needed
cd /var/www/roundcube
./bin/update.sh

# Set permissions
chown -R www-data:www-data /var/www/roundcube

Troubleshooting

Cannot Connect to IMAP/SMTP

Check connectivity from Roundcube container:

openssl s_client -connect mail.example.com:993
openssl s_client -connect mail.example.com:465

Check firewall on mail server:

# On mail server
ss -tlnp | grep -E '993|465'

Verify certificates:

openssl s_client -connect mail.example.com:993 2>/dev/null | openssl x509 -noout -dates

Database Connection Failed

# Test local database connection (as root via unix_socket)
mariadb roundcubemail -e "SELECT 1;"

# Check MariaDB is running
systemctl status mariadb

# Check Roundcube tables exist
mariadb roundcubemail -e "SHOW TABLES;"

# Verify roundcube user exists
mariadb -e "SELECT user, host FROM mysql.user WHERE user='roundcube';"

PHP Errors

# Check PHP-FPM status
systemctl status php8.4-fpm

# Check PHP errors
tail -f /var/log/roundcube/php-errors.log

# Verify socket exists
ls -la /run/php/php-fpm.sock

Login Fails But Credentials Are Correct

  1. Check $config['imap_host'] format (needs ssl:// prefix for port 993)
  2. Verify $config['imap_auth_type'] matches Dovecot config
  3. Check Dovecot logs on mail server: journalctl -u dovecot -f

SSL Certificate Verification Failed

# Check CA certificates are installed
ls /etc/ssl/certs/ca-certificates.crt

# Test certificate chain
openssl s_client -connect mail.example.com:993 -CAfile /etc/ssl/certs/ca-certificates.crt

# If self-signed (not recommended), temporarily set in config:
# 'allow_self_signed' => true,

CrowdSec Agent Not Detecting Failed Logins

Check log directory permissions:

ls -la /var/www/roundcube/logs/
# Should be drwxr-xr-x (755)

# Fix if needed
chmod 755 /var/www/roundcube/logs
systemctl restart crowdsec

Check logs are being acquired:

cscli metrics | grep "file:"

Both /var/log/nginx/roundcube_access.log and /var/www/roundcube/logs/errors.log should appear.

Check imap_log is enabled in Roundcube config:

grep imap_log /var/www/roundcube/config/config.inc.php
# Should show: $config['imap_log'] = true;

Check agent connection:

cscli lapi status

Check scenario is loaded:

cscli scenarios list | grep roundcube

Security Recommendations

  1. Keep Roundcube updated – Check for updates monthly
  2. Use strong passwords – Minimum 12 characters for all accounts
  3. CrowdSec protection – Agent reports brute force to central LAPI, proxy bouncers block attackers
  4. Monitor logs – Set up alerting for authentication failures
  5. Regular backups – Back up the database and config regularly
  6. Use firewall rules – Only allow necessary ports between containers
  7. Log permissions – Ensure CrowdSec can read Roundcube logs (chmod 755 /var/www/roundcube/logs)

Conclusion

You now have a fully functional Roundcube webmail installation on a separate Incus container, connecting to your mail server over the network. This architecture provides:

  • Security through isolation – Compromise of webmail doesn’t expose mail services
  • Flexibility – Can be hosted on different servers
  • Scalability – Easy to add more webmail instances behind a load balancer
  • Maintainability – Update webmail without touching mail services

This completes our mail server series! You now have:

  • ✅ Postfix (SMTP)
  • ✅ Dovecot 2.4 (IMAP/POP3)
  • ✅ PostfixAdmin (Web Management)
  • ✅ CrowdSec (Security)
  • ✅ Rspamd (Spam Filtering)
  • ✅ Roundcube (Webmail)

All components work together to provide a complete, secure, self-hosted email solution.

Similar Posts