Linux

FreeIPA HBAC: From allow_all to Least Privilege with hbactest

The default allow_all rule that FreeIPA ships with lets every enrolled user log in to every enrolled host on every service. That is fine for the five minutes after ipa-server-install finishes. It is not fine when you have contractors, developers, and DBAs sharing the same realm. Host-Based Access Control (HBAC) is how you replace that wide-open default with rules that match how your team actually works.

Original content from computingforgeeks.com - post 167500

This guide walks through building a complete least-privilege HBAC policy from scratch. You will plan an access model, create the rules, validate every combination with hbactest, disable allow_all safely without locking yourself out, layer service-specific controls (sshd vs sudo vs Cockpit), handle AD-trusted users via external groups, and debug rules with sssctl when login attempts fail. Twelve real scenarios are captured with the actual command output and Web UI screenshots from a 4-node lab.

The CLI surface (ipa hbacrule-*, ipa hbactest, ipa hbacsvc-add, sssctl user-checks) is identical on FreeIPA 4.9 (RHEL/Rocky/AlmaLinux 8), 4.11 (9.x), and 4.12 (10.x). The only version-specific section is the RHEL 10 pitfall callout near the end.

How HBAC works in FreeIPA

HBAC controls who can authenticate to which host for which service. A rule has four parts:

  • Who — a user, a user group, or all users (the special category=all
  • Where — a host, a host group, or all hosts
  • What — an HBAC service (sshd, sudo, login, cockpit), a service group, or all services
  • State — enabled or disabled

HBAC rules are allow-only. There is no explicit deny. A login attempt is permitted if any enabled rule matches all three of (user, host, service); otherwise it is denied. PAM consults SSSD which consults the IPA server’s HBAC plugin on every login. Caching means rule changes propagate within the SSSD refresh interval (default 5400 seconds, tunable per realm).

HBAC does not apply to AD-trusted users by default. They need an external group inside the IdM realm that maps the AD security identifier (SID) into a referenceable HBAC subject. We cover that pattern in section 12.

Prerequisites

  • One FreeIPA server reachable on its FQDN (we use ipa.cfg-lab.local, IP 192.168.1.121)
  • Three IPA-enrolled clients in the same realm: web01, db01, bastion01
  • IPA admin credentials (Kerberos ticket via kinit admin)
  • Server and clients on RHEL 8.x / 9.x / 10.x family (Rocky, AlmaLinux, RHEL). Lab here is Rocky 10.1.
  • SSSD client on each enrolled host (installed automatically by ipa-client-install)

If you do not have a running IPA realm yet, follow our FreeIPA server install guide first, then come back here. For a containerized setup the FreeIPA in Docker / Podman walkthrough achieves the same end state without dedicating a full VM.

The default allow_all rule and why it is dangerous

Right after install, every realm has exactly one HBAC rule: allow_all. List it:

kinit admin
ipa hbacrule-find

The output shows the rule with three category fields set to “all”: user, host, service. That means any account, on any enrolled host, for any service. Combined with the default password policy (8-character minimum, no lockout history), it is one stolen developer password away from total realm compromise.

FreeIPA HBAC Rules list with allow_all enabled on Rocky Linux 10
FreeIPA allow_all rule detail showing User, Host, and Service all set to category=all on Rocky Linux 10

The fix is not to delete allow_all. Disable it once the replacement rules are in place. Deleting it would leave nothing for emergency recovery if you misconfigure the replacements and lose all admin sessions at the same time. We disable rather than delete in step 9.

Plan your access model

Map out the access matrix before touching the CLI. The five-team lab below is small enough to fit in a paragraph and large enough to cover every HBAC primitive.

GroupMembersAllowed hostsAllowed servicesNotes
sysadminsaliceall hostsany servicebreak-glass for everything
web-adminsbobwebservers hostgroupsshd, sudoweb tier only
db-adminscharliedbservers hostgroupsshd, sudodatabase tier only
developersdianabastions hostgroupsshdno sudo anywhere
contractorsevebastions hostgroupsshdaccount expires in 7 days

The model has three host groups (webservers, dbservers, bastions), a fourth production group that nests webservers and dbservers, and five user groups. Nesting lets a future “all-production-readonly” rule target both web and db tiers without restating membership.

Seed the lab: users, groups, hosts, host groups

Get a Kerberos ticket and create the user groups first:

kinit admin
ipa group-add sysadmins   --desc='System administrators (break-glass)'
ipa group-add web-admins  --desc='Web tier admins'
ipa group-add db-admins   --desc='Database tier admins'
ipa group-add developers  --desc='Application developers'
ipa group-add contractors --desc='Time-limited contractors'

Create the five users. The --random flag generates a one-time password that the user must change on first login.

ipa user-add alice   --first=Alice   --last=Admin     [email protected]   --random
ipa user-add bob     --first=Bob     --last=Web       [email protected]     --random
ipa user-add charlie --first=Charlie --last=Database  [email protected] --random
ipa user-add diana   --first=Diana   --last=Developer [email protected]   --random
ipa user-add eve     --first=Eve     --last=External  [email protected]     --random

Capture the random passwords from the command output; you need them when testing logins from each client. Add users to their groups:

ipa group-add-member sysadmins   --users=alice
ipa group-add-member web-admins  --users=bob
ipa group-add-member db-admins   --users=charlie
ipa group-add-member developers  --users=diana
ipa group-add-member contractors --users=eve

Time-bound the contractor account so it expires automatically. RHEL 10’s IdM exposes the principal expiration directly:

EXP_DATE=$(date -u -d '+7 days' '+%Y-%m-%dT%H:%M:%SZ')
ipa user-mod eve --principal-expiration="$EXP_DATE"

Now the host groups (the hosts themselves are already enrolled):

ipa hostgroup-add webservers --desc='Web tier'
ipa hostgroup-add dbservers  --desc='Database tier'
ipa hostgroup-add bastions   --desc='Bastion / jump hosts'
ipa hostgroup-add production --desc='Production (web + db, nested)'

ipa hostgroup-add-member webservers --hosts=web01.cfg-lab.local
ipa hostgroup-add-member dbservers  --hosts=db01.cfg-lab.local
ipa hostgroup-add-member bastions   --hosts=bastion01.cfg-lab.local
ipa hostgroup-add-member production --hostgroups=webservers --hostgroups=dbservers

Verify the nested membership:

ipa hostgroup-show production --all | grep -E 'Member|host'
FreeIPA user groups list showing sysadmins, web-admins, db-admins, developers, contractors on Rocky Linux 10
FreeIPA sysadmins group detail with member alice in IPA Web UI on Rocky Linux 10

Build least-privilege HBAC rules

Five rules cover the access matrix. Build them with allow_all still enabled so a misconfigured rule does not lock anyone out mid-rollout.

Rule 1: sysadmins go everywhere

ipa hbacrule-add sysadmins-all \
  --desc='Sysadmins: all hosts, any service' \
  --servicecat=all --hostcat=all

ipa hbacrule-add-user sysadmins-all --groups=sysadmins

Note the --servicecat=all and --hostcat=all. The “all” categories override individual service or host bindings on the same rule, so sysadmins reach every enrolled host. We do not set --usercat=all because the user side is restricted to the sysadmins group.

Rule 2: web admins on the web tier

ipa hbacrule-add webadmins-web \
  --desc='Web admins: webservers, sshd + sudo'

ipa hbacrule-add-user    webadmins-web --groups=web-admins
ipa hbacrule-add-host    webadmins-web --hostgroups=webservers
ipa hbacrule-add-service webadmins-web --hbacsvcs=sshd --hbacsvcs=sudo

Rule 3: db admins on the database tier

ipa hbacrule-add dbadmins-db \
  --desc='DB admins: dbservers, sshd + sudo'

ipa hbacrule-add-user    dbadmins-db --groups=db-admins
ipa hbacrule-add-host    dbadmins-db --hostgroups=dbservers
ipa hbacrule-add-service dbadmins-db --hbacsvcs=sshd --hbacsvcs=sudo

Rule 4: developers on the bastion (sshd only)

ipa hbacrule-add developers-bastion \
  --desc='Developers: bastion ssh only, no sudo'

ipa hbacrule-add-user    developers-bastion --groups=developers
ipa hbacrule-add-host    developers-bastion --hostgroups=bastions
ipa hbacrule-add-service developers-bastion --hbacsvcs=sshd

Omitting sudo from the service list means developers log in but cannot escalate. This is the common “deploy via CI, debug via read-only” pattern.

Rule 5: contractors on the bastion (time-limited)

ipa hbacrule-add contractors-bastion \
  --desc='Contractors: bastion ssh only'

ipa hbacrule-add-user    contractors-bastion --groups=contractors
ipa hbacrule-add-host    contractors-bastion --hostgroups=bastions
ipa hbacrule-add-service contractors-bastion --hbacsvcs=sshd

The time limit comes from the principal expiration we set on eve, not the HBAC rule itself. HBAC has no native time-of-day or date-range filter. Principal expiry and Kerberos ticket policy together cover most “temporary access” cases.

FreeIPA sysadmins-all HBAC rule detail with host category and service category set to all on Rocky Linux 10
FreeIPA webadmins-web HBAC rule detail with web-admins user group, webservers host group, sshd and sudo services on Rocky Linux 10
FreeIPA developers-bastion HBAC rule restricted to sshd service only (no sudo) on Rocky Linux 10

Test every rule with hbactest before disabling allow_all

The ipa hbactest command simulates the IPA server’s HBAC decision engine for any (user, host, service) tuple without the user actually logging in. This is the single most important habit in HBAC operations. Test every combination before you change which rules are enabled.

The flag set for hbactest:

FlagWhat it does
--userThe user being tested
--hostThe target host FQDN
--serviceThe HBAC service (sshd, sudo, cockpit, login)
--rulesRestrict the test to specific rules (defaults to all enabled)
--enabled / --disabledInclude only enabled or only disabled rules
--nodetailJust print Access granted/denied without per-rule trace

Run nine representative tests covering each rule’s positive and negative paths:

# Sysadmin alice → web01 → sshd (should ALLOW via sysadmins-all)
ipa hbactest --user=alice --host=web01.cfg-lab.local --service=sshd

# Web admin bob → web01 → sshd (ALLOW via webadmins-web)
ipa hbactest --user=bob --host=web01.cfg-lab.local --service=sshd

# Web admin bob → db01 → sshd (DENY, bob is not in db-admins)
ipa hbactest --user=bob --host=db01.cfg-lab.local --service=sshd

# DB admin charlie → db01 → sudo (ALLOW)
ipa hbactest --user=charlie --host=db01.cfg-lab.local --service=sudo

# Developer diana → bastion01 → sshd (ALLOW)
ipa hbactest --user=diana --host=bastion01.cfg-lab.local --service=sshd

# Developer diana → bastion01 → sudo (DENY, sudo not in developers-bastion service list)
ipa hbactest --user=diana --host=bastion01.cfg-lab.local --service=sudo

# Developer diana → web01 → sshd (DENY, wrong hostgroup)
ipa hbactest --user=diana --host=web01.cfg-lab.local --service=sshd

# Contractor eve → bastion01 → sshd (ALLOW)
ipa hbactest --user=eve --host=bastion01.cfg-lab.local --service=sshd

# Force the test to ignore allow_all (test the new rules only)
ipa hbactest --user=alice --host=web01.cfg-lab.local --service=sshd \
  --rules=sysadmins-all,webadmins-web,dbadmins-db,developers-bastion,contractors-bastion

The expected output for the first two scenarios looks like this:

###### Scenario 1: alice (sysadmin) → web01 → sshd ######
--------------------
Access granted: True
--------------------
  Matched rules: sysadmins-all
  Not matched rules: allow_systemd-user
  Not matched rules: contractors-bastion
  Not matched rules: dbadmins-db
  Not matched rules: developers-bastion
  Not matched rules: sysadmins-cockpit
  Not matched rules: webadmins-web

###### Scenario 2: bob (web-admin) → web01 → sshd ######
--------------------
Access granted: True
--------------------
  Matched rules: webadmins-web
  Not matched rules: allow_systemd-user
  Not matched rules: contractors-bastion
  Not matched rules: dbadmins-db
  Not matched rules: developers-bastion
  Not matched rules: sysadmins-all
  Not matched rules: sysadmins-cockpit
ipa hbactest output showing Access granted True with matched rules on Rocky Linux 10

The matched rule appears in every successful test. The “Not matched” rules show which other rules were evaluated and rejected, which is gold when troubleshooting why a rule you expected to match did not.

The HBAC Test page in the Web UI walks through the same logic with a wizard:

FreeIPA HBAC Test wizard in the IPA Web UI on Rocky Linux 10

Every test should return either Access granted: True with the matching rule, or Access granted: False with a list of rules that were checked and failed to match. If the result does not match the expected outcome, fix the rule before moving on.

One more useful invocation: run hbactest as if only the new rules existed, ignoring allow_all entirely. This shows what the realm looks like the moment you flip allow_all off.

ipa hbactest --user=bob --host=db01.cfg-lab.local --service=sshd \
  --rules=sysadmins-all,webadmins-web,dbadmins-db,developers-bastion,contractors-bastion

With allow_all still active, bob can technically SSH to db01 because allow_all matches. With allow_all excluded from the test, bob is denied. This is exactly the behavior we want post-cutover.

Disable allow_all safely

Now flip the switch. Two safety steps first.

Step 1. Open a second SSH session to the IPA server as a sysadmin user (alice from our setup, or an admin account). Keep it open. If anything goes wrong with the cutover, this session lets you re-enable allow_all without scrambling.

Step 2. Confirm at least one of your new rules covers an account that can administer the realm. The sysadmins-all rule plus alice’s membership covers it; verify both:

ipa hbacrule-show sysadmins-all
ipa group-show sysadmins
ipa hbactest --user=alice --host=ipa.cfg-lab.local --service=sshd \
  --rules=sysadmins-all

The hbactest must return Access granted: True. If it does not, do NOT disable allow_all.

When both checks pass, disable the default rule:

ipa hbacrule-disable allow_all

SSSD on each enrolled host caches HBAC decisions for the duration of krb5_renew_interval (default 3600 s). Force a refresh so the change takes effect immediately:

sudo sss_cache -E
sudo systemctl restart sssd

Run this on every enrolled host. Then re-test from each host with an actual SSH login from a denied account; the deny should now fire.

After the disable, the rules list shows allow_all with its status flipped to Disabled:

FreeIPA HBAC Rules list with allow_all disabled and five least-privilege rules enabled on Rocky Linux 10

Re-run a representative hbactest for a denied combination. Bob is in web-admins, not db-admins, so SSH to db01 should now fail:

ipa hbactest showing Access granted False after disabling allow_all on Rocky Linux 10

Service-specific HBAC (cockpit, su, sftp)

FreeIPA ships HBAC services for the common PAM stack entries: sshd, login, su, su-l, sudo, sudo-i, passwd, and a few more. List them:

ipa hbacsvc-find

Cockpit (the RHEL web console) is not in the default list. Add it so HBAC can gate Web console logins:

ipa hbacsvc-add cockpit --desc='Cockpit web console'

Hook Cockpit’s PAM stack into the new service. On every host that exposes Cockpit, edit /etc/pam.d/cockpit and add the SSSD HBAC check. Rocky 10 already includes pam_sss in the stack via authselect, so the only edit is adding the service=cockpit argument to the pam_sss.so auth line:

echo "auth required pam_sss.so service=cockpit" | sudo tee -a /etc/pam.d/cockpit

Now create an HBAC rule that allows sysadmins (only) to log in via the web console:

ipa hbacrule-add sysadmins-cockpit \
  --desc='Sysadmins web console access' \
  --hostcat=all

ipa hbacrule-add-user    sysadmins-cockpit --groups=sysadmins
ipa hbacrule-add-service sysadmins-cockpit --hbacsvcs=cockpit

Test it:

ipa hbactest --user=alice --host=web01.cfg-lab.local --service=cockpit
ipa hbactest --user=bob   --host=web01.cfg-lab.local --service=cockpit

Alice gets Access granted: True. Bob is denied even though webadmins-web matches him on sshd and sudo, because cockpit is not in that rule’s service list and sysadmins-cockpit does not match the bob user.

HBAC + sudo: how they interact

This trips up most operators. HBAC and sudo are two independent allow lists. To run a command with sudo, both must permit it:

  • HBAC must allow the user to authenticate via the sudo service on the host.
  • An IPA sudo rule must grant the user the right to run the specific command as the target user.

An HBAC rule with --hbacsvcs=sudo only opens the gate at the PAM layer. The user still cannot run anything until a sudo rule authorizes specific commands. The reverse is also true: a sudo rule alone is useless if HBAC denies the sudo service.

Create a minimal sudo rule for the web admins (restart nginx only):

ipa sudocmd-add /usr/bin/systemctl --desc='systemctl'
ipa sudorule-add webadmins-nginx \
  --desc='Web admins: nginx control' \
  --hostcat=all

ipa sudorule-add-user        webadmins-nginx --groups=web-admins
ipa sudorule-add-host        webadmins-nginx --hostgroups=webservers
ipa sudorule-add-allow-command webadmins-nginx --sudocmds='/usr/bin/systemctl'
ipa sudorule-add-option      webadmins-nginx --sudooption='!authenticate'

Confirm both layers agree by combining hbactest with a real sudo invocation:

# bob can pass the HBAC sudo gate on web01
ipa hbactest --user=bob --host=web01.cfg-lab.local --service=sudo

# After login, bob can run only systemctl, nothing else
ssh [email protected] 'sudo -l'

The HBAC Services list and the Cockpit service we added earlier:

FreeIPA HBAC Services list including the custom cockpit service on Rocky Linux 10
FreeIPA HBAC service cockpit detail in the IPA Web UI on Rocky Linux 10
FreeIPA sysadmins-cockpit HBAC rule allowing sysadmins group to use the cockpit service on Rocky Linux 10

HBAC for AD-trusted users (external groups)

HBAC rules cannot reference an Active Directory user or group directly. The pattern is to create a POSIX group inside IdM marked as “external”, add the AD principal to it via SID, and then use that IdM group in HBAC rules.

Assuming a one-way trust to AD.EXAMPLE.COM is already configured (see our FreeIPA-AD trust guide):

# 1. Create an external group to hold AD members
ipa group-add ad-developers --external --desc='Mirror of AD\\Developers'

# 2. Create a POSIX group to use in HBAC rules (external groups cannot be used directly)
ipa group-add ad-developers-posix --desc='POSIX wrapper for ad-developers'

# 3. Nest the external group inside the POSIX one
ipa group-add-member ad-developers-posix --groups=ad-developers

# 4. Add the AD group by name (resolves to SID)
ipa group-add-member ad-developers --external='AD\\Developers'

# 5. Use the POSIX wrapper in an HBAC rule
ipa hbacrule-add ad-devs-bastion --desc='AD developers on bastion'
ipa hbacrule-add-user    ad-devs-bastion --groups=ad-developers-posix
ipa hbacrule-add-host    ad-devs-bastion --hostgroups=bastions
ipa hbacrule-add-service ad-devs-bastion --hbacsvcs=sshd

Two callouts that catch people off guard:

  • IdM password policy and HBAC auth indicators do NOT apply to AD-trusted users. Their authentication is enforced by AD’s policy. HBAC controls only whether the AD-resolved user can reach the host/service combination.
  • The external/POSIX two-group dance is required. Skipping the POSIX wrapper and adding the external group directly to an HBAC rule fails silently — the rule will exist but never match. Audit existing rules for this pattern with ipa hbacrule-find --all | grep -E 'memberuser|external'.

Auth indicator restricted HBAC

FreeIPA can record how a Kerberos ticket was obtained (password, OTP, PKINIT, passkey) and stamp that indicator into the ticket. HBAC rules can then require a specific indicator before granting access. This is FreeIPA’s native step-up authentication mechanism.

Force the sysadmins-cockpit rule to only grant access if the user authenticated with OTP:

# Enable OTP as a valid auth type for the sysadmins group
ipa user-mod alice --user-auth-type=otp,password

# Require the otp indicator on the cockpit service ticket
ipa service-mod HTTP/[email protected] --auth-ind=otp

# The HBAC rule does not need to change — the requirement lives on the service principal

Now alice’s password-only ticket is rejected for the HTTP/web01 service even though HBAC nominally allows it. She has to kinit -T armor.cc alice with OTP first. The full pattern is covered in our upcoming Modern Authentication guide.

Troubleshooting: when HBAC says no but you expected yes

The decision flow when a login fails:

  1. Read the server side answer first: ipa hbactest --user=USER --host=HOST --service=SERVICE. This tells you what the IPA server thinks should happen.
  2. If the server side is correct but the client still rejects the login, the SSSD cache is stale. Force a refresh: sudo sss_cache -E; sudo systemctl restart sssd.
  3. If that does not help, ask SSSD directly: sssctl user-checks USER -s sshd. This runs the same NSS + PAM + access-control walk the real SSH login would do.
  4. Tail the SSSD logs while reproducing the failure: sudo journalctl -u sssd -f in one window, attempt the login in another. The sssd_be and sssd_pam logs show the HBAC decision being made.

Common error patterns:

  • “Access denied” but hbactest says allowed — almost always SSSD cache. sss_cache -E fixes it. If it does not, the host’s SSSD is talking to a stale replica.
  • External AD group rule never matches — missing the POSIX wrapper group (see section 12).
  • Sudo asks for password despite !authenticate — the HBAC service for sudo is not allowed on this host. Sudo falls back to local PAM and prompts. Fix the HBAC rule first.
  • SSSD silently passes despite a deny rule — confirm access_provider = ipa in /etc/sssd/sssd.conf. If it is permit, HBAC is being ignored.
sssctl user-checks and SSSD journal output showing HBAC rule evaluation on Rocky Linux 10

RHEL 10 pitfall: SHA-1 in FIPS mode breaks replica joins

If the realm uses FIPS 140-3 strict mode (the default on RHEL 10 with fips-mode-setup --enable), legacy replicas built on RHEL 9 with SHA-1 service tickets cannot join the realm. The symptom is an HBAC test that returns the right answer but real SSH logins to the legacy replica fail with GSS-API error. Migrate older replicas to RHEL 10.1 before the FIPS switch or accept that they will need to be rebuilt. Track 6.11 covers the full mitigation.

ipa-healthcheck for HBAC drift

Run ipa-healthcheck in JSON mode and filter for the HBAC checks. This catches disabled rules that should not be disabled, missing memberships, and orphaned references.

sudo ipa-healthcheck --source=ipahealthcheck.ipa.hbac --output-type=json | jq .

Pipe the output into Prometheus via the node-exporter textfile collector. The upcoming Track 6.1 guide wires this into a Grafana panel that alerts when allow_all flips back to enabled or when the rule count drops below a threshold.

Frequently asked questions

Does FreeIPA HBAC support deny rules?

No. HBAC rules are allow-only. The deny is implicit: a login fails when no enabled rule matches all three of user, host, and service. Build tighter allow rules instead of trying to express a deny.

Can HBAC restrict access by time of day?

Not directly. HBAC has no schedule field. For temporary access, use principal expiration (ipa user-mod --principal-expiration) or Kerberos ticket policy (ipa krbtpolicy-mod --maxlife) to constrain the lifetime of credentials. For repeating time windows, the common pattern is to enable and disable the rule on a cron job from a dedicated automation account.

What is the difference between an HBAC service and an HBAC service group?

An HBAC service is one PAM service name (sshd, sudo, cockpit). A service group bundles multiple services so a single rule can grant them together. Useful for “all remote access” (sshd + login + cockpit) or “all sudo variants” (sudo + sudo-i + su). Create with ipa hbacsvcgroup-add.

Will HBAC changes propagate immediately?

On the IPA server, yes. On enrolled clients, SSSD caches the decision. Without a manual cache flush the change takes effect within krb5_renew_interval seconds (default 3600). Force immediate propagation with sudo sss_cache -E && sudo systemctl restart sssd on each client.

What’s next

Layer this on top of a freshly enrolled lab and you have a least-privilege foundation that covers dozens of host groups without a flat allow_all. The next steps in the FreeIPA series cover the replication topology that keeps these rules available during partial outages, IdM sudo rule patterns that integrate with these HBAC rules (sudo cookbook, coming next), and the modern authentication options (passkey, OTP, external IdP) that make every rule above stronger. The upstream FreeIPA project documentation and the Red Hat IdM guide for RHEL 10 are the two reference texts to keep open while building your own access model.

If a login is failing right now and you cannot tell whether HBAC, sudo, or SSSD is to blame, run the four-step troubleshooting flow in section 14 and stop at the first one that gives you a deterministic answer.

Related Articles

Kali Linux Password Cracking with Hashcat and John the Ripper on Kali Security 5 Must-Have Linux Security Tools for Your Organization Security Configure RDP, SSH, and VNC Connections in Apache Guacamole Cloud Guardians of the Digital Fortress: Cybersecurity in Corporate Giants

Leave a Comment

Press ESC to close