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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

**Highlights:** Apache Solr runs on Debian, Ubuntu and RHEL 10 and supports Solr 9.11.

### Breaking Changes

* **role:apache_solr**: Solr listens on `127.0.0.1` only and gets 180 seconds to stop gracefully, as upstream ships it, instead of listening on all addresses and being killed after 15 seconds. Set `apache_solr__http_bind_address: '0.0.0.0'` for a Solr that other hosts have to reach.
* **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 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* **role:apache_solr**: Supports Debian 12 and 13, RHEL 10 and Ubuntu 22.04, 24.04 and 26.04, with the Java the Solr version needs installed from the distribution.
* **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 +67,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

* **role:apache_solr**: Downloads Solr from the Apache CDN, which is much faster than the Apache archive, and falls back to the archive for releases the CDN no longer carries.
* **role:apache_solr**: The run aborts for a user whose password equals the username, since Solr 9.11 and newer reject such logins.
* **role:apache_solr**: A role holding the `all` permission is allowed every request, also one that a more specific permission of another role matches first.
* **role:apache_solr**: The Solr program files belong to `root`, as with Solr's own installation script, and files that Solr creates are no longer readable by other users.
* 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:apache_httpd**: Bump the OWASP Core Rule Set to 4.30.0.
* **role:apache_httpd**: The OWASP Core Rule Set is downloaded on the Ansible controller, so the target no longer needs access to GitHub.
Expand Down Expand Up @@ -93,6 +101,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* **role:apache_solr**: Passwords no longer show up in the output of a run, and a user without `state` no longer aborts it.
* **role:apache_solr**: The role runs without EPEL on RHEL and no longer restarts Solr on every run.
* **role:apache_solr**: A role with several permissions takes effect, where Solr discarded it as invalid so far.
* **role:apache_solr**: Users can log in on hosts without `xxd`, such as a minimal RHEL 9, where the role wrote an empty password hash.
* **role:apache_httpd**: On Debian and Ubuntu, the first run against a fresh host no longer fails reloading Apache, since the role points `PidFile` to where `apachectl` looks; a server started by an earlier version of the role is restarted once.
* **role:apache_httpd**: Apache reloads after the OWASP Core Rule Set is updated, instead of applying the previous rules until its next reload.
* **role:apache_httpd**: The OWASP Core Rule Set deploys on Debian and Ubuntu, where the run failed until now, and on Ubuntu 22.04, whose ModSecurity 2.9.5 is too old for it, the run aborts with a message saying so.
Expand Down
2 changes: 1 addition & 1 deletion COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Which Ansible role is proven to run on which OS?
| alternatives | x | x | x | x | (x) | x | x | (x) | |
| ansible_init | | | | | | | | | Fedora 35+ |
| apache_httpd | x | x | x | x | x | x | x | x | |
| apache_solr | | | x | x | (x) | | | | |
| apache_solr | x | x | x | x | x | x | x | x | |
| apache_tomcat | | | x | x | (x) | | | | |
| apps | (x) | (x) | x | x | x | (x) | (x) | (x) | |
| at | x | x | x | x | x | x | x | x | Fedora 35 |
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ Before adding an `allow` rule, work out what it buys an attacker **on top of** t

#### Other

* `apache_solr`: OpenJDK package per Solr major version (9 → 17, 8 → 8) via `vars/main.yml`.
* `apache_solr`: OpenJDK package per platform and Solr major version via `vars/<platform>.yml` (RHEL 10 and Debian 13 ship no Java 17), asserted per platform; `security.json` hashes computed on the controller with a host- and user-derived salt, so the file only changes with a password.
* `mongodb`: `state: skip` in `mongodb__databases` / `mongodb__users` leaves an entry untouched.
* `moodle`: patch tag discovered via `api.github.com/repos/moodle/moodle/tags`, first match of `^v<configured-version>`.
* `nextcloud`: a state file skips install-only tasks after the initial installation; concise "Tags" README section.
Expand Down
2 changes: 2 additions & 0 deletions extensions/molecule/apache_solr/converge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- name: 'Converge apache_solr playbook'
ansible.builtin.import_playbook: 'linuxfabrik.lfops.apache_solr'
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
apache_solr__checksum: 'sha512:0cf320f15662b03844e2d3e983b2a50abab3e643d9aff53ed5a4481ef870776420c7bd8dfde377183d44304dcc589f6784ab020eb7c6675000f86fe32fff4057'
apache_solr__heap: '640m'
apache_solr__version: '9.11.0'

# A role that holds several permissions and a role holding 'all' cover how the role translates
# its role-to-permissions list into Solr's permission entries.
apache_solr__roles__host_var:
- name: 'reader'
permissions:
- 'health'
- 'read'
- 'schema-read'
- name: 'admin'
permissions:
- 'all'

apache_solr__users__host_var:
- username: 'solr-admin'
password: 'linuxfabrik'
role: 'admin'
- username: 'solr-reader'
password: 'linuxfabrik-reader'
role: 'reader'
state: 'present'
- username: 'solr-removed'
password: 'linuxfabrik-removed'
role: 'admin'
state: 'absent'
20 changes: 20 additions & 0 deletions extensions/molecule/apache_solr/inventory/hosts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# yamllint disable rule:empty-values

# apache_solr targets 'lfops_apache_solr' (see playbooks/apache_solr.yml: hosts).
#
# The hosts cover every Java package the role picks for Solr 9: OpenJDK 17 on RHEL 8 and 9,
# Debian 12 and Ubuntu, OpenJDK 21 on RHEL 10 and Debian 13, which ship no Java 17.
lfops_apache_solr:
children:
systems_under_test:

systems_under_test:
hosts:
debian12-vm:
debian13-vm:
rocky8-vm:
rocky9-vm:
rocky10-vm:
ubuntu2204-vm:
ubuntu2404-vm:
ubuntu2604-vm:
1 change: 1 addition & 0 deletions extensions/molecule/apache_solr/molecule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Molecule scenario marker
137 changes: 137 additions & 0 deletions extensions/molecule/apache_solr/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# verify.yml runs after converge and again after the idempotence step. It asserts the observable
# end state: Solr runs the configured version on the Java the role picked for the platform, applies
# the configured settings, and lets in exactly the users and permissions from the inventory. Every
# value is read back from the running Solr, not from the files the role deployed.
- name: 'Verify Apache Solr is running and uses the configured values'
hosts: 'systems_under_test'
gather_facts: true
become: true

vars:

# RHEL 10 and Debian 13 ship no Java 17, so the role falls back to Java 21 there.
__molecule__apache_solr_java_major: '{{
"21" if (ansible_facts["os_family"] ~ ansible_facts["distribution_major_version"]) in ["RedHat10", "Debian13"]
else "17"
}}'
__molecule__apache_solr_url: 'http://127.0.0.1:{{ apache_solr__http_bind_port | d(8983) }}/solr'

tasks:

- name: 'Get the service facts'
ansible.builtin.service_facts: # yamllint disable-line rule:empty-values

- name: 'Assert solr.service is enabled and running'
ansible.builtin.assert:
that:
- 'ansible_facts["services"]["solr.service"]["state"] == "running"'
- 'ansible_facts["services"]["solr.service"]["status"] == "enabled"'
fail_msg: 'solr.service is {{ ansible_facts["services"]["solr.service"] | d("absent") }}'

- name: 'Wait for Solr to answer on its port'
ansible.builtin.wait_for:
host: '127.0.0.1'
port: '{{ apache_solr__http_bind_port | d(8983) }}'
timeout: 60

# Solr listens on the loopback only, as upstream ships it, so it is closed on the host's address.
- name: 'Assert Solr is not reachable on {{ ansible_facts["default_ipv4"]["address"] }}'
ansible.builtin.wait_for:
host: '{{ ansible_facts["default_ipv4"]["address"] }}'
port: '{{ apache_solr__http_bind_port | d(8983) }}'
state: 'stopped'
timeout: 5

# blockUnknown: a request without credentials is turned away before any permission applies.
- name: 'curl {{ __molecule__apache_solr_url }}/admin/info/system without credentials'
ansible.builtin.uri:
url: '{{ __molecule__apache_solr_url }}/admin/info/system'
status_code: 401

