Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* **role:redis**: The role's inventory variables are type-checked when it starts, so a mistyped value fails the run right away instead of surfacing further in as a confusing error.
* **role:graylog_datanode, role:graylog_server**: Add template for Graylog 7.2.
* **role:duplicity**: `duplicity__archive_dir` sets the directory of duplicity's local metadata cache, for example to keep the cache of a separate long-term backup apart from the daily one. Without it, the cache stays in `~/.cache/duplicity`.
* **role:systemd_unit**: Manages systemd socket units with `systemd_unit__sockets__host_var` / `systemd_unit__sockets__group_var`, for example to forward a TCP port with `systemd-socket-proxyd` without installing a proxy server.
Expand Down Expand Up @@ -83,6 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

* **role:redis, role:valkey**: Redis and Valkey also listen on a Unix socket that only the members of their group may use, on RHEL at the path the package ships (`/run/redis/redis.sock`, `/run/valkey/valkey.sock`).
* **role:apache_httpd**: Apache answers requests for PHP files that do not exist with 404 itself instead of passing them to PHP-FPM, so scanners no longer tie up pool workers or fill the error log.
* **playbook:setup_basic**: Checks the variables of all its roles, the compiled firewall in the Firewall Builder repository and the reachability of the Icinga2 master before it changes anything on the host, so a missing prerequisite no longer aborts the run halfway through, after SSH was already hardened.
* Handlers that a role notified before a later task failed run anyway, so a changed configuration reaches the running service; until now it stayed inactive, also across later runs, which reported no change (all playbooks).
Expand Down Expand Up @@ -122,6 +124,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* **role:redis, role:valkey**: On RHEL, a run after a reboot no longer reports the configuration file as changed, since the role keeps the ownership that the package's tmpfiles.d rule restores at every boot.
* **role:borg_local**: The Icinga downtime for `clamd@scan` during a backup is set, where Icinga had rejected the request as invalid JSON.
* **role:acme_sh**: A changed `acme-sh.service` or `acme-sh.timer`, for example a new schedule, takes effect in the same run. Until now systemd kept the old unit until the next reload.
* **role:clamav**: A changed `clamav-clamdscan.service` takes effect in the same run. Until now only a changed timer reloaded systemd.
Expand Down
44 changes: 39 additions & 5 deletions extensions/molecule/valkey/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@
port: '{{ valkey__conf_port | d(6379) }}'
timeout: 30

# The socket is what local clients such as Nextcloud connect through. Its path is read back
# from the running server, and the PING over it proves the server actually listens there.
- name: 'valkey-cli CONFIG GET unixsocket'
ansible.builtin.command: '{{ __molecule__valkey_cli }} CONFIG GET unixsocket'
register: '__molecule__valkey_unixsocket_result'
changed_when: false

- name: 'valkey-cli -s <unixsocket> PING'
ansible.builtin.command: '{{ __molecule__valkey_cli }} -s {{ __molecule__valkey_unixsocket }} PING'
register: '__molecule__valkey_socket_ping_result'
changed_when: false
vars:
__molecule__valkey_unixsocket: '{{ __molecule__valkey_unixsocket_result["stdout_lines"][1] }}'

- name: 'stat the Unix socket'
ansible.builtin.stat:
path: '{{ __molecule__valkey_unixsocket_result["stdout_lines"][1] }}'
register: '__molecule__valkey_socket_stat_result'

# 0770 and the valkey group: a role that needs access adds its service user to that group,
# and nobody else reaches the socket.
- name: 'Assert valkey answers on its Unix socket, which only its group may use'
ansible.builtin.assert:
that:
- '__molecule__valkey_socket_ping_result["stdout"] == "PONG"'
- '__molecule__valkey_socket_stat_result["stat"]["mode"] == "0770"'
- '__molecule__valkey_socket_stat_result["stat"]["gr_name"] == "valkey"'
fail_msg: 'CONFIG GET unixsocket: {{ __molecule__valkey_unixsocket_result["stdout_lines"] | to_json }} - stat: {{ __molecule__valkey_socket_stat_result["stat"] | d({}) | to_json }}'

