Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
92c8500
feat(roles/valkey): listen on a Unix socket that the valkey group may…
markuslf Sep 22, 2026
4b009ac
feat(roles/redis): listen on a Unix socket that the redis group may use
markuslf Sep 22, 2026
2688f0d
feat(roles/redis): add meta/argument_specs.yml
markuslf Sep 22, 2026
5d12e17
fix(roles/valkey): keep the ownership of valkey.conf that the package…
markuslf Sep 22, 2026
0b1ac9e
fix(roles/redis): keep the ownership of redis.conf that the package sets
markuslf Sep 22, 2026
222b5f1
test(molecule): assert the valkey Unix socket and the config ownership
markuslf Sep 22, 2026
f915739
fix(roles/nextcloud): start the LDAP remnants report from its own timer
markuslf Sep 22, 2026
0f80183
fix(roles/nextcloud): apply ownership and SELinux label to nextcloud_…
markuslf Sep 22, 2026
390c264
fix(roles/nextcloud): add missing primary keys and run expensive repa…
markuslf Sep 22, 2026
704dd8e
fix(roles/nextcloud): pass the passwords to occ on stdin
markuslf Sep 22, 2026
dca8f36
fix(roles/nextcloud): make nextcloud-update readable by root only
markuslf Sep 22, 2026
e133657
fix(roles/nextcloud)!: verify the certificate of the SMTP server
markuslf Sep 22, 2026
36f00f4
docs(roles/nextcloud): document the subkeys of nextcloud__apps and ne…
markuslf Sep 22, 2026
c3235c3
refactor(roles/nextcloud): drop forbidden filename characters Nextclo…
markuslf Sep 22, 2026
b35b2c0
feat(roles/fail2ban): add a nextcloud filter and jail
markuslf Sep 22, 2026
36f9d81
feat(roles/nextcloud)!: log to /var/log/nextcloud
markuslf Sep 22, 2026
4a9e9c5
feat(playbooks/setup_nextcloud): optionally run fail2ban, never ban t…
markuslf Sep 22, 2026
ace4f4c
feat(roles/nextcloud)!: abort when PHP or MariaDB do not fit the inst…
markuslf Sep 22, 2026
8bbf3ce
fix(playbooks/setup_nextcloud): configure SELinux before the services
markuslf Sep 22, 2026
76110af
feat(roles/nextcloud)!: connect to Redis / Valkey through the Unix so…
markuslf Sep 22, 2026
fa1d651
fix(roles/nextcloud): restart PHP-FPM and switch to cron only on a ch…
markuslf Sep 22, 2026
77322f7
fix(roles/nextcloud): retry the installer after a failed attempt
markuslf Sep 22, 2026
9deef41
fix(roles/nextcloud): keep the Icinga API password out of the process…
markuslf Sep 22, 2026
8981116
feat(roles/nextcloud)!: make nextcloud__version mandatory
markuslf Sep 22, 2026
28604b2
feat(roles/nextcloud)!: connect to MariaDB as a dedicated user
markuslf Sep 22, 2026
ae978c9
docs(roles/nextcloud): describe how to connect Collabora Online
markuslf Sep 22, 2026
a563dfb
fix(roles/nextcloud): stop installing the APCu PHP extension
markuslf Sep 22, 2026
54d4bbf
feat(roles/nextcloud)!: remove the PHP modules Nextcloud does not use
markuslf Sep 22, 2026
9c0702d
test(molecule): assert nextcloud-update is readable by root only
markuslf Sep 22, 2026
bb8fab9
docs(roles/nextcloud): describe the requirements without naming versions
markuslf Sep 22, 2026
e55918b
fix(roles/nextcloud): look up the MariaDB socket for the platform
NavidSassan Sep 23, 2026
d4ecc13
docs(roles/nextcloud): list fail2ban as off by default and align the …
NavidSassan Sep 23, 2026
77eefb1
test(molecule): assert the Redis / Valkey socket and config ownership…
NavidSassan Sep 23, 2026
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking Changes

