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.yaml
tasks/main.yaml
handlers/main.yaml
templates/issue.j2
templates/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.

yaml
# roles/linux_baseline/defaults/main.yaml
linux_baseline_packages:
- curl
- vim
- chrony
linux_baseline_services:
- chronyd
linux_baseline_manage_issue: true
linux_baseline_manage_sshd_dropin: true
linux_baseline_ssh_password_authentication: false
linux_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.

yaml
# inventories/lab/group_vars/suse.yaml
linux_baseline_services:
- chronyd
# inventories/lab/group_vars/debian.yaml
linux_baseline_services:
- chrony

Tasks: packages, services and templates

yaml
# roles/linux_baseline/tasks/main.yaml
- name: Install baseline packages
ansible.builtin.package:
name: "{{ linux_baseline_packages }}"
state: present
- name: Ensure baseline services are enabled and started
ansible.builtin.service:
name: "{{ item }}"
enabled: true
state: started
loop: "{{ linux_baseline_services }}"
- name: Render login banner
ansible.builtin.template:
src: issue.j2
dest: /etc/issue
owner: root
group: root
mode: '0644'
when: linux_baseline_manage_issue
- name: Install sshd baseline drop-in
ansible.builtin.template:
src: sshd_config.d/forgeone-baseline.conf.j2
dest: /etc/ssh/sshd_config.d/99-forgeone-baseline.conf
owner: root
group: root
mode: '0644'
validate: /usr/sbin/sshd -t -f %s
notify: Reload sshd
when: 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.

jinja2
# roles/linux_baseline/templates/sshd_config.d/forgeone-baseline.conf.j2
PasswordAuthentication {{ '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

yaml
# roles/linux_baseline/handlers/main.yaml
- name: Reload sshd
ansible.builtin.service:
name: sshd
state: reloaded

Baseline playbook

yaml
# playbooks/linux-baseline.yaml
- name: Apply Linux baseline
hosts: linux
become: true
gather_facts: true
roles:
- 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.

bash
ansible-inventory -i inventories/lab/hosts.yaml --graph
ansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yaml --syntax-check
ansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yaml --check --diff
ansible-playbook -i inventories/lab/hosts.yaml playbooks/linux-baseline.yaml
ansible-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.