# The bind default carries the upstream '-::1', where the '-' makes the address optional. Two
# halves to that: it answers on the IPv6 loopback where the host has one (asserted here), and
# it merely warns where the host has none, instead of aborting - which the service assertion
Expand All @@ -60,20 +89,25 @@

# The regression this role was split out for: on RHEL 10 the config file was deployed with a
# hardcoded `redis` group while the package creates a `valkey` one, so the run died with
# "chgrp failed: failed to look up group redis". Assert the ownership the package expects.
# "chgrp failed: failed to look up group redis". Assert the ownership the package expects,
# which on RHEL is the valkey:root that its tmpfiles.d rule restores at every boot, and on
# Debian and Ubuntu the root:valkey their package ships.
- name: 'stat /etc/valkey/valkey.conf'
ansible.builtin.stat:
path: '/etc/valkey/valkey.conf'
register: '__molecule__valkey_conf_result'

- name: 'Assert the config file is owned by root:valkey and not world-readable'
- name: 'Assert the config file has the ownership of the package and is not world-readable'
ansible.builtin.assert:
that:
- '__molecule__valkey_conf_result["stat"]["exists"]'
- '__molecule__valkey_conf_result["stat"]["pw_name"] == "root"'
- '__molecule__valkey_conf_result["stat"]["gr_name"] == "valkey"'
- '__molecule__valkey_conf_result["stat"]["pw_name"] == __molecule__valkey_conf_owner'
- '__molecule__valkey_conf_result["stat"]["gr_name"] == __molecule__valkey_conf_group'
- '__molecule__valkey_conf_result["stat"]["mode"] == "0640"'
fail_msg: '/etc/valkey/valkey.conf is {{ __molecule__valkey_conf_result["stat"] | d("missing") }}'
fail_msg: '/etc/valkey/valkey.conf is {{ __molecule__valkey_conf_result["stat"] | d("missing") | to_json }}'
vars:
__molecule__valkey_conf_group: '{{ (ansible_facts["os_family"] == "RedHat") | ternary("root", "valkey") }}'
__molecule__valkey_conf_owner: '{{ (ansible_facts["os_family"] == "RedHat") | ternary("valkey", "root") }}'

# The systemd drop-in is only proven by what the unit ends up with, not by the file on disk.
- name: 'systemctl show {{ __molecule__valkey_service_name }} --property=LimitNOFILE' # noqa command-instead-of-module (read-only state query)
Expand Down
16 changes: 16 additions & 0 deletions roles/redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ Variables for `redis.conf` directives and their default values, defined and supp
* Type: Number.
* Default: unset

`redis__conf_unixsocket`

