Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Read registry credentials in the CLI, not the images helper - #1874

Open
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc
Open

Read registry credentials in the CLI, not the images helper#1874
hsbt wants to merge 2 commits into
apple:mainfrom
hsbt:claude/stoic-sammet-214ffc

Conversation

@hsbt

@hsbthsbt commented Jun 30, 2026

Copy link
Copy Markdown

Summary

After a successful container registry login, container image pull and container image push (and base-image pulls during container build) fail to read registry credentials from the keychain, even when the login keychain is unlocked. This change moves the keychain read out of the container-core-images helper and into the container CLI, which is the process that wrote the item and therefore satisfies its access control. The resolved Authorization header is forwarded to the helper over XPC, and the helper no longer touches the keychain. This addresses the long-standing "Error querying keychain" / -25308 family of reports, such as #1733, #1253, and #816.

Symptom

container image pull --platform linux/amd64 docker.io/rubylang/all-ruby:latest
Error: error querying keychain for registry-1.docker.io
(cause: "queryError("query failure: unhandledError(status: -25308)")")

-25308 is errSecInteractionNotAllowed. It reproduces even for public images, because the pull and push paths resolve stored credentials before contacting the registry. container registry login itself succeeds, and security find-internet-password -s registry-1.docker.io -w returns the password, so the credential exists and is readable by the CLI. The failure happens only at pull and push time.

Investigation

Credentials are stored in the macOS file-based login keychain. Both the store and the fetch live in the pinned containerization dependency (ContainerizationOS/Keychain/KeychainQuery.swift, surfaced through ContainerizationOCI.KeychainHelper). The write is a plain SecItemAdd with no kSecAttrAccess or kSecAttrAccessControl, and without kSecUseDataProtectionKeychain.

varquery:[String:Any]=[
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: securityDomain, // "com.apple.container.registry"
kSecAttrServer: hostname,
kSecAttrAccount: username,
kSecValueData: passwordEncoded,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable:false,]SecItemAdd(query asCFDictionary,nil)

Because no access control is supplied, macOS creates a default ACL that trusts only the creating process. As a result, the process that writes the credential and the process that reads it are different.

RoleProcessCode identifierTeam
writer (registry login creates the ACL)/usr/local/bin/containercom.apple.container.cliUPBK2H6LZM
reader (pull/push lookup)container-core-images helpercom.apple.container.container-core-imagesUPBK2H6LZM

The item's ACL, seen via security dump-keychain -a, trusts only the CLI.

class: "inet" srvr="registry-1.docker.io" sdmn="com.apple.container.registry"
access entry 0:
authorizations: decrypt derive export_clear export_wrapped mac sign
applications (1):
0: /usr/local/bin/container
requirement: identifier "com.apple.container.cli" and anchor apple generic ...
partition_id entry: teamid:UPBK2H6LZM

When the helper performs the lookup, the decrypt ACL check fails and securityd tries to prompt for approval. The helper is a non-interactive XPC service, so it cannot show the prompt and the call fails.

container-core-images (Security) SecItemCopyMatching_ios
securityd: code requirement check failed (-67050), client is not Apple-signed
securityd: CSSMERR_CSP_NO_USER_INTERACTION
container-core-images [ImagesHelper] route handler threw [route=imagePull]
error querying keychain ... unhandledError(status: -25308)

Adding the helper to the item's trusted-application list with security ... -T does not help. The modern SecItemCopyMatching path evaluates the partition list and code identity rather than the file ACL's application list. That is the direction taken by #997, and it is why that approach does not resolve the failure.

Tracing every keychain reader in the tree confirms the asymmetry. container registry list, container registry logout, and MachineClient.fetchMachineArtifact (which additionally swallows the error with try?) all run inside the container CLI process and so match the writer's ACL. The only reader that runs under a different identity is ImagesService.withAuthentication, shared by both pull and push. container build pulls base images through ClientImage.pull and ClientImage.fetch, which also run in the CLI process.

This fix was built with the project's supported toolchain (Swift 6.3 / macOS 26, matching CI) and verified end to end: after registry login, container image pull and container run succeed with no -25308.

Why this approach

KeychainQuery and KeychainHelper live in the containerization dependency, pinned to an exact version, so changing the write-time access control to a team-scoped ACL, or switching to the data-protection keychain with a shared access group, cannot be done from this repository. The shared-access-group direction is tracked separately in #1257. Reading the credential in the CLI and forwarding it is the option that is both correct and achievable here, and it matches the direction of #1215, which extracts keychain access into a client-side RegistryKeychainClient.

Code

ClientImage.registryAuthorization(for:) resolves the credential from the keychain on the client side and returns the Authorization header value. It keys the lookup by resolvedDomain (e.g. registry-1.docker.io), matching what registry login stores, and returns nil when no entry exists so anonymous pulls keep working. ClientImage.pull and ClientImage.push set this value on the XPC request under a new registryAuthorization key. These run in the container CLI binary, so the read satisfies the item's ACL.

ImagesServiceHarness.authentication(from:) decodes that key into an Authentication and hands it to ImagesService.pull and push, which now take an auth parameter. ImagesService.withAuthentication no longer reads the keychain. It uses the forwarded credential and keeps the existing environment-variable precedence (CONTAINER_REGISTRY_* still wins) and the 401/403 handling. The forwarded value is wrapped in a small ResolvedAuthentication whose token() returns it verbatim. No credential or token is logged.

Both readers are fixed because pull and push share withAuthentication, and container build is covered because it goes through ClientImage.pull.

Testing

Built with the supported toolchain (Swift 6.3 / macOS 26) and verified manually: after registry login, container image pull and container run succeed with no -25308. The same build, packaged and installed on a macOS 27 beta, was confirmed to work there as well. A new ContainerImagesServiceTests target pins that the helper builds its Authentication from the forwarded Authorization header rather than reading the keychain, and returns nil when no credential is forwarded. The existing suite never exercised this path: it only pulls public images anonymously and never logs in, which is why the regression went unnoticed by CI.

Related

Same failure: #1733, #1253, #816, #1310, #254, and the earlier #704, #532, #976. Competing fix directions: #1215 (client-side keychain client, the same direction as this change), #1257 (shared keychain access group), and #997 (grant the helper keychain access, which the investigation above shows does not work). The keychain identifier itself was settled in #644 and #652.

hsbtand others added 2 commits July 1, 2026 07:09
registry login stores the keychain item with an ACL that trusts only the
writing process (the CLI, com.apple.container.cli), so reading it from the
container-core-images helper, which has a different code identity, fails with
errSecInteractionNotAllowed (-25308) in a non-interactive XPC service. Resolve
the credential in the CLI where login wrote it and forward the Authorization
header to the helper over XPC, fixing both pull and push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that the images helper builds its Authentication from the Authorization
header forwarded over XPC rather than reading the keychain itself, guarding
against a regression to the errSecInteractionNotAllowed (-25308) failure.
Extracts the harness credential decoding into authentication(from:) so it can
be exercised without a running daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hsbt