Stopping Login Keychain Prompt Loops Before They Escalate
Automate detection and self-service repair of macOS login keychain sync failures so technicians stop closing the same credential prompt ticket weekly.

This playbook covers
Table of Contents
Table of contents
#The Old Way vs the New Way
The old way looks like this: a user changes their network account password through the identity provider, and by lunchtime they are opening three, four, sometimes ten dialog boxes a day asking them to allow Mail, Safari, or Slack to use the login keychain. The L1 fix has historically been a remote session where the technician walks the user through System Settings, deletes the login keychain, and lets macOS rebuild it – usually losing saved Wi-Fi passwords, certificates, and app tokens in the process. It is slow, it is destructive, and it repeats itself every password rotation cycle for the same population of users.
The new way treats keychain drift as a telemetry signal, not a helpdesk conversation. We detect the unlock failure before the user files a ticket, and we remediate it with a scoped, non-destructive script that the user can run themselves from Self Service, or that fires automatically off an identity provider event. No technician touches the machine. No saved credentials are lost.
#The Everyday Problem: Login Keychain Drift
On macOS, the login keychain (~/Library/Keychains/login.keychain-db) is unlocked automatically at login because its keychain password is set to match the account’s login password. When a password changes anywhere other than the standard local login screen – through Platform SSO
This is distinct from a corrupted keychain database (rare) and from an expired certificate prompt (a different signature entirely). Drift is a sync problem, not a corruption problem, and it has a clean, scriptable fix that does not require deleting anything.