# Also proves that the hash the role computed is the one Solr expects.
- name: 'curl --user solr-admin {{ __molecule__apache_solr_url }}/admin/info/system'
ansible.builtin.uri:
url: '{{ __molecule__apache_solr_url }}/admin/info/system'
url_username: 'solr-admin'
url_password: 'linuxfabrik'
force_basic_auth: true
return_content: true
register: '__molecule__apache_solr_system_result'
retries: 6 # Solr answers on its port shortly before its handlers are ready
delay: 5
until: '__molecule__apache_solr_system_result["status"] == 200'

- name: 'Assert Solr runs the configured version on the expected Java with the configured settings'
ansible.builtin.assert:
that:
- '__molecule__apache_solr_system["lucene"]["solr-spec-version"] == apache_solr__version'
- '__molecule__apache_solr_system["jvm"]["spec"]["version"] == __molecule__apache_solr_java_major'
- '"-Xmx" ~ apache_solr__heap in __molecule__apache_solr_system["jvm"]["jmx"]["commandLineArgs"]'
- '"-Djava.security.manager" in __molecule__apache_solr_system["jvm"]["jmx"]["commandLineArgs"]'
fail_msg: >-
Solr {{ __molecule__apache_solr_system["lucene"]["solr-spec-version"] }} on Java
{{ __molecule__apache_solr_system["jvm"]["spec"]["version"] }} with
{{ __molecule__apache_solr_system["jvm"]["jmx"]["commandLineArgs"] }}
vars:
__molecule__apache_solr_system: '{{ __molecule__apache_solr_system_result["json"] }}'

# The role translates its role-to-permissions list into one Solr permission entry per name.
# 'reader' holds 'health', so it gets in there and nowhere else; 'admin' holds 'all', so it
# also gets past the 'health' entry, which matches first.
- name: 'curl --user <user> {{ __molecule__apache_solr_url }}/<path>'
ansible.builtin.uri:
url: '{{ __molecule__apache_solr_url }}/{{ item["path"] }}'
url_username: '{{ item["username"] }}'
url_password: '{{ item["password"] }}'
force_basic_auth: true
status_code: '{{ item["status"] }}'
loop:
- username: 'solr-admin'
password: 'linuxfabrik'
path: 'admin/info/health'
status: 200
- username: 'solr-admin'
password: 'linuxfabrik'
path: 'admin/authorization'
status: 200
- username: 'solr-reader'
password: 'linuxfabrik-reader'
path: 'admin/info/health'
status: 200
- username: 'solr-reader'
password: 'linuxfabrik-reader'
path: 'admin/authorization'
status: 403
- username: 'solr-removed'
password: 'linuxfabrik-removed'
path: 'admin/info/health'
status: 401
loop_control:
label: '{{ item["username"] }} {{ item["path"] }} -> {{ item["status"] }}'

# As with Solr's install_solr_service.sh, the program files belong to root, so that Solr cannot
# modify them.
- name: 'stat /opt/solr/bin/solr'
ansible.builtin.stat:
path: '/opt/solr/bin/solr'
register: '__molecule__apache_solr_bin_result'

- name: 'Assert the Solr program files belong to root'
ansible.builtin.assert:
that:
- '__molecule__apache_solr_bin_result["stat"]["pw_name"] == "root"'
- '__molecule__apache_solr_bin_result["stat"]["mode"] == "0755"'
fail_msg: '/opt/solr/bin/solr is {{ __molecule__apache_solr_bin_result["stat"] | d("missing") }}'

# Solr writes its PID file after it started, so its mode shows the UMask of solr.service.
- name: 'stat /var/solr/solr-{{ apache_solr__http_bind_port | d(8983) }}.pid'
ansible.builtin.stat:
path: '/var/solr/solr-{{ apache_solr__http_bind_port | d(8983) }}.pid'
register: '__molecule__apache_solr_pid_result'

- name: 'Assert files Solr creates are not world-readable'
ansible.builtin.assert:
that:
- '__molecule__apache_solr_pid_result["stat"]["pw_name"] == "solr"'
- '__molecule__apache_solr_pid_result["stat"]["mode"] == "0640"'
fail_msg: 'The PID file is {{ __molecule__apache_solr_pid_result["stat"] | d("missing") }}'
Loading
Loading