feat(ansible): add linux-baseline role and day0_linux_baseline playbook

Introduces a single, idempotent baseline role to supersede the
overlapping day0-baseline and common roles. Capabilities are
feature-flagged so they can be toggled per-host:

  - packages (common + OS-family + per-host extras)
  - timezone + locale
  - chrony time sync against sundial
  - baseline users (jarvis admin account with SSH key + NOPASSWD sudo)
  - SSH hardening via /etc/ssh/sshd_config.d/ drop-in
  - unattended security upgrades (Debian family)
  - sysctl drop-in at /etc/sysctl.d/99-mk-labs.conf
  - journald retention caps
  - branded MOTD

Ubuntu/Debian is first-class; vars/RedHat.yml provides a placeholder
for future distros via the ansible_os_family pattern.

The legacy day0-baseline and common roles remain in place for now and
will be removed during the playbook cleanup sweep, alongside the
existing playbook naming inconsistencies.
This commit is contained in:
Hermes Agent service account
2026-05-29 20:40:04 -05:00
parent 1dfa7889ab
commit 91b5817e5f
25 changed files with 741 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
---
# ============================================================================
# journald retention
# ============================================================================
- name: Ensure journald drop-in directory exists
ansible.builtin.file:
path: /etc/systemd/journald.conf.d
state: directory
owner: root
group: root
mode: "0755"
- name: Deploy journald retention drop-in
ansible.builtin.template:
src: journald-retention.conf.j2
dest: /etc/systemd/journald.conf.d/10-mk-labs-retention.conf
owner: root
group: root
mode: "0644"
notify: Restart systemd-journald

View File

@@ -0,0 +1,63 @@
---
# ============================================================================
# linux-baseline / main entrypoint
# ============================================================================
# Orchestrates baseline application. Each capability sits behind a
# feature flag in baseline_features so individual concerns can be
# disabled per-host without forking the role.
# ============================================================================
- name: Preflight checks
ansible.builtin.import_tasks: preflight.yml
tags: [baseline, preflight, always]
- name: Load OS-family-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
tags: [baseline, always]
- name: Apply package baseline
ansible.builtin.import_tasks: packages.yml
when: baseline_features.packages | bool
tags: [baseline, packages]
- name: Configure timezone and locale
ansible.builtin.import_tasks: timezone.yml
when: baseline_features.timezone | bool
tags: [baseline, timezone]
- name: Configure time synchronisation
ansible.builtin.import_tasks: time_sync.yml
when: baseline_features.time_sync | bool
tags: [baseline, time-sync]
- name: Provision baseline users
ansible.builtin.import_tasks: users.yml
when: baseline_features.users | bool
tags: [baseline, users]
- name: Harden SSH
ansible.builtin.import_tasks: ssh_hardening.yml
when: baseline_features.ssh_hardening | bool
tags: [baseline, ssh, hardening]
- name: Configure unattended security upgrades
ansible.builtin.import_tasks: unattended_upgrades.yml
when:
- baseline_features.unattended_upgrades | bool
- ansible_os_family == "Debian"
tags: [baseline, unattended-upgrades]
- name: Apply sysctl baseline
ansible.builtin.import_tasks: sysctl.yml
when: baseline_features.sysctl | bool
tags: [baseline, sysctl]
- name: Configure journald retention
ansible.builtin.import_tasks: journald.yml
when: baseline_features.journald | bool
tags: [baseline, journald]
- name: Apply MOTD branding
ansible.builtin.import_tasks: motd.yml
when: baseline_features.motd | bool
tags: [baseline, motd]

View File

@@ -0,0 +1,12 @@
---
# ============================================================================
# MOTD branding
# ============================================================================
- name: Deploy mk-labs MOTD
ansible.builtin.template:
src: motd.j2
dest: /etc/motd
owner: root
group: root
mode: "0644"

View File

@@ -0,0 +1,32 @@
---
# ============================================================================
# Package baseline
# ============================================================================
- name: Update apt cache (Debian family)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install baseline packages (common)
ansible.builtin.package:
name: "{{ baseline_packages_common }}"
state: present
- name: Install baseline packages (OS family)
ansible.builtin.package:
name: "{{ baseline_packages_family }}"
state: present
- name: Install baseline packages (host extras)
ansible.builtin.package:
name: "{{ baseline_packages_extra }}"
state: present
when: baseline_packages_extra | length > 0
- name: Perform full system upgrade (opt-in)
ansible.builtin.package:
name: "*"
state: latest # noqa package-latest - intentional under feature flag
when: baseline_features.full_upgrade | bool

View File

@@ -0,0 +1,18 @@
---
# ============================================================================
# Preflight: fail fast on unsupported platforms.
# ============================================================================
- name: Assert a supported OS family
ansible.builtin.assert:
that:
- ansible_os_family in ["Debian", "RedHat"]
fail_msg: >-
linux-baseline currently supports the Debian and RedHat OS families.
Host {{ inventory_hostname }} is running {{ ansible_distribution }}
{{ ansible_distribution_version }} ({{ ansible_os_family }}).
success_msg: "Detected {{ ansible_distribution }} {{ ansible_distribution_version }}."
- name: Gather package facts
ansible.builtin.package_facts:
manager: auto

View File

@@ -0,0 +1,43 @@
---
# ============================================================================
# SSH hardening
# ============================================================================
# Delivered as a drop-in under /etc/ssh/sshd_config.d/. The main
# sshd_config remains untouched so distro upgrades don't conflict and
# rollback is trivial (delete the drop-in).
#
# Some older sshd builds (Ubuntu < 20.04, Debian < 11) do not honour
# sshd_config.d/. We fall back to lineinfile on those.
# ============================================================================
- name: Check whether sshd supports the Include directive
ansible.builtin.command:
cmd: sshd -T
register: sshd_test
changed_when: false
check_mode: false
- name: Determine if /etc/ssh/sshd_config.d/ is honoured
ansible.builtin.stat:
path: /etc/ssh/sshd_config.d
register: sshd_dropin_dir
- name: Ensure /etc/ssh/sshd_config.d exists when honoured
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
when: sshd_dropin_dir.stat.exists
- name: Deploy SSH hardening drop-in
ansible.builtin.template:
src: sshd_hardening.conf.j2
dest: /etc/ssh/sshd_config.d/10-mk-labs-hardening.conf
owner: root
group: root
mode: "0644"
validate: "sshd -t -f %s"
when: sshd_dropin_dir.stat.exists
notify: Restart sshd

View File

@@ -0,0 +1,16 @@
---
# ============================================================================
# sysctl baseline
# ============================================================================
# Delivered as a drop-in under /etc/sysctl.d/ so each setting is
# self-documenting and removable.
# ============================================================================
- name: Deploy mk-labs sysctl drop-in
ansible.builtin.template:
src: 99-mk-labs.conf.j2
dest: /etc/sysctl.d/99-mk-labs.conf
owner: root
group: root
mode: "0644"
notify: Reload sysctl

View File

@@ -0,0 +1,25 @@
---
# ============================================================================
# Time synchronisation via chrony
# ============================================================================
- name: Install chrony
ansible.builtin.package:
name: chrony
state: present
- name: Deploy chrony configuration
ansible.builtin.template:
src: chrony.conf.j2
dest: "{{ baseline_chrony_config_path }}"
owner: root
group: root
mode: "0644"
backup: true
notify: Restart chrony
- name: Ensure chrony is enabled and running
ansible.builtin.systemd:
name: "{{ baseline_chrony_service }}"
state: started
enabled: true

View File

@@ -0,0 +1,24 @@
---
# ============================================================================
# Timezone and locale
# ============================================================================
- name: Set system timezone
community.general.timezone:
name: "{{ baseline_timezone }}"
- name: Ensure locale package present (Debian)
ansible.builtin.apt:
name: locales
state: present
when: ansible_os_family == "Debian"
- name: Generate locale
community.general.locale_gen:
name: "{{ baseline_locale }}"
state: present
- name: Set system locale
ansible.builtin.command:
cmd: "localectl set-locale LANG={{ baseline_locale }}"
changed_when: false

View File

@@ -0,0 +1,37 @@
---
# ============================================================================
# Unattended security upgrades (Debian family)
# ============================================================================
- name: Ensure unattended-upgrades is installed
ansible.builtin.apt:
name:
- unattended-upgrades
- apt-listchanges
state: present
- name: Deploy 50unattended-upgrades configuration
ansible.builtin.template:
src: 50unattended-upgrades.j2
dest: "{{ baseline_unattended_config_dir }}/50unattended-upgrades"
owner: root
group: root
mode: "0644"
backup: true
- name: Enable periodic upgrade timers
ansible.builtin.copy:
dest: "{{ baseline_unattended_config_dir }}/20auto-upgrades"
owner: root
group: root
mode: "0644"
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
- name: Ensure unattended-upgrades timer is running
ansible.builtin.systemd:
name: unattended-upgrades
state: started
enabled: true

View File

@@ -0,0 +1,51 @@
---
# ============================================================================
# Baseline users
# ============================================================================
# Creates each user defined in baseline_users with their primary group,
# SSH key(s), and optional passwordless sudo.
# ============================================================================
- name: Create primary group for baseline users
ansible.builtin.group:
name: "{{ item.name }}"
state: present
loop: "{{ baseline_users }}"
loop_control:
label: "{{ item.name }}"
- name: Create baseline users
ansible.builtin.user:
name: "{{ item.name }}"
comment: "{{ item.comment | default('') }}"
group: "{{ item.name }}"
groups: "{{ item.groups | default(omit) }}"
append: true
shell: "{{ item.shell | default('/bin/bash') }}"
create_home: true
state: present
loop: "{{ baseline_users }}"
loop_control:
label: "{{ item.name }}"
- name: Deploy SSH authorized keys for baseline users
ansible.posix.authorized_key:
user: "{{ item.0.name }}"
key: "{{ item.1 }}"
state: present
loop: "{{ baseline_users | subelements('ssh_authorized_keys', skip_missing=True) }}"
loop_control:
label: "{{ item.0.name }}"
- name: Grant passwordless sudo to baseline users
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ item.name }}"
content: "{{ item.name }} ALL=(ALL) NOPASSWD:ALL\n"
owner: root
group: root
mode: "0440"
validate: "visudo -cf %s"
loop: "{{ baseline_users }}"
loop_control:
label: "{{ item.name }}"
when: item.sudo_nopasswd | default(false) | bool