ansible-role-hardening: a systemd security baseline for Debian, Ubuntu and AlmaLinux
Ansible role to apply a security baseline. Systemd edition.
At a glance
- What is it?
- The konstruktoid hardening role applies a server security baseline with Ansible, focused on systemd, covering SSH, PAM, auditd, sysctl, the firewall and login policy, and it is tested with Molecule on QEMU and containers. It suits an administrator who wants a documented baseline applied idempotently, and it is opinionated enough that you have to test it outside production first.
- Who is it for?
- Adopt ansible-role-hardening if you run AlmaLinux, Debian or Ubuntu servers with systemd and want a broad, documented baseline applied from a single role rather than a pile of ad hoc tasks. Do not adopt it as a drop-in on a production fleet, because the role sets default deny firewall policies and changes login behaviour, and the README says not to use it without testing in a non-operational environment.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Jinja, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A baseline for three distributions, written for systemd
The konstruktoid hardening role is an Ansible role designed to enhance the security of servers running AlmaLinux, Debian or Ubuntu. It is systemd focused, which is the single most consequential word in the README, and it requires Ansible version 2.18 or higher.
The supported operating systems are stated as the ones the role is tested against and supports: AlmaLinux 10, Debian 13 (trixie), and Ubuntu 26.04. That list is short and specific, and it is a test matrix rather than a wish list, which tells you something about how the role is maintained.
The scope of what gets changed is wide, and the role arguments table is the best evidence of that. The listed template options include an adduser.conf template, PAM common-account, common-auth and common-password templates, a systemd coredump.conf template, a faillock.conf template, an auditd hardening rules template, hosts.allow and hosts.deny templates, a profile initpath.sh template, an /etc/issue template, a systemd journald.conf template, a limits.conf template, an /etc/login.defs template, a PAM login template, a systemd logind.conf template, a logrotate template, an /etc/motd template, a pwquality.conf template, a systemd resolved.conf template, an rkhunter configuration template, OpenSSH ssh_config and sshd_config templates, an OpenSSH tmpfiles template, AppArmor and IPv6 sysctl templates, and more beyond the visible part of the table.
So the role touches authentication, password quality, account lockout, auditing, logging, kernel parameters, SSH configuration and the login banner. It is a baseline in the CIS-style sense: a broad set of defaults applied consistently, with every knob exposed as a variable rather than hard coded.
Two of the defaults in the table illustrate the philosophy. The auditd rules template default is etc/audit/rules.d/hardening.rules.j2, meaning the audit rules are a template you can point elsewhere, and the hardening_rules_template name itself makes clear that audit rules are part of the baseline rather than an afterthought.
The primary language of the repository is Jinja, which is the templating layer for all of those files, and the licence is Apache-2.0.
The role is being replaced by a collection
The first note in the README is a migration notice, and an adopter should read it before anything else.
The role has been migrated to the ansible-collection-hardening collection, and the README recommends switching to that for continued updates and support. That is the maintainer telling you that the standalone role form is on its way out, and it is the kind of statement that is easy to miss and expensive to miss later, because the pinned versions and the support window differ between the two forms.
The practical consequence is visible in the example. The requirements block in the README pins the role to a version and a source:
---
roles:
- name: konstruktoid.hardening
version: v4.6.0
src: https://github.com/konstruktoid/ansible-role-hardening.git
scm: gitPinning is the right instinct here, and it is what the README demonstrates, but the pinned artefact is a role rather than a collection. If you are starting a new automation, reading the collection documentation first is likely to save you a migration later, and this role is still a perfectly reasonable thing to use in the meantime, since the releases continue: v4.4.1 in March 2026, v4.5.0 in May 2026 and v4.6.0 in June 2026, with the last push to the repository on 2026-10-19.
Two other repository-level signals point at how the project is run. There is a SECURITY.md file, so there is a defined way to report a problem in the role, and a CODEOWNERS file, so ownership of the code is assigned rather than implicit. There is also a renovate.json, which means dependency updates in the role's own requirements are proposed automatically.
The documentation approach is worth noting too, because it is unusual for a role. All options and defaults are documented in defaults/main.yml and meta/argument_specs.yml, and the README says ansible-doc -t role can be used to view the documentation for the role as well. Generating the reference from the same files that drive the behaviour is the right way to avoid a documentation drift problem.
Playing the role with explicit variables
The README's playbook example is short, and the shape of it is the thing to notice: the role is included, and every deviation from the default is stated as a variable.
---
- name: Import and use the hardening role
hosts: localhost
any_errors_fatal: true
tasks:
- name: Import the hardening role
ansible.builtin.include_role:
name: konstruktoid.hardening
vars:
kernel_lockdown: true
manage_suid_sgid_permissions: falseThree things are worth reading off this. The role is included with ansible.builtin.include_role rather than applied as a role dependency, which means the variables are passed at the include site and the role runs at that point in the play. The play sets any_errors_fatal: true, so a failed hardening task stops the run rather than continuing with a half-hardened host. And the variables show the granularity: kernel lockdown is switched on, while management of SUID and SGID permissions is explicitly turned off, which is the kind of decision a default cannot make for you.
The full example goes further, with sshd_admin_net listing the networks from which admin access is allowed, sshd_allow_groups listing sudo as an allowed group, sshd_update_moduli to refresh the OpenSSH moduli, sshd_match_users with per-user rules, and ufw_rate_limit to turn on rate limiting in the firewall. The per-user match rules are the interesting part, because they show the role can express different SSH policy for different accounts, such as one user allowed with password authentication and another with no banner.
There is a second example, a local playbook that clones the repository into a roles directory and configures it from there. It installs git, checks out the role at a specific version into /etc/ansible/roles/konstruktoid.hardening, removes git again, and then includes the role with SSH variables:
sshd_allow_groups:
- ubuntu
sshd_login_grace_time: 60
sshd_max_auth_tries: 10
sshd_use_dns: false
sshd_update_moduli: trueThat pattern is a self-contained demo: no dependency on a galaxy install, a pinned version, and a small set of SSH settings that show the role's interface. It is also the fastest way to see the role's effect on a machine you can throw away.
Firewall handling that does not reset ufw every run
One behaviour of the role deserves its own treatment, because it is the kind of detail that decides whether a role can be applied repeatedly.
Instead of resetting ufw on every run and by doing so causing network traffic disruption, the role deletes every ufw rule that does not have a comment ending with ansible managed. The rules the role owns are marked, and anything else is left in place unless it lacks that marker, in which case it is removed.
That is a clever ownership convention, and it has a consequence worth stating plainly. Any firewall rule you add by hand, or through another tool, will be deleted on the next run unless it carries a comment ending with ansible managed. For a team that manages the firewall in Git with the same repository, that is a reasonable model. For a team that has firewall rules in a cloud security group, in a different Ansible role, or set up once by a human, it means the hardening role will remove them.
The second part of the note is about defaults. The role sets default deny policies, which means that firewall rules need to be created for any additional ports except those specified in the sshd_ports and ufw_outgoing_traffic variables. Default deny with a managed rule set is the security-conscious choice, and it is also the choice that can cut off the services you did not think about, which is why the variable names matter.
The README links the ufw manual page for details. The two variables to look at first are sshd_ports, which defines what is open for SSH, and ufw_outgoing_traffic, which defines what is permitted outbound. Get those wrong and a default deny policy will lock you out of your own machine, which is the practical reason the README opens with a warning about testing in a non-operational environment.
Molecule on QEMU and containers, plus a role tree
The role is tested with Molecule, using QEMU virtual machines in the default scenario and containers in the docker scenario, and the README points at TESTING.md for the details. Testing a hardening role with containers alone would be a weak test, since a container cannot exercise systemd, PAM, auditd or real firewall behaviour, so the default scenario running a full virtual machine is the one that matters.
The repository layout shows the machinery. There is a molecule/ directory for the scenarios, a tests/ directory, a runTests.sh script, a tox.ini for the test environment, an action-lint/ directory for validating GitHub Actions, a .ansible-lint configuration with an accompanying .ansible-lint-ignore file, a .pre-commit-config.yaml, requirements-dev.txt, requirements-upstream.txt and requirements.yml. There is also a generate_molecule_env.sh script alongside generate_defaults.py and generate_doc_defaults.sh.
Those three generator scripts are worth a second look because they explain how the role stays consistent. If defaults, argument specs and documentation are generated from a single source, then the ansible-doc output cannot drift from the behaviour, and the Molecule environments can be regenerated rather than hand-edited. That is a level of process discipline in a role that many commercial hardening configurations do not have.
The task structure is documented separately in STRUCTURE.md, which the README points to for the tree of the role structure, and there are templates/, tasks/, handlers/ and defaults/ directories that mirror the documented argument names.
For a team, the practical value of the Molecule setup is that you can run the role against a throwaway virtual machine and see what it changes before it touches anything you care about. That is also the answer to the README's own warning about testing in a non-operational environment, and it is the step I would put first in an adoption plan.
Where this role is the wrong tool
The first limitation is that the role is opinionated, and opinionated security defaults are a design decision rather than a defect. Default deny firewall policies, kernel lockdown, account lockout through faillock, password quality rules through pwquality, an auditd ruleset and a hardened sshd configuration are all applied unless you turn them off. On a host that has a legitimate need for a permissive setting, the role will close it, and closing it can mean an outage rather than a warning.
The second is the systemd focus. The role is designed around systemd, and it targets AlmaLinux, Debian and Ubuntu. It is not a portable answer for a mixed estate, and for a distribution without systemd you would be adapting rather than applying.
The third is the migration question raised at the top of the README. If you pick the role, you are picking a form the maintainer wants you to leave. For a long-lived automation estate, the collection is the destination, and starting there is cheaper than migrating later.
The fourth is scope. The role is a baseline, and a baseline is a starting point rather than a compliance answer. The topics list includes cmmc and security-compliance, and the presence of auditd rules and rkhunter configuration gives you settings to point at, but the role does not produce an assessment report, does not track a control framework, and does not tell you which of its settings satisfy which requirement. If your need is evidence for an audit rather than a hardened host, that is a different category of tool.
The fifth is the Ansible version floor. Ansible 2.18 or higher is required, and the role is written against a current Ansible, which is the right choice but narrows who can run it.
There is a related option worth weighing, which is the hardened images route. The README points at a separate repository, konstruktoid/hardened-images, with hardened Ubuntu Amazon Machine Images and Azure virtual machine images, built with Packer using this role for configuration. If your problem is bootstrapping new hosts rather than hardening existing ones, starting from a pre-built image and then applying the role for drift is a different and often better workflow than running the role on a stock image.
Licence, releases and the supply chain signals
The licence is Apache-2.0, with the LICENSE file at the root of the repository, and this article does not interpret it. For an infrastructure component that gets applied with root privileges to every server you run, a permissive licence with an explicit patent grant is a comfortable choice, and the role's own licence terms matter less than the terms of the distribution you layer on top of it.
The release history is active: v4.4.1 on 2026-03-05, v4.5.0 on 2026-05-02 and v4.6.0 on 2026-06-01, with the last push on 2026-10-19. The repository is not archived. Given the topic tags include hacktoberfest, part of that activity is likely contribution driven rather than purely maintainer driven, which is normal for a widely used role and is a reason to read the changelog before bumping a major version.
The supply chain signals are the strongest part of the project. There is a SLSA artifact present under the slsa action workflow for verification, which means the release artefacts can be traced back to a build, and that is a meaningful answer to the question of whether the tarball you install matches the source in the repository. There is renovate.json for dependency updates, a pre-commit configuration, ansible-lint with an ignore file, and a SECURITY.md for responsible disclosure. The action-lint/ directory suggests the GitHub Actions workflows themselves are linted, which catches a class of mistake that is common in roles.
The last detail is the packaged images. If you use the Packer-built Amazon Machine Images or Azure images, the same questions about provenance apply to them, and the SLSA work in this repository is the starting point for checking that.
Taken together, this is a role with the process discipline of a maintained security project and the opinionated defaults of a baseline. The sensible adoption path is to read defaults/main.yml and meta/argument_specs.yml, run the Molecule scenarios, apply it to a representative host, and only then point it at production, which is more or less what the README asks for when it tells you not to use the role without testing in a non-operational environment.
Editorial conclusion
Adopt ansible-role-hardening if you run AlmaLinux, Debian or Ubuntu servers with systemd and want a broad, documented baseline applied from a single role rather than a pile of ad hoc tasks. Do not adopt it as a drop-in on a production fleet, because the role sets default deny firewall policies and changes login behaviour, and the README says not to use it without testing in a non-operational environment. Verify first that you understand what the variables in defaults/main.yml and meta/argument_specs.yml change, that the sshd_match_users and sshd_allow_groups settings match your access model, and whether you should track this role or the ansible-collection-hardening collection, since the maintainer recommends migrating, the licence is Apache-2.0, and the last push was on 2026-10-19.
Frequently asked questions
Which operating systems and Ansible version does the hardening role support?
The role is systemd focused, requires Ansible 2.18 or higher, and is tested against AlmaLinux 10, Debian 13 (trixie) and Ubuntu 26.04. Hardened Ubuntu Amazon Machine Images and Azure virtual machine images are built from the role with Packer.
How do I run the konstruktoid hardening role from a playbook?
The README example includes the role with ansible.builtin.include_role and passes the settings as variables, for example kernel_lockdown: true, manage_suid_sgid_permissions: false, sshd_admin_net, sshd_allow_groups and ufw_rate_limit, with any_errors_fatal: true on the play.
How does the role handle existing ufw firewall rules?
Rather than resetting ufw on every run and disrupting network traffic, it deletes every ufw rule that does not have a comment ending with ansible managed, and it sets default deny policies, so rules must be created for any additional ports beyond those in the sshd_ports and ufw_outgoing_traffic variables.
Is the ansible-role-hardening role still maintained or should I use the collection?
The README says the role has been migrated to the ansible-collection-hardening collection and recommends switching for continued updates and support, while releases continue here, with v4.6.0 tagged on 2026-06-01 and the last push on 2026-10-19. The project is licensed Apache-2.0 and is tested with Molecule.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/konstruktoid-ansible-role-hardening)Was this useful?