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

### Added

* **role:system_update**: Add a security lane for Rocky Linux. A second timer (twice a day by default) installs only Rocky Linux security hot-fixes from the dedicated `security` repository (provided by `repo_baseos`) and reboots the host if needed. The reboot time is steered per host group (for example immediately on test hosts, deferred to the evening on production hosts). Enabled by default; a no-op where the `security` repository is not enabled, and can be turned off with `system_update__security_enabled: false`. This keeps critical security fixes flowing daily while the regular update lane stays on its weekly schedule.
* **role:mariadb_server**: Add `mariadb_server__cnf_innodb_snapshot_isolation` variable (MariaDB 10.6+), defaulting to `'ON'`.

### Security
Expand Down
36 changes: 34 additions & 2 deletions roles/system_update/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This role configures the server to do (weekly) system updates by deploying two s
* applies all updates
* and, if necessary, automatically reboots the host after the updates.

On Rocky Linux hosts the role additionally runs a separate security lane: a second timer (`security-update`, twice a day by default) that installs only Rocky Linux security hot-fixes from the dedicated `security` repository (provided by the `repo_baseos` role) and reboots the host if needed. It is enabled by default and is a no-op on hosts where the `security` repository is not present; turn it off with `system_update__security_enabled: false`. The reboot time is steered per host group via `system_update__security_reboot_time__*` (for example immediately on test hosts, deferred to the evening on production hosts).


*Available since LFOps `2.0.0`.*

Expand All @@ -24,18 +26,19 @@ Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/RE
Manual steps:

* On Debian, install needrestart by running the [apps](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/apps.yml) playbook (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)).
* The security lane installs from the Rocky `security` repository, which is provided by the [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) role (enabled by default). On hosts where that repository is not present, the security lane is a no-op.


## Tags

`system_update`

* Sets up automatic system update via systemd timer.
* Sets up automatic system update via systemd timer, and on Rocky Linux hosts the optional security-update timer.
* Triggers: none.

`system_update:state`

* Determines whether notify-and-schedule.timer is enabled.
* Determines whether notify-and-schedule.timer and security-update.timer are enabled.
* Triggers: none.


Expand Down Expand Up @@ -125,6 +128,30 @@ Manual steps:
* Type: String.
* Default: unset

`system_update__security_enabled`

* Enables or disables the security lane (the `security-update` timer), analogous to `systemctl enable/disable --now`. Rocky Linux only. When enabled but the `security` repository is not enabled on the host, the lane is a no-op.
* Type: Bool.
* Default: `true`

`system_update__security_on_calendar`

