diff --git a/.env.sample b/.env.sample index cd1d23f..577bcfb 100644 --- a/.env.sample +++ b/.env.sample @@ -198,3 +198,5 @@ REJECT_UNLISTED_RECIPIENT= # NOTE(3wc): Other values are not supported, as compose.yml does not contain # configuration for them. Pull requests welcome! DB_FLAVOR=sqlite + +ENABLE_BACKUPS=true \ No newline at end of file diff --git a/README.md b/README.md index b246f37..c120be2 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Coöp Cloud + [Mailu][Mailu] = 🥳 * **Category**: Apps -* **Status**: 1, alpha +* **Status**: 2, beta * **Image**: [`mailu/*`](https://hub.docker.com/r/mailu/), 4, upstream * **Healthcheck**: No -* **Backups**: No +* **Backups**: Yes * **Email**: N/A * **Tests**: No * **SSO**: No diff --git a/abra.sh b/abra.sh index 43a8597..ebf8add 100644 --- a/abra.sh +++ b/abra.sh @@ -1,3 +1,4 @@ export CERTDUMPER_POST_VERSION=v1 export POSTFIX_OVERRIDE_VERSION=v17 export SENDER_LOGIN_VERSIONS=v2 +export BACKUP_SQLITE_VERSION=v1 diff --git a/backup_sqlite.py b/backup_sqlite.py new file mode 100644 index 0000000..972a7df --- /dev/null +++ b/backup_sqlite.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import os +import sqlite3 +from pathlib import Path + +source_path = Path("/data/main.db") +backup_dir = Path("/data/backup") +temporary_path = backup_dir / "main.db.tmp" +backup_path = backup_dir / "main.db" + +os.umask(0o077) +backup_dir.mkdir(mode=0o700, parents=True, exist_ok=True) +temporary_path.unlink(missing_ok=True) + +try: + # Read-only mode prevents silently creating an empty source database. + source = sqlite3.connect( + f"{source_path.as_uri()}?mode=ro", + uri=True, + timeout=60, + ) + destination = sqlite3.connect(temporary_path) + try: + source.backup(destination, pages=256, sleep=0.1) + result = destination.execute("PRAGMA integrity_check").fetchall() + if result != [("ok",)]: + raise RuntimeError(f"SQLite integrity check failed: {result}") + finally: + destination.close() + source.close() + # Only publish a fully written, verified snapshot. + os.replace(temporary_path, backup_path) + print(f"SQLite backup ready: {backup_path}", flush=True) +except Exception: + temporary_path.unlink(missing_ok=True) + raise diff --git a/compose.yml b/compose.yml index a8e98bc..61e443c 100644 --- a/compose.yml +++ b/compose.yml @@ -55,7 +55,7 @@ x-environment: services: app: - image: ghcr.io/mailu/nginx:2024.06.64 + image: ghcr.io/mailu/nginx:2024.06.67 logging: driver: json-file networks: @@ -96,15 +96,18 @@ services: - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - - "coop-cloud.${STACK_NAME}.version=3.1.5+2024.06.61" + - "coop-cloud.${STACK_NAME}.version=4.0.0+2024.06.67" + - "backupbot.backup=${ENABLE_BACKUPS:-true}" + - "backupbot.backup.volumes.traefik_letsencrypt=false" + - "backupbot.backup.volumes.mailu.path=backup" db: - image: redis:8.10.2-alpine + image: redis:8.10.3-alpine volumes: - "redis:/data" admin: - image: ghcr.io/mailu/admin:2024.06.64 + image: ghcr.io/mailu/admin:2024.06.67 environment: *default-env healthcheck: disable: true @@ -115,9 +118,16 @@ services: - "mailu:/data" networks: - default + configs: + - source: backup_sqlite + target: /usr/local/bin/backup_sqlite.py + mode: 0444 + deploy: + labels: + backupbot.backup.pre-hook: "python3 /usr/local/bin/backup_sqlite.py" imap: - image: ghcr.io/mailu/dovecot:2024.06.64 + image: ghcr.io/mailu/dovecot:2024.06.67 environment: *default-env secrets: - secret_key @@ -131,7 +141,7 @@ services: - default smtp: - image: ghcr.io/mailu/postfix:2024.06.64 + image: ghcr.io/mailu/postfix:2024.06.67 environment: *default-env secrets: - secret_key @@ -143,7 +153,7 @@ services: - app antispam: - image: ghcr.io/mailu/rspamd:2024.06.64 + image: ghcr.io/mailu/rspamd:2024.06.67 environment: *default-env secrets: - secret_key @@ -154,7 +164,7 @@ services: disable: true webmail: - image: ghcr.io/mailu/webmail:2024.06.64 + image: ghcr.io/mailu/webmail:2024.06.67 # entrypoint: "tail -f /dev/null" environment: *default-env networks: @@ -221,6 +231,10 @@ configs: certdumper_post: name: ${STACK_NAME}_certdumper_post_${CERTDUMPER_POST_VERSION} file: certdumper_post.sh + backup_sqlite: + name: ${STACK_NAME}_backup_sqlite_${BACKUP_SQLITE_VERSION} + file: backup_sqlite.py + secrets: secret_key: diff --git a/release/3.2.0+2024.06.61 b/release/3.2.0+2024.06.61 new file mode 100644 index 0000000..83d1d51 --- /dev/null +++ b/release/3.2.0+2024.06.61 @@ -0,0 +1 @@ +Contains mitigation measures for php2shell \ No newline at end of file diff --git a/release/3.2.1+2024.06.66 b/release/3.2.1+2024.06.66 new file mode 100644 index 0000000..8314bc6 --- /dev/null +++ b/release/3.2.1+2024.06.66 @@ -0,0 +1 @@ +Actual mitigation for php2shell pending further alpine image upgrades \ No newline at end of file diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5db72dd --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +}