Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/benchmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,9 @@ jobs:
tool: 'go'
benchmark-data-dir-path: "dev/bench/macOS/${{ env.OS_VERSION }}/${{ env.ARCH }}"
output-file-path: benchmark.txt
alert-threshold: "200%"
fail-on-alert: true
comment-on-alert: true
alert-comment-cc-users: '@runfinch/maintainers'
- name: Push benchmark result
run: git push 'https://github.com/runfinch/finch.git' gh-pages:gh-pages
593 changes: 593 additions & 0 deletions .github/workflows/test-cred-security.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ test-coverage.*
*.syso
msi-builder/build/
contrib/packaging/rpm/rpmbuild
*.log
3 changes: 3 additions & 0 deletions Dockerfile.test-creds
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM 299170649678.dkr.ecr.us-west-2.amazonaws.com/test:nginx
# Test dockerfile for credential bridge functionality
# This pulls from private ECR to test credential access during build
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ MIN_MACOS_VERSION ?= 11.0
FINCH_DAEMON_LOCATION_ROOT ?= $(FINCH_OS_IMAGE_LOCATION_ROOT)/finch-daemon
FINCH_DAEMON_LOCATION ?= $(FINCH_DAEMON_LOCATION_ROOT)/finch-daemon
FINCH_DAEMON_CREDHELPER_LOCATION ?= $(FINCH_DAEMON_LOCATION_ROOT)/docker-credential-finch
FINCH_CREDHELPER_DIR ?= $(OUTDIR)/finch-credhelper
FINCH_CREDHELPER_SOCKET_LOCATION ?= $(FINCH_CREDHELPER_DIR)/native-creds.sock

GOOS ?= $(shell $(GO) env GOOS)
ifeq ($(GOOS),windows)
Expand Down Expand Up @@ -79,7 +81,10 @@ 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/[email protected]
# Include credential helper targets
include Makefile.creds

remote-all: arch-test finch make-creds install.finch-core-dependencies finch.yaml networks.yaml config.yaml $(OUTDIR)/finch-daemon/[email protected]

ifeq ($(BUILD_OS), Windows_NT)
include Makefile.windows
Expand Down Expand Up @@ -260,6 +265,8 @@ download-licenses:

mkdir -p "$(LICENSEDIR)/github.com/lima-vm/lima"
curl https://raw.githubusercontent.com/lima-vm/lima/master/LICENSE --output "$(LICENSEDIR)/github.com/lima-vm/lima/LICENSE"
mkdir -p "$(LICENSEDIR)/github.com/docker/docker-credential-helpers"
curl https://raw.githubusercontent.com/docker/docker-credential-helpers/master/LICENSE --output "$(LICENSEDIR)/github.com/docker/docker-credential-helpers/LICENSE"

### system-level dependencies - end ###

Expand Down Expand Up @@ -406,6 +413,14 @@ mdlint:
mdlint-ctr:
$(BINARYNAME) run --rm -v "$(shell pwd):/repo:ro" -w /repo avtodev/markdown-lint:v1 --ignore CHANGELOG.md '**/*.md'

