groupware: port opencloud_full to Stalwart 0.16 (only the built-in IDM LDAP option for now)

This commit is contained in:
Pascal Bleser
2026-06-10 10:47:17 +02:00
parent fe000d08e9
commit 6eda879e6a
6 changed files with 202 additions and 119 deletions

View File

@@ -0,0 +1 @@
{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,"bufferSize":134217728,"poolWorkers":null}

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,113 +0,0 @@
authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.fallback-admin.user = "mailadmin"
authentication.master.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.master.user = "master"
directory.idmldap.attributes.class = "objectClass"
directory.idmldap.attributes.description = "displayName"
directory.idmldap.attributes.email = "mail"
directory.idmldap.attributes.groups = "memberOf"
directory.idmldap.attributes.name = "cn"
directory.idmldap.attributes.secret = "userPassword"
directory.idmldap.base-dn = "o=libregraph-idm"
directory.idmldap.bind.auth.method = "default"
directory.idmldap.bind.dn = "uid=reva,ou=sysusers,o=libregraph-idm"
directory.idmldap.bind.secret = "admin"
directory.idmldap.cache.size = 1048576
directory.idmldap.cache.ttl.negative = "10m"
directory.idmldap.cache.ttl.positive = "1h"
directory.idmldap.filter.email = "(&(|(objectClass=person)(objectClass=groupOfNames))(mail=?))"
directory.idmldap.filter.name = "(&(|(objectClass=person)(objectClass=groupOfNames))(cn=?))"
directory.idmldap.timeout = "15s"
directory.idmldap.tls.allow-invalid-certs = true
directory.idmldap.tls.enable = true
directory.idmldap.type = "ldap"
directory.idmldap.url = "ldaps://opencloud:9235"
directory.keycloak.auth.method = "user-token"
directory.keycloak.cache.size = 1048576
directory.keycloak.cache.ttl.negative = "10m"
directory.keycloak.cache.ttl.positive = "1h"
directory.keycloak.endpoint.method = "introspect"
directory.keycloak.endpoint.url = "http://keycloak:8080/realms/openCloud/protocol/openid-connect/userinfo"
directory.keycloak.fields.email = "email"
directory.keycloak.fields.full-name = "name"
directory.keycloak.fields.username = "preferred_username"
directory.keycloak.timeout = "15s"
directory.keycloak.type = "oidc"
directory.ldap.attributes.class = "objectClass"
directory.ldap.attributes.description = "displayName"
directory.ldap.attributes.email = "mail"
directory.ldap.attributes.email-alias = "mailAlias"
directory.ldap.attributes.groups = "memberOf"
directory.ldap.attributes.name = "uid"
directory.ldap.attributes.secret = "userPassword"
directory.ldap.attributes.secret-changed = "pwdChangedTime"
directory.ldap.base-dn = "dc=opencloud,dc=eu"
directory.ldap.bind.auth.dn = "cn=?,ou=users,dc=opencloud,dc=eu"
directory.ldap.bind.auth.enable = true
directory.ldap.bind.auth.search = true
directory.ldap.bind.dn = "cn=admin,dc=opencloud,dc=eu"
directory.ldap.bind.secret = "admin"
directory.ldap.cache.ttl.negative = "10m"
directory.ldap.cache.ttl.positive = "1h"
directory.ldap.filter.email = "(&(|(objectClass=person)(objectClass=groupOfNames))(|(uid=?)(mail=?)(mailAlias=?)(cn=?)))"
directory.ldap.filter.name = "(&(|(objectClass=person)(objectClass=groupOfNames))(|(uid=?)(cn=?)))"
directory.ldap.timeout = "5s"
directory.ldap.tls.allow-invalid-certs = true
directory.ldap.tls.enable = true
directory.ldap.type = "ldap"
directory.ldap.url = "ldap://ldap-server:1389"
email.encryption.enable = true
email.encryption.append = false
http.allowed-endpoint = 200
http.hsts = true
http.permissive-cors = false
http.url = "'https://' + config_get('server.hostname')"
http.use-x-forwarded = true
metrics.prometheus.auth.secret = "secret"
metrics.prometheus.auth.username = "metrics"
metrics.prometheus.enable = true
server.listener.http.bind = "0.0.0.0:8080"
server.listener.http.protocol = "http"
server.listener.https.bind = "0.0.0.0:443"
server.listener.https.protocol = "http"
server.listener.https.tls.implicit = true
server.listener.imap.bind = "0.0.0.0:143"
server.listener.imap.protocol = "imap"
server.listener.imaptls.bind = "0.0.0.0:993"
server.listener.imaptls.protocol = "imap"
server.listener.imaptls.tls.implicit = true
server.listener.pop3.bind = "0.0.0.0:110"
server.listener.pop3.protocol = "pop3"
server.listener.pop3s.bind = "0.0.0.0:995"
server.listener.pop3s.protocol = "pop3"
server.listener.pop3s.tls.implicit = true
server.listener.sieve.bind = "0.0.0.0:4190"
server.listener.sieve.protocol = "managesieve"
server.listener.smtp.bind = "0.0.0.0:25"
server.listener.smtp.protocol = "smtp"
server.listener.submission.bind = "0.0.0.0:587"
server.listener.submission.protocol = "smtp"
server.listener.submissions.bind = "0.0.0.0:465"
server.listener.submissions.protocol = "smtp"
server.listener.submissions.tls.implicit = true
server.max-connections = 8192
server.socket.backlog = 1024
server.socket.nodelay = true
server.socket.reuse-addr = true
server.socket.reuse-port = true
storage.blob = "rocksdb"
storage.data = "rocksdb"
storage.directory = "idmldap"
storage.fts = "rocksdb"
storage.lookup = "rocksdb"
store.rocksdb.compression = "lz4"
store.rocksdb.path = "/opt/stalwart/data"
store.rocksdb.type = "rocksdb"
tracer.console.ansi = true
tracer.console.buffered = true
tracer.console.enable = true
tracer.console.level = "trace"
tracer.console.lossy = false
tracer.console.multiline = false
tracer.console.type = "stdout"
sharing.allow-directory-query = false

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Snapshots the current configuration of a Stalwart >= 0.16 server.
# Requires having stalwart-cli installed.
set -euo pipefail
exec stalwart-cli -k --url 'https://stalwart.opencloud.test' --user admin --password secret snapshot \
--include-secrets \
Tenant \
Domain \
Directory \
Authentication \
DkimSignature \
AcmeProvider \
Certificate \
DnsServer \
Role \
Authentication \
Account \
NetworkListener \
Tracer \
Sharing \
SystemSettings \
DataRetention \
BlobStore \
InMemoryStore \
SearchStore \
--allow-unresolved PublicKey \
"$@"

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env node
// Processes a Stalwart snapshot JSON file to perform the following:
// - replace masked passwords with their correct value
// - replace the defaultHostname system setting
const fs = require('fs');
const readline = require('readline');
const args = process.argv.slice(2);
const inputs = args.length > 0
? args.map(file => fs.createReadStream(file))
: [process.stdin];
async function processLines() {
for (const stream of inputs) {
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
});
for await (const line of rl) {
const trimmed = line.trim();
if (!trimmed) continue;
const item = JSON.parse(trimmed);
const t = item['@type'];
const o = item['object'];
if (t === 'create' && o === 'Account') {
const value = item['value'] || {};
for (const [id, account] of Object.entries(value)) {
const name = account['name'];
if (name === 'master' || name === 'admin') {
const credentials = account['credentials'] || {};
for (const [cid, creds] of Object.entries(credentials)) {
if (creds['@type'] === 'Password') {
creds['secret'] = 'supersecret1234';
}
}
}
}
}
if (t === 'create' && o === 'Directory') {
const value = item['value'] || {};
for (const [id, dir] of Object.entries(value)) {
if (dir['@type'] === 'Ldap') {
if (dir['bindSecret']) {
dir['bindSecret']['secret'] = 'admin';
}
}
}
}
if (t === 'update' && o === 'SystemSettings') {
if (item['value']) {
item['value']['defaultHostname'] = 'stalwart.opencloud.test';
}
}
console.log(JSON.stringify(item));
}
}
}
processLines();

View File

@@ -7,20 +7,22 @@ services:
- ${STALWART_DOMAIN:-stalwart.opencloud.test}
stalwart:
image: ghcr.io/stalwartlabs/stalwart:v0.15.5-alpine
image: ghcr.io/stalwartlabs/stalwart:v0.16.7-alpine
hostname: ${STALWART_DOMAIN:-stalwart.opencloud.test}
networks:
- opencloud-net
ports:
- "127.0.0.1:8443:443"
- "127.0.0.1:8080:8080"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
- "127.0.0.1:1465:465"
volumes:
- /etc/localtime:/etc/localtime:ro
- "./config/stalwart/${STALWART_AUTH_DIRECTORY:-idmldap}.toml:/opt/stalwart/etc/config.toml"
- stalwart-data:/opt/stalwart/data
- ./config/stalwart/config.json:/etc/stalwart/config.json
- stalwart-data:/var/lib/stalwart
environment:
STALWART_AUTH_DIRECTORY: "${STALWART_AUTH_DIRECTORY:-idmldap}"
STALWART_RECOVERY_ADMIN: "admin:secret"
STALWART_PUBLIC_URL: "https://${STALWART_DOMAIN:-stalwart.opencloud.test}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.stalwart.entrypoints=https"
@@ -30,7 +32,83 @@ services:
- "traefik.http.services.stalwart.loadbalancer.server.port=8080"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
restart: unless-stopped
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ ! -f /var/lib/stalwart/.initialized ]; then
echo "Not initialized, starting in recovery mode"
STALWART_RECOVERY_MODE=1 /usr/local/bin/stalwart --config /etc/stalwart/config.json &
pid=$$!
while [ ! -f /var/lib/stalwart/.initialized ]; do sleep 1; done
echo "Initialized, stopping recovery mode"
kill -TERM $$pid
wait $$pid
fi
echo "Starting Stalwart in production mode"
exec /usr/local/bin/stalwart --config /etc/stalwart/config.json
stalwart-import:
image: stalwart-cli:latest
build:
context: ./config/stalwart/
args:
VERSION: "1.0.8"
dockerfile_inline: |
FROM alpine:latest AS downloader
ARG VERSION
RUN apk add --no-cache curl tar xz
RUN arch=$(apk --print-arch) && \
mkdir /cli && \
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/stalwartlabs/cli/releases/download/v$${VERSION}/stalwart-cli-$${arch}-unknown-linux-musl.tar.xz \
| tar xJf - --strip-components=1 -C /cli
FROM scratch
COPY --from=downloader /cli/stalwart-cli /stalwart-cli
CMD ["/stalwart-cli"]
networks:
- opencloud-net
volumes:
- ./config/stalwart/${STALWART_AUTH_DIRECTORY:-idmldap}.json:/snapshot.json:ro
- stalwart-data:/var/lib/stalwart
environment:
STALWART_URL: "http://stalwart:8080"
STALWART_USER: "admin"
STALWART_PASSWORD: "secret"
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ -f /var/lib/stalwart/.initialized ]; then
echo "Already initialized, skipping stalwart-cli."
exit 0
fi
echo "Waiting for the Stalwart recovery API to become live"
until wget -qO- $$STALWART_URL/healthz/live >/dev/null 2>&1; do sleep 1; done
echo "Applying configuration"
/stalwart-cli --debug apply --file /snapshot.json
touch /var/lib/stalwart/.initialized
echo "Successfully initialized"
stdin_open: true
tty: true
depends_on:
- stalwart
stalwart-reset:
image: alpine:latest
profiles:
- maintenance
volumes:
- stalwart-data:/var/lib/stalwart
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ -f /var/lib/stalwart/.initialized ]; then
rm -f /var/lib/stalwart/.initialized
echo "Success: Lockfile removed. Next time 'stalwart' boots, it will run in recovery mode."
fi
volumes:
stalwart-data: