After setting up a Postfix and Dovecot Server and securing it with Rspamd and ClamAV we are still lacking a nice web interface to send and receive mail with. This is where SOGo comes in.
SOGo is a fully fledged groupware server providing a mail client, calendar, address book, CalDAV, CardDAV, Microsoft ActiveSync and more. There are freely available nightly builds of SOGo which we will use with our SOGo Docker Image to create a groupware server on mydomain.com.

In this article we discuss the necessary configuration to set up SOGo and provide Ansible roles for SOGo and some other services not yet covered in other articles.

Prerequisites

We assume that you have a host that is accessible via Ansible and that you are running Traefik as well as the internal Docker network described in the Traefik article. We will outsource our user database to an Apache LDAP Server. You can of course use another LDAP server. Make sure to adapt the SOGo LDAP configuration. You also need a SMTP and IMAP server like the Postfix and Dovecot Server from a previous article. Spam and virus filtering is not required for this article, but highly recommended.
We will not target full Unicode compliance with our setup to keep it simple. There is a guide in the SOGo docs on how to make SOGo Unicode compliant. We will mention it a few times where applicable.

SOGo Database Setup

SOGo uses a MySQL/MariaDB database to save contacts, calendar entries, settings and more. There are no restrictions on how the database must be run: native, in a Docker container or using a cloud service. In this example we use a Docker container, mostly because it’s easy to set up.

Currently I’m running MariaDB 10.3 for SOGo so that’s what we use for this Ansible role. My MariaDB instance already fulfilled the requirements for SOGo Unicode compliance. You might want to check your settings.

roles/mariadb/tasks/main.yml
# This is not very clean, you might wanna remove the coupling
- name: Include SOGo vars
include_vars:
dir: ../../sogo/vars

- name: Ensure mariadb container
docker_container:
name: mariadb
image: mariadb:10.3
networks:
- name: internal
networks_cli_compatible: yes
volumes:
- "{{ mariadb_data_directory }}:/var/lib/mysql"
env:
MYSQL_ROOT_PASSWORD: "{{ mariadb_password }}"
MYSQL_DATABASE: sogo
MYSQL_USER: "{{ sogo_db_user }}"
MYSQL_PASSWORD: "{{ sogo_db_password }}"
labels:
traefik.enable: "false"

The variables referenced in the MariaDB setup tasks:

roles/mariadb/vars/main.yml
mariadb_data_directory: /srv/data/mariadb/data
mariadb_password: ...

Be sure to pick a good password.

SOGo Memcached Setup

SOGo also uses Memcached to cache certain data. This is very easy to set up:

roles/memcached/tasks/main.yml
- name: Bring up memcached
docker_container:
name: memcached
image: memcached:1.6.17-alpine
entrypoint: memcached -m 256
networks:
- name: internal
networks_cli_compatible: yes
labels:
traefik.enable: "false"

LDAP Structure for SOGo

We have to tell SOGo where in your LDAP structure it can find the user information and how it is formatted. We assume the same LDAP structure as in the Postfix and Dovecot Server setup guide where we find the users under ou=people,o=bored. SOGo needs a user with uid sogo and a password of your choice to access the LDAP directory.
To separate actual users from the SOGo user we create a new organizationalUnit branch in our bored partition with uid serviceusers. This service user doesn’t need all the information that the inetOrgPerson structure could hold. Instead, we create it as structural object account and simpleSecurityObject as auxiliary. The final dn is now:

uid=sogo,ou=servicesuers,o=bored.

And the partition structure is:

# Partition
o=bored
# Users
ou=people,o=bored
# Example User
cn=john+sn=doe,ou=people,o=bored
# Service Users
ou=servicesuers,o=bored
# Example User
uid=sogo,ou=servicesuers,o=bored

Set a SSHA256 password of your choice. Note that an account object uses uid instead of cn+sn to identify itself.

SOGo Configuration

Using our SOGo Docker image we have to configure SOGo itself plus the Apache webserver running in the container.

SOGo Main Config

