Hard Limits For Logins And For Services, Which Are Not The Same

Nothing limits an account on a stock EL 10 host: limits.d is empty and what ulimit reports is systemd's ceiling. This role sets hard limits on processes, open files and core dumps in both places that decide, because a systemd service never goes through PAM at all. The live test reads a real login session and a real service, and tries to raise both. Original role, live-tested on Rocky Linux 10.

ansibleSecurity & Secrets

Verification

Live-tested

Really deployed to a container sandbox, proven idempotent (a second run changes nothing), verified against the role’s assertions, then torn down.

Conformance

  • Static validation (yamllint · ansible-lint)

Provenance

  • SHA-256 checksum
  • Signature (pending)

Functional

  • Live-tested - applied, verified, destroyed

Last verified 2026-09-27 · podman 4.9.3 · ansible 2.21.4 · how we verify

Cite it in your README

badge · attribution

Paste this beside the module in the repository that uses it. The badge is rendered from this artifact's verification record, so it reads live-tested because the record says so, and the link lands on this page.

README.md, GitLab, Gitea
[![IaC Bazaar: live-tested](https://www.iac-bazaar.com/api/artifacts/ansible-resource-limits/badge)](https://www.iac-bazaar.com/catalog/ansible-resource-limits?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)

Ansible role 1.0.0, live-tested on IaC Bazaar: [Hard Limits For Logins And For Services, Which Are Not The Same](https://www.iac-bazaar.com/catalog/ansible-resource-limits?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)

```yaml
# Hard Limits For Logins And For Services, Which Are Not The Same: https://www.iac-bazaar.com/catalog/ansible-resource-limits (download from your IaC Bazaar account)
```

Preview:IaC Bazaar: live-tested

Documentation

resource-limits

Nothing limits an account on a stock EL 10 host. /etc/security/limits.d is empty - the pam package ships the directory and no file in it - and limits.conf has no active line, so what ulimit -u reports is systemd's ceiling of 4194304 rather than a policy. This role sets hard limits on processes, open files and core dumps, and sets them in BOTH places: a login session reads limits.d, and a systemd service never goes through PAM at all.

No download, and no version to pin. pam and systemd are the distribution. What the role owns is one file under /etc/security/limits.d and one under /etc/systemd/system.conf.d, and what it proves is the number the kernel enforces on a real session and on a real service.

A soft limit is advice; a hard limit is the policy. A process may raise its own soft limit as far as the hard one whenever it likes: measured on a host with no policy at all, where an account moved its soft nproc back to the 4194304 ceiling with one ulimit command. Raising the hard limit is refused - with this policy in force the same account's ulimit -Hu answered "Operation not permitted". Both halves are written, and the live test proves the hard one by trying to lift it.

The same file fails closed one way and open the other. A value pam_limits cannot PARSE is logged ("wrong limit value 'notanumber'") and ignored, leaving a policy that reads strict and is not. A value it cannot APPLY refuses the session: root hard nproc unlimited on a host whose ambient ceiling was lower logged "Could not set limit for 'nproc': Operation not permitted" and every su after it answered "cannot open session: Permission denied". That is how a limits file locks every account out of a host, so this role refuses to write unlimited anywhere and checks before it writes anything.

pam_limits does not bind a service. A transient unit running as the same account had nproc 4194304 and an unlimited core size while that account's login session was held to this policy. /etc/systemd/system.conf.d is the other half, and the live test reads both separately - a role that set only the PAM half would pass a test that only looked at a login.

systemctl daemon-reload is enough for system.conf. It runs as a handler, once, when the file changes. Measured: a transient unit's limits were unchanged immediately after the file was written, and correct after a plain daemon-reload. The common advice to use daemon-reexec is not needed here, and re-executing PID 1 is a heavier thing to ask of a running host.

The number in the filename decides who wins, the opposite way round from sshd. pam_limits reads limits.conf and then limits.d/*.conf in name order, and the LAST match wins, so this role's file is numbered 90. sshd takes the FIRST occurrence of a directive, which is why the ssh-hardening role in this catalogue numbers its drop-in 01. Two drop-in systems in one distribution with opposite precedence, and nothing tells you which is which. The live test plants a competing file numbered 10 and requires this policy to win - without it, the claim is untestable on a host where limits.d is empty.

maxlogins is not shipped. pam_limits counts concurrent sessions from utmp; su - writes no utmp record, so the lane cannot produce a second counted session and the control could be written but not demonstrated. It is in resource_limits_extra_lines' documentation rather than in the defaults.

License

Commercial - IaC Bazaar EULA. (c) IaC Bazaar.

Usage code & full reference need an account

The complete copy-paste usage, the full input/output reference, and operational notes are free with an account - shown here and bundled in the download. Sign in and this section fills in.

  • Variables
  • Test

Related modules

Live-tested

ansible-aide

AIDE from AppStream on EL 10: a watched tree, a baseline database built once, and the oneshot unit and timer the package does not ship. The live test passes on an unchanged host, FAILS when a file appears inside a watched path and names it, ignores one inside an excluded path, and passes again once the change is gone. Original role, live-tested on Rocky Linux 10.

View module
Live-tested

ansible-authelia

Authelia from the upstream release (sha256-verified) as a hardened system service on loopback with file users and sqlite; its secrets are generated once on the host and reach the service as AUTHELIA_*_FILE variables. The live test hashes a password with Authelia's hasher, logs in, sees a wrong password refused and an anonymous visitor sent to the portal. Original role, live-tested on Rocky 10.

View module
Live-tested

ansible-bandit

bandit on EL 10 from PyPI into a venv of its own with a link in the PATH; the live test runs pip check, then scans a module that passes a string to subprocess.call with shell=True (B602, High, exit 1) and a clean module (exit 0). Pinned. Original role, live-tested on Rocky Linux 10.

View module
Live-tested

ansible-boundary

boundary on EL 10 from releases.hashicorp.com; SHA256SUMS is signature-checked against HashiCorp's key before get_url trusts it, the live test re-checks both, then runs authenticate against a dead address ('connection refused') and database init with a throwaway config (parsed, root key loaded, then the database refused). Pinned. Original role, live-tested on Rocky Linux 10.

View module
Live-tested

ansible-dockle

dockle on EL 10 from the GitHub release; get_url refuses it unless its SHA-256 is in the vendor's checksums file, and the live test re-checks it, then scans a one-layer image whose config names no user, expecting CIS-DI-0001 in the output, exit code 1 with --exit-level warn, and the warning counted in the JSON form. Pinned. Original role, live-tested on Rocky Linux 10.

View module
Live-tested

ansible-gitleaks

gitleaks on EL 10 from the GitHub release, refused by Ansible's get_url unless its SHA-256 is the one in the project's checksums file, and re-checked with sha256sum -c by the live test, which then plants a file holding an AWS access key that is not one and runs gitleaks detect over it, expecting 'leaks found: 1'. Original role, live-tested on Rocky Linux 10.

View module