HBAC Integration
On a Linux estate managed by FreeIPA, who may log in where is already
decided by HBAC rules: the coding-agents group may reach sshd on
development hosts, never on production. When an AI agent then asks, over
RFC 9396, to run cargo on that host, two questions must both be answered:
- May this user reach this service on this host? (HBAC)
- Are the requested
authorization_detailsacceptable? (rules)
With the hbac feature, authz_details_rs::hbac::HostScopedPolicy
answers both at once, reusing your existing hbac-rs policy instead of
re-encoding host checks as $.host field conditions.
[dependencies]
authz-details-rs = { version = "0.2", features = ["hbac"] }
hbac-rs = "0.2"
Composing HBAC with rules
A HostScopedRequest pairs an HbacRequest (user, target host, service)
with the requested details. The two dimensions are combined with the
workspace-wide CombineMode, HBAC first:
#![allow(unused)]
fn main() {
use authz_details_rs::hbac::{HostScopedPolicy, HostScopedRequest};
use authz_details_rs::prelude::*;
use hbac_rs::{HbacPolicy, HbacRequest, HbacRule};
use serde_json::json;
let mut hbac = HbacPolicy::new();
hbac.add_rule(
HbacRule::builder("coding-agents-on-dev")
.user_group("coding-agents")
.host("dev01.example.com")
.service("sshd")
.enabled(true)
.build()
.unwrap(),
)
.unwrap();
let mut rules = AuthzDetailsPolicy::new();
rules
.add_rule(
AuthzDetailsRule::builder("unprivileged-build-tools")
.type_id("linux_exec")
.field("$.privileged").eq(false)
.field("$.commands[*]").is_in(["cargo", "git", "make"])
.build()
.unwrap(),
)
.unwrap();
let policy = HostScopedPolicy::new(hbac, rules, CombineMode::And);
let agent = Subject::builder().id("agent").role("coding-agents").build().unwrap();
let cargo = || {
vec![AuthorizationDetail::new(
"linux_exec",
json!({"actions": ["run"], "commands": ["cargo"], "privileged": false}),
)]
};
let on_dev = HostScopedRequest::new(HbacRequest::new(agent.clone(), "dev01.example.com", "sshd"), cargo());
let result = policy.evaluate(&on_dev);
assert!(result.is_allowed());
assert_eq!(result.hbac.matched_rules, ["coding-agents-on-dev"]);
assert_eq!(result.details.matched_rules, ["unprivileged-build-tools"]);
// Same command on a production host: HBAC has no rule for it
let on_prod = HostScopedRequest::new(HbacRequest::new(agent, "prod01.example.com", "sshd"), cargo());
assert!(!policy.check_access(&on_prod));
}
The result carries both dimensions’ results (hbac with its matched HBAC
rules, details with the rules’ verdict), so a denial can be attributed to
the right policy. evaluate_at / check_access_at evaluate at a fixed
Timestamp; HBAC temporal rules and authz-details temporal rules then use
the same instant.
CombineMode | Outcome |
|---|---|
And | HBAC and the rules must both allow (the usual choice) |
Or | Either may allow |
PreferFirst | HBAC decides when an HBAC rule matched; otherwise the rules decide |
PreferSecond | The rules decide when a rule applies; otherwise HBAC decides |
Python
authz_details.HostScopedPolicy takes an hbac_rs.HbacPolicy and a rule
policy. The HBAC rules (including temporal and disabled rules) are copied
when the policy is created; subjects may be acls_rs.Subject or
hbac_rs.Subject objects, and host/service groups are keyword arguments:
import hbac_rs
from acls_rs import Subject
from authz_details import AuthzDetailsPolicy, AuthzDetailsRule, CombineMode, HostScopedPolicy
hbac = hbac_rs.HbacPolicy()
hbac.add_rule(
hbac_rs.HbacRuleBuilder("coding-agents-on-dev")
.user_group("coding-agents")
.host_group("dev-hosts")
.service("sshd")
.enabled(True)
.build()
)
rules = AuthzDetailsPolicy([
AuthzDetailsRule.builder("unprivileged-build-tools")
.type_id("linux_exec")
.field("$.privileged").eq(False)
.field("$.commands[*]").is_in(["cargo", "git", "make"])
.build()
])
policy = HostScopedPolicy(hbac, rules, CombineMode.AND)
agent = Subject("agent")
agent.add_role("coding-agents")
cargo = [{"type": "linux_exec", "actions": ["run"], "commands": ["cargo"], "privileged": False}]
result = policy.evaluate(agent, "dev01.example.com", "sshd", cargo, host_groups=["dev-hosts"])
assert result and result.hbac_matched_rules == ["coding-agents-on-dev"]
assert not policy.check_access(agent, "prod01.example.com", "sshd", cargo)
The result exposes allowed, hbac_allowed, hbac_matched_rules and the
rules’ details result; evaluate_at / check_access_at take a
timestamp. Like hbac_rs.HbacPolicy, a HostScopedPolicy is bound to the
thread that created it.
Composing with other policies
HostScopedPolicy is built from the shared composition primitives in
acls_rs::policy::composition (see the acls-rs Bridge).
Both HbacPolicy and AuthzDetailsPolicy implement PolicyDimension, so
you can combine them – or any other dimension – with combine directly
when you need a different shape of request.
Next steps
The runnable agent_linux_workstation example
(cargo run -p authz-details-rs --features hbac --example agent_linux_workstation)
walks through a coding agent’s consent, delegation, task-scoped tokens and
host enforcement end to end.