SOGo has one main config file and we use it mostly to point SOGo to all the services around it: SMTP, IMAP, MariaDB, Memcached and LDAP. We supply the respective users to access the services and their passwords.
When we configure LDAP access we use the service user from above and direct SOGo to accept everyone in ou=people,o=bored as user.
We assume that all your services run as Docker containers on the same Docker network and are named like their respective service. So the Docker DNS would resolve postfix to your Postfix container.

roles/sogo/files/sogo/sogo.conf.j2
{
/* MariaDB from above */
SOGoProfileURL = "mysql://{{ sogo_db_user }}:{{ sogo_db_password }}@mariadb:3306/sogo/sogo_user_profile";
OCSFolderInfoURL = "mysql://{{ sogo_db_user }}:{{ sogo_db_password }}@mariadb:3306/sogo/sogo_folder_info";
OCSSessionsFolderURL = "mysql://{{ sogo_db_user }}:{{ sogo_db_password }}@mariadb:3306/sogo/sogo_sessions_folder";

/* Your IMAP and SMTP server */
SOGoIMAPServer = "imaps://mydomain.com:993";
SOGoSieveScriptsEnabled = YES;
/* You can leave this out if you do not use sieve */
SOGoSieveServer = "sieve://mydomain.com:4190/?tls=YES";
SOGoSMTPServer = "smtp://postfix:25";
SOGoMailDomain = mydomain.com;
SOGoMailingMechanism = smtp;

/* Authentication */
SOGoPasswordChangeEnabled = YES;

/* LDAP authentication */
SOGoUserSources = (
{
type = ldap;
/* Adapt these fields if you have a different structure */
CNFieldName = cn;
UIDFieldName = cn;
IDFieldName = cn; // first field of the DN for direct binds
bindFields = (cn); // array of fields to use for indirect binds
baseDN = "ou=people,o=bored";
bindDN = "uid=sogo,ou=servicesuers,o=bored";
bindPassword = {{ sogo_ldap_password }};
canAuthenticate = YES;
displayName = "Local Users";
hostname = "ldap://apacheds:389";
id = myhostname; // This can be anything
isAddressBook = YES; // Share user list as address book.
userPasswordAlgorithm = ssha256; // Has to be the same as in LDAP
}
);

/* Web Interface */
SOGoPageTitle = mydomain.com;

/* General - SOGoTimeZone *MUST* be defined. Adapt as desired */
SOGoLanguage = English;
SOGoTimeZone = Europe/Berlin;

SOGoMemcachedHost = "memcached";
}

We left out the switch for SOGo Unicode compliance. But if you followed the SOGo guide it should be easy to add.

SOGo Apache Config

Our SOGo container will also run an Apache instance to serve the SOGo web interface. There is already a Apache config included in the SOGo container. We just have to set our domain name and redirect the output to the console so that Docker can pick it up.

roles/sogo/files/apache2/sogo.conf
<VirtualHost *:80>
# Set to our domain
ServerName https://mydomain.com

ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

# Here we redirect the output to the console
ErrorLog /dev/stderr
CustomLog /dev/stdout common

# Config file provided by SOGo, already present in the SOGo image
Include conf.d/SOGo.conf
</VirtualHost>

To make the address book work with Apple Addressbook we need a separate redirect from port 8800 to SOGo. There is already a config example from SOGo which we will just used with our own domain name.

roles/sogo/files/apache2/sogo-addressbook.conf
<VirtualHost *:8800>
ServerName https://mydomain.com:8800
RewriteEngine Off
ProxyRequests Off
SetEnv proxy-nokeepalive 1
ProxyPreserveHost On
ProxyPassInterpolateEnv On
ProxyPass /principals http://127.0.0.1:20000/SOGo/dav/ interpolate
ProxyPass /SOGo http://127.0.0.1:20000/SOGo interpolate
ProxyPass / http://127.0.0.1:20000/SOGo/dav/ interpolate

<Location />
Order allow,deny
Allow from all
</Location>
<Proxy http://127.0.0.1:20000>
RequestHeader set "x-webobjects-server-port" "8800"
RequestHeader set "x-webobjects-server-name" "mydomain.com:8800"
RequestHeader set "x-webobjects-server-url" "https://mydomain.com:8800"
RequestHeader set "x-webobjects-server-protocol" "HTTP/1.0"
RequestHeader set "x-webobjects-remote-host" "127.0.0.1"
AddDefaultCharset UTF-8
</Proxy>
# Again: redirect output to console
ErrorLog /dev/stderr
CustomLog /dev/stdout common
</VirtualHost>

