Ansible: Your First Useful Role for Working SysAdmins

Ansible: Your First Useful Role for Working SysAdmins

Most Ansible tutorials teach you to ping localhost and feel productive. That is fine for five minutes. On a real job you need a role that installs the packages you always install, drops the sshd settings you always forget, and does not brick SSH when a variable defaults to empty.

I have locked myself out once — not from malice, from a role that set PasswordAuthentication no while the admin user’s authorized_keys task silently skipped because the key variable was blank. Console access saved the afternoon. This guide is the first useful role I want a working sysadmin to merge: baseline packages, an admin user, and sshd hygiene with a break-glass mindset.

Career arc if you need it later: SysAdmin to DevOps roadmap. Today we build one role well.

Why a role, not a 400-line playbook

Playbooks are glue. Roles are reusable chunks: tasks, templates, defaults, handlers. When you have twelve Ubuntu boxes and three Rocky boxes, you want import_role: name: baseline, not copy-paste YAML archaeology.

Common wrong assumption: “I’ll keep everything in one playbook until it’s messy, then refactor.” The refactor never comes. Start with a role directory even if only one playbook calls it.

How to check prerequisites (before any YAML)

  1. SSH already works with a key to a non-root sudo user. If keys fail, fix that first — SSH permission denied publickey. Automation on a broken trust path is just faster failure.
  2. You have console/serial/VNC access to the lab VM. Seriously. Do not batch-disable password auth without a way in.
  3. Ansible can ping:
ansible all -i inventories/lab/hosts.ini -m ping

Success: pong from each host. If you see unreachable, stop — fix network/SSH before roles.

Layout that won’t embarrass you

mkdir -p roles/baseline/{tasks,handlers,templates,defaults,meta}
touch roles/baseline/tasks/main.yml
touch roles/baseline/handlers/main.yml
touch roles/baseline/defaults/main.yml
touch roles/baseline/meta/main.yml

Keep defaults conservative. Override per group in inventory. Do not hardcode your laptop username as deploy on prod.

Inventory: start boring

# inventories/lab/hosts.ini
[web]
lab-web1.example.com
lab-web2.example.com

[web:vars]
ansible_user=ubuntu

Use hostnames you can resolve. Mixing cloud IPs and nicknames without /etc/hosts or DNS is how people run playbooks against the wrong box.

defaults/main.yml — knobs you admit exist

---
baseline_packages:
  - curl
  - vim
  - htop
  - unzip
baseline_ssh_password_auth: "no"
baseline_ssh_permit_root: "no"
baseline_admin_user: deploy
baseline_admin_pubkey: ""   # MUST override per env — empty is intentional danger flag

Password auth off by default. If a vendor box needs it temporarily, override in host_vars — and leave a calendar reminder. Pair habits with SSH hardening checklist 2026.

Opinion: an empty default for the pubkey is better than a sample key that someone forgets to change. Fail loud in tasks if it is empty when hardening runs.

tasks/main.yml — the useful meat

---
- name: Install baseline packages
  ansible.builtin.package:
    name: "{{ baseline_packages }}"
    state: present

- name: Ensure admin group exists
  ansible.builtin.group:
    name: "{{ baseline_admin_user }}"
    state: present

- name: Ensure admin user exists
  ansible.builtin.user:
    name: "{{ baseline_admin_user }}"
    groups: sudo
    append: true
    shell: /bin/bash
    create_home: true

- name: Install admin authorized_key
  ansible.builtin.authorized_key:
    user: "{{ baseline_admin_user }}"
    key: "{{ baseline_admin_pubkey }}"
    state: present
  when: baseline_admin_pubkey | length > 0

- name: Fail clearly if hardening without a key
  ansible.builtin.fail:
    msg: "baseline_admin_pubkey is empty — refusing to tighten sshd"
  when:
    - baseline_ssh_password_auth == "no"
    - baseline_admin_pubkey | length == 0

- name: Deploy sshd_config drop-in
  ansible.builtin.template:
    src: sshd_hardening.conf.j2
    dest: /etc/ssh/sshd_config.d/99-baseline.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload sshd

That fail task is me being petty on purpose. A common mistake is disabling password auth and keys in the same change. Console access is not paranoia; it is how you keep a job.

On RHEL-family hosts the sudo group may be wheel — override with a variable when you add Rocky/Alma. Do not pretend one group name fits every distro.

