Managed Preferences 到底贏不贏得過本機設定?自己寫工具測給自己看

分享
Managed Preferences 到底贏不贏得過本機設定?自己寫工具測給自己看
Photo by Levi Trimmer / Unsplash

在管理一批用 MDM 集中控管的 Mac 時,常常會遇到一個問題:某個 preference key,使用者自己用 defaults write 改過一次,管理員可能又用 sudo defaults write 在系統層級動過一次,最後 MDM 又把同一個 key 標成 Managed(強制)。這時候系統實際生效的到底是哪一個值?

Apple 官方文件對 CFPreferencesAppValueIsForced 這支 API 的說明相當簡略,只告訴你這個 API 可以拿來判斷某個 key 是不是被強制,卻沒有把整個 preference 搜尋順序裡各層級的先後關係講清楚。網路上能找到的資訊也大多是憑印象轉述,沒有人真的擺出證據。

與其對著文件猜,不如自己寫一支工具,實際測給自己看。以下這篇的測試環境是 macOS Tahoe 26.6.1,Apple Silicon 機器。

先想清楚要驗證什麼

要判斷一個 key 的最終值是被誰決定的,其實只需要同時攤開四樣東西比對:

1. CFPreferencesAppValueIsForced 的結果,也就是這個 key 是不是被判定為 Managed

2. CFPreferencesCopyAppValue 解析出來的值,也就是系統實際上會拿去用的值

3. 使用者層級 plist(~/Library/Preferences/<domain>.plist)裡的原始值

4. 系統層級 plist(/Library/Preferences/<domain>.plist)裡的原始值

把這四樣東西擺在一起看,就能一眼看出哪個值贏了。於是我用 Xcode 建了一個最陽春的 macOS command line tool 專案,取名叫 Prefcli,吃兩個參數(domain 跟 key),把上面四項印出來:

import Foundation
func printUsageAndExit() -> Never {
    let toolName = (CommandLine.arguments.first as NSString?)?.lastPathComponent ?? "Prefcli"
    print("用法: \(toolName) <preference-domain> <key>")
    exit(1)
}

guard CommandLine.arguments.count == 3 else {
    printUsageAndExit()
}

let domain = CommandLine.arguments[1]
let key = CommandLine.arguments[2]

func describe(_ value: Any?) -> String {
    guard let value = value else { return "(不存在 / nil)" }
    return "\(value)   <型別: \(type(of: value))>"
}

func readRawPlistValue(atPath path: String, key: String) -> Any? {
    guard FileManager.default.fileExists(atPath: path),
          let data = FileManager.default.contents(atPath: path) else {
        return nil
    }
    guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else {
        return nil
    }
    return plist[key]
}

let isForced = CFPreferencesAppValueIsForced(key as CFString, domain as CFString)
let resolvedValue = CFPreferencesCopyAppValue(key as CFString, domain as CFString) as Any?

let userPlistPath = NSString(string: "~/Library/Preferences/\(domain).plist").expandingTildeInPath
let userRawValue = readRawPlistValue(atPath: userPlistPath, key: key)

let systemPlistPath = "/Library/Preferences/\(domain).plist"
let systemRawValue = readRawPlistValue(atPath: systemPlistPath, key: key)

print("[1] CFPreferencesAppValueIsForced: \(isForced)")
print("[2] CFPreferencesCopyAppValue:     \(describe(resolvedValue))")
print("[3] 使用者層級原始值:                \(describe(userRawValue))")
print("[4] 系統層級原始值:                  \(describe(systemRawValue))")

這支工具沒有處理任何錯誤情境,也沒有寫測試,純粹是為了肉眼比對而存在,能跑出結果就夠了。

第一輪:使用者層級對上系統層級

工具寫好之後,先用一個自訂的測試 domain,不去動任何真實的系統元件。第一步用一般權限寫入一個值:

defaults write com.prefcli.test TestKey UserLevelValue

再用 sudo 寫進系統層級:

sudo defaults write /Library/Preferences/com.prefcli.test TestKey SystemLevelValue

跑一次 Prefcli,結果是:

[2] CFPreferencesCopyAppValue: UserLevelValue
[3] 使用者層級原始值:            UserLevelValue
[4] 系統層級原始值:              SystemLevelValue

兩個層級都確實寫入了各自的值,但系統實際採用的還是使用者層級那份。也就是說,就算你是管理員,用 sudo 蓋掉系統層級的 plist,只要使用者自己也動過同一個 key,使用者層級還是贏。

第二輪:想找一台真的被 MDM 管的機器來測

自己的開發機沒辦法真的模擬 MDM 環境,於是準備了一台獨立的測試機,用 Jamf Pro 打政策上去。原本打算走 Apple 這幾年主推的 DDM(Declarative Device Management),透過 Jamf 的 Blueprint 推一個 declaration,把同一個 domain/key 標成 Forced。

結果卡關了。去翻了 Apple 公開在 GitHub 上的 DDM schema 定義才發現 com.apple.configuration.legacy 的 Payload 就只有一個欄位叫 ProfileURL,而且官方寫得很白:

The URL of the profile to download and install, which needs to start with `https://`, and must be hosted by the MDM server.

意思是這個 declaration 期待的是一個「由 MDM server 自己代管」的網址,裝置會自己去下載,並不支援把 profile 內容直接寫死在 declaration 裡面。所以後來還是改回用 Jamf Pro 裡的 Configuration Profiles 本來就有一個叫 Application & Custom Settings 的 payload,專門就是為了推送任意 preference domain 而存在的:填一個 domain,貼一份 property list,Jamf 自己會把它包成 com.apple.ManagedClient.preferences 的 Forced payload,簽好章、走傳統 MDM 協定推下去。不用處理 URL 代管,也不用處理簽章問題。

Preference Domain: com.prefcli.test

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>TestKey</key>
	<string>ManagedForcedValue</string>
</dict>
</plist>

部署到測試機之後,SSH 進去跑 Prefcli:

[1] CFPreferencesAppValueIsForced: true
[2] CFPreferencesCopyAppValue:     ManagedForcedValue
[3] 使用者層級原始值:                (不存在 / nil)
[4] 系統層級原始值:                  (不存在 / nil)

IsForced 正確回報 true,Managed 的值也確實生效了。

三個層級同時擺在一起,才是真正的驗證

只測 Managed 單獨存在的情況還不夠有說服力,畢竟這時候使用者層級跟系統層級根本沒有值可以競爭。想要真的驗證優先權,就得讓三種來源同時存在同一個 key 上。於是在測試機上,補寫了使用者層級跟系統層級的值:

defaults write com.prefcli.test TestKey UserLevelValue
sudo defaults write /Library/Preferences/com.prefcli.test TestKey SystemLevelValue

這時候三種值同時掛在同一個 key 底下,再跑一次 Prefcli:

[1] CFPreferencesAppValueIsForced: true
[2] CFPreferencesCopyAppValue:     ManagedForcedValue
[3] 使用者層級原始值:                UserLevelValue
[4] 系統層級原始值:                  SystemLevelValue

使用者層級跟系統層級的值都確實寫進去了,CFPreferencesCopyAppValue 拿到的還是 Managed 的那個值。也就是說,Managed(Forced) 完全無視前面兩者,不管使用者或管理員怎麼改本機設定,只要 MDM 標成 Forced,系統一律採用 MDM 給的值。

小結

整理起來,這幾層的優先權關係是:

Managed (Forced, current user)
  > Managed (Forced, any user)
  > ~/Library/Preferences   (使用者層級)
  > /Library/Preferences    (系統層級)
  > Registration domain     (程式內建預設值)

這對做 MDM 政策設計的人來說其實蠻實際的:如果你把某個 key 標成 Forced,就不用再擔心使用者自己改設定、或是某個舊的部署腳本殘留的系統層級設定會蓋過去,MDM 的值永遠贏。反過來說,如果你發現某個 Forced 的政策看起來沒生效,那問題大概不會出在優先權上,而是要往別的地方找了,比如 profile 有沒有真的推送成功、payload 裡的 domain 跟 key 名稱是不是打錯了。

