Back to blog
FILE 0x9F·ERRSECINTERNALCOMPONENT: SIGNING IOS BUILDS OVER SSH

errSecInternalComponent: signing iOS builds over SSH

July 8, 2026 · ios, codesign, ci, macos

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.