Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SSSD Integration

SSSD (System Security Services Daemon) runs on every FreeIPA-enrolled host. It periodically fetches HBAC rules from the FreeIPA LDAP directory and caches them locally for fast evaluation at login time. hbac-rs fits into this architecture as the evaluation engine.

Architecture

graph LR
    A[FreeIPA LDAP] -->|periodic sync| B[SSSD<br/>hbac-rs cache]
    B -->|fast lookup| C[PAM/NSS]
    C -->|auth decision| D[User Login]

The flow:

  1. SSSD connects to FreeIPA LDAP and fetches HBAC rules.
  2. Rules are filtered to those applicable to the local host and stored in an IndexedCache.
  3. When a user attempts to log in, PAM calls into the evaluation engine.
  4. The engine returns allow or deny in sub-microsecond time.

Setting up the pipeline

The local host’s identity determines which rules to cache:

#![allow(unused)]
fn main() {
use hbac_rs::cache::*;

let hostname = std::fs::read_to_string("/etc/hostname")
    .unwrap()
    .trim()
    .to_string();

let host_groups = vec!["web-servers".to_string()]; // from SSSD/FreeIPA

let mut pipeline = RulePipeline::builder()
    .with_filter(
        HostFilter::for_host(&hostname)
            .with_groups(host_groups)
    )
    .with_cache(IndexedCache::new())
    .build();
}

Implementing a rule source

hbac-rs does not include an LDAP client. You implement the RuleSource trait for your preferred LDAP library. Here is an illustrative example:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;
use hbac_rs::cache::{RuleSource, RuleSourceError};

struct LdapRuleSource {
    connection: LdapConn,
    base_dn: String,
}

impl RuleSource for LdapRuleSource {
    fn fetch_all(&mut self) -> Result<Vec<HbacRule>, RuleSourceError> {
        let filter = "(objectClass=ipahbacrule)";
        let results = self.connection
            .search(&self.base_dn, Scope::Subtree, filter, vec!["*"])
            .map_err(|e| RuleSourceError::ConnectionFailed(e.to_string()))?;

        let rules = results
            .into_iter()
            .filter_map(|entry| parse_ldap_entry_to_hbac_rule(entry))
            .collect();

        Ok(rules)
    }

    fn fetch_updated_since(
        &mut self,
        timestamp: u64,
    ) -> Result<Vec<HbacRule>, RuleSourceError> {
        let since = format_ldap_generalized_time(timestamp);
        let filter = format!(
            "(&(objectClass=ipahbacrule)(modifyTimestamp>={}))",
            since
        );

        let results = self.connection
            .search(&self.base_dn, Scope::Subtree, &filter, vec!["*"])
            .map_err(|e| RuleSourceError::ConnectionFailed(e.to_string()))?;

        let rules = results
            .into_iter()
            .filter_map(|entry| parse_ldap_entry_to_hbac_rule(entry))
            .collect();

        Ok(rules)
    }
}
}

The parse_ldap_entry_to_hbac_rule function would map LDAP attributes (ipaUniqueID, memberUser, memberHost, memberService, ipaEnabledFlag, accessRuleType) to HbacRule fields.

Periodic refresh

Run incremental updates in a background thread to pick up rule changes without a full reload:

#![allow(unused)]
fn main() {
use hbac_rs::cache::*;
use std::thread;
use std::time::Duration;

// Initial full load
pipeline.refresh().expect("initial HBAC sync failed");

// Background updates every 5 minutes
thread::spawn(move || {
    loop {
        thread::sleep(Duration::from_secs(300));

        match pipeline.incremental_update() {
            Ok(count) => {
                log::info!("Updated {} HBAC rules", count);
            }
            Err(e) => {
                log::warn!("HBAC incremental update failed: {}", e);
            }
        }
    }
});
}

incremental_update calls source.fetch_updated_since() with the timestamp of the last successful refresh, so only changed rules are fetched.

Disk persistence

With the serde feature enabled, DiskCache persists rules to disk so the host can evaluate access immediately after a restart, even before LDAP is reachable:

#![allow(unused)]
fn main() {
use hbac_rs::cache::DiskCache;

// On startup: try loading from disk first
let mut disk_cache = DiskCache::new("/var/cache/hbac/rules.json")?;

let cached_rules: Vec<HbacRule> = disk_cache
    .get_all()
    .into_iter()
    .cloned()
    .collect();

if !cached_rules.is_empty() {
    pipeline.load(cached_rules);
    log::info!("Loaded HBAC rules from disk cache");
} else {
    pipeline.refresh()?;
    log::info!("No disk cache, performed full LDAP sync");
}

// After each refresh: persist to disk
disk_cache.store(pipeline.rules().iter().cloned().collect());
disk_cache.flush()?;
}

Evaluating access

When PAM or your application needs to check access:

#![allow(unused)]
fn main() {
use hbac_rs::prelude::*;

fn check_user_access(
    policy: &HbacPolicy,
    username: &str,
    groups: &[String],
    hostname: &str,
    host_groups: &[String],
    service: &str,
) -> bool {
    let mut user = Subject::new(username);
    user.roles.extend(groups.iter().cloned());

    let request = HbacRequest::builder()
        .user(user)
        .targethost(hostname)
        .targethost_group(host_groups[0].clone())
        .service(service)
        .build()
        .unwrap();

    policy.check_access(&request)
}
}

Use check_access for the fastest path when you only need a boolean result.

Monitoring

Track cache health to detect synchronization problems early:

#![allow(unused)]
fn main() {
let stats = pipeline.stats();

log::info!(
    "HBAC cache: {} rules, {} bytes, {:.1}% hit rate",
    stats.rule_count,
    stats.memory_bytes,
    stats.hit_rate() * 100.0,
);
}

A sustained 0% hit rate or a drop in rule count may indicate a broken LDAP connection or a misconfigured host filter.

Composite sources

For resilience, use CompositeSource to fall back from LDAP to a local file:

#![allow(unused)]
fn main() {
use hbac_rs::cache::*;

let mut source = CompositeSource::new();
source.add_source(Box::new(ldap_source));
source.add_source(Box::new(file_source));

let pipeline = RulePipeline::builder()
    .with_source(Box::new(source))
    .with_filter(HostFilter::for_host(&hostname))
    .with_cache(IndexedCache::new())
    .build();
}

If the LDAP source returns Err or is_available() returns false, the pipeline tries the next source.

Next steps

Some SSSD deployments need maintenance windows – time-limited rules that grant access during scheduled operations and auto-expire afterward. The next page covers temporal HBAC rules.

See Temporal Rules.