Why every estate needs a baseline
Look at ten servers that were built over three years by different people. Time zones differ. Two allow root login over SSH. One has a developer’s personal account with passwordless sudo. Several run different versions of the same package. Nobody chose any of this, and nobody can say with confidence which of them is secure.
A baseline role removes the guesswork. It states, in code, what every server must look like, and it keeps stating it. It is the foundation for patching, CIS compliance and every application role that follows, and it is a core part of our Ansible configuration management service.
What good looks like
- A new server reaches the standard configuration with one playbook run, with no manual steps.
- Existing servers converge on the same standard without being rebuilt.
- Access is explicit: named users, keys from a managed source, sudo rules in version control.
- Exceptions exist but are visible as variables with a reason.
- The role is tested on every operating system you run.
How it works in practice
Structure
One role, several task files included by tag, so each area can be run alone:
# roles/baseline/tasks/main.yml
- ansible.builtin.import_tasks: packages.yml
tags: [baseline, packages]
- ansible.builtin.import_tasks: time.yml
tags: [baseline, time]
- ansible.builtin.import_tasks: ssh.yml
tags: [baseline, ssh]
- ansible.builtin.import_tasks: users.yml
tags: [baseline, users]
- ansible.builtin.import_tasks: kernel.yml
tags: [baseline, kernel]
SSH: the part where mistakes lock you out
The one rule is to validate before applying. A broken sshd_config on a remote server with no console is an outage.
- name: Deploy sshd configuration
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0600"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd
The template sets PermitRootLogin no, PasswordAuthentication no (unless policy says otherwise), MaxAuthTries, idle timeouts and an allowed-groups line. Handlers use a reload where the platform supports it, and the playbook keeps an existing connection open until a fresh login is proven to work. We never change SSH settings across a whole fleet in one go.
Users and sudo
Users are defined as data, not as tasks:
baseline_users:
- name: asmith
groups: [sudo]
ssh_keys:
- "ssh-ed25519 AAAA... asmith@example"
baseline_removed_users:
- olduser
The role creates listed users, installs their public keys, and removes accounts that should not exist. Sudo rules are written to /etc/sudoers.d/ with validate: visudo -cf %s. For larger organisations, we integrate with your directory service instead, and the role only configures the client side.
Time
Servers with drifting clocks produce confusing logs, failed TLS handshakes and broken Kerberos or token validation. The role installs chrony, points it at agreed sources, and verifies with chronyc tracking that the system is synchronised. On cloud VMs we often use the provider’s internal time service.
Packages, kernel and services
The role installs the required tooling (for example curl, chrony, auditd), removes packages that should not be present, disables unused services and sets sysctl values with ansible.posix.sysctl. Choices such as disabling IPv6, tightening net.ipv4 parameters or enabling a host firewall are made with your team, not assumed, because they can surprise applications.
Decisions and trade-offs
One baseline or per-role variants. A database host and a bastion legitimately differ. We keep one baseline for the common core and use group variables to switch specific areas, rather than forking the role.
Strict by default or permissive by default. We default to the secure setting and make exceptions explicit. That makes loosening something a visible decision.
Configure in place or build golden images. Baking the baseline into images with Packer gives faster, more consistent launches. Running the role against running servers keeps them from drifting afterwards. Many clients do both, using the same role in both places.
When not to bother. If your hosts are immutable container nodes managed by a cloud provider, much of the baseline is already handled by the image, and you may need only the parts that cover access. Our Kubernetes consulting team can advise on what the node image already covers.
Common failure modes
- Lockout after SSH changes. Prevented by validation, staged rollout and a console fallback.
- Hardening that breaks applications. Prevented by testing and exception variables.
- Key sprawl. Authorised keys edited by hand. The role owns the file, so unmanaged keys are removed.
- Different behaviour across distributions. Handled with OS-specific variable files and tests on each.
- Secrets on disk. Root-readable credentials left by earlier scripts. We search for and relocate them, tied to the secrets work.
Delivery and handover
You receive the baseline role in your repository, tests for every supported operating system, a rollout plan with canary groups, a variable reference listing each switch and its default, and a written baseline standard that your security team can review. Maintenance is simple because it builds on the same role library conventions as everything else.
A rollout example: applying a baseline to servers that already exist
Greenfield servers are easy, because the baseline is simply part of the build. The harder case is a running estate where each server has quirks nobody documented. We roll out in stages. First, run the role in check mode with diff output across a representative sample and read what it would change. Expect surprises: a service account with a shell, an SSH setting a legacy application relies on, a package pinned for a reason nobody remembers.
Second, resolve each surprise as either a fix on the server or a documented exception in group variables, never a quiet weakening of the role. Third, apply the role to a non-critical group, watch for a full business cycle, and only then widen the rollout. Keep the first apply separate from later tuning, so a failure points at one change.
Two controls deserve extra care because they can lock you out: SSH configuration and firewall rules. Validate the SSH configuration before reloading the daemon, keep an existing session open while testing a new one, and make sure there is an out-of-band console route for the worst case. These habits cost minutes and prevent the one incident that makes people distrust automation.
How to tell it is working
- A scheduled check mode run across the fleet reports no unexpected changes.
- Any server can be rebuilt to the baseline from scratch, and a fresh one looks identical to an old one.
- An audit question about SSH settings or user access is answered by reading a file.
- Leavers’ accounts are removed by a pull request, not by memory.
To see how your current servers compare to a sensible baseline, contact us.