From 91b5817e5f04381b21a398975ed33ab310b8de6e Mon Sep 17 00:00:00 2001 From: Hermes Agent service account Date: Fri, 29 May 2026 20:40:04 -0500 Subject: [PATCH] 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. --- ansible/group_vars/all/vars | 9 ++ ansible/playbooks/day0_linux_baseline.yml | 23 ++++ ansible/roles/linux-baseline/README.md | 64 ++++++++++ .../roles/linux-baseline/defaults/main.yml | 120 ++++++++++++++++++ .../roles/linux-baseline/handlers/main.yml | 20 +++ ansible/roles/linux-baseline/meta/main.yml | 25 ++++ .../roles/linux-baseline/tasks/journald.yml | 21 +++ ansible/roles/linux-baseline/tasks/main.yml | 63 +++++++++ ansible/roles/linux-baseline/tasks/motd.yml | 12 ++ .../roles/linux-baseline/tasks/packages.yml | 32 +++++ .../roles/linux-baseline/tasks/preflight.yml | 18 +++ .../linux-baseline/tasks/ssh_hardening.yml | 43 +++++++ ansible/roles/linux-baseline/tasks/sysctl.yml | 16 +++ .../roles/linux-baseline/tasks/time_sync.yml | 25 ++++ .../roles/linux-baseline/tasks/timezone.yml | 24 ++++ .../tasks/unattended_upgrades.yml | 37 ++++++ ansible/roles/linux-baseline/tasks/users.yml | 51 ++++++++ .../templates/50unattended-upgrades.j2 | 26 ++++ .../templates/99-mk-labs.conf.j2 | 5 + .../linux-baseline/templates/chrony.conf.j2 | 28 ++++ .../templates/journald-retention.conf.j2 | 8 ++ .../roles/linux-baseline/templates/motd.j2 | 11 ++ .../templates/sshd_hardening.conf.j2 | 19 +++ ansible/roles/linux-baseline/vars/Debian.yml | 22 ++++ ansible/roles/linux-baseline/vars/RedHat.yml | 19 +++ 25 files changed, 741 insertions(+) create mode 100644 ansible/playbooks/day0_linux_baseline.yml create mode 100644 ansible/roles/linux-baseline/README.md create mode 100644 ansible/roles/linux-baseline/defaults/main.yml create mode 100644 ansible/roles/linux-baseline/handlers/main.yml create mode 100644 ansible/roles/linux-baseline/meta/main.yml create mode 100644 ansible/roles/linux-baseline/tasks/journald.yml create mode 100644 ansible/roles/linux-baseline/tasks/main.yml create mode 100644 ansible/roles/linux-baseline/tasks/motd.yml create mode 100644 ansible/roles/linux-baseline/tasks/packages.yml create mode 100644 ansible/roles/linux-baseline/tasks/preflight.yml create mode 100644 ansible/roles/linux-baseline/tasks/ssh_hardening.yml create mode 100644 ansible/roles/linux-baseline/tasks/sysctl.yml create mode 100644 ansible/roles/linux-baseline/tasks/time_sync.yml create mode 100644 ansible/roles/linux-baseline/tasks/timezone.yml create mode 100644 ansible/roles/linux-baseline/tasks/unattended_upgrades.yml create mode 100644 ansible/roles/linux-baseline/tasks/users.yml create mode 100644 ansible/roles/linux-baseline/templates/50unattended-upgrades.j2 create mode 100644 ansible/roles/linux-baseline/templates/99-mk-labs.conf.j2 create mode 100644 ansible/roles/linux-baseline/templates/chrony.conf.j2 create mode 100644 ansible/roles/linux-baseline/templates/journald-retention.conf.j2 create mode 100644 ansible/roles/linux-baseline/templates/motd.j2 create mode 100644 ansible/roles/linux-baseline/templates/sshd_hardening.conf.j2 create mode 100644 ansible/roles/linux-baseline/vars/Debian.yml create mode 100644 ansible/roles/linux-baseline/vars/RedHat.yml diff --git a/ansible/group_vars/all/vars b/ansible/group_vars/all/vars index b9640b2..0b59acc 100644 --- a/ansible/group_vars/all/vars +++ b/ansible/group_vars/all/vars @@ -19,6 +19,15 @@ terraform_server: "infra01" # Traefik variables traefik_server: "lightning-lane" +# --------------------------------------------------------------------------- +# JARVIS automation account +# --------------------------------------------------------------------------- +# Public key for the 'jarvis' user provisioned by the linux-baseline role on +# every host. Public keys are not secret; the matching private key lives on +# the JARVIS command centre (carousel-of-progress) and, when needed, in +# group_vars/all/vault as vault_jarvis_ssh_private_key. +jarvis_ssh_public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID5sym5ajFvDyzw395BkHv7qVb66XPTx/OF1p19MGuNo jarvis@mk-labs" + step_ca_principal_mappings: - local_user: wed principals: diff --git a/ansible/playbooks/day0_linux_baseline.yml b/ansible/playbooks/day0_linux_baseline.yml new file mode 100644 index 0000000..19a93e6 --- /dev/null +++ b/ansible/playbooks/day0_linux_baseline.yml @@ -0,0 +1,23 @@ +--- +# ============================================================================ +# day0_linux_baseline.yml +# ---------------------------------------------------------------------------- +# Applies the mk-labs Linux baseline (linux-baseline role) to one or more +# hosts. Idempotent and safe to re-run. +# +# Usage: +# ansible-playbook playbooks/day0_linux_baseline.yml +# ansible-playbook playbooks/day0_linux_baseline.yml -e target=figment +# ansible-playbook playbooks/day0_linux_baseline.yml -e target=semaphore_server +# +# To trigger an opt-in full system upgrade: +# ansible-playbook playbooks/day0_linux_baseline.yml \ +# -e target=figment -e 'baseline_features={"full_upgrade": true}' +# ============================================================================ + +- name: Apply mk-labs Linux baseline + hosts: "{{ target | default('all') }}" + become: true + gather_facts: true + roles: + - linux-baseline diff --git a/ansible/roles/linux-baseline/README.md b/ansible/roles/linux-baseline/README.md new file mode 100644 index 0000000..f3d0ae9 --- /dev/null +++ b/ansible/roles/linux-baseline/README.md @@ -0,0 +1,64 @@ +# linux-baseline + +Idempotent baseline for any Linux host in mk-labs. + +## What it does + +Applied on every host, this role guarantees a known-good operational baseline: + +| Capability | Default | Description | +|----------------------|---------|---------------------------------------------------------------| +| `packages` | on | Installs curated common + OS-family packages, plus host extras | +| `timezone` | on | Sets timezone and locale | +| `time_sync` | on | Installs and configures chrony against `sundial` | +| `users` | on | Provisions baseline admin accounts (jarvis) with SSH + sudo | +| `ssh_hardening` | on | Deploys `/etc/ssh/sshd_config.d/10-mk-labs-hardening.conf` | +| `unattended_upgrades`| on | Security-only automatic upgrades on Debian-family hosts | +| `sysctl` | on | Drop-in at `/etc/sysctl.d/99-mk-labs.conf` | +| `journald` | on | Caps journal size + retention | +| `motd` | on | Branded MOTD | +| `full_upgrade` | off | Opt-in full dist-upgrade. Use the day2 update playbook instead | + +Every capability sits behind a feature flag in `baseline_features` so it +can be disabled per-host without forking the role. + +## OS support + +- **First-class:** Ubuntu (22.04, 24.04) and Debian (bullseye, bookworm). +- **Placeholder:** RHEL family — see `vars/RedHat.yml`. Flesh out when a + RHEL-family host joins the fleet. + +OS-specific values are loaded from `vars/{{ ansible_os_family }}.yml`. + +## Required inventory variables + +| Variable | Where | Example | +|-------------------------|-----------------|-------------------------------------------| +| `jarvis_ssh_public_key` | group_vars/all | `ssh-ed25519 AAAA... jarvis@mk-labs` | + +## Usage + +```yaml +- name: Apply linux-baseline + hosts: "{{ target | default('all') }}" + become: true + roles: + - linux-baseline +``` + +## Per-host overrides + +```yaml +# host_vars/some-host/vars +baseline_packages_extra: + - postgresql-client + - redis-tools + +baseline_features: + motd: false +``` + +## Migration notes + +This role supersedes the legacy `day0-baseline` and `common` roles, both +of which are slated for removal once the fleet has been re-baselined. diff --git a/ansible/roles/linux-baseline/defaults/main.yml b/ansible/roles/linux-baseline/defaults/main.yml new file mode 100644 index 0000000..f684af2 --- /dev/null +++ b/ansible/roles/linux-baseline/defaults/main.yml @@ -0,0 +1,120 @@ +--- +# ============================================================================ +# linux-baseline role defaults +# ============================================================================ +# This role applies an idempotent baseline to any Linux host in mk-labs. +# Ubuntu is the first-class target; other distributions plug in via +# vars/.yml. +# ============================================================================ + +# --------------------------------------------------------------------------- +# Feature flags - each capability can be toggled independently +# --------------------------------------------------------------------------- +baseline_features: + packages: true + timezone: true + time_sync: true + ssh_hardening: true + users: true + unattended_upgrades: true + sysctl: true + journald: true + motd: true + # Off by default: a full apt/dnf upgrade is intrusive. Use the dedicated + # day2 update playbook for that. Security patches are handled by + # unattended-upgrades. + full_upgrade: false + +# --------------------------------------------------------------------------- +# Locale & time +# --------------------------------------------------------------------------- +baseline_timezone: America/Chicago +baseline_locale: en_US.UTF-8 +baseline_ntp_server: sundial.local.mk-labs.cloud +baseline_ntp_fallback_pool: 2.pool.ntp.org + +# --------------------------------------------------------------------------- +# Packages +# --------------------------------------------------------------------------- +# Distribution-agnostic baseline. Distro-specific additions are merged in +# from vars/.yml at runtime. +baseline_packages_common: + - curl + - wget + - vim + - htop + - git + - jq + - unzip + - rsync + - ca-certificates + - gnupg + - net-tools + - dnsutils + - acl + - tree + - tmux + +# Per-host opt-in extras. Set in host_vars or group_vars. +baseline_packages_extra: [] + +# --------------------------------------------------------------------------- +# Baseline users +# --------------------------------------------------------------------------- +# 'jarvis' is the universal admin / automation account that this role +# provisions on every host. Service users (e.g. 'cast' for podman +# workloads) are created by their respective application roles, not here. +baseline_users: + - name: jarvis + comment: "JARVIS automation account" + shell: /bin/bash + groups: "{{ baseline_sudo_group }}" + sudo_nopasswd: true + ssh_authorized_keys: + - "{{ jarvis_ssh_public_key }}" + +# --------------------------------------------------------------------------- +# SSH hardening +# --------------------------------------------------------------------------- +# Delivered as a drop-in under /etc/ssh/sshd_config.d/ rather than editing +# the main config file. Safer to re-apply, easier to remove. +baseline_ssh_permit_root_login: "no" +baseline_ssh_password_authentication: "no" +baseline_ssh_pubkey_authentication: "yes" +baseline_ssh_permit_empty_passwords: "no" +baseline_ssh_x11_forwarding: "no" +baseline_ssh_client_alive_interval: 300 +baseline_ssh_client_alive_count_max: 2 +baseline_ssh_max_auth_tries: 4 +baseline_ssh_login_grace_time: 30 + +# --------------------------------------------------------------------------- +# Unattended security upgrades (apt / dnf-automatic) +# --------------------------------------------------------------------------- +baseline_unattended_security_only: true +baseline_unattended_auto_reboot: false +baseline_unattended_auto_reboot_time: "03:00" + +# --------------------------------------------------------------------------- +# sysctl +# --------------------------------------------------------------------------- +# Light hand here. The kernel defaults are generally sensible; we tune +# only what consistently bites us in a homelab. +baseline_sysctl: + vm.swappiness: 10 + fs.inotify.max_user_watches: 524288 + fs.inotify.max_user_instances: 512 + net.ipv4.tcp_keepalive_time: 600 + net.ipv4.tcp_keepalive_intvl: 60 + net.ipv4.tcp_keepalive_probes: 6 + +# --------------------------------------------------------------------------- +# journald retention caps +# --------------------------------------------------------------------------- +baseline_journald_max_use: "1G" +baseline_journald_max_retention: "30day" + +# --------------------------------------------------------------------------- +# MOTD branding +# --------------------------------------------------------------------------- +baseline_motd_banner: "mk-labs" diff --git a/ansible/roles/linux-baseline/handlers/main.yml b/ansible/roles/linux-baseline/handlers/main.yml new file mode 100644 index 0000000..58e1fbc --- /dev/null +++ b/ansible/roles/linux-baseline/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: Restart chrony + ansible.builtin.systemd: + name: "{{ baseline_chrony_service }}" + state: restarted + +- name: Restart sshd + ansible.builtin.systemd: + name: "{{ baseline_sshd_service }}" + state: restarted + +- name: Reload sysctl + ansible.builtin.command: + cmd: sysctl --system + changed_when: false + +- name: Restart systemd-journald + ansible.builtin.systemd: + name: systemd-journald + state: restarted diff --git a/ansible/roles/linux-baseline/meta/main.yml b/ansible/roles/linux-baseline/meta/main.yml new file mode 100644 index 0000000..1ee4541 --- /dev/null +++ b/ansible/roles/linux-baseline/meta/main.yml @@ -0,0 +1,25 @@ +--- +galaxy_info: + role_name: linux_baseline + author: JARVIS + description: >- + Idempotent Linux baseline for mk-labs hosts. Ubuntu first-class, + pluggable for other distributions via vars/.yml. + license: MIT + min_ansible_version: "2.14" + platforms: + - name: Ubuntu + versions: + - jammy + - noble + - name: Debian + versions: + - bullseye + - bookworm + galaxy_tags: + - baseline + - bootstrap + - hardening + - homelab + +dependencies: [] diff --git a/ansible/roles/linux-baseline/tasks/journald.yml b/ansible/roles/linux-baseline/tasks/journald.yml new file mode 100644 index 0000000..d6e788c --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/journald.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/main.yml b/ansible/roles/linux-baseline/tasks/main.yml new file mode 100644 index 0000000..5775a7e --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/main.yml @@ -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] diff --git a/ansible/roles/linux-baseline/tasks/motd.yml b/ansible/roles/linux-baseline/tasks/motd.yml new file mode 100644 index 0000000..71fec2e --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/motd.yml @@ -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" diff --git a/ansible/roles/linux-baseline/tasks/packages.yml b/ansible/roles/linux-baseline/tasks/packages.yml new file mode 100644 index 0000000..a3d81eb --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/packages.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/preflight.yml b/ansible/roles/linux-baseline/tasks/preflight.yml new file mode 100644 index 0000000..03b2636 --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/preflight.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/ssh_hardening.yml b/ansible/roles/linux-baseline/tasks/ssh_hardening.yml new file mode 100644 index 0000000..d321fd5 --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/ssh_hardening.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/sysctl.yml b/ansible/roles/linux-baseline/tasks/sysctl.yml new file mode 100644 index 0000000..b1fa09f --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/sysctl.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/time_sync.yml b/ansible/roles/linux-baseline/tasks/time_sync.yml new file mode 100644 index 0000000..7403f0f --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/time_sync.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/timezone.yml b/ansible/roles/linux-baseline/tasks/timezone.yml new file mode 100644 index 0000000..6f2ff69 --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/timezone.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/unattended_upgrades.yml b/ansible/roles/linux-baseline/tasks/unattended_upgrades.yml new file mode 100644 index 0000000..a3b8e74 --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/unattended_upgrades.yml @@ -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 diff --git a/ansible/roles/linux-baseline/tasks/users.yml b/ansible/roles/linux-baseline/tasks/users.yml new file mode 100644 index 0000000..4c50628 --- /dev/null +++ b/ansible/roles/linux-baseline/tasks/users.yml @@ -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 diff --git a/ansible/roles/linux-baseline/templates/50unattended-upgrades.j2 b/ansible/roles/linux-baseline/templates/50unattended-upgrades.j2 new file mode 100644 index 0000000..f42d9c1 --- /dev/null +++ b/ansible/roles/linux-baseline/templates/50unattended-upgrades.j2 @@ -0,0 +1,26 @@ +// {{ ansible_managed }} +// mk-labs unattended-upgrades policy + +Unattended-Upgrade::Allowed-Origins { +{% if baseline_unattended_security_only %} + "${distro_id}:${distro_codename}-security"; + "${distro_id}ESMApps:${distro_codename}-apps-security"; + "${distro_id}ESM:${distro_codename}-infra-security"; +{% else %} + "${distro_id}:${distro_codename}"; + "${distro_id}:${distro_codename}-security"; + "${distro_id}:${distro_codename}-updates"; +{% endif %} +}; + +Unattended-Upgrade::Package-Blacklist { }; +Unattended-Upgrade::DevRelease "auto"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; +Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "false"; + +Unattended-Upgrade::Automatic-Reboot "{{ baseline_unattended_auto_reboot | string | lower }}"; +Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; +Unattended-Upgrade::Automatic-Reboot-Time "{{ baseline_unattended_auto_reboot_time }}"; + +Unattended-Upgrade::SyslogEnable "true"; diff --git a/ansible/roles/linux-baseline/templates/99-mk-labs.conf.j2 b/ansible/roles/linux-baseline/templates/99-mk-labs.conf.j2 new file mode 100644 index 0000000..653379a --- /dev/null +++ b/ansible/roles/linux-baseline/templates/99-mk-labs.conf.j2 @@ -0,0 +1,5 @@ +# {{ ansible_managed }} +# mk-labs sysctl baseline +{% for key, value in baseline_sysctl.items() %} +{{ key }} = {{ value }} +{% endfor %} diff --git a/ansible/roles/linux-baseline/templates/chrony.conf.j2 b/ansible/roles/linux-baseline/templates/chrony.conf.j2 new file mode 100644 index 0000000..d3336a7 --- /dev/null +++ b/ansible/roles/linux-baseline/templates/chrony.conf.j2 @@ -0,0 +1,28 @@ +# {{ ansible_managed }} +# Managed by Ansible - linux-baseline role +# Local edits will be overwritten. + +# Primary NTP source: mk-labs internal NTP server (sundial) +server {{ baseline_ntp_server }} iburst prefer + +# Fallback: public NTP pool +pool {{ baseline_ntp_fallback_pool }} iburst maxsources 2 + +driftfile /var/lib/chrony/chrony.drift + +# Step the clock if the adjustment is >1s on the first 3 corrections +makestep 1.0 3 + +# Sync RTC from system clock +rtcsync + +# Allow chronyd to use the keyfile for hardware refclock +{% if ansible_os_family == "Debian" %} +keyfile /etc/chrony/chrony.keys +{% else %} +keyfile /etc/chrony.keys +{% endif %} + +# Log selective events +logdir /var/log/chrony +log measurements statistics tracking diff --git a/ansible/roles/linux-baseline/templates/journald-retention.conf.j2 b/ansible/roles/linux-baseline/templates/journald-retention.conf.j2 new file mode 100644 index 0000000..7e4809a --- /dev/null +++ b/ansible/roles/linux-baseline/templates/journald-retention.conf.j2 @@ -0,0 +1,8 @@ +# {{ ansible_managed }} +# mk-labs journald retention policy +[Journal] +SystemMaxUse={{ baseline_journald_max_use }} +MaxRetentionSec={{ baseline_journald_max_retention }} +Storage=persistent +Compress=yes +ForwardToSyslog=no diff --git a/ansible/roles/linux-baseline/templates/motd.j2 b/ansible/roles/linux-baseline/templates/motd.j2 new file mode 100644 index 0000000..6f4fc2d --- /dev/null +++ b/ansible/roles/linux-baseline/templates/motd.j2 @@ -0,0 +1,11 @@ + + ╔═══════════════════════════════════════════════════════════════╗ + ║ ║ + ║ {{ baseline_motd_banner }} - {{ inventory_hostname }}{{ ' ' * (50 - (baseline_motd_banner | length) - (inventory_hostname | length)) }}║ + ║ ║ + ║ {{ ansible_distribution }} {{ ansible_distribution_version }} ({{ ansible_kernel }}){{ ' ' * (53 - (ansible_distribution | length) - (ansible_distribution_version | length) - (ansible_kernel | length)) }}║ + ║ ║ + ║ Managed by Ansible. Local changes will be overwritten. ║ + ║ ║ + ╚═══════════════════════════════════════════════════════════════╝ + diff --git a/ansible/roles/linux-baseline/templates/sshd_hardening.conf.j2 b/ansible/roles/linux-baseline/templates/sshd_hardening.conf.j2 new file mode 100644 index 0000000..9d9cae0 --- /dev/null +++ b/ansible/roles/linux-baseline/templates/sshd_hardening.conf.j2 @@ -0,0 +1,19 @@ +# {{ ansible_managed }} +# mk-labs SSH hardening drop-in +# Managed by Ansible - linux-baseline role + +PermitRootLogin {{ baseline_ssh_permit_root_login }} +PasswordAuthentication {{ baseline_ssh_password_authentication }} +PubkeyAuthentication {{ baseline_ssh_pubkey_authentication }} +PermitEmptyPasswords {{ baseline_ssh_permit_empty_passwords }} +X11Forwarding {{ baseline_ssh_x11_forwarding }} + +ClientAliveInterval {{ baseline_ssh_client_alive_interval }} +ClientAliveCountMax {{ baseline_ssh_client_alive_count_max }} +MaxAuthTries {{ baseline_ssh_max_auth_tries }} +LoginGraceTime {{ baseline_ssh_login_grace_time }} + +# Prefer modern key exchange and ciphers +KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr +MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com diff --git a/ansible/roles/linux-baseline/vars/Debian.yml b/ansible/roles/linux-baseline/vars/Debian.yml new file mode 100644 index 0000000..494e70f --- /dev/null +++ b/ansible/roles/linux-baseline/vars/Debian.yml @@ -0,0 +1,22 @@ +--- +# ============================================================================ +# Debian-family overrides (Ubuntu, Debian) +# ============================================================================ + +# Package facts module needs python3-apt on the target. +baseline_packages_family: + - python3-apt + - apt-transport-https + - software-properties-common + - lsb-release + - unattended-upgrades + +baseline_sudo_group: sudo + +# Service names +baseline_chrony_service: chrony +baseline_chrony_config_path: /etc/chrony/chrony.conf +baseline_sshd_service: ssh + +# Unattended-upgrades file locations +baseline_unattended_config_dir: /etc/apt/apt.conf.d diff --git a/ansible/roles/linux-baseline/vars/RedHat.yml b/ansible/roles/linux-baseline/vars/RedHat.yml new file mode 100644 index 0000000..a96d1e9 --- /dev/null +++ b/ansible/roles/linux-baseline/vars/RedHat.yml @@ -0,0 +1,19 @@ +--- +# ============================================================================ +# RedHat-family overrides (Rocky, AlmaLinux, RHEL) - placeholder +# ============================================================================ +# This file exists to make the OS abstraction visible. It is not exercised +# today because mk-labs is all-Ubuntu. Flesh out when a RHEL-family host +# enters the fleet. + +baseline_packages_family: + - dnf-automatic + - python3-dnf + +baseline_sudo_group: wheel + +baseline_chrony_service: chronyd +baseline_chrony_config_path: /etc/chrony.conf +baseline_sshd_service: sshd + +baseline_unattended_config_dir: /etc/dnf/automatic.conf.d