From a8f817c720f39a2fb91dedb75f6ef78a72dfb07a Mon Sep 17 00:00:00 2001 From: Connor Peshek Date: Tue, 11 Aug 2026 18:58:19 -0500 Subject: [PATCH] ak-sysd: read managed preferences via CFPreferences, not defaults(1) --- Cargo.lock | 2 ++ ak-sysd/Cargo.toml | 4 +++ ak-sysd/src/cfg/managed.rs | 71 ++++++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5457a326..4362e11a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,8 @@ dependencies = [ "chrono", "clap", "clap_complete", + "core-foundation", + "core-foundation-sys", "eyre", "hex", "httptest", diff --git a/ak-sysd/Cargo.toml b/ak-sysd/Cargo.toml index ecff838c..9ffcb82a 100644 --- a/ak-sysd/Cargo.toml +++ b/ak-sysd/Cargo.toml @@ -52,6 +52,10 @@ signal-hook = "0.4" [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.186" +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.10" +core-foundation-sys = "0.8" + [target.'cfg(windows)'.dependencies] windows-service = "0.8" winreg = { version = "0.56", features = ["serialization-serde"] } diff --git a/ak-sysd/src/cfg/managed.rs b/ak-sysd/src/cfg/managed.rs index b5430189..972fc352 100644 --- a/ak-sysd/src/cfg/managed.rs +++ b/ak-sysd/src/cfg/managed.rs @@ -9,29 +9,64 @@ pub struct SysdManagedConfig { /// package. Returns `Ok(None)` if there is no managed config source on this /// platform or no value is currently set (both are normal, not errors). #[cfg(target_os = "macos")] -pub fn load_managed_config() -> Result> { - // `defaults read` resolves the same preferences domain - // (`io.goauthentik.platform`) that Go's CFPreferencesCopyAppValue call - // reads from. Shelling out avoids a raw Core Foundation FFI dependency; - // if strict merging of the MDM-managed preferences layer (as opposed to - // this host's local preferences) turns out to matter, replace this with - // a direct CFPreferencesCopyAppValue call instead. - let read = |key: &str| -> Option { - let out = std::process::Command::new("defaults") - .args(["read", "io.goauthentik.platform", key]) - .output() - .ok()?; - if !out.status.success() { +const MANAGED_APP_ID: &str = "io.goauthentik.platform"; + +/// Reads a single MDM-forced string preference. +/// +/// `CFPreferencesCopyAppValue` is the API Go used, and it is the only way to +/// see the managed layer: profile-delivered values live in +/// `/Library/Managed Preferences/.plist`, and `defaults read ` +/// does **not** resolve them — it reports "domain does not exist" on a machine +/// that plainly has the plist. Shelling out therefore made managed enrollment a +/// silent no-op on every managed Mac. +/// +/// `CFPreferencesAppValueIsForced` gates on the value actually being managed. +/// `CopyAppValue` alone returns the *effective* value, which may come from +/// ordinary user defaults — and this value decides which authentik a root +/// daemon enrolls against, so anything not delivered by MDM is ignored. +#[cfg(target_os = "macos")] +fn managed_string(key: &str) -> Option { + use core_foundation::base::TCFType; + use core_foundation::string::CFString; + use core_foundation_sys::base::{CFGetTypeID, CFRelease}; + use core_foundation_sys::preferences::{ + CFPreferencesAppValueIsForced, CFPreferencesCopyAppValue, + }; + use core_foundation_sys::string::CFStringRef; + + let cf_key = CFString::new(key); + let cf_app = CFString::new(MANAGED_APP_ID); + + unsafe { + if CFPreferencesAppValueIsForced(cf_key.as_concrete_TypeRef(), cf_app.as_concrete_TypeRef()) + == 0 + { return None; } - let val = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if val.is_empty() { None } else { Some(val) } - }; - let Some(url) = read("URL") else { + let value = + CFPreferencesCopyAppValue(cf_key.as_concrete_TypeRef(), cf_app.as_concrete_TypeRef()); + if value.is_null() { + return None; + } + + // Copy* returns +1; we own it from here. + if CFGetTypeID(value) != CFString::type_id() { + CFRelease(value); + tracing::warn!(key, "managed preference is not a string, ignoring"); + return None; + } + let s = CFString::wrap_under_create_rule(value as CFStringRef).to_string(); + if s.is_empty() { None } else { Some(s) } + } +} + +#[cfg(target_os = "macos")] +pub fn load_managed_config() -> Result> { + let Some(url) = managed_string("URL") else { return Ok(None); }; - let Some(registration_token) = read("RegistrationToken") else { + let Some(registration_token) = managed_string("RegistrationToken") else { return Ok(None); }; Ok(Some(SysdManagedConfig {