* **role:nextcloud**: The role removes the PHP extensions APCu, bcmath, IMAP and memcached, which Nextcloud does not use in the setup the role creates: all caches live in Redis or Valkey. An installation that needs one of them, such as the IMAP backend of the External user authentication app, lists it in `php__modules__host_var` with `state: 'present'`.
* **role:nextcloud**: `nextcloud__database_login` is mandatory, and `nextcloud__mariadb_login` is gone. A new installation connects to MariaDB as this user, which `setup_nextcloud` creates with access to the Nextcloud database from `localhost` only, instead of handing the database administrator to the installer, which created an `oc_` user that may connect from any host. Existing installations keep their database user.
* **role:nextcloud**: `nextcloud__version` is mandatory, for example `'latest-35'`. It only picks the release that a new installation downloads.
* **role:nextcloud**: Nextcloud reaches Redis or Valkey through its Unix socket instead of over TCP, and the web server user joins the `redis` or `valkey` group, which may use the socket. Run the complete `setup_nextcloud` playbook rather than the `nextcloud` role alone, so that Redis or Valkey open the socket first. Set `nextcloud__redis_unixsocket: ''` to stay with TCP.
* **role:nextcloud**: The role aborts before it changes anything when the installed PHP or MariaDB does not fit the installed Nextcloud major version, or when it does not know that major version (30 to 35 are known). For example, Nextcloud 35 needs PHP 8.3 to 8.5 and MariaDB 10.11 or newer. Upgrade PHP or MariaDB, as the error message says, before running the role again.
* **role:nextcloud**: Nextcloud logs to `/var/log/nextcloud/nextcloud.log` instead of `nextcloud.log` in the data directory, since fail2ban cannot read the data directory under SELinux. Adjust log shippers and monitoring that read the old file, and remove it once it is no longer needed.
* **role:nextcloud**: Nextcloud verifies the certificate of the SMTP server, where the role had switched the check off. If your mail server presents a self-signed certificate, make Nextcloud trust it, or set `mail_smtpstreamoptions ssl allow_self_signed` to `true` and `mail_smtpstreamoptions ssl verify_peer` and `mail_smtpstreamoptions ssl verify_peer_name` to `false` in `nextcloud__sysconfig__host_var` to keep the previous behaviour.
* **role:bind**: named validates DNSSEC by default, as the bind package does, and follows the system-wide crypto policy. Forged or broken answers for signed zones are answered with `SERVFAIL` instead of being passed on. The `forward`, `static-stub` and `stub` zones in `bind__zones` are excluded from validation; list other internal zones below a signed domain or TLD in `bind__dnssec_validate_except`, or set `bind__dnssec_validation: false`. On RHEL 8 the role fails if it has zones to exclude, since BIND 9.11 cannot; set `bind__dnssec_validation: false` there ([#355](https://github.com/Linuxfabrik/lfops/issues/355), [#356](https://github.com/Linuxfabrik/lfops/issues/356)).
* **role:system_update**: On Debian and Ubuntu, the system update no longer updates the database of an AIDE installed by hand, since it also accepted changes that were pending before the update. On such hosts the daily AIDE mail now lists the files each update changed. Deploy the aide role to have its database updated after updates again.
* **role:grav**: The `grav:cron` tag is gone. Run the role with `--tags grav` to deploy the timers and their services, or with `--tags grav:state` to enable or disable the timers.
Expand Down Expand Up @@ -38,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* **playbook:setup_nextcloud**: `setup_nextcloud__skip_fail2ban: false` runs fail2ban on a Nextcloud host that clients reach directly, where it bans an IP for 8 hours after 5 failed Nextcloud logins within 10 minutes and never bans the reverse proxies listed in the Nextcloud setting `trusted_proxies`.
* **role:fail2ban**: The `nextcloud` filter and the `z10-nextcloud` jail ban IPs with too many failed Nextcloud logins or two-factor challenges, following the Nextcloud hardening guide.
* **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:bind**: Add `meta/argument_specs.yml` declaring the user-facing variables, so role-entry validation catches type mismatches and invalid values before any task runs.
* **role:duplicity**: The backup includes the data of the applications LFOps deploys by default: `/data`, `/srv`, `/var/lib/grafana`, `/var/lib/icinga2` (including the Icinga2 CA), `/var/lib/shiny-server`, `/var/lib/turn`, `/var/mail`, `/var/named`, `/var/solr/data`, `/var/spool/mail` and `/var/www` (without the repository mirrors). Hosts without these directories are not affected. On hosts with large data, for example VM images in `/data`, check the backup size or set the path to `state: 'absent'`.
* **playbook:setup_basic**: Installs AIDE on every host, which checks file integrity twice a day and after every boot; skip it with `setup_basic__skip_aide`.
Expand All @@ -63,6 +73,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`).
* Downloads from the Internet, such as release tarballs, GPG keys, git clones, GitHub release lookups and package installations, are retried up to three times, so a brief outage of a download source or package repository no longer aborts the run (all roles).
* **role:monitoring_plugins**: On Linux, the role no longer stops the Icinga2 agent while it deploys the plugins, so a run no longer interrupts the monitoring of the host.
* **role:icinga2_agent**: The role runs `icinga2 node setup` and restarts the agent only when the agent needs a new certificate or its settings changed, instead of on every run.
Expand Down Expand Up @@ -91,6 +102,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* **role:nextcloud**: A run after a failed installation installs Nextcloud, where it skipped the installer because the failed attempt had left a `config.php` behind.
* **role:nextcloud**: A run against an unchanged host no longer restarts PHP-FPM and reports no change at all.
* **role:nextcloud**: `nextcloud-update` adds missing primary keys and runs the pending mimetype migrations after an update, which Nextcloud leaves to the administrator.
* **role:nextcloud**: A `nextcloud__datadir` other than `/data` gets the ownership and the SELinux label Nextcloud needs, which the role only ever set on `/data`.
* **role:nextcloud**: The monthly LDAP remnants report runs, where its timer started the app update instead.
* **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:aide**: The AIDE check no longer fails on hosts with fwupd, where it reported `/etc/fwupd/fwupd.conf` as changed after the first start of the fwupd daemon.
* **role:aide**: The AIDE check no longer fails on hosts with an EFI system partition, where it reported the files below `/boot/efi` as changed a while after the database was created.
* **role:system_update**: The security lane installs hot-fixes that need a newer package from BaseOS or AppStream, such as the kernel on Rocky 8.3, instead of failing on every run.
Expand Down Expand Up @@ -173,6 +190,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

* **role:nextcloud**: The password of the Icinga API user no longer shows up in the process list while `nextcloud-update` sets or removes the downtime.
* **role:nextcloud**: `/usr/local/bin/nextcloud-update`, which holds the credentials of the Icinga API user, is readable by root only.
* **role:nextcloud**: The database and admin passwords no longer show up in the process list during the installation.
* **role:monitoring_plugins**: The source install checks every pinned Python dependency against the checksums in the lockfile instead of installing whatever PyPI serves, and puts the sudoers drop-ins in place only once `visudo` accepts them.
* **role:github_project_createrepo**: The service can only write to the repositories it maintains instead of to everything below `github_project_createrepo__base_path`, where it could replace other files such as a repository signing key. The role removes the ACL entries it granted before.
* **role:kernel_modules**: Blocks further rarely used kernel modules by default that unprivileged users can get loaded and that are prone to local privilege escalations, among them `ah6`, `pppoe` and `sctp_diag` from [RHSB-2026-011](https://access.redhat.com/security/vulnerabilities/RHSB-2026-011). This stops Bluetooth, L2TP/IPsec, PPPoE, PPTP and IPsec AH; set `enabled: true` for the modules a host needs. The role README lists them all.
Expand Down
36 changes: 36 additions & 0 deletions extensions/molecule/fail2ban/install/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,42 @@
loop_control:
label: '{{ item["item"]["label"] }}'

# The nextcloud filter ships with the role defaults as well. Its jail is wired in by the
# setup_nextcloud playbook, which tests it against a running Nextcloud; here the filter is fed
# log lines in the format of Nextcloud 35, one of each message the filter matches.
- name: 'fail2ban-regex against a {{ item["label"] }}'
ansible.builtin.command:
argv:
- 'fail2ban-regex'
- '{{ item["line"] }}'
- '/etc/fail2ban/filter.d/nextcloud.conf'
register: '__molecule__fail2ban_nextcloud_regex_result'
changed_when: false
check_mode: false
loop:
- label: 'failed WebDAV login'
line: '{"reqId":"arJoPKlNsR1zwnbJUr2K7AAAANE","level":2,"time":"2026-09-22T13:36:28+02:00","remoteAddr":"198.51.100.1","user":"--","app":"core","method":"GET","url":"/remote.php/dav/files/admin/","scriptName":"/remote.php","message":"Login failed: ''admin'' (Remote IP: ''198.51.100.1'')","userAgent":"curl/8.9.1","version":"35.0.0.10","data":{"app":"core"}}'
matched: 1
- label: 'failed login from an IPv6 address'
line: '{"reqId":"arJoPXvkDJYTkOiNa2hqmQAAAJM","level":2,"time":"2026-09-22T13:36:29+02:00","remoteAddr":"2001:db8::1","user":"--","app":"core","method":"GET","url":"/ocs/v2.php/cloud/user?format=json","scriptName":"/ocs/v2.php","message":"Login failed: ''admin'' (Remote IP: ''2001:db8::1'')","userAgent":"curl/8.9.1","version":"35.0.0.10","data":{"app":"core"}}'
matched: 1
- label: 'failed two-factor challenge'
line: '{"reqId":"arJoPXvkDJYTkOiNa2hqmQAAAJN","level":2,"time":"2026-09-22T13:40:00+02:00","remoteAddr":"198.51.100.2","user":"alice","app":"no app in context","method":"POST","url":"/index.php/login/challenge/totp","scriptName":"/index.php","message":"Two-factor challenge failed: alice (Remote IP: 198.51.100.2)","userAgent":"Mozilla/5.0","version":"35.0.0.10","data":[]}'
matched: 1
- label: 'unrelated warning'
line: '{"reqId":"arJoPXvkDJYTkOiNa2hqmQAAAJO","level":2,"time":"2026-09-22T13:42:00+02:00","remoteAddr":"198.51.100.4","user":"admin","app":"PHP","method":"GET","url":"/index.php/core/preview?fileId=8","scriptName":"/index.php","message":"sem_get(): Failed for key 0x7ea: Permission denied","userAgent":"Mozilla/5.0","version":"35.0.0.10","data":{"app":"PHP"}}'
matched: 0
loop_control:
label: '{{ item["label"] }}'

- name: 'Assert the nextcloud filter matches only the failed logins'
ansible.builtin.assert:
that: 'item["stdout"] is search("Lines: 1 lines, 0 ignored, " ~ item["item"]["matched"] ~ " matched")'
fail_msg: '{{ item["item"]["label"] }}: {{ item["stdout"] }}'
loop: '{{ __molecule__fail2ban_nextcloud_regex_result["results"] }}'
loop_control:
label: '{{ item["item"]["label"] }}'

# Proves a value from the inventory reaches the running jail, rather than the server falling
# back to the 10m that jail.conf ships.
- name: 'fail2ban-client get sshd bantime'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,25 @@ mariadb_server__admin_user:
username: 'mariadb-admin'
password: 'linuxfabrik'

nextcloud__database_login:
username: 'nextcloud'
password: 'linuxfabrik'
nextcloud__fqdn: 'nextcloud.example.com'
nextcloud__skip_notify_push: true
# fail2ban is off by default, since it belongs on hosts that clients reach directly. Switched on
# here so verify.yml can prove the jail sees failed logins and never bans the proxy.
setup_nextcloud__skip_fail2ban: false
# The host itself stands in for the reverse proxy, so verify.yml can send a failed login "through
# the proxy" with X-Forwarded-For and check that fail2ban counts the client, not the proxy.
nextcloud__sysconfig__group_var:
- key: 'trusted_proxies 0'
value: '127.0.0.1'
state: 'present'
nextcloud__users:
- username: 'nextcloud-admin'
password: 'linuxfabrik'
group: 'admin'
nextcloud__version: 'latest-35'

repo_mariadb__version: '11.4'

Expand Down
Loading
Loading