.PHONY: dev-clean
dev-clean:
-@rm -rf $(OUTDIR) 2>/dev/null || true
-@$(MAKE) -C $(FINCH_CORE_DIR) clean
-@rm ./*.tar.gz 2>/dev/null || true
-@rm ./*.qcow2 2>/dev/null || true
-@rm ./test-coverage.* 2>/dev/null || true

.PHONY: clean
ifeq ($(GOOS),windows)
clean:
Expand Down
122 changes: 122 additions & 0 deletions Makefile.creds
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Credential helper configuration
CRED_HELPER_BASE_URL := https://github.com/docker/docker-credential-helpers/releases/download/v0.9.4

# Platform-specific artifacts and checksums
ifeq ($(BUILD_OS), Darwin)
ifeq ($(ARCH), arm64)
CRED_HELPER_ARTIFACT := docker-credential-osxkeychain-v0.9.4.darwin-arm64
CRED_HELPER_DIGEST := 8db5b7cbcbe0870276e56aa416416161785e450708af64cda0f1be4c392dc2e5
else
CRED_HELPER_ARTIFACT := docker-credential-osxkeychain-v0.9.4.darwin-amd64
CRED_HELPER_DIGEST := ad76d1a1e03def49edfa57fdb2874adf2c468cfa0438aae1b2589434796f7c01
endif
CRED_HELPER_NAME := docker-credential-osxkeychain
else ifeq ($(BUILD_OS), Windows_NT)
ifeq ($(ARCH), arm64)
CRED_HELPER_ARTIFACT := docker-credential-wincred-v0.9.4.windows-arm64.exe
CRED_HELPER_DIGEST := 80a6ddbbabc51a8952308acf4d03c044308357cf217300461c44df066c57fe03
else
CRED_HELPER_ARTIFACT := docker-credential-wincred-v0.9.4.windows-amd64.exe
CRED_HELPER_DIGEST := 66fdf4b50c83aeb04a9ea04af960abaf1a7b739ab263115f956b98bb0d16aa7e
endif
CRED_HELPER_NAME := docker-credential-wincred.exe
endif

CRED_HELPER_URL := $(CRED_HELPER_BASE_URL)/$(CRED_HELPER_ARTIFACT)
CRED_HELPER_OUTPUT := $(HOME)/.finch/cred-helpers/$(CRED_HELPER_NAME)

# Build finch credential bridge
.PHONY: finch-cred-bridge
finch-cred-bridge:
mkdir -p $(FINCH_CREDHELPER_DIR)
$(GO) build -ldflags $(LDFLAGS) -tags "$(GO_BUILD_TAGS)" -o $(FINCH_CREDHELPER_DIR)/finch-cred-bridge $(PACKAGE)/cmd/finch-credhelper
chmod 700 $(FINCH_CREDHELPER_DIR)/finch-cred-bridge

# Download and verify credential helper
.PHONY: docker-credential-helper
docker-credential-helper:
ifeq ($(BUILD_OS), Linux)
@echo "No credential helper needed for Linux"
else
mkdir -p $(dir $(CRED_HELPER_OUTPUT))
curl -L $(CRED_HELPER_URL) -o $(CRED_HELPER_OUTPUT)
@echo "Verifying SHA256 checksum..."
@if echo "$(CRED_HELPER_DIGEST) $(CRED_HELPER_OUTPUT)" | $(if $(findstring Darwin,$(BUILD_OS)),shasum -a 256,sha256sum) -c -; then \
echo "Checksum verification passed"; \
else \
echo "Checksum verification failed" && exit 1; \
fi
chmod 700 $(CRED_HELPER_OUTPUT)
endif

# macOS LaunchAgent management
ifeq ($(BUILD_OS), Darwin)
PLIST_NAME := com.runfinch.cred-bridge.plist
PLIST_TEMPLATE := installer-builder/templates/$(PLIST_NAME).template
PLIST_DEST := $(HOME)/Library/LaunchAgents/$(PLIST_NAME)

.PHONY: install-launch-agent
install-launch-agent:
@echo "Installing LaunchAgent..."
mkdir -p $(dir $(PLIST_DEST))
sed -e "s|\$$(FINCH_CREDHELPER_DIR)/finch-cred-bridge|$(FINCH_CREDHELPER_DIR)/finch-cred-bridge|g" \
-e "s|\$$(FINCH_CREDHELPER_SOCKET_LOCATION)|$(FINCH_CREDHELPER_SOCKET_LOCATION)|g" \
$(PLIST_TEMPLATE) > $(PLIST_DEST)
-launchctl bootout gui/$$(id -u)/com.runfinch.cred-bridge 2>/dev/null || true
launchctl bootstrap gui/$$(id -u) $(PLIST_DEST)
@echo "LaunchAgent installed and loaded"

.PHONY: uninstall-launch-agent
uninstall-launch-agent:
@echo "Uninstalling LaunchAgent..."
-launchctl unload $(PLIST_DEST) 2>/dev/null || true
-rm $(PLIST_DEST) 2>/dev/null || true
@echo "LaunchAgent uninstalled"

.PHONY: setup-cred-bridge
ifeq ($(INSTALLED), true)
setup-cred-bridge: finch-cred-bridge
@echo "Built credential bridge binary - service installation deferred to installer"
else
setup-cred-bridge: finch-cred-bridge install-launch-agent
@echo "Credential bridge setup complete"
endif

else ifeq ($(BUILD_OS), Windows_NT)
# Windows Service management
.PHONY: install-service
install-service:
@echo "Installing Windows Service..."
sc create "FinchCredBridge" binPath= "$(FINCH_CREDHELPER_DIR)\finch-cred-bridge.exe" start= demand
sc description "FinchCredBridge" "Finch Credential Bridge Service"
@echo "Windows Service installed"

.PHONY: uninstall-service
uninstall-service:
@echo "Uninstalling Windows Service..."
-sc stop "FinchCredBridge" 2>nul
-sc delete "FinchCredBridge" 2>nul
@echo "Windows Service uninstalled"

.PHONY: setup-cred-bridge
ifeq ($(INSTALLED), true)
setup-cred-bridge: finch-cred-bridge
@echo "Built credential bridge binary - service installation deferred to installer"
else
setup-cred-bridge: finch-cred-bridge install-service
@echo "Credential bridge setup complete"
endif

else
.PHONY: install-launch-agent uninstall-launch-agent setup-cred-bridge install-service uninstall-service
install-launch-agent uninstall-launch-agent setup-cred-bridge install-service uninstall-service:
@echo "Service management is platform-specific"
endif

.PHONY: make-creds
ifeq ($(BUILD_OS), Linux)
make-creds:
@echo "No credential helpers needed for Linux"
else
make-creds: finch-cred-bridge docker-credential-helper setup-cred-bridge
endif
1 change: 1 addition & 0 deletions Makefile.darwin
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ $(OS_OUTDIR)/finch.yaml: $(OS_OUTDIR) finch.yaml.d/common.yaml finch.yaml.d/mac.
sed -i.bak -e "s|<finch_daemon_root>|$(FINCH_DAEMON_LOCATION_ROOT)|g" finch.yaml.temp
sed -i.bak -e "s|<finch_daemon_location>|$(FINCH_DAEMON_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s|<finch_daemon_credhelper_location>|$(FINCH_DAEMON_CREDHELPER_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s|<finch_credhelper_socket_location>|$(FINCH_CREDHELPER_SOCKET_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s|<runc_override_aarch64_location>|$(RUNC_OVERRIDE_AARCH64_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s/<runc_override_aarch64_digest>/$(RUNC_OVERRIDE_AARCH64_DIGEST)/g" finch.yaml.temp
sed -i.bak -e "s|<runc_override_x86_64_location>|$(RUNC_OVERRIDE_X86_64_LOCATION)|g" finch.yaml.temp
Expand Down
114 changes: 114 additions & 0 deletions cmd/finch-credhelper/helper-utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)

const (
maxBufferSize = 4096

Check failure on line 13 in cmd/finch-credhelper/helper-utils.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofumpt)
credHelpersDir = "cred-helpers"
finchConfigDir = ".finch"
)

var credentialHelperNames = map[string]string{
"darwin": "docker-credential-osxkeychain",
"windows": "docker-credential-wincred.exe",
}

// Parsing JSON requests

Check failure on line 23 in cmd/finch-credhelper/helper-utils.go

View workflow job for this annotation

GitHub Actions / lint

Comment should end in a period (godot)
func parseCredstoreRequest(request string) (command, input string, err error) {
lines := strings.Split(strings.TrimSpace(request), "\n")
if len(lines) == 0 {
return "", "", fmt.Errorf("empty request")
}

command = strings.TrimSpace(lines[0])
if command == "list" {
return command, "", nil
}
if len(lines) < 2 {
return "", "", fmt.Errorf("command %s requires input", command)
}

return command, strings.TrimSpace(lines[1]), nil
}

// Determining and validating the credential helper path

Check failure on line 41 in cmd/finch-credhelper/helper-utils.go

View workflow job for this annotation

GitHub Actions / lint

Comment should end in a period (godot)
func getCredentialHelperPath() (string, error) {
helperName, exists := credentialHelperNames[runtime.GOOS]
if !exists {
return "", fmt.Errorf("credential helper not supported on this platform")
}

homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory")
}

path := filepath.Join(homeDir, finchConfigDir, credHelpersDir, helperName)
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("credential helper not found")
}

return path, nil
}

// Invoking the platform-specific credential helper binary
func executeCredentialHelper(command, input string) (string, error) {
credHelperPath, err := getCredentialHelperPath()
if err != nil {
return "", err
}

cmd := exec.Command(credHelperPath, command)

Check failure on line 68 in cmd/finch-credhelper/helper-utils.go

View workflow job for this annotation

GitHub Actions / lint

G204: Subprocess launched with variable (gosec)
if input != "" {
cmd.Stdin = strings.NewReader(input)
}
cmd.Env = os.Environ()

output, err := cmd.CombinedOutput()
response := strings.TrimSpace(string(output))

// Handling errors, with special case for "get" command requiring empty cred. JSON
if err != nil {
if command == "get" {
return createEmptyCredentials(input), nil
}
return "", fmt.Errorf("credential helper failed")
}

return response, nil
}

// Creates default credentials when credentials are not found
func createEmptyCredentials(serverURL string) string {
return fmt.Sprintf(`{"ServerURL":"%s","Username":"","Secret":""}`, serverURL)
}

// Process inbound credential requests from Lima VM bridge
func processCredentialRequest(conn interface{ Read([]byte) (int, error); Write([]byte) (int, error) }) error {
buffer := make([]byte, maxBufferSize)
n, err := conn.Read(buffer)
if err != nil {
return fmt.Errorf("failed to read request")
}

request := strings.TrimSpace(string(buffer[:n]))
command, input, err := parseCredstoreRequest(request)
if err != nil {
return err
}

response, err := executeCredentialHelper(command, input)
if err != nil {
return err
}

_, err = conn.Write([]byte(response))
return err
}
29 changes: 29 additions & 0 deletions cmd/finch-credhelper/mac-creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build darwin

package main

import (
"fmt"
"net"
"os"
)

// handleCredstoreRequest processes credential requests via socket activation
func handleCredstoreRequest() error {
conn, err := net.FileConn(os.Stdin)
if err != nil {
return fmt.Errorf("failed to create connection from stdin: %w", err)
}
defer conn.Close()

return processCredentialRequest(conn)
}

func main() {
// macOS credential helper using socket activation via launchd
// launchd passes the socket connection through stdin
if err := handleCredstoreRequest(); err != nil {
fmt.Fprintf(os.Stderr, "credential helper failed\n")
os.Exit(1)
}
}
58 changes: 58 additions & 0 deletions cmd/finch-credhelper/windows-creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//go:build windows

package main

import (
"fmt"
"log"
"net"
"os"
"path/filepath"
)

// Windows socket server (for WSL2 socket forwarding)
func startWindowsCredentialServer() error {

Check failure on line 15 in cmd/finch-credhelper/windows-creds.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofumpt)
userProfile := os.Getenv("USERPROFILE")
if userProfile == "" {
return fmt.Errorf("USERPROFILE not set")
}
socketPath := filepath.Join(userProfile, ".finch", "native-creds.sock")
os.Remove(socketPath)

Check failure on line 21 in cmd/finch-credhelper/windows-creds.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `os.Remove` is not checked (errcheck)

listener, err := net.Listen("unix", socketPath)
if err != nil {
return fmt.Errorf("failed to create socket: %w", err)
}

// set socket file permissions to owner only
if err := os.Chmod(socketPath, 0600); err != nil {
return fmt.Errorf("failed to set socket permissions: %w", err)
}

defer listener.Close()

Check failure on line 33 in cmd/finch-credhelper/windows-creds.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `listener.Close` is not checked (errcheck)

// Accept connections
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Failed to accept connection")
continue
}

// Handle each connection
go func(c net.Conn) {
defer c.Close()

Check failure on line 45 in cmd/finch-credhelper/windows-creds.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `c.Close` is not checked (errcheck)
if err := processCredentialRequest(c); err != nil {
log.Printf("Error processing credential request")
}
}(conn)
}
}

func main() {
if err := startWindowsCredentialServer(); err != nil {
log.Printf("Windows credential server failed")
os.Exit(1)
}
}
Loading
Loading