diff --git a/Cargo.toml b/Cargo.toml index da61265..37399f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ tracing-subscriber = "^0.3.17" tracing = "^0.1.37" himmelblau_unix_common = { path = "src/common" } kanidm_unix_common = { path = "src/glue" } -libhimmelblau = { version = "0.3.6" } +libhimmelblau = { version = "0.3.8" } clap = { version = "^4.5", features = ["derive", "env"] } clap_complete = "^4.4.1" reqwest = { version = "^0.12.2", features = ["json"] } diff --git a/platform/debian/himmelblaud-tasks.service b/platform/debian/himmelblaud-tasks.service index e20fa9d..71a08a3 100644 --- a/platform/debian/himmelblaud-tasks.service +++ b/platform/debian/himmelblaud-tasks.service @@ -13,10 +13,9 @@ ExecStart=/usr/sbin/himmelblaud_tasks CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH # SystemCallFilter=@aio @basic-io @chown @file-system @io-event @network-io @sync ProtectSystem=strict -ReadWritePaths=/home /var/run/himmelblaud +ReadWritePaths=/home /var/run/himmelblaud /tmp /etc/krb5.conf.d RestrictAddressFamilies=AF_UNIX NoNewPrivileges=true -PrivateTmp=true PrivateDevices=true PrivateNetwork=true ProtectHostname=true diff --git a/platform/opensuse/himmelblaud-tasks.service b/platform/opensuse/himmelblaud-tasks.service index 4f3d4ae..39cc419 100644 --- a/platform/opensuse/himmelblaud-tasks.service +++ b/platform/opensuse/himmelblaud-tasks.service @@ -13,10 +13,9 @@ ExecStart=/usr/sbin/himmelblaud_tasks CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH # SystemCallFilter=@aio @basic-io @chown @file-system @io-event @network-io @sync ProtectSystem=strict -ReadWritePaths=/home /var/run/himmelblaud +ReadWritePaths=/home /var/run/himmelblaud /tmp /etc/krb5.conf.d RestrictAddressFamilies=AF_UNIX NoNewPrivileges=true -PrivateTmp=true PrivateDevices=true PrivateNetwork=true ProtectHostname=true diff --git a/src/common/src/constants.rs b/src/common/src/constants.rs index 5fb1f6e..3bb4286 100644 --- a/src/common/src/constants.rs +++ b/src/common/src/constants.rs @@ -45,3 +45,4 @@ pub const BROKER_APP_ID: &str = "29d9ed98-a469-4536-ade2-f981bc1d605e"; pub const BROKER_CLIENT_IDENT: &str = "38aa3b87-a06d-4817-b275-7a316988d93b"; pub const CN_NAME_MAPPING: bool = true; pub const DEFAULT_HELLO_PIN_MIN_LEN: usize = 6; +pub const DEFAULT_CCACHE_DIR: &str = "/tmp/krb5cc_"; diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index abbe4d8..114b98a 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -209,6 +209,33 @@ impl IdProvider for HimmelblauMultiProvider { } } + async fn unix_user_ccaches( + &self, + id: &Id, + old_token: Option<&UserToken>, + tpm: &mut tpm::BoxedDynTpm, + machine_key: &tpm::MachineKey, + ) -> (Vec, Vec) { + let account_id = match old_token { + Some(token) => token.spn.clone(), + None => id.to_string().clone(), + }; + match split_username(&account_id) { + Some((_sam, domain)) => { + let providers = self.providers.read().await; + match providers.get(domain) { + Some(provider) => { + provider + .unix_user_ccaches(id, old_token, tpm, machine_key) + .await + } + None => (vec![], vec![]), + } + } + None => (vec![], vec![]), + } + } + async fn unix_user_prt_cookie( &self, id: &Id, @@ -490,6 +517,39 @@ impl IdProvider for HimmelblauProvider { }) } + async fn unix_user_ccaches( + &self, + id: &Id, + old_token: Option<&UserToken>, + tpm: &mut tpm::BoxedDynTpm, + machine_key: &tpm::MachineKey, + ) -> (Vec, Vec) { + let account_id = match old_token { + Some(token) => token.spn.clone(), + None => id.to_string().clone(), + }; + let prt = match self.refresh_cache.refresh_token(&account_id).await { + Ok(prt) => prt, + Err(e) => { + error!("Failed fetching PRT for Kerberos CCache: {:?}", e); + return (vec![], vec![]); + } + }; + let cloud_ccache = self + .client + .write() + .await + .fetch_cloud_ccache(&prt, tpm, machine_key) + .unwrap_or(vec![]); + let ad_ccache = self + .client + .write() + .await + .fetch_ad_ccache(&prt, tpm, machine_key) + .unwrap_or(vec![]); + (cloud_ccache, ad_ccache) + } + async fn unix_user_prt_cookie( &self, id: &Id, diff --git a/src/common/src/idprovider/interface.rs b/src/common/src/idprovider/interface.rs index 43693d9..7cb56a7 100644 --- a/src/common/src/idprovider/interface.rs +++ b/src/common/src/idprovider/interface.rs @@ -180,6 +180,14 @@ pub trait IdProvider { _machine_key: &tpm::MachineKey, ) -> Result; + async fn unix_user_ccaches( + &self, + _id: &Id, + _old_token: Option<&UserToken>, + _tpm: &mut tpm::BoxedDynTpm, + _machine_key: &tpm::MachineKey, + ) -> (Vec, Vec); + async fn unix_user_prt_cookie( &self, _id: &Id, diff --git a/src/common/src/resolver.rs b/src/common/src/resolver.rs index b4bb669..cb3309a 100644 --- a/src/common/src/resolver.rs +++ b/src/common/src/resolver.rs @@ -10,6 +10,7 @@ // use async_trait::async_trait; use hashbrown::HashSet; +use libc::uid_t; use std::collections::BTreeSet; use std::fmt::Display; use std::num::NonZeroUsize; @@ -647,6 +648,32 @@ where } } + pub async fn get_user_ccaches(&self, account_id: Id) -> Option<(uid_t, Vec, Vec)> { + let token = match self.get_usertoken(account_id.clone()).await { + Ok(Some(token)) => token, + _ => { + error!("Failed to fetch unix user token during access token request!"); + return None; + } + }; + + let mut hsm_lock = self.hsm.lock().await; + + let (cloud_ccache, ad_ccache) = self + .client + .unix_user_ccaches( + &account_id, + Some(&token), + hsm_lock.deref_mut(), + &self.machine_key, + ) + .await; + + drop(hsm_lock); + + Some((token.gidnumber, cloud_ccache, ad_ccache)) + } + pub async fn get_user_prt_cookie(&self, account_id: Id) -> Option { let token = match self.get_usertoken(account_id.clone()).await { Ok(Some(token)) => token, diff --git a/src/common/src/unix_proto.rs b/src/common/src/unix_proto.rs index 141acf7..1146b8f 100644 --- a/src/common/src/unix_proto.rs +++ b/src/common/src/unix_proto.rs @@ -8,6 +8,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use libc::uid_t; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] @@ -133,6 +134,7 @@ pub enum TaskRequest { HomeDirectory(HomeDirectoryInfo), LocalGroups(String), LogonScript(String, String), + KerberosCCache(uid_t, Vec, Vec), } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/config/krb5_himmelblau.conf b/src/config/krb5_himmelblau.conf new file mode 100644 index 0000000..d823cad --- /dev/null +++ b/src/config/krb5_himmelblau.conf @@ -0,0 +1,10 @@ +[libdefaults] + default_ccache_name = DIR:/tmp/krb5cc_%{uid} +[domain_realm] + .windows.net = KERBEROS.MICROSOFTONLINE.COM + .azure.net = KERBEROS.MICROSOFTONLINE.COM +[realm] + KERBEROS.MICROSOFTONLINE.COM = { + kdc = https://login.microsoftonline.com/common/kerberos + kpasswd_server = https://login.microsoftonline.com/common/kerberos + } diff --git a/src/daemon/Cargo.toml b/src/daemon/Cargo.toml index cc6aef3..2ad2ee9 100644 --- a/src/daemon/Cargo.toml +++ b/src/daemon/Cargo.toml @@ -47,9 +47,10 @@ async-trait = "0.1.83" name = "himmelblau" maintainer = "David Mulder " depends = ["libssl3", "libsqlite3-0", "libutf8proc3"] -recommends = ["nss-himmelblau", "pam-himmelblau"] +recommends = ["nss-himmelblau", "pam-himmelblau", "krb5-user"] assets = [ ["../../platform/debian/himmelblau.conf.example", "etc/himmelblau/himmelblau.conf", "644"], + ["../../src/config/krb5_himmelblau.conf", "etc/krb5.conf.d/", "644"], ["target/release/aad-tool", "usr/bin/", "755"], ["../../platform/debian/himmelblaud-tasks.service", "etc/systemd/system/", "644"], ["../../platform/debian/himmelblaud.service", "etc/systemd/system/", "644"], @@ -70,6 +71,7 @@ name = "himmelblau" maintainer = "David Mulder " assets = [ { source = "../../src/config/himmelblau.conf.example", dest = "/etc/himmelblau/himmelblau.conf", mode = "644" }, + { source = "../../src/config/krb5_himmelblau.conf", dest = "/etc/krb5.conf.d/", mode = "644" }, { source = "target/release/aad-tool", dest = "/usr/bin/", mode = "755" }, { source = "../../platform/opensuse/himmelblaud-tasks.service", dest = "/usr/lib/systemd/system/", mode = "644" }, { source = "../../platform/opensuse/himmelblaud.service", dest = "/usr/lib/systemd/system/", mode = "644" }, @@ -88,3 +90,4 @@ assets = [ [package.metadata.generate-rpm.recommends] nss-himmelblau = "*" pam-himmelblau = "*" +krb5 = "*" diff --git a/src/daemon/src/daemon.rs b/src/daemon/src/daemon.rs index 862ee3f..ee026a4 100644 --- a/src/daemon/src/daemon.rs +++ b/src/daemon/src/daemon.rs @@ -328,12 +328,7 @@ async fn handle_client( .map(|pam_auth_response| pam_auth_response.into()) .unwrap_or(ClientResponse::Error) { - ClientResponse::PamAuthenticateStepResponse(resp) => { - macro_rules! ret { - () => { - ClientResponse::PamAuthenticateStepResponse(resp) - }; - } + ClientResponse::PamAuthenticateStepResponse(mut resp) => { match auth_session { AuthSession::Success(account_id) => { match resp { @@ -381,30 +376,79 @@ async fn handle_client( Ok(Ok(status)) => { if status == 2 { debug!("Authentication was explicitly denied by the logon script"); - ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied) - } else { - ret!() + resp = + PamAuthResponse::Denied; } } _ => { error!("Execution of logon script failed"); - ret!() } } } Err(e) => { error!("Execution of logon script failed: {:?}", e); - ret!() } } - } else { - ret!() } + + // Initialize the user Kerberos ccache + if let Some((uid, cloud_ccache, ad_ccache)) = + cachelayer + .get_user_ccaches(Id::Name( + account_id.to_string(), + )) + .await + { + let (tx, rx) = oneshot::channel(); + + match task_channel_tx + .send_timeout( + ( + TaskRequest::KerberosCCache( + uid, + cloud_ccache, + ad_ccache, + ), + tx, + ), + Duration::from_millis(100), + ) + .await + { + Ok(()) => { + // Now wait for the other end OR timeout. + match time::timeout_at( + time::Instant::now() + + Duration::from_secs(60), + rx, + ) + .await + { + Ok(Ok(status)) => { + if status != 0 { + error!("Kerberos credential cache load failed for {}: Status code: {}", account_id, status); + } + } + Ok(Err(e)) => { + error!("Kerberos credential cache load failed for {}: {:?}", account_id, e); + } + Err(e) => { + error!("Kerberos credential cache load failed for {}: {:?}", account_id, e); + } + } + } + Err(e) => { + error!("Kerberos credential cache load failed for {}: {:?}", account_id, e); + } + } + } + + ClientResponse::PamAuthenticateStepResponse(resp) } - _ => ret!(), + _ => ClientResponse::PamAuthenticateStepResponse(resp), } } - _ => ret!(), + _ => ClientResponse::PamAuthenticateStepResponse(resp), } } other => other, diff --git a/src/daemon/src/tasks_daemon.rs b/src/daemon/src/tasks_daemon.rs index 03e3c60..ace0588 100644 --- a/src/daemon/src/tasks_daemon.rs +++ b/src/daemon/src/tasks_daemon.rs @@ -32,13 +32,16 @@ use std::{fs, io}; use bytes::{BufMut, BytesMut}; use futures::{SinkExt, StreamExt}; use himmelblau_unix_common::config::{split_username, HimmelblauConfig}; -use himmelblau_unix_common::constants::DEFAULT_CONFIG_PATH; +use himmelblau_unix_common::constants::{DEFAULT_CCACHE_DIR, DEFAULT_CONFIG_PATH}; use himmelblau_unix_common::unix_proto::{HomeDirectoryInfo, TaskRequest, TaskResponse}; use kanidm_utils_users::{get_effective_gid, get_effective_uid}; +use libc::uid_t; use libc::{lchown, umask}; use sketching::tracing_forest::traits::*; use sketching::tracing_forest::util::*; use sketching::tracing_forest::{self}; +use std::fs::OpenOptions; +use std::io::Write; use std::process::Command; use tokio::net::UnixStream; use tokio::sync::broadcast; @@ -296,6 +299,28 @@ fn execute_user_script(account_id: &str, script: &str, access_token: &str) -> i3 } } +fn write_bytes_to_file(bytes: &[u8], filename: &Path, owner: uid_t) -> i32 { + let mut file = match OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(filename) + { + Ok(file) => file, + Err(_) => return 1, + }; + + if file.write_all(bytes).is_err() { + return 2; + } + + if chown(filename, owner).is_err() { + return 3; + } + + 0 +} + async fn handle_tasks(stream: UnixStream, cfg: &HimmelblauConfig) { let mut reqs = Framed::new(stream, TaskCodec::new()); @@ -346,6 +371,55 @@ async fn handle_tasks(stream: UnixStream, cfg: &HimmelblauConfig) { return; } } + Some(Ok(TaskRequest::KerberosCCache(uid, cloud_ccache, ad_ccache))) => { + let ccache_dir_str = format!("{}{}", DEFAULT_CCACHE_DIR, uid); + let ccache_dir = Path::new(&ccache_dir_str); + let create_dir_ret = match fs::create_dir_all(ccache_dir) { + Ok(_) => 0, + Err(e) => { + error!( + "Failed to create the krb5 ccache directory '{}': {:?}", + ccache_dir.display(), + e + ); + 1 + } + }; + let primary_name = ccache_dir.join("primary"); + let _ = write_bytes_to_file(b"tkt\n", &primary_name, uid); + + let cloud_ret = if !cloud_ccache.is_empty() { + // The cloud_tkt is the primary only if the on-prem isn't + // present. + let name = if !ad_ccache.is_empty() { + "cloud_tkt" + } else { + "tkt" + }; + let cloud_ccache_name = ccache_dir.join(name); + write_bytes_to_file(&cloud_ccache, &cloud_ccache_name, uid) * 10 + } else { + 0 + }; + + let ad_ret = if !ad_ccache.is_empty() { + // If the on-prem ad_tkt exists, it overrides the primary + let name = "tkt"; + let ad_ccache_name = ccache_dir.join(name); + write_bytes_to_file(&ad_ccache, &ad_ccache_name, uid) * 100 + } else { + 0 + }; + + // Indicate the status response + if let Err(e) = reqs + .send(TaskResponse::Success(create_dir_ret + cloud_ret + ad_ret)) + .await + { + error!("Error -> {:?}", e); + return; + } + } other => { error!("Error -> {:?}", other); return;