Skip to content

Repository files navigation

linux-motd

Linux Fleet MOTD Editor. Push a consistent, informative login banner to every host in your fleet over SSH. Back up what is there. Put it all back if you change your mind.

lfme.kosir.info · MIT · agentless

Part of a set: linux-audit collects, linux-harden remediates, linux-motd presents.


What it does

Reads a list of hosts, connects over SSH, escalates with sudo, backs up anything it is about to touch, and installs a small POSIX shell renderer that draws the banner at each login. Hostname, IP, uptime, load, memory, disk, pending updates, reboot-required, failed units, and whatever policy text you put in the template.

==============================================================================
 PRODUCTION  |  web01.example.com
==============================================================================
 IP        : 10.20.4.11      Platform : kvm
 OS        : Red Hat Enterprise Linux 9.4 (Plow)  (5.14.0-427.el9.x86_64)
 Owner     : Platform
 Support   : ops@example.com

 Uptime    : 41d 6h 12m
 Load      : 0.42 / 0.51 / 0.48   (8 cores, 0.05 per core)
 Memory    : 11.2G / 15.5G  (72%)
 Disk /    : 18.1G / 40.0G  (46%)
 Disk /var : 7.9G / 8.0G  (98 !!%)
 Sessions  : 2

 Updates   : 14 pending, 3 !! security
 !! REBOOT REQUIRED
 !! 2 failed unit(s): chronyd.service,rsyslog.service

 INTERNAL. Authorised use only. Activity is logged.
 Raise changes at https://jira.example.com/servicedesk
==============================================================================

Install

pip install linux-motd

Or from source:

git clone https://github.com/vikozs/linux-motd.git
cd linux-motd
python3 -m pip install -e ".[dev]"

Use

# See what one host would look like. Writes nothing.
linux-motd preview -i inventory.yaml -H web01.example.com

# Show the change plan and diffs for the fleet. Writes nothing.
linux-motd plan -i inventory.yaml

# Install, with a per-host approval gate.
linux-motd push -i inventory.yaml

# Password auth with a domain service account.
linux-motd push -i inventory.yaml --ask-ssh-pass --sudo-pass-same-as-ssh

# Unattended, for CI. Still takes backups.
linux-motd push -i inventory.yaml --yes --json run.json

# Put it back.
linux-motd rollback -H web01.example.com
linux-motd remove -i inventory.yaml

Commands

Command What it does
preview Render against one host's live facts. No writes.
plan Pre-flight every host, print the change plan and diffs. No writes.
push Back up, install, verify. Approval-gated.
status Installed version, timer state, cache age per host.
verify Check installed state against what this config would produce.
rollback Restore a backup generation.
remove Uninstall and restore pre-install state.
fields List every template field.

Exit codes

0 all succeeded · 1 a host failed · 2 config/inventory/template error, nothing contacted · 3 nothing to do · 4 aborted by operator · 5 partial apply with rollback, needs a human.

How the banner gets drawn

There is no single MOTD mechanism across a mixed fleet, so the tool detects and adapts.

Family Mechanism Notes
Debian, Ubuntu /etc/update-motd.d/ pam executes these. Native dynamic support.
RHEL/Rocky/Alma 8, 9 /etc/motd.d/ pam reads these as static text. Never executes them. The refresh timer writes the rendered output here.
RHEL/CentOS 7 /etc/profile.d/ That pam version reads /etc/motd only.

Three hook modes, auto-detected, overridable per group:

  • pam — output written where pam looks. Works for every login type, including non-bash shells. Live fields are as fresh as the timer.
  • profile — a /etc/profile.d script calls the renderer at shell start. Every field is live. Bash/sh login shells only.
  • both — static banner plus a live block. Default on RHEL 8/9.

Fast path and slow path

Anything that runs at login runs on every SSH session, including your monitoring checks and backup jobs. So the split is enforced from the first commit:

  • Fast path (linux-motd-render, budget 50 ms): /proc, /etc/os-release, a cache file. No package manager, no network.
  • Slow path (linux-motd-refresh, systemd timer, 30 min with a 5 min random delay): pending updates, reboot-required, failed units. Writes facts.env.

If the cache is missing or corrupt, the fast path omits those fields silently. It never blocks a login.

Safety

  • Backups first. Every write is preceded by a backup into /var/backups/linux-motd/<timestamp>/ with a manifest.json recording content, mode, owner and SELinux context. Backups are never deleted on the failure path.
  • Rollback deletes. The manifest records existed: false for paths that were not there before. Rollback removes them rather than leaving them behind.
  • Read-back verification. Every write is re-read and hashed. A write that reports success but did not land is a failure, not a pass. On SELinux hosts the context is checked too: a file that lands with user_tmp_t is ignored by pam while the tool would otherwise claim success.
  • Per-host atomicity. If any file in the set fails, the whole host rolls back.
  • Foreign management detection. cloud-init, Puppet, Ansible and Salt markers are detected and reported rather than silently overwritten.
  • Approval gate. Unified diff per host, then y / n / a / q / d. --dry-run writes nothing; --yes skips prompts for automation and still takes backups.

The scp problem

A /etc/profile.d script that writes to stdout unconditionally corrupts scp, sftp and rsync, fleet-wide, and the symptom does not obviously point back at a login banner. The generated hook guards on interactivity, on stdout being a tty, and on an opt-out variable, and the test suite runs a real scp against an installed host. Do not remove those guards.

Pre-auth banner

/etc/issue and /etc/issue.net are supported, off by default, behind --enable-issue.

They are displayed before authentication, so they get their own template with a restricted namespace: only meta.* policy fields load, and every host, system and package field is rejected at load time rather than rendering blank. Backslashes are escaped by default, because getty expands \m and \r in /etc/issue while sshd does not expand them in /etc/issue.net, and the same bytes would otherwise leak kernel and hostname in one place and render literally in the other.

Ownership is split with linux-harden and the split is written down:

Concern Owner
/etc/issue content linux-motd, inside sentinel markers
File mode and ownership (CIS 1.7.4-1.7.6) linux-harden
sshd_config Banner directive linux-harden

Only text between the sentinels is replaced; everything outside is preserved byte for byte. The feature stays off by default until linux-harden v1.2 stops writing issue content, so the two tools never fight over one file.

Configuration

Two files, both documented in examples/:

  • inventory.yaml — shared with linux-audit and linux-harden via linux-fleet-common
  • config.yaml — operator-side settings. Never contains passwords.

Templates use {{ field }}, {{# if field }}, {{# each disk.mounts as m }}, and the filters default, pad, warn, upper, lower. Run linux-motd fields for the full field list. An unknown field is a load-time error, not a blank at render time.

Credentials

Passwords travel via stdin or environment, never argv, because /proc is world-readable. sudo -S and bash -s both want stdin, so scripts are staged over SFTP to a mode-0600 temp file and executed by path, with only the password on stdin. Staged files are removed in a finally block.

LINUX_MOTD_SSH_PASSWORD and LINUX_MOTD_SUDO_PASSWORD are read from the environment for unattended runs.

Development

pip install -e ".[dev]"
pytest
ruff check .

The test suite executes the generated shell under a real sh rather than inspecting it as a string. Reviewing generated shell statically does not count as testing it.

Licence

MIT. See LICENSE.

Built by vK, who has been paged at 03:00 and did not enjoy it.

About

Manage the login message of the day across a RHEL 9 fleet over SSH. Audit /etc/motd, /etc/issue, and dynamic motd.d against a template, flag drift and missing warning banners, and standardize them with per-host confirmation.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages