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

@hsbt hsbt 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.

var query: [String: Any] = [
    kSecClass: kSecClassInternetPassword,
    kSecAttrSecurityDomain: securityDomain,   // "com.apple.container.registry"
    kSecAttrServer: hostname,
    kSecAttrAccount: username,
    kSecValueData: passwordEncoded,
    kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
    kSecAttrSynchronizable: false,
]
SecItemAdd(query as CFDictionary, 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.

Role Process Code identifier Team
writer (registry login creates the ACL) /usr/local/bin/container com.apple.container.cli UPBK2H6LZM
reader (pull/push lookup) container-core-images helper com.apple.container.container-core-images UPBK2H6LZM

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.

hsbt and 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>
@hsbt hsbt force-pushed the claude/stoic-sammet-214ffc branch from 00c0734 to 3f905c4 Compare June 30, 2026 22:09
Sign up for free to 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