Read more

Apple 生態系的真實威脅—我們都可能是目標

Apple 生態系的真實威脅—我們都可能是目標

從一場分享開始的警醒 前陣子在 Jamf Nation Live Taipei 分享了 2024-2025 年 Apple 裝置威脅報告,深入探討了全球 Apple 生態系正面臨的資訊安全威脅。 許多人對 Apple 產品有一種根深蒂固的信心—拿著 Mac 或 iPhone,就像拿到了數位世界的「免死金牌」。說實話,Apple 在軟硬體整合上的安全設計確實領先業界,這不是誇大其詞。但也正因為這份信心,我開始想更深入地了解真實情況。 從 Jamf Security 360 報告開始,我觀察了全球情報機構(如 Citizen Lab、Google TAG、Kaspersky)揭露的真實案例。那時我才意識到一個有點殘酷的事實,也正好對應了同事最常對我說的那句話:Apple 設備並非堅不可摧,使用者自身往往才是最大的弱點。 為什麼 Apple

By Glee Tsai

從 Jamf Pro 自定義註冊畫面帶使用者資訊至 Jamf Connect 失敗的處理方式

Jamf Pro 在設定裡面的 Enrollment Customization,本來可以設定在使用者完成設備註冊的當下,同步把 SAML 裡的資訊帶給後面的 Jamf Connect Login,這樣就可以減少一步使用者還需要登入的步驟。但在撰文的當下,Jamf Pro 11.1 仍在這個功能上有問題,在跟 Jamf Support Team 了解後,大概能用一種 Workaround 來解決,只是要特別留意以下事情: 1. 在 Jamf Pro 裡的 Enrollment Customization 不要再打勾 Enable Jamf Pro to pass user information to Jamf Connect 了(因為勾了也沒有用) 2. 確保

By Glee Tsai

在 Apple 零接觸部署下僅允許某個群組啟動設備

在 Jamf Pro 有整合 Single Sign On 的情況下,可以前往 Settings > Enrollment Customization 設定僅允許一個群組中的成員啟用設備。如果這個成員沒有在這個群組裡面的話,就不能夠開箱設備。如果以 Entra ID 為例,需要把 Object ID 填在下方的欄位即可。 對照到我在 Entra ID 上的群組設置: 至於如果不是 Entra ID,而是其它的 SSO Provider 的話,最好可以用 SAML Tracer 這一套 Google Chrome 外掛去看一下自己的 SAML 文件是如何表達群組的,例如下圖能看到這個使用者屬於以下五個群組。 接著有另外一個很重要的事情要提醒,如果你要阻擋註冊的成員,可以登入到 Jamf Pro

By Glee Tsai

在 Conditional Access 條件下設置 Jamf Connect Login 的 MFA 挑戰

Mac 世界的零接觸話題,每年都可以有新的話題出現。圍繞著使用者體驗,讓用戶在拿到電腦的那一刻,就能自動化配置所有公司要求的設定,不需 IT 人員幫忙。這不僅是節省 IT 人員的時間而已,更是讓整個出機流程變得更為流暢。 除了軟體與設定可以全自動化部署以外,使用者帳號當然也可以自助化的設定在電腦裡,而且完全按照公司的規範。所以如果公司正在使用 Entra ID 這樣的目錄服務時,當然也可以把 Entra ID 上面的帳密同步化成電腦本機電腦上的帳密,減少人員帳密疲疺的風險。然而,要求更高等級的公司,可能會要求人員在登入 Entra ID 時必須通過兩階段驗證,而這可能就為同步密碼帶來挑戰。 以 Jamf Connect 來說,這一套軟體可以在 Mac 設備開箱時自動就安裝在電腦上,並且出現一個可客製化的登入視窗給用戶登入 Entra ID。剖析這套軟體,事實上是由兩個 OIDC 授權類別完成的,一個是 Authorization Code Grant,

By Glee Tsai