Skip to content

Conversation

@ayush-panta
Copy link
Contributor

Description of changes: This PR enables Finch to integrate with native OS credential stores (Keychain on macOS, Credential Manager on Windows) for container registry authentication.

How it works:

  • finch login/logout commands execute on host using native credential helpers
  • When running container commands, Finch creates a temporary credential socket between host and VM
  • The VM's docker-credential-finch helper connects to this socket to retrieve credentials
  • Socket is automatically cleaned up when the command completes

Dependencies:

  • Requires finch-daemon PR #382 for credential helper updates
  • Requires finch-core to be updated to point to finch-daemon commit

Testing done: Tested locally; works alongside changes to finch-daemon for standard workflows. Added E2E tests for macOS, which worked locally (see below). Must add tests for Windows.

% go test -v -ginkgo.focus="Native Credential Helper" -timeout=30m
=== RUN   TestVM
Running Suite: Finch Virtual Machine E2E Tests - ../finch/e2e/vm
==================================================================================================
Random Seed: 1767117182

Will run 4 of 225 specs
SS••••SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS

Ran 4 of 225 Specs in 453.266 seconds
SUCCESS! -- 4 Passed | 0 Failed | 0 Pending | 221 Skipped
--- PASS: TestVM (453.29s)
PASS
ok  	github.com/runfinch/finch/e2e/vm	454.011s
  • I've reviewed the guidance in CONTRIBUTING.md

License Acceptance

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

if credHelper == "finch" {
cmdArr = append(cmdArr, configureFinchCredHelperTemplate)
// Add as default in credStore to work with nerdctl command flow.
// TODO: support multi-registry workflows. Will involve set registries as credStore vs credHelpers in config.json.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much more work is this? It'd be nice to have this in the initial implementation so we don't have to think about what happens when user's update

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm giving this some thought right now. I think it is a matter of making ecr-login behave like acr-login:

{
...
"credHelpers": {
    "dkr.acr.builder-tools.aws.dev": "acr-login",
    "dkr.ecr.public.aws....": "ecr-login",
}
...
}

docker-credential-acr-login has a --setup flag that allows it to populate the config file by itself, but docker-credential-ecr-login does not.

I think what I will do is make it so that if ecr-login is set as a credhelper in finch.yaml, we store it as a credHelper for generic ecr links

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ Going to make this a mini-feature after the project.

case "darwin":
helperName = "docker-credential-osxkeychain"
case "windows":
helperName = "docker-credential-wincred.exe"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we expecting the user to install the credential helpers specifically in the finch path? Other tools just require the credential helpers to be in the PATH. This might not be expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user does not have to install anything. I just symlinked the binaries that are packaged with finch-core to .finch/cred-helpers, and then depend on these for credhelper operations.

I did this because as it is right now, if someone configures ecr-login in finch.yaml (for example), docker-credential-ecr-login gets installed into ./finch/cred-helpers.

However, I can try to make it just check if helpers are in PATH while also creating the symlink.

@@ -0,0 +1,113 @@
//go:build darwin || windows
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a new directory/package for all of the credential handling files? Namely: cred_socket.go and cred_helper.go?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}

