Why a Linux baseline matters
A Linux baseline is not full hardening and does not replace CIS, OpenSCAP or a security profile. It is the repeatable base configuration every managed server should receive: baseline packages, services, time synchronization, SSH basics, login notices and a documented acceptance run.
This article builds on the previous Ansible structure. Inventory, group_vars, host_vars and Vault stay the same; the new piece is a concrete role that can be adapted for SLES, Fedora, Rocky, Alma, Debian or Ubuntu.
Target design of the role
Cross-distribution tasks stay generic.
Package lists, service names and exceptions live in variables.
Configuration files are validated before they become active.
Handlers restart or reload services only when something changed.
Check mode and diff mode show the planned changes before applying them.
Role layout
roles/linux_baseline/defaults/main.yamltasks/main.yamlhandlers/main.yamltemplates/issue.j2templates/sshd_config.d/forgeone-baseline.conf.j2
Defaults: start deliberately small
Defaults should be safe but not overloaded. Everything that differs by distribution or customer can later be overridden in group_vars or host_vars.
# roles/linux_baseline/defaults/main.yamllinux_baseline_packages:- curl- vim- chronylinux_baseline_services:- chronydlinux_baseline_manage_issue: truelinux_baseline_manage_sshd_dropin: truelinux_baseline_ssh_password_authentication: falselinux_baseline_ssh_permit_root_login: false
Map distributions explicitly
A common mistake is hardcoding package and service names directly in tasks. Variables per platform family keep the role readable and reusable across environments.
# inventories/lab/group_vars/suse.yamllinux_baseline_services:- chronyd# inventories/lab/group_vars/debian.yamllinux_baseline_services:- chrony
Tasks: packages, services and templates
# roles/linux_baseline/tasks/main.yaml- name: Install baseline packagesansible.builtin.package:name: "{{ linux_baseline_packages }}"state: present- name: Ensure baseline services are enabled and startedansible.builtin.service:name: "{{ item }}"enabled: truestate: startedloop: "{{ linux_baseline_services }}"- name: Render login banneransible.builtin.template:src: issue.j2dest: /etc/issueowner: rootgroup: rootmode: '0644'when: linux_baseline_manage_issue- name: Install sshd baseline drop-inansible.builtin.template:src: sshd_config.d/forgeone-baseline.conf.j2dest: /etc/ssh/sshd_config.d/99-forgeone-baseline.confowner: rootgroup: rootmode: '0644'validate: /usr/sbin/sshd -t -f %snotify: Reload sshdwhen: linux_baseline_manage_sshd_dropin
Template: prefer an SSH drop-in
Where possible, the role should avoid replacing the full sshd_config. A drop-in reduces the risk of overwriting local policy. Validation with sshd -t remains important before a handler reloads the service.
# roles/linux_baseline/templates/sshd_config.d/forgeone-baseline.conf.j2PasswordAuthentication {{ 'yes' if linux_baseline_ssh_password_authentication else 'no' }}PermitRootLogin {{ 'yes' if linux_baseline_ssh_permit_root_login else 'no' }}
Handlers: react only when something changed
# roles/linux_baseline/handlers/main.yaml- name: Reload sshdansible.builtin.service:name: sshdstate: reloaded
Baseline playbook
# playbooks/linux-baseline.yaml- name: Apply Linux baselinehosts: linuxbecome: truegather_facts: trueroles:- role: linux_baseline
Acceptance run
Before applying the baseline, run it dry first. Then run it for real and follow with another check run to reveal unexpected drift.
ansible-inventory -i inventories/lab/hosts.yaml --graphansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yaml --syntax-checkansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yaml --check --diffansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yamlansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yaml --check --diff
Acceptance table
Limits of the baseline
This role is an operational baseline. It does not replace full security hardening, vulnerability handling or compliance acceptance. CIS, OpenSCAP and customer-specific hardening should become separate roles, profiles and acceptance records.
What comes next
The next useful step is controlled patch and reboot management with Ansible: prepare updates, limit target groups, detect reboot requirements, respect maintenance windows and verify services after reboot.