If we don’t redirect the Apache output to console the SOGo container would bloat up with logfiles. Also, we couldn’t see them through docker logs.

Deploying SOGo with Ansible

We create an Ansible role that copies our config files from above to the right directories and starts the SOGo container. Traefik will pick up the labels attached to SOGo and serve the SOGo frontend as well as the addressbook sync route. In our config it will also get a Let’s Encrypt certficate for our domain and redirect insecure HTTP traffic to HTTPS.

roles/sogo/tasks/main.yml
- name: Ensure directories
file:
name: "{{ item }}"
state: directory
loop:
- "{{ sogo_config_directory }}"
- "{{ sogo_apache_config_directory }}"

- name: Render sogo config file
template:
src: files/sogo/sogo.conf.j2
dest: "{{ sogo_config_directory }}/sogo.conf"
register: sogo_config

- name: Render sogo apache config files
template:
src: files/apache2/{{ item }}
dest: "{{ sogo_apache_config_directory }}/{{ item }}"
register: sogo_apache_config
loop:
- sogo.conf
- sogo-addressbook.conf

- name: Ensure SOGo container
docker_container:
name: sogo
image: gevattergaul/sogo:5.8.2-nightly-1
networks_cli_compatible: yes
restart: "{{ sogo_config.changed or sogo_apache_config.changed }}"
networks:
- name: internal
volumes:
- "{{ sogo_config_directory }}:/etc/sogo"
- "{{ sogo_apache_config_directory }}/000-default.conf:/etc/apache2/sites-enabled/sogo.conf"
- "{{ sogo_apache_config_directory }}/sogo-addressbook.conf:/etc/apache2/sites-enabled/sogo-addressbook.conf"
env:
HTTP_HOST: mydomain.com
labels:
// Config for Traefik
traefik.http.routers.sogo.rule: "Host(`mydomain.com`)"
traefik.http.routers.sogo.entrypoints: "web"
traefik.http.routers.sogo.service: "sogo-secure"
// redirect HTTP to HTTPS
traefik.http.routers.sogo.middlewares: sogo-redirect-websecure
traefik.http.middlewares.sogo-redirect-websecure.redirectscheme.scheme: "https"
traefik.http.middlewares.sogo-redirect-websecure.redirectscheme.permanent: "true"
// Our HTTPS entry
traefik.http.routers.sogo-secure.rule: "Host(`mydomain.com`)"
traefik.http.routers.sogo-secure.tls: "true"
traefik.http.routers.sogo-secure.entrypoints: "websecure"
traefik.http.routers.sogo-secure.service: "sogo-secure"
traefik.http.routers.sogo-secure.tls.certresolver: letsEncryptResolver
traefik.http.services.sogo-secure.loadbalancer.server.port: "80"
// The addressbook sync point
traefik.http.routers.sogo-addressbook.rule: "Host(`mydomain.com`)"
traefik.http.routers.sogo-addressbook.tls: "true"
traefik.http.routers.sogo-addressbook.entrypoints: "sogo_addressbook"
traefik.http.routers.sogo-addressbook.service: sogo-addressbook
traefik.http.routers.sogo-addressbook.tls.certresolver: letsEncryptResolver
traefik.http.services.sogo-addressbook.loadbalancer.server.port: "8800"

If you target SOGo Unicode compliance you should add another step that loads the template database before starting SOGo.

Here are the variables we use earlier:

roles/sogo/vars/main.yml
sogo_config_directory: /srv/data/sogo/config
sogo_apache_config_directory: /srv/data/sogo/apache_config
sogo_db_user: sogo
sogo_db_password: "{{ vault_sogo_db_password }}"
sogo_ldap_password: "{{ vault_sogo_ldap_password }}"

And we bring it all together in a playbook. myhostname should be defined in your hosts file.

sogo-playbook.yml
- hosts: myhostname
roles:
... add your other roles here
- memcached
- mariadb
- sogo