Installing Carbonio CE with Incus

1. Intro

I have a Dedicated Server on hetzner.com and I want to install Carbonio CE. I use Incus on the server, and I want to install Carbonio in a container.

2. Preparing the host

To install and setup the Hetzner server, follow the instructions at: https://docker-scripts.gitlab.io/howto/dedicated-rootserver.html.

3. Create the container

The latest stable version of Carbonio requires ubuntu:20.04.

incus launch images:ubuntu/20.04 carbonio
incus info carbonio
incus config show carbonio

Let’s also enable bash-completion and set a better prompt in it:

incus exec carbonio -- bash

# make sure that bash-completion is installed
apt install --yes bash-completion

# customize ~/.bashrc
sed -i ~/.bashrc \
    -e '/^#\?force_color_prompt=/ c force_color_prompt=yes' \
    -e '/bashrc_custom/d'

echo 'source ~/.bashrc_custom' >> ~/.bashrc

cat <<'EOF' > ~/.bashrc_custom
# set a better prompt
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;36m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'

# enable programmable completion features
if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
    source /etc/bash_completion
fi
EOF

source ~/.bashrc

The installation of Carbonio CE also requires Python3 and Perl, so let’s make sure that they are installed:

# incus exec carbonio -- bash
apt install --yes python3 perl

4. Networking requirements

We are making a single-server installation of Carbonio, so only the ports for the external connections are required to be open: 25, 465, 587, 80, 110, 143, 443, 993, 995, 5222, 6071.

The connection of our container to the network goes through the host, which serves as a gateway. So, the Carbonio server is behind NAT and we need to forward these ports from the host.

Forwarding them is easier if the container has a fixed IP (rather then a dynamic one, obtained from DHCP). So, first of all, let’s change the network configuration inside the container so that it has a fixed IP.

Another requirement before starting to install Carbonio is also to disable IPv6 inside the container.

4.1. Set a fixed IP

Network configuration on ubuntu is managed by netplan.

incus exec carbonio -- bash

ip address
ip route