* When the security lane checks for and installs security hot-fixes. Have a look at [systemd.time(7)](https://www.freedesktop.org/software/systemd/man/systemd.time.html) for the format.
* Type: String.
* Default: `'*-*-* 10,16:00'`

`system_update__security_reboot_time__host_var` / `system_update__security_reboot_time__group_var`

* When to reboot after a security hot-fix that requires it. Passed verbatim to `at`. Use this to steer test versus production hosts via inventory group membership: `'now'` reboots immediately, a time such as `'19:00'` defers the reboot.
* Type: String.
* Default: `'now'`

`system_update__security_repos`

* The repositories the security lane installs from. All other repositories are disabled for the security transaction, keeping it separate from the regular update lane.
* Type: List.
* Default: `['security']`

`system_update__update_enabled`

* Enables or disables the system-update timer, analogous to `systemctl enable/disable --now`.
Expand Down Expand Up @@ -173,6 +200,11 @@ system_update__pre_update_code: |-
check_dns 192.0.2.11
system_update__rocketchat_msg_suffix: '@administrator'
system_update__rocketchat_url: 'https://chat.example.com/hooks/abcd1234'
system_update__security_enabled: true
system_update__security_on_calendar: '*-*-* 10,16:00'
system_update__security_reboot_time__group_var: '19:00'
system_update__security_repos:
- 'security'
system_update__update_enabled: true
system_update__update_time: '04:{{ 59 | random(seed=inventory_hostname) }} + 1 days'
```
Expand Down
14 changes: 14 additions & 0 deletions roles/system_update/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,19 @@ system_update__mail_subject_hostname: '$(hostname --short)'
system_update__mail_subject_prefix: ''
system_update__notify_and_schedule_on_calendar: 'mon 10:00'
system_update__rocketchat_msg_suffix: ''
system_update__security_enabled: true
system_update__security_on_calendar: '*-*-* 10,16:00'
system_update__security_reboot_time__dependent_var: ''
system_update__security_reboot_time__group_var: ''
system_update__security_reboot_time__host_var: ''
system_update__security_reboot_time__role_var: 'now'
system_update__security_reboot_time__combined_var: '{{
system_update__security_reboot_time__host_var if (system_update__security_reboot_time__host_var | string | length) else
system_update__security_reboot_time__group_var if (system_update__security_reboot_time__group_var | string | length) else
system_update__security_reboot_time__dependent_var if (system_update__security_reboot_time__dependent_var | string | length) else
system_update__security_reboot_time__role_var
}}'
system_update__security_repos:
- 'security'
system_update__update_enabled: true
system_update__update_time: '04:{{ 59 | random(seed=inventory_hostname) }} + 1 days'
65 changes: 65 additions & 0 deletions roles/system_update/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,68 @@
tags:
- 'system_update'
- 'system_update:state'


# The security lane applies Rocky Linux security hot-fixes from the dedicated
# `security` repository (provided by the repo_baseos role) twice a day,
# independent of the regular (weekly) update lane. Rocky Linux only.
- block:

- name: 'Deploy /usr/local/bin/security-update'
ansible.builtin.template:
backup: true
src: 'usr/local/bin/security-update.j2'
dest: '/usr/local/bin/security-update'
owner: 'root'
group: 'root'
mode: 0o744

- name: 'Deploy /usr/local/bin/security-reboot'
ansible.builtin.template:
backup: true
src: 'usr/local/bin/security-reboot.j2'
dest: '/usr/local/bin/security-reboot'
owner: 'root'
group: 'root'
mode: 0o744

- name: 'Deploy /etc/systemd/system/security-update.service'
ansible.builtin.template:
backup: true
src: 'etc/systemd/system/security-update.service.j2'
dest: '/etc/systemd/system/security-update.service'
owner: 'root'
group: 'root'
mode: 0o644

- name: 'Deploy /etc/systemd/system/security-update.timer'
ansible.builtin.template:
backup: true
src: 'etc/systemd/system/security-update.timer.j2'
dest: '/etc/systemd/system/security-update.timer'
owner: 'root'
group: 'root'
mode: 0o644

when:
- 'ansible_facts["distribution"] == "Rocky"'

tags:
- 'system_update'


- block:

- name: 'systemctl {{ system_update__security_enabled | bool | ternary("enable", "disable") }} security-update.timer --now'
ansible.builtin.systemd:
name: 'security-update.timer'
state: '{{ system_update__security_enabled | bool | ternary("started", "stopped") }}'
enabled: '{{ system_update__security_enabled }}'
daemon_reload: true

when:
- 'ansible_facts["distribution"] == "Rocky"'

tags:
- 'system_update'
- 'system_update:state'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# {{ ansible_managed }}
# 2026052401

[Unit]
Description=security-update Service

[Service]
ExecStart=/usr/local/bin/security-update
Type=oneshot
User=root
KillMode=process

[Install]
WantedBy=basic.target
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# {{ ansible_managed }}
# 2026052401

[Unit]
Description=security-update Timer

[Timer]
OnCalendar={{ system_update__security_on_calendar }}
Unit=security-update.service

[Install]
WantedBy=timers.target
29 changes: 29 additions & 0 deletions roles/system_update/templates/usr/local/bin/security-reboot.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# {{ ansible_managed }}
# 2026052401

# Set an Icinga downtime and reboot. Invoked via `at` by /usr/local/bin/security-update
# so that the downtime is set at reboot time, not when the update was applied.

export LC_ALL=C

REBOOT=$(which reboot)

{% if system_update__icinga2_api_user_login is defined and system_update__icinga2_api_user_login | length %}
# needed for Icinga to set downtime (max. 5 minutes downtime)
START_TIME=$(date +%s)
END_TIME=$(( START_TIME + 300 ))
curl --connect-timeout 5 --insecure --silent --user '{{ system_update__icinga2_api_user_login["username"] }}:{{ system_update__icinga2_api_user_login["password"] }}' --header 'Accept: application/json' --request POST '{{ system_update__icinga2_api_url }}/v1/actions/schedule-downtime' --data-binary @- 1> /dev/null << EOF
{
"type": "Host",
"filter": "match(\"{{ system_update__icinga2_hostname }}\", host.name)",
"start_time": "$START_TIME",
"end_time": "$END_TIME",
"author": "{{ system_update__icinga2_hostname }}",
"comment": "Automatic reboot due to security updates.",
"all_services": true
}
EOF
{% endif %}

"$REBOOT"
79 changes: 79 additions & 0 deletions roles/system_update/templates/usr/local/bin/security-update.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# {{ ansible_managed }}
# 2026052401

# Apply Rocky Linux security hot-fixes from the dedicated `security` repository
# only, then reboot if needed. Runs on a timer (default twice daily). The reboot
# is scheduled via `at`; the time comes from the host's group/inventory
# (e.g. 'now' on test hosts, '19:00' on production hosts).

export LC_ALL=C

# dedicated at queue, so the 10:00 and 16:00 runs don't stack multiple reboots
AT_QUEUE='s'
REPOS="{{ system_update__security_repos | join(',') }}"

send_msg () {
{# note: do not quote the $RECIPIENTS, else they are passed as a single argument #}
echo -n "$MSGBODY" | mail -s "$SUBJECT" -r "$SENDER" $RECIPIENTS
{% if system_update__rocketchat_url is defined and system_update__rocketchat_url | length %}
/usr/bin/curl --silent --output /dev/null --data-urlencode \
"text=${SUBJECT}

${MSGBODY}
{{ system_update__rocketchat_msg_suffix }}" \
--data-urlencode "parse_mode=HTML" --data-urlencode "disable_web_page_preview=true" \
"{{ system_update__rocketchat_url }}"
{% endif %}
}

SUBJECT_PREFIX="{{ system_update__mail_subject_prefix }}{{ system_update__mail_subject_hostname }}"
SENDER="$SUBJECT_PREFIX <{{ system_update__mail_from }}>"
RECIPIENTS="{{ system_update__mail_recipients_updates | join(' ') }}"

# the security lane only acts when at least one of the configured security
# repositories is enabled on this host (provided by the repo_baseos role). skip
# silently otherwise, so the timer can default to enabled without acting on hosts
# where the repo is absent or was opted out via repo_baseos__security_repo_enabled.
have_repo=0
for repo in {{ system_update__security_repos | join(' ') }}; do
if dnf repolist --enabled 2> /dev/null | awk 'NR > 1 { print $1 }' | grep -qxF "$repo"; then
have_repo=1
fi
done
if [ "$have_repo" -eq 0 ]; then
exit 0
fi

# install security hot-fixes from the security repo only. --disablerepo / --enablerepo
# keeps this lane separate from the regular (weekly) update lane and from the frozen
# mirror snapshot.
yum --disablerepo="*" --enablerepo="$REPOS" -y upgrade 1> /tmp/security-update 2> /tmp/security-update-stderr
if [ $? -ne 0 ]; then
SUBJECT="$SUBJECT_PREFIX - Security update failed"
MSGBODY=$(</tmp/security-update-stderr)
send_msg
exit 1
fi

# reboot if a core component (kernel, glibc, ...) needs it, or if running services
# still use the pre-update libraries.
needs-restarting --reboothint &> /dev/null
reboothint_rc=$?
needs-restarting 2> /dev/null > /tmp/security-needs-restarting

if [ "$reboothint_rc" -eq 1 ] || [ -s /tmp/security-needs-restarting ]; then
# drop a reboot scheduled by an earlier run today before scheduling a new one
for job in $(atq -q "$AT_QUEUE" | awk '{print $1}'); do
atrm "$job"
done
echo "/usr/local/bin/security-reboot" | at -q "$AT_QUEUE" {{ system_update__security_reboot_time__combined_var }} 2> /dev/null
SUBJECT="$SUBJECT_PREFIX - Security update installed, reboot scheduled at {{ system_update__security_reboot_time__combined_var }}"
MSGBODY=$(yum history info)
send_msg
exit 0
fi

SUBJECT="$SUBJECT_PREFIX - Security update installed without reboot"
MSGBODY=$(cat /tmp/security-update)
send_msg