diff --git a/.github/workflows/test-cred-security.yaml b/.github/workflows/test-cred-security.yaml new file mode 100644 index 000000000..f2f0bbf45 --- /dev/null +++ b/.github/workflows/test-cred-security.yaml @@ -0,0 +1,591 @@ +name: test-cred-security +on: + pull_request: + branches: + - main + - os-native-credstore + schedule: + - cron: '0 8 * * *' + workflow_dispatch: + inputs: + ref_name: + description: "name of git ref for which to run security tests" + required: false + type: string + +permissions: + # This is required for configure-aws-credentials to request an OIDC JWT ID token to access AWS resources later on. + # More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings + id-token: write + # This is required for actions/checkout + contents: read + +env: + GO_VERSION: '1.24.6' + +jobs: + # taken from finch/.github/workflows/build-and-test-pkg.yaml for consistent testing + get-tag-name: + name: Get tag name + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 2 + outputs: + tag: ${{ steps.check-tag.outputs.tag }} + commit: ${{ steps.export-commit.outputs.commit }} + steps: + - name: Check tag from workflow input and github ref + id: check-tag + run: | + if [ -n "${{ inputs.ref_name }}" ]; then + tag=${{ inputs.ref_name }} + else + tag=${{ github.ref_name }} + fi + echo "using tag=${tag}" + echo "tag=$tag" >> ${GITHUB_OUTPUT} + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ steps.check-tag.outputs.tag }} + fetch-depth: 0 + persist-credentials: false + submodules: true + - name: Export commit hash + id: export-commit + run: | + commit=$(git rev-parse HEAD) + echo "using commit=${commit}" + echo "commit=$commit" >> ${GITHUB_OUTPUT} + + macos-security-test: + strategy: + fail-fast: false + matrix: + arch: [arm64, amd64] + version: [14, 15] + include: + - arch: arm64 + output_arch: aarch64 + - arch: amd64 + output_arch: x86_64 + needs: get-tag-name + runs-on: [self-hosted, macos, "${{ matrix.arch }}", "${{ matrix.version }}", test] + timeout-minutes: 30 + steps: + - name: Clean workspace + run: | + setopt NULL_GLOB && rm -rf ${{ github.workspace }}/* + shell: zsh {0} + + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ needs.get-tag-name.outputs.commit }} + fetch-depth: 0 + persist-credentials: false + submodules: true + + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + + - name: Create test users + run: | + # Create finchUser (primary test user) + sudo dscl . -create /Users/finchUser + sudo dscl . -create /Users/finchUser UserShell /bin/zsh + sudo dscl . -create /Users/finchUser RealName "Finch User" + sudo dscl . -create /Users/finchUser UniqueID 1001 + sudo dscl . -create /Users/finchUser PrimaryGroupID 20 + sudo dscl . -create /Users/finchUser NFSHomeDirectory /Users/finchUser + sudo createhomedir -c -u finchUser + + # Create otherUser (security boundary test user) + sudo dscl . -create /Users/otherUser + sudo dscl . -create /Users/otherUser UserShell /bin/zsh + sudo dscl . -create /Users/otherUser RealName "Other User" + sudo dscl . -create /Users/otherUser UniqueID 1002 + sudo dscl . -create /Users/otherUser PrimaryGroupID 20 + sudo dscl . -create /Users/otherUser NFSHomeDirectory /Users/otherUser + sudo createhomedir -c -u otherUser + + # Give finchUser sudo access for installation + echo "finchUser ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers + shell: zsh {0} + + - name: Build Finch as finchUser + run: | + # Copy workspace to finchUser's directory + sudo cp -R ${{ github.workspace }} /Users/finchUser/finch-build + sudo chown -R finchUser:staff /Users/finchUser/finch-build + + # Setup Homebrew for finchUser and install dependencies + sudo -u finchUser /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + sudo -u finchUser zsh -c 'echo "eval \$(/opt/homebrew/bin/brew shellenv)" >> ~/.zshrc' + sudo -u finchUser zsh -c 'source ~/.zshrc && cd /Users/finchUser/finch-build && brew install lz4 automake autoconf libtool yq llvm httpd' + sudo -u finchUser zsh -c 'cd /Users/finchUser/finch-build && git clean -f -d' + sudo -u finchUser zsh -c 'cd /Users/finchUser/finch-build && make clean' + sudo -u finchUser zsh -c 'cd /Users/finchUser/finch-build && make download-licenses || true' + sudo -u finchUser zsh -c 'cd /Users/finchUser/finch-build && make FINCH_OS_IMAGE_LOCATION_ROOT=/Applications/Finch' + shell: zsh {0} + + - name: Generate and install local PKG as finchUser + run: | + # Generate local unsigned PKG + sudo -u finchUser zsh -c ' + cd /Users/finchUser/finch-build + mkdir -p ./installer-builder/output/origin + cp -RP ./_output ./installer-builder/output/origin/ + ./installer-builder/tools/build-macos-pkg.sh ${{ matrix.output_arch }} ${{ needs.get-tag-name.outputs.tag }} + ' + + # Install the generated PKG + PKG_PATH="/Users/finchUser/finch-build/installer-builder/output/installer/unsigned/package/artifact/Finch.pkg" + sudo -u finchUser installer -pkg "$PKG_PATH" -target / + + # Initialize VM immediately after installation + sudo -u finchUser finch vm init + shell: zsh {0} + + - name: Validate installation as finchUser + run: | + # Check credential helper files and permissions + sudo -u finchUser ls -lah /Users/finchUser/.finch/cred-helpers/ + sudo -u finchUser ls -lah /Users/finchUser/.finch/config.json + sudo -u finchUser ls -lah /Users/finchUser/.finch/finch.yaml + + # Check /Applications/Finch installation (system-wide, not user-specific) + sudo -u finchUser ls -lah /Applications/Finch/finch-credhelper/ + sudo -u finchUser ls -lah /Applications/Finch/bin/ + + # Check LaunchAgent plist file + sudo -u finchUser ls -lah /Users/finchUser/Library/LaunchAgents/ | grep finch || echo ".plist present" + sudo -u finchUser launchctl list | grep finch + + # Test LaunchAgent stop/start cycle + sudo -u finchUser /Applications/Finch/finch-credhelper/native-creds-agent-stop.sh + sudo -u finchUser ls -lah /Users/finchUser/Library/LaunchAgents/ | grep finch || echo ".plist removed" + sudo -u finchUser launchctl list | grep finch || echo "LaunchAgent stopped" + + sudo -u finchUser /Applications/Finch/finch-credhelper/native-creds-agent-start.sh + sudo -u finchUser ls -lah /Users/finchUser/Library/LaunchAgents/ | grep finch || echo ".plist present" + sudo -u finchUser launchctl list | grep finch + shell: zsh {0} + + - name: Set up local private registry with "registry" image + run: | + # Pull registry image (tests uncredentialed pull) + sudo -u finchUser finch pull registry:2 + sudo -u finchUser finch image ls + + # Create htpasswd file for registry authentication + sudo -u finchUser mkdir -p /Users/finchUser/registry-auth + sudo -u finchUser htpasswd -Bbn finchUser finchPass > /Users/finchUser/registry-auth/htpasswd + + # Start private registry with authentication + sudo -u finchUser finch run -d \ + --name test-registry \ + -p 5000:5000 \ + -v /Users/finchUser/registry-auth:/auth \ + -e REGISTRY_AUTH=htpasswd \ + -e REGISTRY_AUTH_HTPASSWD_REALM="Test Registry Realm" \ + -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ + registry:2 + + # Wait for registry to start + sleep 5 + + # Verify registry is running + curl -f http://localhost:5000/v2/ || echo "Registry not accessible without auth (expected)" + shell: zsh {0} + + - name: Test credential security boundaries + run: | + # Login to private registry as finchUser (stores credentials in keychain) + sudo -u finchUser finch login localhost:5000 -u finchUser -p finchPass + + # Test credential enumeration as finchUser (should work) + echo "=== Testing credential enumeration as finchUser ===" + sudo -u finchUser /Users/finchUser/.finch/cred-helpers/docker-credential-osxkeychain list || echo "List failed for finchUser" + + # Verify finchUser can access their own credentials + echo '{"ServerURL":"localhost:5000"}' | sudo -u finchUser /Users/finchUser/.finch/cred-helpers/docker-credential-osxkeychain get || echo "Get failed for finchUser" + + # Test cross-user access as otherUser (should fail) + echo "=== Testing cross-user credential access as otherUser ===" + sudo -u otherUser /Users/finchUser/.finch/cred-helpers/docker-credential-osxkeychain list 2>&1 || echo "Cross-user list blocked (expected)" + + # Test if otherUser can access finchUser's credential helper at all + echo '{"ServerURL":"localhost:5000"}' | sudo -u otherUser /Users/finchUser/.finch/cred-helpers/docker-credential-osxkeychain get 2>&1 || echo "Cross-user get blocked (expected)" + + # Test root access (should also fail - keychain is user-specific) + echo "=== Testing root credential access ===" + sudo /Users/finchUser/.finch/cred-helpers/docker-credential-osxkeychain list 2>&1 || echo "Root list blocked (expected)" + echo '{"ServerURL":"localhost:5000"}' | sudo /Users/finchUser/.finch/cred-helpers/docker-credential-osxkeychain get 2>&1 || echo "Root get blocked (expected)" + + # Test directory permissions prevent access + echo "=== Testing directory permission boundaries ===" + sudo -u otherUser ls -la /Users/finchUser/.finch/ 2>&1 || echo "Directory access blocked (expected)" + sudo -u otherUser cat /Users/finchUser/.finch/config.json 2>&1 || echo "Config access blocked (expected)" + shell: zsh {0} + + - name: Test concurrent operations and Dockerfile build (comprehensive) + run: | + # Pull nginx:alpine and push to private registry + sudo -u finchUser finch pull nginx:alpine + sudo -u finchUser finch tag nginx:alpine localhost:5000/nginx-base + sudo -u finchUser finch push localhost:5000/nginx-base + + # Create multiple tags for concurrent testing + for suffix in a b c d e; do + sudo -u finchUser finch tag nginx:alpine localhost:5000/nginx-test-$suffix + done + + # Concurrent push operations (tests credential helper under load) + echo "=== Starting concurrent pushes ===" + for suffix in a b c d e; do + sudo -u finchUser finch push localhost:5000/nginx-test-$suffix & + done + wait + + # Simple Dockerfile test (tests build-time credentials) + sudo -u finchUser mkdir -p /Users/finchUser/dockerfile-test + sudo -u finchUser tee /Users/finchUser/dockerfile-test/Dockerfile << 'EOF' + FROM localhost:5000/nginx-base + RUN echo "Build test successful" > /test-result + CMD ["cat", "/test-result"] + EOF + + # Build using private registry base image + sudo -u finchUser finch build -t localhost:5000/nginx-custom /Users/finchUser/dockerfile-test/ + sudo -u finchUser finch push localhost:5000/nginx-custom + + # Clean and verify + sudo -u finchUser finch system prune -af + curl -u finchUser:finchPass http://localhost:5000/v2/_catalog + + # Final verification for integrity after push and pull + sudo -u finchUser finch pull localhost:5000/nginx-custom + sudo -u finchUser finch run --rm localhost:5000/nginx-custom + shell: zsh {0} + + + - name: Cleanup and uninstall + if: ${{ always() }} + run: | + # Stop registry container + sudo -u finchUser finch stop test-registry || true + sudo -u finchUser finch rm test-registry || true + + # Run uninstall script as finchUser + sudo -u finchUser /Applications/Finch/uninstall.sh || echo "Uninstall script not found or failed" + + # Verify LaunchAgent is removed + sudo -u finchUser launchctl list | grep finch || echo "LaunchAgent removed (expected)" + sudo -u finchUser ls -lah /Users/finchUser/Library/LaunchAgents/ | grep finch || echo "Plist file removed (expected)" + + # Verify /Applications/Finch is removed + ls -la /Applications/Finch/ || echo "/Applications/Finch removed (expected)" + + # Clean up test users + sudo dscl . -delete /Users/finchUser || true + sudo dscl . -delete /Users/otherUser || true + sudo rm -rf /Users/finchUser || true + sudo rm -rf /Users/otherUser || true + + # Remove from sudoers + sudo sed -i '' '/finchUser ALL=(ALL) NOPASSWD: ALL/d' /etc/sudoers || true + shell: zsh {0} + + windows-cred-manager: + needs: get-tag-name + runs-on: [self-hosted, windows, amd64, test] + timeout-minutes: 30 + steps: + - name: Clean workspace + run: | + Remove-Item -Path "${{ github.workspace }}\*" -Recurse -Force -ErrorAction SilentlyContinue + shell: powershell + + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ needs.get-tag-name.outputs.commit }} + fetch-depth: 0 + persist-credentials: false + submodules: true + + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + + - name: Create test users + run: | + # Create finchUser (primary test user) + $Password = ConvertTo-SecureString "FinchTestPass123!" -AsPlainText -Force + New-LocalUser -Name "finchUser" -Password $Password -Description "Finch Test User" -PasswordNeverExpires + Add-LocalGroupMember -Group "Users" -Member "finchUser" + Add-LocalGroupMember -Group "Administrators" -Member "finchUser" + + # Create otherUser (security boundary test user) + $OtherPassword = ConvertTo-SecureString "OtherTestPass123!" -AsPlainText -Force + New-LocalUser -Name "otherUser" -Password $OtherPassword -Description "Other Test User" -PasswordNeverExpires + Add-LocalGroupMember -Group "Users" -Member "otherUser" + shell: powershell + + - name: Configure git CRLF settings + run: | + git config --global core.autocrlf false + git config --global core.eol lf + shell: powershell + + - name: Clean up previous files + run: | + Remove-Item C:\Users\finchUser\.finch -Recurse -ErrorAction Ignore + Remove-Item C:\Users\finchUser\AppData\Local\.finch -Recurse -ErrorAction Ignore + make clean + shell: powershell + + - name: Build Finch as finchUser + run: | + # Build Finch using standard process + make FINCH_OS_IMAGE_LOCATION_ROOT="C:\Program Files\Finch" + shell: powershell + + - name: Generate and install MSI as finchUser + run: | + # Generate MSI using standard build process + $version = "${{ needs.get-tag-name.outputs.tag }}".TrimStart('v') + if ($version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { + $version = "0.0.1" + } + + powershell .\msi-builder\BuildFinchMSI.ps1 -Version $version + + # Install the generated MSI + $msiPath = ".\msi-builder\build\Finch-$version.msi" + Start-Process msiexec.exe -ArgumentList "/i", $msiPath, "/quiet", "/norestart" -Wait + + # Add Finch to PATH + $env:PATH += ";C:\Program Files\Finch\bin" + + # Initialize VM immediately after installation + & "C:\Program Files\Finch\bin\finch.exe" vm init + shell: powershell + + - name: Validate installation as finchUser + run: | + # Check credential helper files and permissions + Get-ChildItem "C:\Users\finchUser\.finch\cred-helpers\" -Force + Get-ChildItem "C:\Users\finchUser\.finch\config.json" -Force + Get-ChildItem "C:\Users\finchUser\.finch\finch.yaml" -Force + + # Check Program Files installation + Get-ChildItem "C:\Program Files\Finch\finch-credhelper\" -Force + Get-ChildItem "C:\Program Files\Finch\bin\" -Force + + # Check Windows Service + Get-Service -Name "FinchCredentialService" -ErrorAction SilentlyContinue + + # Test service stop/start cycle + & "C:\Program Files\Finch\finch-credhelper\native-creds-service-stop.ps1" + Start-Sleep -Seconds 2 + Get-Service -Name "FinchCredentialService" -ErrorAction SilentlyContinue | Where-Object {$_.Status -eq "Stopped"} + + & "C:\Program Files\Finch\finch-credhelper\native-creds-service-start.ps1" + Start-Sleep -Seconds 2 + Get-Service -Name "FinchCredentialService" | Where-Object {$_.Status -eq "Running"} + shell: powershell + + - name: Set up local private registry with "registry" image + run: | + # Pull registry image (tests uncredentialed pull) + & "C:\Program Files\Finch\bin\finch.exe" pull registry:2 + & "C:\Program Files\Finch\bin\finch.exe" image ls + + # Create registry auth directory and htpasswd file + $authDir = "C:\Users\finchUser\registry-auth" + New-Item -ItemType Directory -Path $authDir -Force + + # Create htpasswd entry (using basic auth format) + $htpasswdContent = "finchUser:`$2y`$10`$abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOP" + Set-Content -Path "$authDir\htpasswd" -Value $htpasswdContent + + # Start private registry with authentication + & "C:\Program Files\Finch\bin\finch.exe" run -d ` + --name test-registry ` + -p 5000:5000 ` + -v "${authDir}:/auth" ` + -e REGISTRY_AUTH=htpasswd ` + -e "REGISTRY_AUTH_HTPASSWD_REALM=Test Registry Realm" ` + -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd ` + registry:2 + + # Wait for registry to start + Start-Sleep -Seconds 5 + + # Verify registry is running + try { + Invoke-WebRequest -Uri "http://localhost:5000/v2/" -ErrorAction Stop + } catch { + Write-Host "Registry not accessible without auth (expected)" + } + shell: powershell + + - name: Test credential security boundaries + run: | + # Login to private registry as finchUser (stores credentials in Windows Credential Manager) + & "C:\Program Files\Finch\bin\finch.exe" login localhost:5000 -u finchUser -p finchPass + + # Test credential enumeration as finchUser (should work) + Write-Host "=== Testing credential enumeration as finchUser ===" + try { + & "C:\Users\finchUser\.finch\cred-helpers\docker-credential-wincred.exe" list + } catch { + Write-Host "List failed for finchUser: $_" + } + + # Verify finchUser can access their own credentials + try { + '{"ServerURL":"localhost:5000"}' | & "C:\Users\finchUser\.finch\cred-helpers\docker-credential-wincred.exe" get + } catch { + Write-Host "Get failed for finchUser: $_" + } + + # Test cross-user access as otherUser (should fail) + Write-Host "=== Testing cross-user credential access as otherUser ===" + + # Create credential for otherUser to test isolation + $otherUserCreds = New-Object System.Management.Automation.PSCredential("otherUser", (ConvertTo-SecureString "OtherTestPass123!" -AsPlainText -Force)) + + try { + Start-Process -FilePath "C:\Users\finchUser\.finch\cred-helpers\docker-credential-wincred.exe" -ArgumentList "list" -Credential $otherUserCreds -Wait -NoNewWindow -RedirectStandardError "error.txt" + Write-Host "Cross-user list blocked (expected)" + } catch { + Write-Host "Cross-user access properly blocked: $_" + } + + # Test Administrator access (should work but access different credential store) + Write-Host "=== Testing Administrator credential access ===" + try { + & "C:\Users\finchUser\.finch\cred-helpers\docker-credential-wincred.exe" list + } catch { + Write-Host "Admin access result: $_" + } + + # Test directory permissions prevent access + Write-Host "=== Testing directory permission boundaries ===" + try { + Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "dir", "C:\Users\finchUser\.finch\" -Credential $otherUserCreds -Wait -NoNewWindow -RedirectStandardError "dir_error.txt" + Write-Host "Directory access blocked (expected)" + } catch { + Write-Host "Directory access blocked: $_" + } + shell: powershell + + - name: Test concurrent operations and Dockerfile build (comprehensive) + run: | + # Pull nginx and push to private registry + & "C:\Program Files\Finch\bin\finch.exe" pull nginx:alpine + & "C:\Program Files\Finch\bin\finch.exe" tag nginx:alpine localhost:5000/nginx-base + & "C:\Program Files\Finch\bin\finch.exe" push localhost:5000/nginx-base + + # Create multiple tags for concurrent testing + $suffixes = @("a", "b", "c", "d", "e") + foreach ($suffix in $suffixes) { + & "C:\Program Files\Finch\bin\finch.exe" tag nginx:alpine "localhost:5000/nginx-test-$suffix" + } + + # Concurrent push operations (tests credential helper under load) + Write-Host "=== Starting concurrent pushes ===" + $jobs = @() + foreach ($suffix in $suffixes) { + $jobs += Start-Job -ScriptBlock { + param($suffix) + & "C:\Program Files\Finch\bin\finch.exe" push "localhost:5000/nginx-test-$suffix" + } -ArgumentList $suffix + } + $jobs | Wait-Job | Receive-Job + + # Simple Dockerfile test (tests build-time credentials) + $dockerfileDir = "C:\Users\finchUser\dockerfile-test" + New-Item -ItemType Directory -Path $dockerfileDir -Force + + # Create simple Dockerfile content + "FROM localhost:5000/nginx-base" | Out-File -FilePath "$dockerfileDir\Dockerfile" -Encoding ASCII + "RUN echo 'Build test successful' > /test-result" | Out-File -FilePath "$dockerfileDir\Dockerfile" -Append -Encoding ASCII + "CMD ['cat', '/test-result']" | Out-File -FilePath "$dockerfileDir\Dockerfile" -Append -Encoding ASCII + + # Build using private registry base image + & "C:\Program Files\Finch\bin\finch.exe" build -t localhost:5000/nginx-custom $dockerfileDir + & "C:\Program Files\Finch\bin\finch.exe" push localhost:5000/nginx-custom + + # Clean and verify + & "C:\Program Files\Finch\bin\finch.exe" system prune -af + + # Verify registry catalog + $authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("finchUser:finchPass")) + try { + Invoke-WebRequest -Uri "http://localhost:5000/v2/_catalog" -Headers @{Authorization="Basic $authHeader"} + } catch { + Write-Host "Registry catalog check failed: $_" + } + + # Final verification for integrity after push and pull + & "C:\Program Files\Finch\bin\finch.exe" pull localhost:5000/nginx-custom + & "C:\Program Files\Finch\bin\finch.exe" run --rm localhost:5000/nginx-custom + shell: powershell + + - name: Cleanup and uninstall + if: ${{ always() }} + run: | + # Stop registry container + try { + & "C:\Program Files\Finch\bin\finch.exe" stop test-registry + & "C:\Program Files\Finch\bin\finch.exe" rm test-registry + } catch { + Write-Host "Registry cleanup failed or already stopped" + } + + # Run uninstall (MSI uninstall) + $productCode = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -like "*Finch*" } | Select-Object -ExpandProperty IdentifyingNumber + if ($productCode) { + msiexec /x $productCode /qn + } + + # Verify service is removed + try { + Get-Service -Name "FinchCredentialService" -ErrorAction Stop + Write-Host "Service still exists (unexpected)" + } catch { + Write-Host "Service removed (expected)" + } + + # Verify Program Files is removed + if (Test-Path "C:\Program Files\Finch") { + Write-Host "Program Files directory still exists (unexpected)" + } else { + Write-Host "Program Files directory removed (expected)" + } + shell: powershell + + - name: Remove Finch VM and Clean Up Previous Environment + if: ${{ always() }} + timeout-minutes: 5 + shell: pwsh + run: | + ./scripts/cleanup_wsl.ps1 + make clean + + # Clean up test users + try { + Remove-LocalUser -Name "finchUser" -ErrorAction SilentlyContinue + Remove-LocalUser -Name "otherUser" -ErrorAction SilentlyContinue + Remove-Item -Path "C:\Users\finchUser" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path "C:\Users\otherUser" -Recurse -Force -ErrorAction SilentlyContinue + } catch { + Write-Host "User cleanup completed with warnings: $_" + } + exit 0 # Cleanup may set the exit code; just ignore + diff --git a/Makefile b/Makefile index 6ce060aa7..7c5be1e3e 100644 --- a/Makefile +++ b/Makefile @@ -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) @@ -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/finch@.service +# 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/finch@.service ifeq ($(BUILD_OS), Windows_NT) include Makefile.windows @@ -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 ### @@ -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: diff --git a/Makefile.creds b/Makefile.creds new file mode 100644 index 000000000..83c6b265c --- /dev/null +++ b/Makefile.creds @@ -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 diff --git a/Makefile.darwin b/Makefile.darwin index 3f0b9b6cb..0ca4b2913 100644 --- a/Makefile.darwin +++ b/Makefile.darwin @@ -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_LOCATION_ROOT)|g" finch.yaml.temp sed -i.bak -e "s||$(FINCH_DAEMON_LOCATION)|g" finch.yaml.temp sed -i.bak -e "s||$(FINCH_DAEMON_CREDHELPER_LOCATION)|g" finch.yaml.temp + sed -i.bak -e "s||$(FINCH_CREDHELPER_SOCKET_LOCATION)|g" finch.yaml.temp sed -i.bak -e "s||$(RUNC_OVERRIDE_AARCH64_LOCATION)|g" finch.yaml.temp sed -i.bak -e "s//$(RUNC_OVERRIDE_AARCH64_DIGEST)/g" finch.yaml.temp sed -i.bak -e "s||$(RUNC_OVERRIDE_X86_64_LOCATION)|g" finch.yaml.temp diff --git a/cmd/finch-credhelper/helper-utils.go b/cmd/finch-credhelper/helper-utils.go new file mode 100644 index 000000000..ff1f9c82f --- /dev/null +++ b/cmd/finch-credhelper/helper-utils.go @@ -0,0 +1,120 @@ +// Package main implements the finch credential helper bridge. +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +const ( + maxBufferSize = 4096 + credHelpersDir = "cred-helpers" + finchConfigDir = ".finch" +) + +var credentialHelperNames = map[string]string{ + "darwin": "docker-credential-osxkeychain", + "windows": "docker-credential-wincred.exe", +} + +// Parses JSON requests. +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 +} + +// Determines and validates the credential helper path. +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 +} + +// Invokes the platform-specific credential helper binary. +func executeCredentialHelper(command, input string) (string, error) { + credHelperPath, err := getCredentialHelperPath() + if err != nil { + return "", err + } + + // #nosec G204 -- credHelperPath is validated and command is from trusted source + cmd := exec.Command(credHelperPath, command) + 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) +} + +// Processes inbound credential requests from Lima VM bridge. +func processCredentialRequest(conn interface { + Read([]byte) (int, error) + Write([]byte) (int, error) +}) error { + buffer := make([]byte, 0, maxBufferSize) + buffer = buffer[: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 +} diff --git a/cmd/finch-credhelper/mac-creds.go b/cmd/finch-credhelper/mac-creds.go new file mode 100644 index 000000000..e7a7f00ed --- /dev/null +++ b/cmd/finch-credhelper/mac-creds.go @@ -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) + } +} diff --git a/cmd/finch-credhelper/windows-creds.go b/cmd/finch-credhelper/windows-creds.go new file mode 100644 index 000000000..da3ed8dc9 --- /dev/null +++ b/cmd/finch-credhelper/windows-creds.go @@ -0,0 +1,57 @@ +//go:build windows + +package main + +import ( + "fmt" + "log" + "net" + "os" + "path/filepath" +) + +// Windows socket server (for WSL2 socket forwarding) +func startWindowsCredentialServer() error { + userProfile := os.Getenv("USERPROFILE") + if userProfile == "" { + return fmt.Errorf("USERPROFILE not set") + } + socketPath := filepath.Join(userProfile, ".finch", "native-creds.sock") + _ = os.Remove(socketPath) // Ignore error if file doesn't exist + + 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 func() { _ = listener.Close() }() + + // 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 func() { _ = c.Close() }() + 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) + } +} diff --git a/finch.yaml.d/mac.yaml b/finch.yaml.d/mac.yaml index 1be4e99ee..c3a235f83 100644 --- a/finch.yaml.d/mac.yaml +++ b/finch.yaml.d/mac.yaml @@ -7,6 +7,9 @@ provision: - mode: boot script: | modprobe virtiofs + - mode: boot + script: | + dnf install -y socat # port this to common.yaml after windows socket forwarding is added - mode: user script: | @@ -57,3 +60,6 @@ hostResolver: portForwards: - guestSocket: "/run/finch.sock" hostSocket: "{{.Dir}}/sock/finch.sock" +- guestSocket: "/tmp/native-creds.sock" + hostSocket: "" + reverse: true \ No newline at end of file diff --git a/installer-builder/darwin/Resources/uninstall.sh b/installer-builder/darwin/Resources/uninstall.sh index 2b358b9e5..8f204dd34 100755 --- a/installer-builder/darwin/Resources/uninstall.sh +++ b/installer-builder/darwin/Resources/uninstall.sh @@ -37,6 +37,24 @@ else echo "[2/4] [ERROR] Could not delete application informations" >&2 fi +#remove credential bridge LaunchAgent +# handle the user who ran sudo ./uninstall.sh, or all users if run directly as root +REAL_USER="${SUDO_USER:-}" +if [ -n "$REAL_USER" ] && [ "$REAL_USER" != "root" ]; then + # Single user cleanup (most common case) + sudo -u "$REAL_USER" launchctl bootout gui/$(id -u "$REAL_USER")/com.runfinch.cred-bridge 2>/dev/null || true + rm -f "/Users/$REAL_USER/Library/LaunchAgents/com.runfinch.cred-bridge.plist" 2>/dev/null || true +else + # Cleanup for all users (fallback) + for user_home in /Users/*; do + if [ -d "$user_home/Library/LaunchAgents" ]; then + username=$(basename "$user_home") + sudo -u "$username" launchctl bootout gui/$(id -u "$username")/com.runfinch.cred-bridge 2>/dev/null || true + rm -f "$user_home/Library/LaunchAgents/com.runfinch.cred-bridge.plist" 2>/dev/null || true + fi + done +fi + #remove application source distribution [ -e "/Applications/Finch" ] && rm -rf /Applications/Finch && rm -rf /opt/finch && rm -rf /private/var/run/finch-lima && rm -rf /private/etc/sudoers.d/finch-lima if [ $? -eq 0 ] diff --git a/installer-builder/darwin/scripts/postinstall b/installer-builder/darwin/scripts/postinstall index 421bf914a..8041d8d71 100755 --- a/installer-builder/darwin/scripts/postinstall +++ b/installer-builder/darwin/scripts/postinstall @@ -33,4 +33,15 @@ pkgutil --pkgs | grep '^org\.Finch\.' | grep -v '^org\.Finch\.__VERSION__' | whi sudo pkgutil --forget "$pkg" done +# Secure credential helper directory after blanket 777 +chmod 750 /Applications/Finch/finch-credhelper +chmod 750 /Applications/Finch/finch-credhelper/finch-cred-bridge +chmod 750 /Applications/Finch/finch-credhelper/native-creds-agent-start.sh +chmod 750 /Applications/Finch/finch-credhelper/native-creds-agent-stop.sh +chmod 640 /Applications/Finch/finch-credhelper/com.runfinch.cred-bridge.plist.template +# Socket will be created by LaunchAgent with correct 600 permissions + +# Create and add native credhelper .plist to LaunchAgents +/Applications/Finch/finch-credhelper/native-creds-agent-start.sh + echo "Post installation process finished." diff --git a/installer-builder/templates/com.runfinch.cred-bridge.plist.template b/installer-builder/templates/com.runfinch.cred-bridge.plist.template new file mode 100644 index 000000000..62a63b7a8 --- /dev/null +++ b/installer-builder/templates/com.runfinch.cred-bridge.plist.template @@ -0,0 +1,34 @@ + + + + + + Label + com.runfinch.cred-bridge + + ProgramArguments + + $(FINCH_CREDHELPER_DIR)/finch-cred-bridge + + + Sockets + + Listeners + + + SockPathName + $(FINCH_CREDHELPER_SOCKET_LOCATION) + SockPathMode + 384 + + + + + inetdCompatibility + + Wait + + + + + \ No newline at end of file diff --git a/installer-builder/tools/build-macos-pkg.sh b/installer-builder/tools/build-macos-pkg.sh index ef267ef24..46ca6fb48 100755 --- a/installer-builder/tools/build-macos-pkg.sh +++ b/installer-builder/tools/build-macos-pkg.sh @@ -40,6 +40,12 @@ buildPkgInstaller() { cp ./installer-builder/darwin/Resources/uninstall.sh $INSTALLER_FULL_PATH/darwinpkg/Applications/Finch sed -i '' -e 's/__VERSION__/'"${VERSION}"'/g' $INSTALLER_FULL_PATH/darwinpkg/Applications/Finch/uninstall.sh + #copy credential helper tools and templates, need to be accessible by postinstall and for re-init on error + mkdir -p $INSTALLER_FULL_PATH/darwinpkg/Applications/Finch/finch-credhelper + cp ./installer-builder/templates/com.runfinch.cred-bridge.plist.template $INSTALLER_FULL_PATH/darwinpkg/Applications/Finch/finch-credhelper/ + cp ./installer-builder/tools/native-creds-agent-start.sh $INSTALLER_FULL_PATH/darwinpkg/Applications/Finch/finch-credhelper/ + cp ./installer-builder/tools/native-creds-agent-stop.sh $INSTALLER_FULL_PATH/darwinpkg/Applications/Finch/finch-credhelper/ + #construct pkg directory mkdir -p $INSTALLER_FULL_PATH/package mkdir -p $INSTALLER_FULL_PATH/signed diff --git a/installer-builder/tools/native-creds-agent-start.sh b/installer-builder/tools/native-creds-agent-start.sh new file mode 100644 index 000000000..70168084a --- /dev/null +++ b/installer-builder/tools/native-creds-agent-start.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +# Setting relevant paths +FINCH_INSTALL_DIR="/Applications/Finch" +TEMPLATE_PATH="$FINCH_INSTALL_DIR/finch-credhelper/com.runfinch.cred-bridge.plist.template" +PLIST_PATH="$HOME/Library/LaunchAgents/com.runfinch.cred-bridge.plist" +mkdir -p "$HOME/Library/LaunchAgents" + +# Replace placeholders in template +sed -e "s|\$(FINCH_CREDHELPER_DIR)/finch-cred-bridge|$FINCH_INSTALL_DIR/finch-cred-bridge|g" \ + -e "s|\$(FINCH_CREDHELPER_SOCKET_LOCATION)|$HOME/.finch/native-creds.sock|g" \ + "$TEMPLATE_PATH" > "$PLIST_PATH" + +# Load the .plist into LaunchAgent +launchctl bootstrap gui/$(id -u) "$PLIST_PATH" + +echo "Finch credential bridge LaunchAgent installed and loaded" \ No newline at end of file diff --git a/installer-builder/tools/native-creds-agent-stop.sh b/installer-builder/tools/native-creds-agent-stop.sh new file mode 100644 index 000000000..97caf413c --- /dev/null +++ b/installer-builder/tools/native-creds-agent-stop.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +PLIST_PATH="$HOME/Library/LaunchAgents/com.runfinch.cred-bridge.plist" + +# Unload the LaunchAgent +launchctl bootout gui/$(id -u)/com.runfinch.cred-bridge 2>/dev/null || true + +# Remove the plist file +rm -f "$PLIST_PATH" + +echo "Finch credential bridge LaunchAgent uninstalled" \ No newline at end of file diff --git a/msi-builder/BuildFinchMSI.ps1 b/msi-builder/BuildFinchMSI.ps1 index fce453af0..b708b40c1 100644 --- a/msi-builder/BuildFinchMSI.ps1 +++ b/msi-builder/BuildFinchMSI.ps1 @@ -63,6 +63,11 @@ Copy-Item -Path (Join-Path -Path $scriptDirectory -ChildPath "uninstall.bat") -D Copy-Item -Path (Join-Path -Path $scriptDirectory -ChildPath "removevm.bat") -Destination (Join-Path -Path $scriptDirectory -ChildPath "build\Finch") Copy-Item -Path (Join-Path -Path $scriptDirectory -ChildPath "finch.ico") -Destination (Join-Path -Path $scriptDirectory -ChildPath "build\Finch") Copy-Item -Path (Join-Path -Path $scriptDirectory -ChildPath "LICENSE.rtf") -Destination (Join-Path -Path $scriptDirectory -ChildPath "build\Finch") + +# Copy credential service management scripts to finch-credhelper directory +$credHelperDir = Join-Path -Path $scriptDirectory -ChildPath "build\Finch\finch-credhelper" +Copy-Item -Path (Join-Path -Path $scriptDirectory -ChildPath "native-creds-service-start.ps1") -Destination $credHelperDir +Copy-Item -Path (Join-Path -Path $scriptDirectory -ChildPath "native-creds-service-stop.ps1") -Destination $credHelperDir Write-Host "Files copied successfully." # 5. Copy WiX template and update resources path and version diff --git a/msi-builder/FinchMSITemplate.wxs b/msi-builder/FinchMSITemplate.wxs index aa1823d17..ae045dabd 100644 --- a/msi-builder/FinchMSITemplate.wxs +++ b/msi-builder/FinchMSITemplate.wxs @@ -89,6 +89,13 @@ + + + + + + + @@ -100,6 +107,7 @@ + diff --git a/msi-builder/install-service.bat b/msi-builder/install-service.bat new file mode 100644 index 000000000..186ac6551 --- /dev/null +++ b/msi-builder/install-service.bat @@ -0,0 +1,22 @@ +@echo off +REM Install Finch Credential Bridge Windows Service + +set SERVICE_NAME=FinchCredBridge +set SERVICE_DISPLAY_NAME=Finch Credential Bridge +set SERVICE_DESCRIPTION=Provides credential bridge functionality for Finch containers +set SERVICE_BINARY=%~dp0finch-credhelper\finch-cred-bridge.exe + +echo Installing %SERVICE_DISPLAY_NAME% service... + +REM Create the service +sc create "%SERVICE_NAME%" binPath= "\"%SERVICE_BINARY%\"" start= demand DisplayName= "%SERVICE_DISPLAY_NAME%" +if %ERRORLEVEL% neq 0 ( + echo Failed to create service + exit /b 1 +) + +REM Set service description +sc description "%SERVICE_NAME%" "%SERVICE_DESCRIPTION%" + +echo %SERVICE_DISPLAY_NAME% service installed successfully +echo Use services.msc to manage the service \ No newline at end of file diff --git a/msi-builder/native-creds-service-start.ps1 b/msi-builder/native-creds-service-start.ps1 new file mode 100644 index 000000000..da8283a69 --- /dev/null +++ b/msi-builder/native-creds-service-start.ps1 @@ -0,0 +1,39 @@ +param( + [string]$InstallDir = $env:ProgramFiles + "\Finch" +) + +$ServiceName = "FinchCredBridge" +$ServiceDisplayName = "Finch Credential Bridge" +$ServiceDescription = "Provides credential bridge functionality for Finch containers" +$ServiceBinary = Join-Path $InstallDir "finch-credhelper\finch-cred-bridge.exe" + +Write-Host "Installing $ServiceDisplayName service..." + +# Check if service already exists +$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($existingService) { + Write-Host "Service already exists, stopping and removing..." + Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue + & sc.exe delete $ServiceName + Start-Sleep -Seconds 2 +} + +# Create the service to run as the current user +$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$result = & sc.exe create $ServiceName binPath= "`"$ServiceBinary`"" start= demand DisplayName= $ServiceDisplayName obj= $currentUser +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create service: $result" + exit 1 +} + +# Set service description +& sc.exe description $ServiceName $ServiceDescription + +# Secure credential helper files (Windows equivalent of chmod 700) +$credHelperDir = Join-Path $InstallDir "finch-credhelper" +icacls "$credHelperDir" /inheritance:r /grant:r "$env:USERNAME:(F)" /grant:r "Administrators:(F)" 2>$null +icacls "$credHelperDir\finch-cred-bridge.exe" /inheritance:r /grant:r "$env:USERNAME:(F)" /grant:r "Administrators:(F)" 2>$null +icacls "$credHelperDir\native-creds-service-start.ps1" /inheritance:r /grant:r "$env:USERNAME:(F)" /grant:r "Administrators:(F)" 2>$null +icacls "$credHelperDir\native-creds-service-stop.ps1" /inheritance:r /grant:r "$env:USERNAME:(F)" /grant:r "Administrators:(F)" 2>$null + +Write-Host "$ServiceDisplayName service installed successfully" \ No newline at end of file diff --git a/msi-builder/native-creds-service-stop.ps1 b/msi-builder/native-creds-service-stop.ps1 new file mode 100644 index 000000000..bc714c063 --- /dev/null +++ b/msi-builder/native-creds-service-stop.ps1 @@ -0,0 +1,21 @@ +$ServiceName = "FinchCredBridge" + +Write-Host "Stopping and removing $ServiceName service..." + +# Check if service exists +$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($existingService) { + # Stop the service + Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue + + # Remove the service + & sc.exe delete $ServiceName + + if ($LASTEXITCODE -eq 0) { + Write-Host "$ServiceName service removed successfully" + } else { + Write-Warning "Failed to remove service, but continuing..." + } +} else { + Write-Host "$ServiceName service not found, nothing to remove" +} \ No newline at end of file diff --git a/msi-builder/postinstall.bat b/msi-builder/postinstall.bat index e393bffd1..a0527bc5f 100644 --- a/msi-builder/postinstall.bat +++ b/msi-builder/postinstall.bat @@ -1,6 +1,7 @@ @echo off SET InstallDir=%~1 SET FilePath=%InstallDir%\os\finch.yaml +SET CredHelperDir=%InstallDir%\finch-credhelper if exist "%FilePath%" ( powershell -Command "$installPath = '%InstallDir%'.Replace('\', '/'); $content = Get-Content '%FilePath%' -Raw; $content = $content -replace '__INSTALLFOLDER__', $installPath; $content = $content.Replace(\"`r`n\", \"`n\"); $utf8NoBom = New-Object System.Text.UTF8Encoding $false; [System.IO.File]::WriteAllText('%FilePath%', $content, $utf8NoBom)" @@ -8,6 +9,11 @@ if exist "%FilePath%" ( icacls "%InstallDir%\lima\data" /grant Users:(OI)(CI)M +:: Install credential bridge service +if exist "%CredHelperDir%\native-creds-service-start.ps1" ( + powershell -ExecutionPolicy Bypass -File "%CredHelperDir%\native-creds-service-start.ps1" -InstallDir "%InstallDir%" +) + :: Delete files and directories if they exist if exist "%InstallDir%\lima\data\finch\" rmdir /s /q "%InstallDir%\lima\data\finch\" if exist "%InstallDir%\lima\data\_config\override.yaml" del /f /q "%InstallDir%\lima\data\_config\override.yaml" diff --git a/msi-builder/uninstall.bat b/msi-builder/uninstall.bat index 42b278102..d5920e22d 100644 --- a/msi-builder/uninstall.bat +++ b/msi-builder/uninstall.bat @@ -1,5 +1,11 @@ @echo off SET InstallDir=%~1 +SET CredHelperDir=%InstallDir%\finch-credhelper + +:: Stop and remove credential bridge service +if exist "%CredHelperDir%\native-creds-service-stop.ps1" ( + powershell -ExecutionPolicy Bypass -File "%CredHelperDir%\native-creds-service-stop.ps1" +) :: Stop and remove any running instance finch.exe vm stop -f ^ & diff --git a/pkg/config/defaults_darwin.go b/pkg/config/defaults_darwin.go index edb0274f3..9a1ed9c17 100644 --- a/pkg/config/defaults_darwin.go +++ b/pkg/config/defaults_darwin.go @@ -59,6 +59,12 @@ func cpuDefault(cfg *Finch, deps LoadSystemDeps) { } } +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 +81,6 @@ func applyDefaults( } vmDefault(cfg, supportsVz) rosettaDefault(cfg) - + credHelperDefault(cfg) return cfg } diff --git a/pkg/config/defaults_windows.go b/pkg/config/defaults_windows.go index 397a987c8..6ee7c9a3d 100644 --- a/pkg/config/defaults_windows.go +++ b/pkg/config/defaults_windows.go @@ -18,6 +18,13 @@ func vmDefault(cfg *Finch) { } } +// Different because the other defaults use single values; this uses a slice +func credHelperDefault(cfg *Finch) { + if len(cfg.CredsHelpers) == 0 { + cfg.CredsHelpers = []string{"wincred"} + } +} + // applyDefaults sets default configuration options if they are not already set. func applyDefaults( cfg *Finch, @@ -26,5 +33,6 @@ func applyDefaults( _ command.Creator, ) *Finch { vmDefault(cfg) + credHelperDefault(cfg) return cfg } diff --git a/pkg/config/nerdctl_config_applier.go b/pkg/config/nerdctl_config_applier.go index 55d7f1339..fdeb82e29 100644 --- a/pkg/config/nerdctl_config_applier.go +++ b/pkg/config/nerdctl_config_applier.go @@ -25,6 +25,34 @@ const ( nerdctlRootfulCfgPath = "/etc/nerdctl/nerdctl.toml" ) +//nolint:gosec // G101: False positive - this is a shell script template, not hardcoded credentials +const osxkeychainCredHelperScript = `#!/bin/bash +input=$(cat) +printf "%s\n%s\n" "$1" "$input" | socat - UNIX-CONNECT:/tmp/native-creds.sock 2>/dev/null +exit_code=$? +if [ $exit_code -ne 0 ]; then + echo '{"error": "credential helper connection failed"}' + exit 1 +fi +` + +//nolint:gosec // G101: False positive - this is a shell script template, not hardcoded credentials +const wincredCredHelperScript = `#!/bin/bash +input=$(cat) +SOCKET_PATH=$(wslpath "$(wslvar USERPROFILE)")/.finch/native-creds.sock +printf "%s\n%s\n" "$1" "$input" | socat - UNIX-CONNECT:"$SOCKET_PATH" 2>/dev/null +exit_code=$? +if [ $exit_code -ne 0 ]; then + echo '{"error": "credential helper connection failed"}' + exit 1 +fi +` + +var helperTemplateScripts = map[string]string{ + "osxkeychain": osxkeychainCredHelperScript, + "wincred": wincredCredHelperScript, +} + type nerdctlConfigApplier struct { dialer fssh.Dialer fs afero.Fs @@ -95,12 +123,27 @@ 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 ] || \ + const generalCredHelperTemplate = `([ -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)` + //nolint:gosec // G101: Potential hardcoded credentials false positive + const nativeCredHelperTemplate = `[ -x /usr/local/bin/docker-credential-%s ] || ( + (sudo mkdir -p /usr/local/bin) && \ + (echo '%s' | sudo tee /usr/local/bin/docker-credential-%s > /dev/null) && \ + (sudo chmod 700 /usr/local/bin/docker-credential-%s))` + for _, credHelper := range fc.CredsHelpers { - cmdArr = append(cmdArr, fmt.Sprintf(configureCredHelperTemplate, credHelper, credHelper, credHelper, credHelper)) + // Add the credhelper to config by default, removes need for user to add + cmdArr = append(cmdArr, fmt.Sprintf(`echo '{"credsStore": "%s"}' > "$FINCH_DIR"/config.json`, credHelper)) + + // If using native credstore, use overwrite instead of symlink + if credHelper == "osxkeychain" || credHelper == "wincred" { + helperScript := helperTemplateScripts[credHelper] + cmdArr = append(cmdArr, fmt.Sprintf(nativeCredHelperTemplate, credHelper, helperScript, credHelper, credHelper)) + } else { + cmdArr = append(cmdArr, fmt.Sprintf(generalCredHelperTemplate, credHelper, credHelper, credHelper, credHelper)) + } } awsDir := fmt.Sprintf("%s/.aws", homeDir)