rm /etc/netplan/*.yaml
cat <<EOF > /etc/netplan/01-netcfg.yaml
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: no
      addresses:
        - 10.210.64.201/8
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
      routes:
        - to: default
          via: 10.210.64.1
EOF

netplan apply

ip address
ip route
ping 8.8.8.8

4.2. Forward ports

We can use the command incus network forward to forward these ports to the internal IP of the Carbonio container:

HOST_IP=10.11.12.13           # the public IP of the host
CONTAINER_IP=10.210.64.201

incus network forward create incusbr0 $HOST_IP
incus network forward list incusbr0

incus network forward port add incusbr0 \
    $HOST_IP tcp 25,465,587,110,143,993,995,5222,6071 \
    $CONTAINER_IP
incus network forward show incusbr0 $HOST_IP
Test port forwarding

We can use netcat to test that ports are forwarded correctly. On the server run:

incus exec carbonio -- nc -l 110

Outside the server run:

nc mail.example.org 110
We are assuming that mail.example.org is resolved to the external IP of the server.

Every line that is typed outside the server should be displayed inside the server.

4.3. Forward the TCP ports 80 and 443

Forwarding these two ports is a bit more complex and cannot be done with the same method that was used above. This is because these ports need to be used by other applications as well, beside Carbonio. We need to forward these ports to different applications or containers, based on the domain that is being used. We can use sniproxy for this: https://docker-scripts.gitlab.io/howto/install-sniproxy.html

Make sure that the configuration file etc/sniproxy.conf looks like this:

table {
    # . . . . .

    mail.example.org     10.210.64.201
    .*.mail.example.org  10.210.64.201

    # . . . . .
}
We are using 10.210.64.201, which is the fixed IP of the carbonio container.

4.4. Disable IPv6 inside the container

Let’s test it:

incus restart carbonio
incus exec carbonio -- ip addr

We should also remove the IPv6 entries from /etc/hosts:

incus shell carbonio
cat /etc/hosts
sed -i /etc/hosts -e '/::/d'
cat /etc/hosts

5. Minimal DNS setup

Before starting to install the mail server, let’s make sure that we already have some minimal DNS setup, which should look like this:

mail.example.org.  IN    A           10.11.12.13
example.org.       IN    MX    10    mail.example.org.
example.org.       IN    MX    20    mail.example.org.
example.org.       IN    TXT         "v=spf1 mx -all"

The last line basically tells to the other SMTP servers that only this server is allowed to send emails on behalf of this domain, and no other servers. This is done to prevent spammers from faking your email addresses. If a spammer tries to send a mail as if it is coming from your domain, the SMTP server that is getting this email will consult this DNS record and will figure out that the server of the spammer is not allowed to send emails on behalf of example.org.

You can use dig to verify that these DNS records have been activated:

dig MX example.org +short
dig A mail.example.org +short
dig TXT example.org +short

However, keep in mind that DNS changes may take some time to propagate.

6. Installing Carbonio inside the container

We are going to make a single-server installation, following these instructions: https://docs.zextras.com/carbonio-ce/html/single-server-installation.html

6.1. Repository configuration

apt install --yes gnupg2

apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \
            --recv-keys 52FD40243E584A21

cat <<EOF > /etc/apt/sources.list.d/zextras.list
deb https://repo.zextras.io/release/ubuntu focal main
EOF

apt update

6.2. Setting hostname

hostnamectl set-hostname mail.example.org
hostname

echo "$(hostname -I)    $(hostname -f) mail" >> /etc/hosts
cat /etc/hosts

6.3. Install packages

apt update
apt upgrade --yes
apt install --yes \
    service-discover-server \
    carbonio-directory-server \
    carbonio-proxy \
    carbonio-webui \
    carbonio-files-ui \
    carbonio-mta \
    carbonio-appserver \
    carbonio-user-management \
    carbonio-files-ce \
    carbonio-files-db \
    carbonio-storages-ce \
    carbonio-preview-ce \
    carbonio-docs-connector-ce \
    carbonio-docs-editor \
    carbonio-prometheus \
    postgresql-12

Check the status of Carbonio services:

systemctl status carbonio-*
systemctl restart carbonio-prometheus-nginx-exporter.service

6.4. Configuration

carbonio-bootstrap

service-discover setup-wizard

# apt install --yes python3-pip
# pip install requests
apt install --yes python3-requests
pending-setups -a

6.5. DB setup

# create main db role and database
DB_ADM_PWD=Ee5hfaevVo7vieri
su - postgres -c "psql --command=\"CREATE ROLE carbonio_adm WITH LOGIN SUPERUSER encrypted password '$DB_ADM_PWD';\""
su - postgres -c "psql --command=\"CREATE DATABASE carbonio_adm owner carbonio_adm;\""

# bootstrap carbonio files databases
PGPASSWORD=$DB_ADM_PWD carbonio-files-db-bootstrap carbonio_adm 127.0.0.1

# restart the main mailbox process as the zextras user
su - zextras -c 'zmcontrol stop'
su - zextras -c 'zmcontrol start'

7. Setup

7.1. SSL certificate

We are going to use a LetsEncrypt certificate.

  1. First, let’s install certbot:

    apt install --yes snapd
    snap install core
    snap refresh core
    snap install --classic certbot
    ln -s /snap/bin/certbot /usr/bin/certbot
  2. Get a certificate:

    DOMAIN=mail.example.org
    EMAIL=user@gmail.com
    
    certbot certonly \
        --standalone \
        --preferred-chain "ISRG Root X1" \
        --domains $DOMAIN \
        --email $EMAIL \
        --agree-tos \
        --non-interactive \
        --keep-until-expiring \
        --dry-run
    certbot certonly \
        --standalone \
        --preferred-chain "ISRG Root X1" \
        --domains $DOMAIN \
        --email $EMAIL \
        --agree-tos \
        --non-interactive \
        --keep-until-expiring

    The certificate is saved at /etc/letsencrypt/live/$DOMAIN/.

  3. Copy privkey.pem to the Carbonio directory:

    cp /etc/letsencrypt/live/$DOMAIN/privkey.pem \
       /opt/zextras/ssl/carbonio/commercial/commercial.key
    chown zextras:zextras \
       /opt/zextras/ssl/carbonio/commercial/commercial.key
  4. Proceed and deploy the SSL certificates:

    cp /etc/letsencrypt/live/$DOMAIN/cert.pem /tmp
    cp /etc/letsencrypt/live/$DOMAIN/chain.pem /tmp
  5. Download the ISRG Root X1 chain as below:

    apt install --yes wget
    wget -O /tmp/ISRG-X1.pem \
        https://letsencrypt.org/certs/isrgrootx1.pem.txt
    cat /tmp/ISRG-X1.pem >> /tmp/chain.pem
    rm /tmp/ISRG-X1.pem
  6. Verify the certificate:

    su - zextras \
       -c 'zmcertmgr verifycrt comm \
              /opt/zextras/ssl/carbonio/commercial/commercial.key \
    	  /tmp/cert.pem \
    	  /tmp/chain.pem'
    Fix zmcertmgr

    If the verification above fails, try to fix /opt/zextras/bin/zmcertmgr, as described in this discussion:

    my $ssl = $self->Openssl;
    my $keydg =
        # $self->run("$ssl rsa -noout -modulus -in '$keyf' | $ssl sha256");
        $self->run("$ssl pkey -pubout -in '$keyf' | $ssl sha256");
    my $crtdg =
        # $self->run("$ssl x509 -noout -modulus -in '$crtf' | $ssl sha256");
        $self->run("$ssl x509 -noout -pubkey -in '$crtf' | $ssl sha256");

    Then try to verify again.

  7. Finally, deploy the certificate and restart the services to finish the deployment:

    su - zextras -c \
        'zmcertmgr deploycrt comm /tmp/cert.pem /tmp/chain.pem'
    su - zextras -c 'zmcertmgr viewdeployedcrt'
    su - zextras -c 'zmcontrol restart'

7.2. Add a DKIM record

  1. Generate DKIM record:

    su - zextras -c \
        '/opt/zextras/libexec/zmdkimkeyutil -a -d example.org'
  2. Add DKIM record to DNS settings.

  3. To check that it has been added:

    dig txt D43CB080-8FE0-11EC-88DF-9958FFC5EFF5._domainkey.example.org

7.3. Set the admin password

su - zextras -c \
    'carbonio prov setpassword zextras@example.org pass123'