From e62397b06f48828eab813587819850acfa886b70 Mon Sep 17 00:00:00 2001 From: Markus Pesch Date: Mon, 2 Mar 2026 18:48:41 +0100 Subject: [PATCH] Initial Commit --- .ansible-lint | 4 + .editorconfig | 12 ++ .gitea/workflows/ansible-linters.yaml | 20 ++ .gitea/workflows/markdown-linters.yaml | 18 ++ .gitea/workflows/release.yaml | 22 +++ .gitignore | 1 + .markdownlint.yaml | 137 ++++++++++++++ .vscode/settings.json | 14 ++ .yamllint.yaml | 19 ++ LICENSE | 20 ++ README.md | 62 ++++++ defaults/main.yaml | 101 ++++++++++ handlers/main.yaml | 11 ++ meta/argument_specs.yaml | 176 ++++++++++++++++++ meta/main.yaml | 28 +++ renovate.json | 9 + tasks/dkim_create.yaml | 48 +++++ tasks/dkim_delete.yaml | 11 ++ tasks/logging_create.yaml | 9 + tasks/logging_delete.yaml | 6 + tasks/main.yaml | 121 ++++++++++++ tasks/multimaps.yaml | 73 ++++++++ tasks/redis_create.yaml | 9 + tasks/redis_delete.yaml | 6 + tasks/worker_controller_create.yaml | 23 +++ tasks/worker_controller_delete.yaml | 6 + .../etc/rspamd/local.d/allowlist_from.map.j2 | 6 + .../etc/rspamd/local.d/allowlist_ips.map.j2 | 6 + .../etc/rspamd/local.d/blocklist_from.map.j2 | 6 + .../etc/rspamd/local.d/blocklist_ips.map.j2 | 6 + .../rspamd/local.d/classifier-bayes.conf.j2 | 5 + .../etc/rspamd/local.d/dkim_signing.conf.j2 | 23 +++ templates/etc/rspamd/local.d/logging.inc.j2 | 9 + .../etc/rspamd/local.d/milter_headers.conf.j2 | 13 ++ templates/etc/rspamd/local.d/multimap.conf.j2 | 39 ++++ templates/etc/rspamd/local.d/options.inc.j2 | 10 + templates/etc/rspamd/local.d/redis.conf.j2 | 27 +++ .../rspamd/local.d/worker-controller.inc.j2 | 22 +++ .../override.d/classifier-bayes.conf.j2 | 5 + vars/Archlinux.yaml | 4 + 40 files changed, 1147 insertions(+) create mode 100644 .ansible-lint create mode 100644 .editorconfig create mode 100644 .gitea/workflows/ansible-linters.yaml create mode 100644 .gitea/workflows/markdown-linters.yaml create mode 100644 .gitea/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .markdownlint.yaml create mode 100644 .vscode/settings.json create mode 100644 .yamllint.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 defaults/main.yaml create mode 100644 handlers/main.yaml create mode 100644 meta/argument_specs.yaml create mode 100644 meta/main.yaml create mode 100644 renovate.json create mode 100644 tasks/dkim_create.yaml create mode 100644 tasks/dkim_delete.yaml create mode 100644 tasks/logging_create.yaml create mode 100644 tasks/logging_delete.yaml create mode 100644 tasks/main.yaml create mode 100644 tasks/multimaps.yaml create mode 100644 tasks/redis_create.yaml create mode 100644 tasks/redis_delete.yaml create mode 100644 tasks/worker_controller_create.yaml create mode 100644 tasks/worker_controller_delete.yaml create mode 100644 templates/etc/rspamd/local.d/allowlist_from.map.j2 create mode 100644 templates/etc/rspamd/local.d/allowlist_ips.map.j2 create mode 100644 templates/etc/rspamd/local.d/blocklist_from.map.j2 create mode 100644 templates/etc/rspamd/local.d/blocklist_ips.map.j2 create mode 100644 templates/etc/rspamd/local.d/classifier-bayes.conf.j2 create mode 100644 templates/etc/rspamd/local.d/dkim_signing.conf.j2 create mode 100644 templates/etc/rspamd/local.d/logging.inc.j2 create mode 100644 templates/etc/rspamd/local.d/milter_headers.conf.j2 create mode 100644 templates/etc/rspamd/local.d/multimap.conf.j2 create mode 100644 templates/etc/rspamd/local.d/options.inc.j2 create mode 100644 templates/etc/rspamd/local.d/redis.conf.j2 create mode 100644 templates/etc/rspamd/local.d/worker-controller.inc.j2 create mode 100644 templates/etc/rspamd/override.d/classifier-bayes.conf.j2 create mode 100644 vars/Archlinux.yaml diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..23a163f --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,4 @@ +--- + +exclude_paths: +- .gitea/ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ad4c311 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false diff --git a/.gitea/workflows/ansible-linters.yaml b/.gitea/workflows/ansible-linters.yaml new file mode 100644 index 0000000..f7f6c07 --- /dev/null +++ b/.gitea/workflows/ansible-linters.yaml @@ -0,0 +1,20 @@ +name: Ansible Linter + +on: + pull_request: + types: [ "opened", "reopened", "synchronize" ] + push: + branches: [ '**' ] + tags-ignore: [ '**' ] + +jobs: + ansible-lint: + runs-on: + - ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + - name: Run ansible-lint + uses: ansible/ansible-lint@v26.2.0 + with: + args: "--config-file .ansible-lint" + setup_python: "true" diff --git a/.gitea/workflows/markdown-linters.yaml b/.gitea/workflows/markdown-linters.yaml new file mode 100644 index 0000000..89b44a3 --- /dev/null +++ b/.gitea/workflows/markdown-linters.yaml @@ -0,0 +1,18 @@ +name: Lint Markdown files + +on: + pull_request: + types: [ "opened", "reopened", "synchronize" ] + push: + branches: [ '**' ] + tags-ignore: [ '**' ] + +jobs: + markdown-lint: + runs-on: + - ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + - uses: DavidAnson/markdownlint-cli2-action@v21.0.0 + with: + globs: '**/*.md' diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..f4dc263 --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,22 @@ +name: Release Ansible Role + +on: + push: + tags: + - '**' + workflow_dispatch: {} + +jobs: + release: + name: Release Ansible Role + runs-on: ubuntu-latest + steps: + - name: Install Ansible Galaxy + run: | + apt update --yes + apt install --yes ansible + - env: + ANSIBLE_GALAXY_TOKEN: ${{ secrets.ANSIBLE_GALAXY_TOKEN }} + name: Update Ansible Role in Ansible Galaxy + run: | + ansible-galaxy role import --token=${ANSIBLE_GALAXY_TOKEN} volker-raschek ${GITHUB_REPOSITORY#*/} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..feaf22e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.ansible \ No newline at end of file diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..0cca4db --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,137 @@ +# markdownlint YAML configuration +# https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD003/heading-style/header-style - Heading style +MD003: + # Heading style + style: "atx" + +# MD004/ul-style - Unordered list style +MD004: + style: "dash" + +# MD007/ul-indent - Unordered list indentation +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: false + +# MD009/no-trailing-spaces - Trailing spaces +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs - Hard tabs +MD010: + # Include code blocks + code_blocks: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 120 + # Number of characters for headings + heading_line_length: 120 + # Number of characters for code blocks + code_block_line_length: 120 + # Include code blocks + code_blocks: false + # Include tables + tables: false + # Include headings + headings: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +MD026: + # Punctuation characters + punctuation: ".,;:!。,;:!" + +# MD029/ol-prefix - Ordered list item prefix +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space - Spaces after list markers +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD033/no-inline-html - Inline HTML +MD033: + # Allowed elements + allowed_elements: [] + +# MD035/hr-style - Horizontal rule style +MD035: + # Horizontal rule style + style: "---" + +# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD044/proper-names - Proper names should have the correct capitalization +MD044: + # List of proper names + names: + - gitea + # Include code blocks + code_blocks: false + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: "fenced" + +# MD048/code-fence-style - Code fence style +MD048: + # Code fence syle + style: "backtick" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e112343 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "ansible.python.interpreterPath": "/bin/python", + "files.associations": { + "**/.gitea/**/*.yml": "yaml", + "**/.gitea/**/*.yaml": "yaml", + "docker-compose*.yml": "dockercompose", + "*.yml": "ansible", + "*.yaml": "ansible", + ".yamllint": "yaml", + ".yamllint.yml": "yaml", + ".yamllint.yaml": "yaml" + }, + "rewrap.wrappingColumn": 120 +} \ No newline at end of file diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..e0e4d53 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,19 @@ +--- + +# +# Documentation: +# https://yamllint.readthedocs.io/en/stable/ +# + +rules: + brackets: + forbid: false + min-spaces-inside: 0 + max-spaces-inside: 2 + min-spaces-inside-empty: 0 + max-spaces-inside-empty: 0 + indentation: + spaces: 2 + indent-sequences: false + line-length: + max: 360 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..51ff935 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2022 Markus Pesch + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..64ac2bc --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# volker-raschek.rspamd + +![Ansible Role](https://img.shields.io/ansible/role/d/volker-raschek/rspamd) + +The ansible role `volker-raschek.rspamd` install rspamd - a spam milter. For example for Arch Linux, Fedora and Ubuntu. +Furthermore, rspamd can be integration in some MTA's like postfix. + +## Examples + +The following configuration enables DKIM signature validation and redis as backend. Furthermore, it configures custom +DNS servers for DNS queries. + +```yaml +## @param rspamd_dkim_enabled Create `dkim_sining.conf`. +## @param rspamd_dkim_domains DKIM Domain configuration. +rspamd_dkim_enabled: true +rspamd_dkim_domains: +- name: my.example.local + selector: "2020" + +## @section DNS +## @param List of DNS servers used for DNS lookups. +rspamd_dns_servers: +- 8.8.4.4 +- 8.8.8.8 + +## @section Redis +## https://docs.rspamd.com/configuration/redis/#available-redis-options +## @param rspamd_redis_database Number of redis database. +## @param rspamd_redis_password Password to connect to redis. +## @param rspamd_redis_username Username to connect to redis. +## @param rspamd_redis_servers List of upstream redis server for read and write requests. +## @param rspamd_redis_timeout Timeout in seconds to get reply from redis. For example `0.5s`, `1min`. +## @param rspamd_redis_disabled_modules List of disabled modules. +rspamd_redis_enabled: true +rspamd_redis_database: "0" +rspamd_redis_password: "my-password" +rspamd_redis_username: "my-username" +rspamd_redis_servers: +- "127.0.0.1" +rspamd_redis_timeout: "5s" +rspamd_redis_disabled_modules: +- "ratelimit" +``` + +## Further ansible roles + +This ansible role is used in combination with other ansible roles of `volker-raschek`. You can search for the other +ansible roles via the following command. + +```bash +$ ansible-galaxy role search --author "volker-raschek" + +Found roles matching your search: + + Name Description + ---- ----------- + volker-raschek.bind9 Role to install and configure bind9 on different distributions + volker-raschek.dhcpd Role to install and configure dhcpd on different distributions + volker-raschek.renovate Role to configure renovate as container image + ... +``` diff --git a/defaults/main.yaml b/defaults/main.yaml new file mode 100644 index 0000000..edc18ff --- /dev/null +++ b/defaults/main.yaml @@ -0,0 +1,101 @@ +--- + +## @section ACLs +## @param rspamd_acl_allowlist_from Allow emails by sender. +## @param rspamd_acl_allowlist_ips Allow emails by ip addresses. +## @param rspamd_acl_blocklist_from Reject emails by sender. +## @param rspamd_acl_blocklist_ips Reject emails by ip addresses. +rspamd_acl_allowlist_from: [] +# - from: "max.mustermann@example.local" +# - from: "*@example.local" +rspamd_acl_allowlist_ips: [] +# - ip: "10.11.12.13" +rspamd_acl_blocklist_from: [] +# - from: "max.mustermann@example.local" +# - from: "*@example.local" +rspamd_acl_blocklist_ips: [] +# - ip: "10.11.12.13" + + +## @section DKIM Singing configuration +## @param rspamd_dkim_enabled Create `dkim_sining.conf`. +## @param rspamd_dkim_allow_username_mismatch Enable DKIM signing for alias sender addresses. +## @param rspamd_dkim_directory Directory of the DKIM keys. +## @param rspamd_dkim_domains DKIM Domain configuration. +rspamd_dkim_enabled: false +rspamd_dkim_allow_username_mismatch: false +rspamd_dkim_dir: "/var/lib/rspamd/dkim" +rspamd_dkim_domains: [] +# - name: example.local +# selector: "2020" + +## @section DNS +## @param List of DNS servers used for DNS lookups. +rspamd_dns_servers: [] +# - 8.8.4.4 +# - 8.8.8.8 + +## @section Logging configuration +## https://docs.rspamd.com/configuration/logging/ +## @param rspamd_logging_enabled Create `logging.inc.conf`. +## @param rspamd_logging_filename Path to log file for logging. Require logging_type `file`. +## @param rspamd_logging_level Log level. Allowed values: `error`, `warning`, `notice`, `info`, `silent` and `debug`. +## @param rspamd_logging_type Log type. Allowed values: `console`, `file` and `syslog`. +rspamd_logging_enabled: true +rspamd_logging_filename: "" +rspamd_logging_level: "info" +rspamd_logging_type: "syslog" + +## @section Redis +## https://docs.rspamd.com/configuration/redis/#available-redis-options +## @param rspamd_redis_enabled Create `redis.conf`. +## @param rspamd_redis_database Number of redis database. +## @param rspamd_redis_password Password to connect to redis. +## @param rspamd_redis_username Username to connect to redis. +## @param rspamd_redis_servers List of upstream redis server for read and write requests. +## @param rspamd_redis_read_servers List of redis servers for read requests. Usually redis replication instances. +## @param rspamd_redis_timeout Timeout in seconds to get reply from redis. For example `0.5s`, `1min`. +## @param rspamd_redis_write_servers List of redis servers for write requests. Usually redis primary instances. +## @param rspamd_redis_disabled_modules List of disabled modules. +rspamd_redis_enabled: false +rspamd_redis_database: "0" +rspamd_redis_password: "" +rspamd_redis_username: "" +rspamd_redis_servers: [] +# - "redis.example.local" +rspamd_redis_read_servers: [] +# - "replica-0.redis.example.local" +# - "replica-1.redis.example.local" +rspamd_redis_timeout: "5s" +rspamd_redis_write_servers: [] +# - "primary-0.redis.example.local" +# - "primary-1.redis.example.local" +rspamd_redis_disabled_modules: [] +# - "ratelimit" + +## @section Worker Controller +## https://docs.rspamd.com/workers/#common-worker-options +## @param rspamd_worker_controller_enabled Create `worker-controller.conf`. +## @param rspamd_worker_controller_read_password Password required for read-only commands. +## @param rspamd_worker_controller_read_password Password required for write (privileged) commands. +## @param rspamd_worker_controller_secure_ips List of secure IP-Addresses for password-less access. If using a reverse proxy with X-Forwarded-For, include both proxy and client IPs. +## @param rspamd_worker_controller_ssl_cert Path to PEM certificate file (required when using ssl bind sockets). +## @param rspamd_worker_controller_ssl_key Path to PEM private key file (required when using ssl bind sockets). +rspamd_worker_controller_enabled: false +rspamd_worker_controller_bind_socket: "localhost:11334" +rspamd_worker_controller_read_password: "" +rspamd_worker_controller_write_password: "" +rspamd_worker_controller_secure_ips: +- "localhost" +rspamd_worker_controller_ssl_cert: "" +rspamd_worker_controller_ssl_key: "" + +## @section Drop-In directories +## @param rspamd_local_d_dir Drop-In directory to customize rspamd configurations. +## @param rspamd_modules_d_dir Drop-In directory to customize rspamd modules. +## @param rspamd_override_d_dir Drop-In directory to customize rspamd plugins. +## @param rspamd_plugins_d_dir Drop-In directory to override rspamd configurations. +rspamd_local_d_dir: "/etc/rspamd/local.d" +rspamd_modules_d_dir: "/etc/rspamd/modules.d" +rspamd_override_d_dir: "/etc/rspamd/override.d" +rspamd_plugins_d_dir: "/etc/rspamd/plugins.d" diff --git a/handlers/main.yaml b/handlers/main.yaml new file mode 100644 index 0000000..873e1d8 --- /dev/null +++ b/handlers/main.yaml @@ -0,0 +1,11 @@ +--- + +- name: Restart rspamd + ansible.builtin.systemd: + name: rspamd + state: restarted + +- name: Restart nginx + ansible.builtin.systemd: + name: nginx + state: restarted diff --git a/meta/argument_specs.yaml b/meta/argument_specs.yaml new file mode 100644 index 0000000..51a7d5d --- /dev/null +++ b/meta/argument_specs.yaml @@ -0,0 +1,176 @@ +--- +argument_specs: + main: + short_description: "Role to install and configure rspamd mail filter" + description: + - "This role configures rspamd with support for DKIM signing, Redis integration, logging, ACLs, and worker controller settings." + author: "Markus Pesch" + options: + # ACLs + rspamd_acl_allowlist_from: + description: "Allow emails by sender." + type: list + elements: dict + default: [] + rspamd_acl_allowlist_ips: + description: "Allow emails by IP addresses." + type: list + elements: dict + default: [] + rspamd_acl_blocklist_from: + description: "Reject emails by sender." + type: list + elements: dict + default: [] + rspamd_acl_blocklist_ips: + description: "Reject emails by IP addresses." + type: list + elements: dict + default: [] + + # DKIM Signing + rspamd_dkim_enabled: + description: "Create dkim_signing.conf." + type: bool + default: false + rspamd_dkim_allow_username_mismatch: + description: "Enable DKIM signing for alias sender addresses." + type: bool + default: false + rspamd_dkim_dir: + description: "Directory of the DKIM keys." + type: str + default: "/var/lib/rspamd/dkim" + rspamd_dkim_domains: + description: "DKIM Domain configuration." + type: list + elements: dict + default: [] + + # DNS + rspamd_dns_servers: + description: "List of DNS servers used for DNS lookups." + type: list + elements: str + default: [] + + # Logging + rspamd_logging_enabled: + description: "Create logging.inc.conf." + type: bool + default: true + rspamd_logging_filename: + description: "Path to log file for logging. Requires logging_type 'file'." + type: str + default: "" + rspamd_logging_level: + description: "Log level." + type: str + default: "info" + choices: + - error + - warning + - notice + - info + - silent + - debug + rspamd_logging_type: + description: "Log type." + type: str + default: "syslog" + choices: + - console + - file + - syslog + + # Redis + rspamd_redis_enabled: + description: "Create redis.conf." + type: bool + default: false + rspamd_redis_database: + description: "Number of redis database." + type: str + default: "0" + rspamd_redis_password: + description: "Password to connect to redis." + type: str + default: "" + rspamd_redis_username: + description: "Username to connect to redis." + type: str + default: "" + rspamd_redis_servers: + description: "List of upstream redis servers for read and write requests." + type: list + elements: str + default: [] + rspamd_redis_read_servers: + description: "List of redis servers for read requests. Usually redis replication instances." + type: list + elements: str + default: [] + rspamd_redis_timeout: + description: "Timeout in seconds to get reply from redis. For example '0.5s', '1min'." + type: str + default: "5s" + rspamd_redis_write_servers: + description: "List of redis servers for write requests. Usually redis primary instances." + type: list + elements: str + default: [] + rspamd_redis_disabled_modules: + description: "List of disabled modules." + type: list + elements: str + default: [] + + # Worker Controller + rspamd_worker_controller_enabled: + description: "Create worker-controller.conf." + type: bool + default: false + rspamd_worker_controller_bind_socket: + description: "Bind socket for worker controller." + type: str + default: "localhost:11334" + rspamd_worker_controller_read_password: + description: "Password required for read-only commands." + type: str + default: "" + rspamd_worker_controller_write_password: + description: "Password required for write (privileged) commands." + type: str + default: "" + rspamd_worker_controller_secure_ips: + description: "List of secure IP addresses for password-less access. If using a reverse proxy with X-Forwarded-For, include both proxy and client IPs." + type: list + elements: str + default: + - localhost + rspamd_worker_controller_ssl_cert: + description: "Path to PEM certificate file (required when using ssl bind sockets)." + type: str + default: "" + rspamd_worker_controller_ssl_key: + description: "Path to PEM private key file (required when using ssl bind sockets)." + type: str + default: "" + + # Drop-In directories + rspamd_local_d_dir: + description: "Drop-In directory to customize rspamd configurations." + type: str + default: "/etc/rspamd/local.d" + rspamd_modules_d_dir: + description: "Drop-In directory to customize rspamd modules." + type: str + default: "/etc/rspamd/modules.d" + rspamd_override_d_dir: + description: "Drop-In directory to override rspamd configurations." + type: str + default: "/etc/rspamd/override.d" + rspamd_plugins_d_dir: + description: "Drop-In directory to customize rspamd plugins." + type: str + default: "/etc/rspamd/plugins.d" diff --git a/meta/main.yaml b/meta/main.yaml new file mode 100644 index 0000000..36179c4 --- /dev/null +++ b/meta/main.yaml @@ -0,0 +1,28 @@ +dependencies: [] +galaxy_info: + author: "Markus Pesch" + company: "Cryptic Systems" + description: "Role to install and configure rspamd mail filter" + galaxy_tags: + - rspamd + - mail + - spam + - filter + - email + license: "MIT" + min_ansible_version: "2.9" + namespace: volker-raschek + platforms: + - name: ArchLinux + versions: + - all + - name: EL + versions: + - all + - name: Fedora + versions: + - all + - name: Ubuntu + versions: + - all + role_name: "rspamd" diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..84f88f7 --- /dev/null +++ b/renovate.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>volker.raschek/renovate-config:default#master", + "local>volker.raschek/renovate-config:container#master", + "local>volker.raschek/renovate-config:actions#master", + "local>volker.raschek/renovate-config:regexp#master" + ] +} \ No newline at end of file diff --git a/tasks/dkim_create.yaml b/tasks/dkim_create.yaml new file mode 100644 index 0000000..f8096eb --- /dev/null +++ b/tasks/dkim_create.yaml @@ -0,0 +1,48 @@ +--- + +- name: "Create directory for dkim keys" + ansible.builtin.file: + path: "{{ rspamd_dkim_dir }}" + owner: "root" + group: "root" + mode: "0755" + state: "directory" + +- name: "Generate dkim keys" + ansible.builtin.command: + cmd: "bash -c \"rspamadm dkim_keygen -b 2048 -s {{ item.selector }} -k {{ rspamd_dkim_dir }}/{{ item.name }}.{{ item.selector }}.key > {{ rspamd_dkim_dir }}/{{ item.name }}.{{ item.selector }}.txt\"" + creates: "{{ rspamd_dkim_dir }}/{{ item.name }}.{{ item.selector }}.txt" + with_items: "{{ rspamd_dkim_domains }}" + notify: Restart rspamd + +- name: "Change ownership for dkim key files" + ansible.builtin.file: + path: "{{ rspamd_dkim_dir }}/{{ item.name }}.{{ item.selector }}.txt" + owner: "{{ rspamd_unix_user }}" + group: "{{ rspamd_unix_group }}" + mode: "0440" + with_items: "{{ rspamd_dkim_domains }}" + +- name: "Change ownership for dkim dns record files" + ansible.builtin.file: + path: "{{ rspamd_dkim_dir }}/{{ item.name }}.{{ item.selector }}.key" + owner: "{{ rspamd_unix_user }}" + group: "{{ rspamd_unix_group }}" + mode: "0440" + with_items: "{{ rspamd_dkim_domains }}" + +- name: "Create dkim_signing.conf" + ansible.builtin.template: + src: etc/rspamd/local.d/dkim_signing.conf.j2 + dest: "{{ rspamd_local_d_dir }}/dkim_signing.conf" + owner: "root" + group: "root" + mode: "0644" + +- name: Link ARC module to same DKIM configuration + ansible.builtin.file: + src: "{{ rspamd_local_d_dir }}/dkim_signing.conf" + dest: "{{ rspamd_local_d_dir }}/arc.conf" + owner: "root" + group: "root" + state: link diff --git a/tasks/dkim_delete.yaml b/tasks/dkim_delete.yaml new file mode 100644 index 0000000..5e662d6 --- /dev/null +++ b/tasks/dkim_delete.yaml @@ -0,0 +1,11 @@ +--- + +- name: "Delete directory for dkim keys" + ansible.builtin.file: + path: "{{ rspamd_dkim_dir }}" + state: "absent" + +- name: "Delete dkim_signing.conf" + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/dkim_signing.conf" + state: "absent" diff --git a/tasks/logging_create.yaml b/tasks/logging_create.yaml new file mode 100644 index 0000000..190f0d3 --- /dev/null +++ b/tasks/logging_create.yaml @@ -0,0 +1,9 @@ +--- + +- name: "Create logging.inc" + ansible.builtin.template: + src: etc/rspamd/local.d/logging.inc.j2 + dest: "{{ rspamd_local_d_dir }}/logging.inc" + owner: "root" + group: "root" + mode: "0644" diff --git a/tasks/logging_delete.yaml b/tasks/logging_delete.yaml new file mode 100644 index 0000000..5b1e9cc --- /dev/null +++ b/tasks/logging_delete.yaml @@ -0,0 +1,6 @@ +--- + +- name: "Delete logging.inc" + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/logging.inc" + state: "absent" diff --git a/tasks/main.yaml b/tasks/main.yaml new file mode 100644 index 0000000..bfa0468 --- /dev/null +++ b/tasks/main.yaml @@ -0,0 +1,121 @@ +--- + +- name: Include OS-specific variables + tags: [ letsencrypt-hooks ] + ansible.builtin.include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_facts['distribution'] }}_{{ ansible_facts['architecture'] }}.yaml" + - "{{ ansible_facts['distribution'] }}.yaml" + - "{{ ansible_facts['os_family'] }}_{{ ansible_facts['architecture'] }}.yaml" + - "{{ ansible_facts['os_family'] }}.yaml" + - main.yaml + paths: + - vars + +- name: Install rspamd + ansible.builtin.package: + name: rspamd + state: "present" + +- name: Create directory for custom rspamd configurations + ansible.builtin.file: + path: "{{ item }}" + owner: "root" + group: "root" + mode: "0755" + state: directory + with_items: + - "{{ rspamd_local_d_dir }}" + - "{{ rspamd_modules_d_dir }}" + - "{{ rspamd_override_d_dir }}" + - "{{ rspamd_plugins_d_dir }}" + +- name: Manage dkim configuration + notify: + - Restart rspamd + block: + - name: Create dkim configuration + when: rspamd_dkim_enabled + ansible.builtin.include_tasks: dkim_create.yaml + - name: Delete dkim configuration + when: not rspamd_dkim_enabled + ansible.builtin.include_tasks: dkim_delete.yaml + +- name: Manage logging configuration + notify: + - Restart rspamd + block: + - name: Create logging configuration + when: rspamd_logging_enabled + ansible.builtin.include_tasks: logging_create.yaml + - name: Delete logging configuration + when: not rspamd_logging_enabled + ansible.builtin.include_tasks: logging_delete.yaml + +- name: Manage multimaps + ansible.builtin.include_tasks: multimaps.yaml + +- name: Manage redis configuration + notify: + - Restart rspamd + block: + - name: Create redis configuration + when: rspamd_redis_enabled + ansible.builtin.include_tasks: redis_create.yaml + - name: Delete redis configuration + when: not rspamd_redis_enabled + ansible.builtin.include_tasks: redis_delete.yaml + +- name: Manage worker controller + notify: + - Restart rspamd + block: + - name: Create worker controller configuration + when: rspamd_worker_controller_enabled + ansible.builtin.include_tasks: worker_controller_create.yaml + - name: Delete worker controller configuration + when: not rspamd_worker_controller_enabled + ansible.builtin.include_tasks: worker_controller_delete.yaml + +- name: Template local.d files + vars: + selector_name: "{{ rspamd_dkim_selector_name | default('2020') }}" + ansible.builtin.template: + src: "{{ item }}.j2" + dest: "{{ rspamd_local_d_dir }}/{{ item | basename }}" + owner: "root" + group: "root" + mode: "0644" + with_items: + - etc/rspamd/local.d/classifier-bayes.conf + - etc/rspamd/local.d/milter_headers.conf + - etc/rspamd/local.d/options.inc + notify: + - Restart rspamd + +- name: Template override.d files + ansible.builtin.template: + src: "{{ item }}.j2" + dest: "{{ rspamd_override_d_dir }}/{{ item | basename }}" + owner: "root" + group: "root" + mode: "0644" + with_items: + - etc/rspamd/override.d/classifier-bayes.conf + notify: + - Restart rspamd + +- name: Test rspamd configuration + ansible.builtin.command: + cmd: rspamadm configtest --strict --config /etc/rspamd/rspamd.conf + register: _rspamd_rspamadm_configtest + changed_when: false + failed_when: _rspamd_rspamadm_configtest.rc > 0 + +- name: Start and enable rspamd + ansible.builtin.systemd: + name: rspamd + state: started + enabled: true diff --git a/tasks/multimaps.yaml b/tasks/multimaps.yaml new file mode 100644 index 0000000..575df47 --- /dev/null +++ b/tasks/multimaps.yaml @@ -0,0 +1,73 @@ +--- + +- name: "Create multimap.conf" + ansible.builtin.template: + src: etc/rspamd/local.d/multimap.conf.j2 + dest: "{{ rspamd_local_d_dir }}/multimap.conf" + owner: "root" + group: "root" + mode: "0644" + +- name: "Manage sender based allowlist" + block: + - name: "Create sender based allowlist" + when: rspamd_acl_allowlist_from | length > 0 + ansible.builtin.template: + src: etc/rspamd/local.d/allowlist_from.map.j2 + dest: "{{ rspamd_local_d_dir }}/allowlist_from.map" + owner: "root" + group: "root" + mode: "0644" + - name: "Delete sender based allowlist" + when: rspamd_acl_allowlist_from | length == 0 + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/allowlist_from.map" + state: "absent" + +- name: "Manage ip based allowlist" + block: + - name: "Create ip based allowlist" + when: rspamd_acl_allowlist_ips | length > 0 + ansible.builtin.template: + src: etc/rspamd/local.d/allowlist_ips.map.j2 + dest: "{{ rspamd_local_d_dir }}/allowlist_ips.map" + owner: "root" + group: "root" + mode: "0644" + - name: "Delete ip based allowlist" + when: rspamd_acl_allowlist_ips | length == 0 + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/allowlist_ips.map" + state: "absent" + +- name: "Manage sender based blocklist" + block: + - name: "Create sender based blocklist" + when: rspamd_acl_blocklist_from | length > 0 + ansible.builtin.template: + src: etc/rspamd/local.d/blocklist_from.map.j2 + dest: "{{ rspamd_local_d_dir }}/blocklist_from.map" + owner: "root" + group: "root" + mode: "0644" + - name: "Delete sender based blocklist" + when: rspamd_acl_blocklist_from | length == 0 + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/blocklist_from.map" + state: "absent" + +- name: "Manage ip based blocklist" + block: + - name: "Create ip based blocklist" + when: rspamd_acl_blocklist_ips | length > 0 + ansible.builtin.template: + src: etc/rspamd/local.d/blocklist_ips.map.j2 + dest: "{{ rspamd_local_d_dir }}/blocklist_ips.map" + owner: "root" + group: "root" + mode: "0644" + - name: "Delete ip based blocklist" + when: rspamd_acl_blocklist_ips | length == 0 + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/blocklist_ips.map" + state: "absent" diff --git a/tasks/redis_create.yaml b/tasks/redis_create.yaml new file mode 100644 index 0000000..1999457 --- /dev/null +++ b/tasks/redis_create.yaml @@ -0,0 +1,9 @@ +--- + +- name: "Create redis.conf" + ansible.builtin.template: + src: etc/rspamd/local.d/redis.conf.j2 + dest: "{{ rspamd_local_d_dir }}/redis.conf" + owner: "root" + group: "root" + mode: "0644" diff --git a/tasks/redis_delete.yaml b/tasks/redis_delete.yaml new file mode 100644 index 0000000..3e4eeeb --- /dev/null +++ b/tasks/redis_delete.yaml @@ -0,0 +1,6 @@ +--- + +- name: "Delete redis.conf" + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/redis.conf" + state: "absent" diff --git a/tasks/worker_controller_create.yaml b/tasks/worker_controller_create.yaml new file mode 100644 index 0000000..dc5f1ed --- /dev/null +++ b/tasks/worker_controller_create.yaml @@ -0,0 +1,23 @@ +--- + +- name: Generate salted read password + when: rspamd_worker_controller_read_password | length > 0 + ansible.builtin.command: + cmd: "rspamadm pw --password {{ rspamd_worker_controller_read_password }}" + changed_when: false + register: rspamd_worker_controller_read_password_salted + +- name: Generate salted write password + when: rspamd_worker_controller_write_password | length > 0 + ansible.builtin.command: + cmd: "rspamadm pw --password {{ rspamd_worker_controller_write_password }}" + changed_when: false + register: rspamd_worker_controller_write_password_salted + +- name: "Create worker-controller.inc" + ansible.builtin.template: + src: etc/rspamd/local.d/worker-controller.inc.j2 + dest: "{{ rspamd_local_d_dir }}/worker-controller.inc" + owner: "root" + group: "root" + mode: "0644" diff --git a/tasks/worker_controller_delete.yaml b/tasks/worker_controller_delete.yaml new file mode 100644 index 0000000..ed33db3 --- /dev/null +++ b/tasks/worker_controller_delete.yaml @@ -0,0 +1,6 @@ +--- + +- name: "Delete worker-controller.inc" + ansible.builtin.file: + path: "{{ rspamd_local_d_dir }}/worker-controller.inc" + state: "absent" diff --git a/templates/etc/rspamd/local.d/allowlist_from.map.j2 b/templates/etc/rspamd/local.d/allowlist_from.map.j2 new file mode 100644 index 0000000..6fac83a --- /dev/null +++ b/templates/etc/rspamd/local.d/allowlist_from.map.j2 @@ -0,0 +1,6 @@ +# +# {{ ansible_managed }} +# +{% for item in rspamd_acl_allowlist_from %} +{{ item.from }} +{% endfor %} \ No newline at end of file diff --git a/templates/etc/rspamd/local.d/allowlist_ips.map.j2 b/templates/etc/rspamd/local.d/allowlist_ips.map.j2 new file mode 100644 index 0000000..e109a73 --- /dev/null +++ b/templates/etc/rspamd/local.d/allowlist_ips.map.j2 @@ -0,0 +1,6 @@ +# +# {{ ansible_managed }} +# +{% for item in rspamd_acl_allowlist_ips %} +{{ item.ip }} +{% endfor %} \ No newline at end of file diff --git a/templates/etc/rspamd/local.d/blocklist_from.map.j2 b/templates/etc/rspamd/local.d/blocklist_from.map.j2 new file mode 100644 index 0000000..b19e62f --- /dev/null +++ b/templates/etc/rspamd/local.d/blocklist_from.map.j2 @@ -0,0 +1,6 @@ +# +# {{ ansible_managed }} +# +{% for item in rspamd_acl_blocklist_from %} +{{ item.from }} +{% endfor %} \ No newline at end of file diff --git a/templates/etc/rspamd/local.d/blocklist_ips.map.j2 b/templates/etc/rspamd/local.d/blocklist_ips.map.j2 new file mode 100644 index 0000000..1d27c6a --- /dev/null +++ b/templates/etc/rspamd/local.d/blocklist_ips.map.j2 @@ -0,0 +1,6 @@ +# +# {{ ansible_managed }} +# +{% for item in rspamd_acl_blocklist_ips %} +{{ item.ip }} +{% endfor %} \ No newline at end of file diff --git a/templates/etc/rspamd/local.d/classifier-bayes.conf.j2 b/templates/etc/rspamd/local.d/classifier-bayes.conf.j2 new file mode 100644 index 0000000..5a9b633 --- /dev/null +++ b/templates/etc/rspamd/local.d/classifier-bayes.conf.j2 @@ -0,0 +1,5 @@ +# +# {{ ansible_managed }} +# + +backend = "redis"; \ No newline at end of file diff --git a/templates/etc/rspamd/local.d/dkim_signing.conf.j2 b/templates/etc/rspamd/local.d/dkim_signing.conf.j2 new file mode 100644 index 0000000..3f82ab9 --- /dev/null +++ b/templates/etc/rspamd/local.d/dkim_signing.conf.j2 @@ -0,0 +1,23 @@ +# +# {{ ansible_managed }} +# + +# +# Documentation: https://rspamd.com/doc/modules/dkim_signing.html#configuration +# + +# Default DKIM Keys +# selector = ""; +# path = ""; + +allow_username_mismatch = {{ rspamd_dkim_allow_username_mismatch | lower }}; + +# Domain specific settings +domain { +{% for dkim_domain in rspamd_dkim_domains %} + {{ dkim_domain.name }} { + path = "{{ rspamd_dkim_dir }}/{{ dkim_domain.name }}.{{ dkim_domain.selector }}.key"; + selector = "{{ dkim_domain.selector }}"; + } +{% endfor %} +} \ No newline at end of file diff --git a/templates/etc/rspamd/local.d/logging.inc.j2 b/templates/etc/rspamd/local.d/logging.inc.j2 new file mode 100644 index 0000000..ce9bc4d --- /dev/null +++ b/templates/etc/rspamd/local.d/logging.inc.j2 @@ -0,0 +1,9 @@ +# +# {{ ansible_managed }} +# + +{% if rspamd_logging_type == "file" and rspamd_logging_filename | length > 0 %} +filename = "{{ rspamd_logging_filename }}"; +{% endif %} +level = "{{ rspamd_logging_level }}"; +type = "{{ rspamd_logging_type}}"; diff --git a/templates/etc/rspamd/local.d/milter_headers.conf.j2 b/templates/etc/rspamd/local.d/milter_headers.conf.j2 new file mode 100644 index 0000000..33ce300 --- /dev/null +++ b/templates/etc/rspamd/local.d/milter_headers.conf.j2 @@ -0,0 +1,13 @@ +# +# {{ ansible_managed }} +# + +authenticated_headers = [ + "authentication-results", +]; + +use = [ + "x-spamd-bar", + "x-spam-level", + "authentication-results", +]; diff --git a/templates/etc/rspamd/local.d/multimap.conf.j2 b/templates/etc/rspamd/local.d/multimap.conf.j2 new file mode 100644 index 0000000..dc0e72b --- /dev/null +++ b/templates/etc/rspamd/local.d/multimap.conf.j2 @@ -0,0 +1,39 @@ +# +# {{ ansible_managed }} +# +{% if rspamd_acl_blocklist_from | length > 0 %} +BLACKLIST_FROM { + type = "from"; + map = "$CONFDIR/local.d/blocklist_from.map"; + description = "Local from blocklist"; + action = "reject"; +} + +{% endif %} +{% if rspamd_acl_blocklist_ips | length > 0 %} +BLACKLIST_IP { + type = "ip"; + map = "$CONFDIR/local.d/blocklist_ips.map"; + description = "Local ip blocklist"; + action = "reject"; +} + +{% endif %} +{% if rspamd_acl_allowlist_from | length > 0 %} +WHITELIST_FROM { + type = "from"; + map = "$CONFDIR/local.d/allowlist_from.map"; + description = "Local from allowlist"; + action = "accept"; +} + +{% endif %} +{% if rspamd_acl_allowlist_ips | length > 0 %} +WHITELIST_IP { + type = "ip"; + map = "$CONFDIR/local.d/allowlist_ips.map"; + description = "Local ip allowlist"; + action = "accept"; +} + +{% endif %} diff --git a/templates/etc/rspamd/local.d/options.inc.j2 b/templates/etc/rspamd/local.d/options.inc.j2 new file mode 100644 index 0000000..10102e3 --- /dev/null +++ b/templates/etc/rspamd/local.d/options.inc.j2 @@ -0,0 +1,10 @@ +# +# {{ ansible_managed }} +# + +{% if rspamd_dns_server is defined and rspamd_dns_servers | length > 0 %} +dns { + nameserver = "{{ rspamd_dns_server }}"; +} +{% endif %} +enable_test_patterns = true; diff --git a/templates/etc/rspamd/local.d/redis.conf.j2 b/templates/etc/rspamd/local.d/redis.conf.j2 new file mode 100644 index 0000000..82f352e --- /dev/null +++ b/templates/etc/rspamd/local.d/redis.conf.j2 @@ -0,0 +1,27 @@ +#jinja2: lstrip_blocks: True, trim_blocks: True +# +# {{ ansible_managed }} +# + +db = {{ rspamd_redis_database }}; +{% if rspamd_redis_disabled_modules | length > 0 %} +disabled_modules = [ +{% for item in rspamd_redis_disabled_modules %} + "{{ item }}", +{% endfor %} +]; +{% endif %} +{% if rspamd_redis_password | length > 0 %} +password = "{{ rspamd_redis_password }}"; +{% endif %} +{% if rspamd_redis_username| length > 0 %} +username = "{{ rspamd_redis_username }}"; +{% endif %} +servers = "{{ rspamd_redis_servers | join(',') }}"; +{% if rspamd_redis_read_servers | length > 0 %} +read_servers = "{{ rspamd_redis_read_servers | join(',') }}"; +{% endif %} +{% if rspamd_redis_write_servers | length > 0 %} +write_servers = "{{ rspamd_redis_write_servers | join(',') }}"; +{% endif %} +timeout = {{ rspamd_redis_timeout }}; diff --git a/templates/etc/rspamd/local.d/worker-controller.inc.j2 b/templates/etc/rspamd/local.d/worker-controller.inc.j2 new file mode 100644 index 0000000..61da4b4 --- /dev/null +++ b/templates/etc/rspamd/local.d/worker-controller.inc.j2 @@ -0,0 +1,22 @@ +# +# {{ ansible_managed }} +# + +{% if rspamd_worker_controller_bind_socket | length > 0 %} +bind_socket = "{{ rspamd_worker_controller_bind_socket }}"; +{% endif %} +{% if rspamd_worker_controller_write_password_salted.stdout is defined and rspamd_worker_controller_write_password_salted.stdout | length > 0 %} +enable_password = "{{ rspamd_worker_controller_write_password_salted.stdout }}"; +{% endif %} +{% if rspamd_worker_controller_read_password_salted.stdout is defined and rspamd_worker_controller_read_password_salted.stdout | length > 0 %} +password = "{{ rspamd_worker_controller_read_password_salted.stdout }}"; +{% endif %} +{% if rspamd_worker_controller_secure_ips | length > 0 %} +secure_ip = "{{ rspamd_worker_controller_secure_ips | join(',') }}"; +{% endif %} +{% if rspamd_worker_controller_ssl_cert | length > 0 %} +ssl_cert = "{{ rspamd_worker_controller_ssl_cert }}"; +{% endif %} +{% if rspamd_worker_controller_ssl_key | length > 0 %} +ssl_key = "{{ rspamd_worker_controller_ssl_key }}"; +{% endif %} \ No newline at end of file diff --git a/templates/etc/rspamd/override.d/classifier-bayes.conf.j2 b/templates/etc/rspamd/override.d/classifier-bayes.conf.j2 new file mode 100644 index 0000000..de73e0a --- /dev/null +++ b/templates/etc/rspamd/override.d/classifier-bayes.conf.j2 @@ -0,0 +1,5 @@ +# +# {{ ansible_managed }} +# + +autolearn = true; \ No newline at end of file diff --git a/vars/Archlinux.yaml b/vars/Archlinux.yaml new file mode 100644 index 0000000..a6bdb5d --- /dev/null +++ b/vars/Archlinux.yaml @@ -0,0 +1,4 @@ +--- + +rspamd_unix_user: rspamd +rspamd_unix_group: rspamd