Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
591 changes: 591 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