* Path of the Unix socket Redis listens on, in addition to the TCP port. Set it to `''` to listen on no Unix socket. [redis.conf](https://github.com/redis/redis/blob/8.8/redis.conf)
* Type: String.
* Default: `'/run/redis/redis.sock'` on RHEL, `'/run/redis/redis-server.sock'` on Debian and Ubuntu
* Deviates from the upstream default on Debian and Ubuntu, whose package listens on no Unix socket: local clients such as Nextcloud connect faster through the socket than through TCP, and `redis__conf_unixsocketperm` keeps it closed to anyone outside the `redis` group.

`redis__conf_unixsocketperm`

* Permissions of the Unix socket. Only the `redis` user and the members of the `redis` group can connect with `770`; a role that needs access adds its service user to the group. [redis.conf](https://github.com/redis/redis/blob/8.8/redis.conf)
* Type: String.
* Default: `'770'`
* Deviates from the upstream default, which leaves the mode to the umask of the service, so that the socket is writable, and thus usable, by the `redis` user only.

Example:

```yaml
Expand All @@ -213,6 +227,8 @@ redis__conf_tls_ca_cert_file: '/etc/redis/ca.pem'
redis__conf_tls_cert_file: '/etc/redis/redis.pem'
redis__conf_tls_key_file: '/etc/redis/redis.key'
redis__conf_tls_port: 6379
redis__conf_unixsocket: '/run/redis/redis.sock'
redis__conf_unixsocketperm: '770'
```


Expand Down
2 changes: 2 additions & 0 deletions roles/redis/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ redis__conf_maxmemory_policy: 'noeviction'
redis__conf_port: 6379
redis__conf_protected_mode: 'yes'
redis__conf_replica_serve_stale_data: 'yes'
redis__conf_unixsocket: '{{ __redis__conf_unixsocket }}' # upstream default: /run/redis/redis.sock on RHEL, off on Debian / Ubuntu
redis__conf_unixsocketperm: '770' # upstream default: unset, the mode then follows the umask

redis__service_enabled: true
redis__service_limit_nofile: 10240
Expand Down
154 changes: 154 additions & 0 deletions roles/redis/meta/argument_specs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# argument_specs validates required variables and types automatically at role entry.
# use this for simple "is defined" / type checks. for complex validations
# (value ranges, cross-variable logic), use ansible.builtin.assert in the tasks.
argument_specs:
main:
options:

redis__conf_appendonly:
type: 'str'
required: false
default: 'no'
description: 'Enables the append only file (AOF) persistence mode.'

redis__conf_auto_aof_rewrite_min_size:
type: 'str'
required: false
default: '64mb'
description: 'Minimum size of the append only file before a rewrite is triggered.'

redis__conf_bind:
type: 'str'
required: false
default: '127.0.0.1 -::1'
description: 'Space-separated list of addresses Redis listens on.'

redis__conf_databases:
type: 'int'
required: false
default: 16
description: 'Number of databases.'

redis__conf_loglevel:
type: 'str'
required: false
default: 'notice'
description: 'Server verbosity level.'

redis__conf_maxmemory:
type: 'str'
required: false
default: '50M'
description: 'Memory limit after which the eviction policy takes effect.'

redis__conf_maxmemory_policy:
type: 'str'
required: false
default: 'noeviction'
description: 'How Redis evicts keys once the memory limit is reached.'

redis__conf_port:
type: 'int'
required: false
default: 6379
description: 'TCP port to listen on. Set to 0 to disable the TCP socket.'

redis__conf_protected_mode:
type: 'str'
required: false
default: 'yes'
description: 'Refuses queries from non-loopback addresses while no password is set.'

redis__conf_replica_serve_stale_data:
type: 'str'
required: false
default: 'yes'
description: 'Lets a replica answer queries while the link to the primary is down.'

redis__conf_requirepass:
type: 'str'
required: false
description: 'Password clients have to authenticate with.'

redis__conf_save:
type: 'list'
elements: 'str'
required: false
description: 'Snapshotting rules, each entry being a "<seconds> <changes>" pair.'

redis__conf_tls_auth_clients:
type: 'str'
required: false
description: 'Whether clients have to authenticate with a certificate.'

redis__conf_tls_ca_cert_file:
type: 'str'
required: false
description: 'Path to the CA certificate used to verify clients.'

redis__conf_tls_cert_file:
type: 'str'
required: false
description: 'Path to the TLS certificate.'

redis__conf_tls_key_file:
type: 'str'
required: false
description: 'Path to the TLS private key.'

redis__conf_tls_port:
type: 'int'
required: false
description: 'TLS port to listen on. Set redis__conf_port to 0 to only listen with TLS.'

redis__conf_unixsocketperm:
type: 'str'
required: false
default: '770'
description: 'Permissions of the Unix socket.'

redis__kernel_settings__sysctl__dependent_var:
type: 'list'
elements: 'dict'
required: false
description: 'Kernel settings injected into the kernel_settings role.'

redis__kernel_settings__transparent_hugepages__dependent_var:
type: 'str'
required: false
default: 'madvise'
description: 'Transparent hugepages setting injected into the kernel_settings role.'

redis__service_enabled:
type: 'bool'
required: false
default: true
description: 'Enables or disables the Redis service.'

redis__service_limit_nofile:
type: 'int'
required: false
default: 10240
description: 'systemd: resource limit for the number of file descriptors.'

redis__service_state:
type: 'str'
required: false
choices:
- 'reloaded'
- 'restarted'
- 'started'
- 'stopped'
description: 'Changes the state of the Redis service.'

redis__service_timeout_start_sec:
type: 'str'
required: false
default: '90s'
description: 'systemd: time to wait for start-up.'

redis__service_timeout_stop_sec:
type: 'str'
required: false
default: '90s'
description: 'systemd: time to wait for the service to stop.'
4 changes: 2 additions & 2 deletions roles/redis/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
backup: true
src: 'etc/redis/{{ redis__installed_version }}-redis.conf.j2'
dest: '{{ __redis__config_dir }}/{{ __redis__config_file }}'
owner: 'root'
group: 'redis'
owner: '{{ __redis__config_file_owner }}'
group: '{{ __redis__config_file_group }}'
mode: 0o640
notify: 'redis: restart redis'

Expand Down
6 changes: 5 additions & 1 deletion roles/redis/templates/etc/redis/7.0-redis.conf.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2024061301
# 2026092201

# Redis v7.0 configuration file.
#
Expand Down Expand Up @@ -157,6 +157,10 @@ tcp-backlog 511
#
# unixsocket /run/redis.sock
# unixsocketperm 700
{% if redis__conf_unixsocket | length > 0 %}
unixsocket {{ redis__conf_unixsocket }}
unixsocketperm {{ redis__conf_unixsocketperm }}
{% endif %}

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
Expand Down
6 changes: 5 additions & 1 deletion roles/redis/templates/etc/redis/7.2-redis.conf.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2024061301
# 2026092201

# Redis v7.2 configuration file.
#
Expand Down Expand Up @@ -157,6 +157,10 @@ tcp-backlog 511
#
# unixsocket /run/redis.sock
# unixsocketperm 700
{% if redis__conf_unixsocket | length > 0 %}
unixsocket {{ redis__conf_unixsocket }}
unixsocketperm {{ redis__conf_unixsocketperm }}
{% endif %}

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
Expand Down
6 changes: 5 additions & 1 deletion roles/redis/templates/etc/redis/7.4-redis.conf.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2024080701
# 2026092201

# Redis v7.4 configuration file.
#
Expand Down Expand Up @@ -158,6 +158,10 @@ tcp-backlog 511
#
# unixsocket /run/redis.sock
# unixsocketperm 700
{% if redis__conf_unixsocket | length > 0 %}
unixsocket {{ redis__conf_unixsocket }}
unixsocketperm {{ redis__conf_unixsocketperm }}
{% endif %}

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
Expand Down
6 changes: 5 additions & 1 deletion roles/redis/templates/etc/redis/8.0-redis.conf.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2025051601
# 2026092201

# Redis v8.0 configuration file.
#
Expand Down Expand Up @@ -165,6 +165,10 @@ tcp-backlog 511
#
# unixsocket /run/redis.sock
# unixsocketperm 700
{% if redis__conf_unixsocket | length > 0 %}
unixsocket {{ redis__conf_unixsocket }}
unixsocketperm {{ redis__conf_unixsocketperm }}
{% endif %}

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
Expand Down
6 changes: 5 additions & 1 deletion roles/redis/templates/etc/redis/8.2-redis.conf.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2025110701
# 2026092201

# Redis v8.2 configuration file.
#
Expand Down Expand Up @@ -160,6 +160,10 @@ tcp-backlog 511
#
# unixsocket /run/redis.sock
# unixsocketperm 700
{% if redis__conf_unixsocket | length > 0 %}
unixsocket {{ redis__conf_unixsocket }}
unixsocketperm {{ redis__conf_unixsocketperm }}
{% endif %}

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
Expand Down
Loading
Loading