diff --git a/Makefile b/Makefile index 75da162e2..a7677dcb7 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ endif FINCH_CORE_DIR := $(CURDIR)/deps/finch-core -remote-all: arch-test finch install.finch-core-dependencies finch.yaml networks.yaml config.yaml $(OUTDIR)/finch-daemon/finch@.service +remote-all: arch-test finch finch-cred-daemon docker-credential-helper install.finch-core-dependencies finch.yaml networks.yaml config.yaml $(OUTDIR)/finch-daemon/finch@.service ifeq ($(BUILD_OS), Windows_NT) include Makefile.windows @@ -169,6 +169,43 @@ finch-native: finch-all finch-all: $(GO) build -ldflags $(LDFLAGS) -tags "$(GO_BUILD_TAGS)" -o $(OUTDIR)/bin/$(BINARYNAME) $(PACKAGE)/cmd/finch +.PHONY: finch-cred-daemon +finch-cred-daemon: + $(GO) build -ldflags $(LDFLAGS) -o $(OUTDIR)/bin/finch-cred-daemon $(PACKAGE)/cmd/finch/cred-helper + +CRED_HELPER_VERSION ?= v0.9.4 + +.PHONY: docker-credential-helper +ifeq ($(BUILD_OS), Darwin) +docker-credential-helper: + # Download real binary for host daemon + mkdir -p $(OUTDIR)/bin/cred-helpers +ifeq ($(ARCH),arm64) + curl -L https://github.com/docker/docker-credential-helpers/releases/download/$(CRED_HELPER_VERSION)/docker-credential-osxkeychain-$(CRED_HELPER_VERSION).darwin-arm64 -o $(OUTDIR)/bin/cred-helpers/docker-credential-osxkeychain +else + curl -L https://github.com/docker/docker-credential-helpers/releases/download/$(CRED_HELPER_VERSION)/docker-credential-osxkeychain-$(CRED_HELPER_VERSION).darwin-amd64 -o $(OUTDIR)/bin/cred-helpers/docker-credential-osxkeychain +endif + chmod +x $(OUTDIR)/bin/cred-helpers/docker-credential-osxkeychain + # Create dummy script for VM (will be overwritten at runtime) + mkdir -p ~/.finch/cred-helpers + echo '#!/bin/bash' > ~/.finch/cred-helpers/docker-credential-osxkeychain + echo 'echo "Dummy credential helper - will be replaced by bridge script"' >> ~/.finch/cred-helpers/docker-credential-osxkeychain + chmod +x ~/.finch/cred-helpers/docker-credential-osxkeychain +else ifeq ($(BUILD_OS), Windows_NT) +docker-credential-helper: + # Download real binary for host daemon + mkdir -p $(OUTDIR)/bin/cred-helpers + curl -L https://github.com/docker/docker-credential-helpers/releases/download/$(CRED_HELPER_VERSION)/docker-credential-wincred-$(CRED_HELPER_VERSION).windows-amd64.exe -o $(OUTDIR)/bin/cred-helpers/docker-credential-wincred.exe + # Create dummy script for VM (will be overwritten at runtime) + mkdir -p ~/.finch/cred-helpers + echo '#!/bin/bash' > ~/.finch/cred-helpers/docker-credential-wincred + echo 'echo "Dummy credential helper - will be replaced by bridge script"' >> ~/.finch/cred-helpers/docker-credential-wincred + chmod +x ~/.finch/cred-helpers/docker-credential-wincred +else +docker-credential-helper: + @echo "No credential helper needed for Linux" +endif + .PHONY: release release: check-licenses all download-licenses diff --git a/cmd/finch/cred-helper/main.go b/cmd/finch/cred-helper/main.go new file mode 100644 index 000000000..988a1f81d --- /dev/null +++ b/cmd/finch/cred-helper/main.go @@ -0,0 +1,110 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "log" + "net" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" +) + +// const sockAddress = "/tmp/cred.sock" +const hostAddr = "0.0.0.0:8080" // Listen on all interfaces + +func main() { + + // start the socket + fmt.Println("Credential helper daemon started") + socket, err := net.Listen("tcp", hostAddr) + if err != nil { + log.Fatal(err) + } + + // cleanup of socket as goroutine + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + // os.Remove(hostAddr) + os.Exit(1) + }() + + for { + + // accept the connection + conn, err := socket.Accept() + if err != nil { + log.Printf("Accept error: %v", err) + continue + } + + go func(conn net.Conn) { + defer conn.Close() + + // create buffer for socket + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + log.Printf("Read error: %v", err) + return + } + + // need to read in the data + message := strings.TrimSpace(string(buf[:n])) + lines := strings.Split(message, "\n") + if len(lines) == 0 { + conn.Write([]byte("Error: empty message")) + return + } + + // Parse command and data + cmdName := strings.TrimSpace(lines[0]) + var stdinData string + if len(lines) > 1 { + // Decode HTML entities like " -> " + stdinData = strings.ReplaceAll(strings.Join(lines[1:], "\n"), """, "\"") + } + + // Normalize Docker Hub URLs to canonical format + if cmdName == "get" && stdinData != "" { + if strings.Contains(stdinData, "docker.io") || strings.Contains(stdinData, "registry-1.docker.io") { + stdinData = "https://index.docker.io/v1/" + } + } + + fmt.Printf("[RECV] Command: '%s', Data: '%s'\n", cmdName, stdinData) + + binaryPath := "/Users/ayushkp/Documents/finch-creds/finch/_output/bin/cred-helpers/docker-credential-osxkeychain" + cmd := exec.Command(binaryPath, cmdName) + if stdinData != "" { + cmd.Stdin = strings.NewReader(stdinData) + } + + // Capture both stdout and stderr + output, err := cmd.CombinedOutput() + if err != nil { + // Check if it's a "credentials not found" error + outputStr := string(output) + if strings.Contains(outputStr, "credentials not found") || + strings.Contains(outputStr, "could not be found in the keychain") || + strings.Contains(outputStr, "not correct") { + fmt.Printf("[SEND] No credentials found, allowing anonymous access\n") + conn.Write([]byte("")) // Empty = no credentials, try anonymous + return + } + fmt.Printf("[SEND] Error: %v, Output: '%s'\n", err, outputStr) + conn.Write([]byte("")) // Return empty for any error to allow fallback + return + } + + fmt.Printf("[SEND] Success: '%s'\n", string(output)) + conn.Write(output) + }(conn) + } +} diff --git a/deps/finch-core b/deps/finch-core index 30e5e1f2e..b1e34ab47 160000 --- a/deps/finch-core +++ b/deps/finch-core @@ -1 +1 @@ -Subproject commit 30e5e1f2eb742e8bda9a536a171ae1e71a607219 +Subproject commit b1e34ab47a162a63e068d64ffb8257d77cb96288 diff --git a/finch.yaml.d/common.yaml b/finch.yaml.d/common.yaml index 10b6e52d8..4b96cf651 100644 --- a/finch.yaml.d/common.yaml +++ b/finch.yaml.d/common.yaml @@ -92,6 +92,10 @@ provision: sudo systemctl daemon-reload sudo systemctl restart containerd.service +portForwards: +- guestSocket: "/run/cred.sock" + hostSocket: "/private/tmp/cred.sock" + env: # Containerd namespace is used by the lima cidata script # 40-install-containerd.sh. Specifically this variable is defining the diff --git a/finch.yaml.d/mac.yaml b/finch.yaml.d/mac.yaml index 1be4e99ee..4f8522dc0 100644 --- a/finch.yaml.d/mac.yaml +++ b/finch.yaml.d/mac.yaml @@ -57,3 +57,5 @@ hostResolver: portForwards: - guestSocket: "/run/finch.sock" hostSocket: "{{.Dir}}/sock/finch.sock" +- guestSocket: "/run/cred.sock" + hostSocket: "/private/tmp/cred.sock" diff --git a/pkg/config/defaults_darwin.go b/pkg/config/defaults_darwin.go index edb0274f3..85699a6b9 100644 --- a/pkg/config/defaults_darwin.go +++ b/pkg/config/defaults_darwin.go @@ -59,6 +59,13 @@ func cpuDefault(cfg *Finch, deps LoadSystemDeps) { } } +// Different because the other defaults use single values; this uses a slice +func credHelperDefault(cfg *Finch) { + if cfg.CredsHelpers == nil || len(cfg.CredsHelpers) == 0 { + cfg.CredsHelpers = []string{"osxkeychain"} + } +} + // applyDefaults sets default configuration options if they are not already set. func applyDefaults( cfg *Finch, @@ -75,6 +82,6 @@ func applyDefaults( } vmDefault(cfg, supportsVz) rosettaDefault(cfg) - + credHelperDefault(cfg) return cfg } diff --git a/pkg/config/nerdctl_config_applier.go b/pkg/config/nerdctl_config_applier.go index 55d7f1339..9a340fb0e 100644 --- a/pkg/config/nerdctl_config_applier.go +++ b/pkg/config/nerdctl_config_applier.go @@ -95,11 +95,21 @@ func updateEnvironment(fs afero.Fs, fc *Finch, finchDir, homeDir, limaVMHomeDir } //nolint:gosec // G101: Potential hardcoded credentials false positive - const configureCredHelperTemplate = `([ -e "$FINCH_DIR"/cred-helpers/docker-credential-%s ] || \ - (echo "error: docker-credential-%s not found in $FINCH_DIR/cred-helpers directory.")) && \ - ([ -L /usr/local/bin/docker-credential-%s ] || sudo ln -s "$FINCH_DIR"/cred-helpers/docker-credential-%s /usr/local/bin)` + const configureCredHelperTemplate = `[ -x /usr/local/bin/docker-credential-%s ] || ( +echo "Creating credential helper script for %s" && \ +sudo tee /usr/local/bin/docker-credential-%s > /dev/null << 'CREDEOF' +#!/bin/bash +# Forward to host daemon via TCP +stdin_data=$(cat) +response=$(echo -e "$1\n$stdin_data" | nc host.lima.internal 8080) +echo "$response" +CREDEOF +sudo chmod +x /usr/local/bin/docker-credential-%s +)` for _, credHelper := range fc.CredsHelpers { + cmdArr = append(cmdArr, fmt.Sprintf(`echo '{"credsStore": "%s"}' > "$FINCH_DIR"/config.json`, credHelper)) + // Create VM-side bridge script that forwards to host TCP daemon (only if it doesn't exist) cmdArr = append(cmdArr, fmt.Sprintf(configureCredHelperTemplate, credHelper, credHelper, credHelper, credHelper)) }