helperPath := filepath.Join(homeDir, ".finch", "cred-helpers", helperName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path neednot be in the home dir for example

/opt/homebrew/bin/docker-credential-osxkeychain

i would say we neednot handle packaging them at all. Just have the process in the docs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to find the path depending on OS you can do a which or where

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should think a little more here whether to package it or not. Packaging it will potentially add maintenance so i am not inclined, but makes it easier for customers

@pendo324 what do you think?

cmd := exec.Command(helperPath, action)

// Set input based on action
if action == "store" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if else is not usually desirable in function, try to have single functionality per function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes it easier to test also.

}
credJSON, err := json.Marshal(cred)
if err != nil {
return nil, fmt.Errorf("failed to marshal credentials: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont print the err here, it might leak creds.


output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("credential helper failed: %w - %s", err, string(output))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.


// Break if already running
if cs.listener != nil {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need an error saying concurrent cred request not supported in finch mac and windows version.

return nil
}

socketPath := filepath.Join(finchRootPath, "lima", "data", "finch", "sock", "creds.sock")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be part of makefile, similar to finch,sock

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if socket not there we just throw an error.

"load": "Load an image from a tar archive or STDIN",
"login": "Log in to a container registry",
"logout": "Log out from a container registry",
// "login": "Log in to a container registry",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: commented out code.

finchRootPath := filepath.Dir(filepath.Dir(execPath))

// Wrap nerdctl execution with credential socket
return withCredSocket(finchRootPath, func() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will prevent any concurrent execution but thats fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its good to wrap it for functionalities we actually need creds, otherwise it might un-necessary hold it. This part is actually flimsy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it can slow down the run of other commands.

return nil
}

func (iva *initVMAction) ensureNativeCredentialHelpers() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would like to avoid having to manage the installation rather find it from the environment.

return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}

helperPath := filepath.Join(homeDir, ".finch", "cred-helpers", helperName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://pkg.go.dev/os/exec -> this probably can be used to get the path.
Not sure it works for windows but we can try.

}

// withCredSocket wraps command execution with credential socket lifecycle.
func withCredSocket(finchRootPath string, fn func() error) error {
Copy link
Contributor

@Shubhranshu153 Shubhranshu153 Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will this handle detach case?
Wont it keep holding the console, Lets do a check for it

finch run -d alpine sleep 10

something like that.

Also lets verify the behavior if a image exists then what happens will the request still come?

script: |
# Create user-writable directory in /run for credential socket
mkdir -p /run/finch-user-sockets
chmod 777 /run/finch-user-sockets
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems overly permissive.

return loginWithNativeCredStore(serverAddress, username, password, cmd.OutOrStdout())
}

func loginWithNativeCredStore(serverAddress, username, password string, stdout io.Writer) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if !scanner.Scan() {
return
}
serverURL := strings.TrimSpace(scanner.Text())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serverURL probably needs some sanitization with regex to check its a valid server request, but probably docker cred helper also does it?

defer cs.mu.Unlock()

// Break if already running
if cs.listener != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what it does, if there are separate request you wont see them across CLI calls. so this is not a valid check for concurrent connection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably do flock on the socket to check if its in use or not
"github.com/go-frs/flock"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should handle crashes as it uses system calls.

testNonDefaultOptions(o, *e2e.Installed)
testSupportBundle(o)
testCredHelper(o, *e2e.Installed, *e2e.Registry)
testNativeCredHelper(o, *e2e.Installed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

windows also will need it?

@@ -0,0 +1,51 @@
//go:build darwin || windows
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the naming or is for linux i think, for vm we use remote as we are seeing from perspective of from inside the vm.

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

//go:build darwin || windows
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need test for:

docker login: This is the most direct. It triggers a store action to save new credentials into your helper/keychain.

docker push: Before uploading layers, Docker calls get to retrieve the registry token or password.

docker pull: Even for public images on Docker Hub, Docker often calls get to check for an account to avoid "Pull Rate Limits." For private registries, it is mandatory.

docker logout: Triggers the erase action to remove credentials from the store.

docker run: Implicitly pulls the image if it’s missing. Should check the detached mode (only run has a detached mode which is relevant for our case)

docker create: Same as run; pulls the image metadata and layers.

docker build / docker buildx: If the FROM line in your Dockerfile references a private image or a remote registry, the builder will request credentials to fetch that base image

socketPath := filepath.Join(finchRootPath, "lima", "data", "finch", "sock", "creds.sock")
if err := os.MkdirAll(filepath.Dir(socketPath), 0750); err != nil {
return fmt.Errorf("failed to create socket directory: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will the credential behave if the command crashes. i think it will just release the conncetion the other end will get a 500 internal error but lets confirm its graceful enough and doesnt hold the connection. If it does we cannot run any further commands.

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.

3 participants