templates/sshd_hardening.conf.j2

# Managed by Ansible role baseline — do not edit by hand
PasswordAuthentication {{ baseline_ssh_password_auth }}
PermitRootLogin {{ baseline_ssh_permit_root }}
KbdInteractiveAuthentication no

Drop-ins under sshd_config.d/ beat editing the giant main file. Confirm your distro’s sshd actually includes that directory (modern Ubuntu/Debian/RHEL do).

handlers/main.yml

---
- name: Reload sshd
  ansible.builtin.service:
    name: sshd
    state: reloaded

Some Debian packages use service name ssh instead of sshd. Check with systemctl list-units 'ssh*' on a sample host and make the name a variable if you mix families.

Playbook that calls the role

---
- name: Apply baseline
  hosts: web
  become: true
  roles:
    - baseline

Run narrowly first:

ansible-playbook -i inventories/lab/hosts.ini playbooks/baseline.yml --limit lab-web1.example.com

Verify success

# from your laptop, as the admin user
ssh -o BatchMode=yes [email protected] 'echo ok && sudo -n true && systemctl is-active ssh'
# confirm drop-in exists on host
ssh [email protected] 'grep PasswordAuthentication /etc/ssh/sshd_config.d/99-baseline.conf'

Success: key login works; sudo works; password auth is off in the drop-in; a second playbook run reports changed=0 for the boring parts (idempotence).

If still stuck / locked out: use console, temporarily set PasswordAuthentication yes in a drop-in or main config, reload sshd, fix keys, re-run the role. Do not reboot as a personality trait — reload is enough for sshd. If package installs fail, check mirrors and apt/dnf basics before blaming Ansible.

Tradeoffs I want you to own

  • One role = one job. Baseline is not also nginx and Docker. Split when the README needs sections.
  • Secrets: do not put production private keys in git. Use Ansible Vault or a secret manager for real environments; lab keys can be lab-only.
  • Idempotence matters because the second run is when you discover your task always “changes.”
  • Don’t harden SSH in the same change as rebuilding users on a host you cannot console into.

When you outgrow single-host Compose toys and need config drift control across a fleet, this role is the right kind of boring. Containers come after — Docker Compose for ops.

A worked lab run (what I actually type)

Assume VirtualBox or a small cloud VM named lab-web1, Ubuntu 22.04/24.04, user ubuntu with sudo, and your pubkey already in ~ubuntu/.ssh/authorized_keys.

  1. Put your pubkey into inventories/lab/group_vars/web.yml:
baseline_admin_pubkey: "ssh-ed25519 AAAA... your@laptop"
  1. Run once with limit, watch for failed/unreachable.
  2. SSH as deploy (or whatever you set) and prove sudo.
  3. Run the playbook a second time — package and user tasks should be ok/changed=0.
  4. Only then point the same role at a second host.

If step 3 fails, do not “fix” by re-enabling password auth on a dozen hosts via Ansible. Fix the one lab host from console, adjust the role, re-test. Speed comes after the role is trustworthy.

Wrong assumption: --check mode proves SSH will still work. Check mode does not always exercise handlers the same way, and it cannot save you from a bad key variable. Use check for syntax/drift hints; use a lab host for truth.

When NOT to use this tip

Do not drop this role onto a vendor appliance where sshd is managed by their own config management. Do not fight a golden image that already templates sshd until you know who wins the file war. And do not use Ansible as a substitute for understanding why a single host is broken — if nginx is 502’ing, debug that host with logs first (502 guide), then encode the fix.

FAQ — real doubts

Do I need Ansible Galaxy collections on day one?

Not for this baseline. Stick to ansible.builtin until you need something specific. Galaxy is useful; it is also a way to import other people’s assumptions.

Should I disable root login and password auth in the first PR?

In lab, yes — after keys and console are proven. In prod, split into two changes: (1) ensure keys and sudo, (2) tighten sshd. Same role, different variable overrides and change windows.

What about firewall / fail2ban in this role?

Tempting. I keep them separate. SSH hardening and host firewalls interact; see fail2ban vs security groups. One PR that “does security” is how you get unreviewable diffs.

How do I test without twelve VMs?

One lab VM, --check mode for reading, then a real run, then a second real run for idempotence. Multiplying hosts before the role is stable just multiplies lockouts.

Sources

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *