errSecInternalComponent: signing iOS builds over SSH
I build a personal iOS app on a headless Mac and push it to a physical device. No screen, no clicking — just ssh in, xcodebuild, devicectl install. It had worked for weeks. Then one afternoon every build died at the same step:
CodeSign .../MyApp.debug.dylib
Signing Identity: "Apple Development: me (XXXXXXXXXX)"
.../MyApp.debug.dylib: errSecInternalComponent
Command CodeSign failed with a nonzero exit code
Compilation succeeded. Linking succeeded. It fell over the instant codesign reached for the signing key.
What errSecInternalComponent actually means
The name is useless, so here's the translation: **codesign asked securityd for a private key and got refused.** Two things cause that, and they look identical from the outside.
1. The keychain is locked, or the key's partition list forbids non-interactive access. The classic fix:
security unlock-keychain -p "$PW" ~/Library/Keychains/login.keychain-db
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$PW" ~/Library/Keychains/login.keychain-db
That last command is the one people forget. Unlocking lets you read the keychain; the partition list is what lets codesign and apple-tool use a key without a GUI "Allow" prompt.
A trap I walked straight into: security unlock-keychain -p "" returned success, so I assumed the password was empty. It wasn't — an already-unlocked keychain accepts any password on unlock and returns 0. set-key-partition-list doesn't play along; it actually validates, and told me the passphrase was wrong. That mismatch is the tell that your "empty" password is a lie.
2. The SSH session isn't in the GUI login session. This is the one that got me, and no amount of unlocking or partition-listing fixes it. securityd scopes some key access to the Aqua (GUI) session. A plain SSH shell runs in its own session, so even a perfectly unlocked keychain with a perfect partition list hands codesign an errSecInternalComponent.
The fix: run the build in the GUI session
launchctl asuser <uid> runs a command inside a target user's GUI session bootstrap. Wrap the build in it and codesign suddenly sees the same securityd context it would in Xcode:
# uid 501 = the logged-in GUI user
launchctl asuser 501 sudo -u builduser \
xcodebuild -project MyApp.xcodeproj -scheme MyApp \
-destination "id=<device-udid>" \
-derivedDataPath build \
CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=XXXXXXXXXX build
launchctl asuser needs root, hence the outer privilege and the inner sudo -u to drop back to the build user. The same wrapper works for xcrun devicectl device install — anything that touches signing.
To isolate which of the two causes you have, sign one file by hand over SSH:
codesign --force --sign <identity-hash> --generate-entitlement-der some.dylib
If it works after unlock + partition-list, you had case 1. If it still fails and only starts working under launchctl asuser, you had case 2.
What I'd do differently
I burned three build cycles assuming this was a keychain-lock problem because that's the answer every search result gives you. It's the common case, not the only case. The faster diagnosis is to skip straight to the one-file manual codesign and watch whether the session wrapper is what flips it green — that tells you which layer is actually refusing you instead of guessing.