Roundcube Webmail on a Separate Incus Container
Part 6 of the Building a Modern Mail Server on Debian 13 series
Series Navigation:
- Part 1: Preparation & Prerequisites – DNS, hostname, certificates
- Part 2: Core Mail Server – Postfix, Dovecot 2.4, PostfixAdmin
- Part 3: CrowdSec Protection – Security hardening
- Part 4: Rspamd Spam Filtering – DKIM, SPF, DMARC, greylisting
- Part 5: Advanced Filtering – ClamAV, neural networks, Razor/Pyzor
- Part 6: Roundcube Webmail – Web-based email access (this guide)
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:
- The same host as your mail server (different container)
- 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_IPIf using IPv6:
webmail.example.com. IN AAAA YOUR_PROXY_IPv6Section 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 list1.2 Enter the Container
incus shell roundcube1.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.shEdit the file with your actual values:
vi /root/roundcube-vars.shSource the variables before running any commands:
source /root/roundcube-vars.shTip: 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-dnsutils1.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_SERVERNote: 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 nginx2.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-fpmNote: 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 mariadbSection 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
EOF3.2 Create Log Directory
mkdir -p /var/log/roundcube
chown www-data:www-data /var/log/roundcube3.3 Restart PHP-FPM
systemctl restart php8.4-fpmSection 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/roundcube4.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/logsSection 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-installationAnswer 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;
EOF5.3 Initialize Database Schema
Import the Roundcube database schema:
mariadb roundcubemail < /var/www/roundcube/SQL/mysql.initial.sql5.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.sh6.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',
);
EOFNote: 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.phpSection 7: Configure Nginx (Roundcube Container)
7.1 Create Virtual Host
Source the variables file (includes SERVER_NAME and PROXY_IP):
source /root/roundcube-vars.shThis 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";
}
}
EOF7.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 nginxSection 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.sh8.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;
}
}
EOFEnable the site and reload Nginx:
ln -s /etc/nginx/sites-available/${SERVER_NAME} /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx8.3 Issue SSL Certificate
Using acme.sh with webroot validation:
acme.sh --issue -d ${SERVER_NAME} -w /var/www/acmeCreate 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;
}
}
EOFNote: 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 nginxThe 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 updateInstall CrowdSec Agent Only
apt install -y crowdsecNote: 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 || trueConfigure 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}
EOFRestart and Verify Connection
systemctl restart crowdsec
cscli lapi statusYou 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
EOFFix Log Directory Permissions
CrowdSec needs to read the Roundcube logs directory. By default, it may be too restrictive:
chmod 755 /var/www/roundcube/logs9.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
EOFCreate 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
EOFThis 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-bfscenario enabled- Both log files being read
On the LAPI server:
cscli machines listYou 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 crowdsecIf 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 dovecotThis 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 listYou 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_IPSection 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}:99310.3 Test SMTP Connection
openssl s_client -connect ${MAIL_SERVER}:46510.4 Test Web Access
- Open https://webmail.example.com in your browser
- Log in with an existing mail account
- Test sending an email to yourself
- Test receiving the email
- Test archiving a message
- 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.logSection 11: Post-Installation Cleanup
11.1 Remove Installer
Important: Delete the installer directory after setup:
rm -rf /var/www/roundcube/installer11.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/temp11.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
}
EOFSection 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/roundcubeTroubleshooting
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:465Check 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 -datesDatabase 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.sockLogin Fails But Credentials Are Correct
- Check
$config['imap_host']format (needsssl://prefix for port 993) - Verify
$config['imap_auth_type']matches Dovecot config - 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 crowdsecCheck 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 statusCheck scenario is loaded:
cscli scenarios list | grep roundcubeSecurity Recommendations
- Keep Roundcube updated – Check for updates monthly
- Use strong passwords – Minimum 12 characters for all accounts
- CrowdSec protection – Agent reports brute force to central LAPI, proxy bouncers block attackers
- Monitor logs – Set up alerting for authentication failures
- Regular backups – Back up the database and config regularly
- Use firewall rules – Only allow necessary ports between containers
- 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.