#Prerequisites and Permissions
- MDM platform with custom Extension Attribute and Self Service (or equivalent) support – examples below use Jamf Pro syntax but the logic ports to any MDM with a scripting and policy layer.
- Local script execution occurs as the logged-in user, not root, because
securitykeychain operations must run in the user’s security context. - No storage of plaintext passwords anywhere – scripts must pipe credentials directly into the
securitybinary and never write them to disk, logs, or MDM inventory fields. - Read-only API scope on the identity provider (Okta System Log or Entra ID Audit Log) if you are wiring up the proactive webhook trigger in Step 5.
- Jamf Pro API role with
Send Computer Remote CommandandCustom Triggerpermissions only – do not grant full API admin to the webhook listener service account. - Test scope: a single Smart Group of 10–20 pilot machines before fleet-wide deployment. Blast radius if the detection logic misfires is a false-positive notification, not data loss, because the remediation script never deletes keychain data.
#Implementation Steps
- Step 1 – Deploy a lightweight drift detector as a LaunchAgent. Action: push a script and LaunchAgent plist that checks whether the login keychain is unlocked without a password challenge at login. Expected result: a flag file is written only when the keychain fails to auto-unlock. Evidence to capture: presence and timestamp of
/Users/Shared/.kc_drift_flagon affected machines.1#!/bin/bash 2# /usr/local/kby/keychain-drift-check.sh 3# Runs as the logged-in user via LaunchAgent, not root. 4 5FLAG="/Users/Shared/.kc_drift_flag" 6LOGIN_KC="$HOME/Library/Keychains/login.keychain-db" 7 8# Attempt a benign read against the login keychain. 9# A drifted keychain returns error -25293 (SecKeychainItemCopyContent / auth failed) 10# when the OS tries to silently unlock it. 11OUTPUT=$(/usr/bin/security find-generic-password -a "$USER" -s "com.apple.safari.https" "$LOGIN_KC" 2u003eu00261) 12 13if echo "$OUTPUT" | /usr/bin/grep -q "25293"; then 14 /usr/bin/touch "$FLAG" 15 echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) drift-detected" u003eu003e "$FLAG" 16else 17 /bin/rm -f "$FLAG" 18fi - Step 2 – Register the LaunchAgent so the check runs every login and every four hours. Action: deploy the plist via MDM configuration profile or a package postinstall script. Expected result:
launchctl listshows the agent loaded for the console user. Evidence to capture:launchctl print gui/$(id -u)/com.kby.keychaindriftcheckoutput showing staterunningor last exit code 0.1u003c?xml version="1.0" encoding="UTF-8"?u003e 2u003c!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"u003e 3u003cplist version="1.0"u003e 4u003cdictu003e 5 u003ckeyu003eLabelu003c/keyu003e 6 u003cstringu003ecom.kby.keychaindriftchecku003c/stringu003e 7 u003ckeyu003eProgramArgumentsu003c/keyu003e 8 u003carrayu003e 9 u003cstringu003e/usr/local/kby/keychain-drift-check.shu003c/stringu003e 10 u003c/arrayu003e 11 u003ckeyu003eRunAtLoadu003c/keyu003e 12 u003ctrue/u003e 13 u003ckeyu003eStartIntervalu003c/keyu003e 14 u003cintegeru003e14400u003c/integeru003e 15u003c/dictu003e 16u003c/plistu003e - Step 3 – Surface the flag as a Jamf Pro Extension Attribute and Smart Group. Action: create an EA script read at inventory update that reports true or false based on flag presence. Expected result: computers with drift populate a Smart Group named
Keychain-Drift-Suspectedwithin one inventory cycle. Evidence to capture: Smart Group membership count and the EA value shown against the device record.1#!/bin/bash 2FLAG="/Users/Shared/.kc_drift_flag" 3if [ -f "$FLAG" ]; then 4 echo "u003cresultu003etrueu003c/resultu003e" 5else 6 echo "u003cresultu003efalseu003c/resultu003e" 7fi - Step 4 – Build the non-destructive remediation Self Service policy. Action: publish a Self Service item scoped to
Keychain-Drift-Suspectedthat prompts the user once for their current account password, uses it to resynchronise the keychain password in place, then discards the value immediately. Expected result: subsequent app launches no longer trigger keychain dialogs. Evidence to capture: exit code 0 in the policy log and the drift flag cleared on next detector run.1#!/bin/bash 2# Runs in the user context via Self Service. 3 4CURRENT_PW=$(/usr/bin/osascript -e 'Tell application "System Events" to display dialog "Enter your current account password to resync your keychain:" default answer "" with hidden answer' -e 'text returned of result' 2u003e/dev/null) 5 6if [ -z "$CURRENT_PW" ]; then 7 echo "User cancelled remediation." 8 exit 1 9fi 10 11# Resync the login keychain password to match the current account password. 12# -o = old password, -p = new password. Piped directly, never written to disk. 13/usr/bin/security set-keychain-password -o "$CURRENT_PW" -p "$CURRENT_PW" "$HOME/Library/Keychains/login.keychain-db" 14RESULT=$? 15 16unset CURRENT_PW 17 18if [ $RESULT -eq 0 ]; then 19 /bin/rm -f "/Users/Shared/.kc_drift_flag" 20 /usr/bin/osascript -e 'display notification "Keychain sync repaired. No restart needed." with title "IT Self Service"' 21 exit 0 22else 23 /usr/bin/osascript -e 'display notification "Password did not match. Contact IT if prompts continue." with title "IT Self Service"' 24 exit 1 25fiExpected output in the Jamf policy log:
Script exit code: 0and a cleared drift flag confirmed by the next scheduled inventory submission. - Step 5 – Wire the proactive trigger from the identity provider. Action: subscribe to the password-change event in your IdP’s system log and fire a lightweight webhook the moment a rotation happens, so the user is nudged to run the Self Service item before drift even causes a prompt. Expected result: the notification lands within seconds of the password change event. Evidence to capture: webhook delivery log and the timestamp gap between IdP event and notification push.
1{ 2 "eventType": "user.account.update_password", 3 "actor": { 4 "id": "00u1a2b3c4d5e6f7g8h9", 5 "type": "User", 6 "alternateId": "jane.doe@kby.example" 7 }, 8 "published": "2024-11-02T09:14:32.101Z", 9 "outcome": { 10 "result": "SUCCESS" 11 }, 12 "target": [ 13 { 14 "id": "00u1a2b3c4d5e6f7g8h9", 15 "type": "User", 16 "alternateId": "jane.doe@kby.example" 17 } 18 ] 19}1from flask import Flask, request, abort 2import hmac, hashlib, os, requests 3 4app = Flask(__name__) 5WEBHOOK_SECRET = os.environ["OKTA_WEBHOOK_SECRET"] 6JAMF_BASE = os.environ["JAMF_BASE_URL"] 7JAMF_TOKEN = os.environ["JAMF_API_TOKEN"] 8 9@app.route("/okta/password-change", methods=["POST"]) 10def handle_event(): 11 signature = request.headers.get("X-Okta-Verification-Signature", "") 12 computed = hmac.new(WEBHOOK_SECRET.encode(), request.data, hashlib.sha256).hexdigest() 13 if not hmac.compare_digest(signature, computed): 14 abort(401) 15 16 payload = request.get_json() 17 event_type = payload.get("eventType", "") 18 if event_type != "user.account.update_password": 19 return "", 204 20 21 user_email = payload["actor"]["alternateId"] 22 23 # Trigger a Jamf Pro custom trigger that pushes a Self Service reminder 24 # notification to the matching device record. No password data is handled here. 25 requests.post( 26 f"{JAMF_BASE}/JSSResource/computercommands/command/CustomEvent", 27 headers={"Authorization": f"Bearer {JAMF_TOKEN}"}, 28 json={"trigger": "prompt-keychain-resync", "user": user_email}, 29 timeout=10 30 ) 31 return "", 200
#Verification and Expected Evidence
Confirm success across three layers. First, the detector: run /usr/local/kby/keychain-drift-check.sh manually and confirm no flag file is created on a healthy machine. Second, the Smart Group: after remediation, the device drops out of Keychain-Drift-Suspected within one inventory cycle – capture a before-and-after screenshot of group membership count. Third, the user experience: ask the pilot user to open Mail or Safari and confirm zero keychain prompts across a full working day. Log this as a closed-loop verification note on the change record, not a ticket.
#Rollback
The remediation script is non-destructive by design – it resynchronises the existing keychain password rather than deleting the keychain, so there is no data loss to roll back. If the script fails validation during pilot (exit code 1, or the drift flag persists after a successful-looking run), disable the Self Service policy scope back to the pilot Smart Group, unload the LaunchAgent via a removal script pushed through MDM, and revert affected users to the manual System Settings walkthrough until the script logic is fixed. Keep the webhook listener paused (return 503 to the IdP) rather than deleting the endpoint, so the IdP does not silently drop the subscription.

#Failure and Escalation Conditions
Escalate to a human technician when any of the following occur: the remediation script returns exit code 1 after the user has confirmed the password entered was correct, which indicates the underlying keychain file may be corrupted rather than merely desynchronised, and needs the destructive rebuild path with proper data-loss consent. Escalate when a single device triggers the drift flag more than three times in 24 hours, since repeated drift after a successful resync usually points to a Platform SSO configuration fault or a password sync agent looping. Escalate when the webhook listener logs signature verification failures, which may indicate a compromised or misconfigured IdP integration and should be treated as a security event, not a helpdesk item. The monitoring signal to watch on the automation side is the ratio of Self Service remediation runs to Smart Group entries per day; a sustained spike suggests an upstream password policy change is causing mass drift and warrants a broader fix rather than per-device remediation.
#Measuring Ticket Deflection
Baseline the current volume by pulling the last 90 days of tickets tagged with keywords like “keychain”, “always asking for password”, and “wants to use login keychain” from your ticketing system. After deployment, track three numbers monthly: Smart Group entries into Keychain-Drift-Suspected, Self Service remediation runs with exit code 0, and residual tickets on the same keyword filter. A successful rollout typically shows Smart Group entries holding steady or rising slightly (better detection) while ticket volume on the same keyword set drops close to zero within one full password rotation cycle, since that is when the old-way tickets used to spike. Report deflected tickets as detections resolved by Self Service divided by total detections, and keep the webhook-to-notification latency as a secondary KPI to prove the proactive path is catching drift before users notice it.
Evidence trail
Sources and verification
Primary documentation and external technical references used in this article.
Comments
Add a thoughtful note on Stopping Login Keychain Prompt Loops Before They Escalate. Comments are checked for spam and held for moderation before appearing.
Related articles
Automation & Scripting
Silencing Chatty Login Window Errors With launchd Log Triage
Deploy a launchd-scheduled log query that detects macOS login window failures and alerts technicians before users file a ticket.
Device Management
Fixing Stalled Automated Device Enrollment Loops for Good
Stop firefighting stuck Mac enrollments manually — detect and auto-remediate ADE and ABM assignment failures before they ever become a ticket.
Security & Operations
Hunting BGP Route Hijacking in Real-Time Networks
Real-time BGP route hijacking detection using RPKI validation, AS-path anomaly scoring, and prefix origin tracking to stop network traffic diversion.
DevOps & Automation
Designing a Verifiable DevOps Workflow with GitHub Actions
A bounded GitHub Actions build-test-deploy workflow, designed with least-privilege permissions, OIDC federation, environment gating, explicit validation evidence and a concrete rollback path.
Learn More About KBY
About KBY
Learn about our mission, editorial standards, and commitment to trusted engineering knowledge.
Why Trust KBY
Explore the processes and policies that ensure our publications are accurate, useful, and responsible.
Newsletter
Get our latest editorial publications, research and practical insights sent directly to your inbox.
Was this useful?
Operate smarter, with fewer recurring tickets.
Receive new operational playbooks, incident-prevention guidance, automation scripts and recovery runbooks.