diff --git a/.github/ISSUE_TEMPLATE/digital-experience-request.md b/.github/ISSUE_TEMPLATE/digital-experience-request.md deleted file mode 100644 index 97b697ec8037..000000000000 --- a/.github/ISSUE_TEMPLATE/digital-experience-request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: 🌐 Digital Experience request [FKA Website request] -about: Propose a new feature or enhancement to fleetdm.com. -title: 'TODO: ' -labels: '#g-digital-experience' -assignees: '' - ---- - -## Goal - -| User story | -|:---------------------------------------------------------------------------| -| As a _________________________________________, -| I want to _________________________________________ -| so that I can _________________________________________. - ->For help creating a user story, see ["Writing a good user story"](https://fleetdm.com/handbook/company/development-groups#writing-a-good-user-story) in the website handbook. - - -## How? - -- [ ] TODO - -### Context - - - diff --git a/.github/workflows/test-db-changes.yml b/.github/workflows/test-db-changes.yml index 2bd89ab82b1e..10d1ec4ddea2 100644 --- a/.github/workflows/test-db-changes.yml +++ b/.github/workflows/test-db-changes.yml @@ -40,33 +40,6 @@ jobs: with: fetch-depth: 0 - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version-file: 'go.mod' - - - name: Start Infra Dependencies - # Use & to background this - run: docker compose up -d mysql_test & - - - name: Wait for mysql - run: | - echo "waiting for mysql..." - until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do - echo "." - sleep 1 - done - echo "mysql is ready" - - - name: Verify test schema changes - run: | - make dump-test-schema - if [[ $(git diff server/datastore/mysql/schema.sql) ]]; then - echo "❌ fail: uncommited changes in schema.sql" - echo "please run `make dump-test-schema` and commit the changes" - exit 1 - fi - # TODO: This doesn't cover all scenarios since other PRs might # be merged into `main` after this check has passed. # @@ -84,8 +57,8 @@ jobs: base_ref=$(git tag --list "fleet-v*" --sort=-creatordate | head -n 1) fi - all_migrations=($(ls server/datastore/mysql/migrations/tables/20*_*.go | sort -r)) - new_migrations=($(git diff --find-renames --name-only --diff-filter=A $base_ref -- server/datastore/mysql/migrations/tables/20\*_\*.go | sort -r)) + all_migrations=($(ls server/datastore/mysql/migrations/tables/20*_*.go | sort -r | grep -v '_test.go')) + new_migrations=($(git diff --find-renames --name-only --diff-filter=A $base_ref -- server/datastore/mysql/migrations/tables/20\*_\*.go ':(exclude,glob)server/datastore/mysql/migrations/tables/20*_*_test.go' | sort -r)) index=0 for migration in "${new_migrations[@]}"; do @@ -110,3 +83,31 @@ jobs: echo "Ref: https://github.com/fleetdm/fleet/blob/main/handbook/engineering/scaling-fleet.md#foreign-keys-and-locking" exit 1 fi + + + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + + - name: Start Infra Dependencies + # Use & to background this + run: docker compose up -d mysql_test & + + - name: Wait for mysql + run: | + echo "waiting for mysql..." + until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do + echo "." + sleep 1 + done + echo "mysql is ready" + + - name: Verify test schema changes + run: | + make dump-test-schema + if [[ $(git diff server/datastore/mysql/schema.sql) ]]; then + echo "❌ fail: uncommited changes in schema.sql" + echo "please run `make dump-test-schema` and commit the changes" + exit 1 + fi \ No newline at end of file diff --git a/articles/exe-install-scripts.md b/articles/exe-install-scripts.md new file mode 100644 index 000000000000..3ed2f437191b --- /dev/null +++ b/articles/exe-install-scripts.md @@ -0,0 +1,334 @@ +# Windows EXE install scripts + +## What are EXE install scripts? + +EXE install scripts are a way to install software on Windows. EXE installers, such as `Figma-124.3.2.exe`, are self-contained packages with all the files and instructions needed to install software on a Windows device. EXE installers are fully customizable and do not follow the same installation process as MSI installers. + +For EXE installers, there is no unique script or command that will work for all installers. MSI installers are typically preferred over EXE installers because they provide a standardized installation process, easier silent deployment, and better integration with Windows Installer Service. If available, MSI installers offer more predictable results in enterprise environments. + +Some EXE installers and uninstallers require additional switches or flags to run silently. Common flags include `/S`, `/q`, `/quiet`, `/silent`, or `--silent`. + +## Device-scoped install scripts + +The recommended way to install software on Windows devices is to use device-scoped install scripts. These scripts install the software for all users on the device and run the installation process with administrator privileges. + +Fleet defaults to a device-scoped install script when you add software using an EXE installer. + +## User-scoped install scripts + +Some software can only be installed for a specific user. In this case, you can use user-scoped install scripts. The software is installed only for the user currently logged in, and the installation process is run with the user's privileges. + +### Example user-scoped install script + +The install script creates a scheduled task that will automatically be run as the current (logged-in) user. The EXE installer is copied to a public directory accessible by the user, ensuring that even non-administrator users can run the scheduled task to complete the installation. After the task finishes, the installer and the task are deleted. + +The use of scheduled tasks allows the installer to run with user-level permissions, which is especially useful when installing software for non-admin users without requiring administrator credentials at the time of execution. + +Since the installation is run by the current user, the script does not output the installer's messages to the console. If you need to see the output, you can modify the script to redirect it to a file and append it to the script output. + +```powershell +# Some installers require a flag to run silently. +# Each installer might use a different argument (usually it's "/S" or "/s") +$installArgs = "/S" + +$exeFilePath = "${env:INSTALLER_PATH}" + +$exitCode = 0 + +try { + +# Copy the installer to a public folder so that all can access it +# users +$exeFilename = Split-Path $exeFilePath -leaf +Copy-Item -Path $exeFilePath -Destination "${env:PUBLIC}" -Force +$exeFilePath = "${env:PUBLIC}\$exeFilename" + +# Task properties. The task will be started by the logged in user +$action = New-ScheduledTaskAction -Execute "$exeFilePath" ` + -Argument "$installArgs" +$trigger = New-ScheduledTaskTrigger -AtLogOn +$userName = Get-CimInstance -ClassName Win32_ComputerSystem | + Select-Object -expand UserName +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries + +# Create a task object with the properties defined above +$task = New-ScheduledTask -Action $action -Trigger $trigger ` + -Settings $settings + +# Register the task +$taskName = "fleet-install-$exeFilename" +Register-ScheduledTask "$taskName" -InputObject $task -User "$userName" + +# keep track of the start time to cancel if taking too long to start +$startDate = Get-Date + +# Start the task now that it is ready +Start-ScheduledTask -TaskName "$taskName" -TaskPath "\" + +# Wait for the task to be running +$state = (Get-ScheduledTask -TaskName "$taskName").State +Write-Host "ScheduledTask is '$state'" + +while ($state -ne "Running") { + Write-Host "ScheduledTask is '$state'. Waiting to run .exe..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 1 + $state = (Get-ScheduledTask -TaskName "$taskName").State +} + +# Wait for the task to be done +$state = (Get-ScheduledTask -TaskName "$taskName").State +while ($state -eq "Running") { + Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 10 + $state = (Get-ScheduledTask -TaskName "$taskName").State +} + +# Remove task +Write-Host "Removing ScheduledTask: $taskName." +Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} finally { + # Remove installer + Remove-Item -Path $exeFilePath -Force +} + +Exit $exitCode +``` + +### Example user-scoped uninstall script + +The uninstall script creates a scheduled task that will automatically be run as the current (logged-in) user. The uninstaller creates a separate PowerShell script for the user. After the task finishes, the script and the task are deleted. + +Since the uninstall script is run by the current user, it does not output its messages to the console. If you need to see the output, you can modify the main script to redirect it to a file and append it to the output. + +```powershell +# Fleet extracts the name from the installer (EXE) and saves it to PACKAGE_ID +# variable +$softwareName = $PACKAGE_ID + +# Script to uninstall software as the current logged-in user. +$userScript = @' +$softwareName = $PACKAGE_ID + +# Using the exact software name here is recommended to avoid +# uninstalling unintended software. +$softwareNameLike = "*$softwareName*" + +# Some uninstallers require additional flags to run silently. +# Each uninstaller might use a different argument (usually it's "/S" or "/s") +$uninstallArgs = "/S" + +$uninstallCommand = "" +$exitCode = 0 + +try { + +$userKey = ` + 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' +[array]$uninstallKeys = Get-ChildItem ` + -Path @($userKey) ` + -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath } + +$foundUninstaller = $false +foreach ($key in $uninstallKeys) { + # If needed, add -notlike to the comparison to exclude certain similar + # software + if ($key.DisplayName -like $softwareNameLike) { + $foundUninstaller = $true + # Get the uninstall command. Some uninstallers do not include + # 'QuietUninstallString' and require a flag to run silently. + $uninstallCommand = if ($key.QuietUninstallString) { + $key.QuietUninstallString + } else { + $key.UninstallString + } + + # The uninstall command may contain command and args, like: + # "C:\Program Files\Software\uninstall.exe" --uninstall --silent + # Split the command and args + $splitArgs = $uninstallCommand.Split('"') + if ($splitArgs.Length -gt 1) { + if ($splitArgs.Length -eq 3) { + $uninstallArgs = "$( $splitArgs[2] ) $uninstallArgs".Trim() + } elseif ($splitArgs.Length -gt 3) { + Throw ` + "Uninstall command contains multiple quoted strings. " + + "Please update the uninstall script.`n" + + "Uninstall command: $uninstallCommand" + } + $uninstallCommand = $splitArgs[1] + } + Write-Host "Uninstall command: $uninstallCommand" + Write-Host "Uninstall args: $uninstallArgs" + + $processOptions = @{ + FilePath = $uninstallCommand + PassThru = $true + Wait = $true + } + if ($uninstallArgs -ne '') { + $processOptions.ArgumentList = "$uninstallArgs" + } + + # Start the process and track the exit code + $process = Start-Process @processOptions + $exitCode = $process.ExitCode + + # Prints the exit code + Write-Host "Uninstall exit code: $exitCode" + # Exit the loop once the software is found and uninstalled. + break + } +} + +if (-not $foundUninstaller) { + Write-Host "Uninstaller for '$softwareName' not found." + $exitCode = 1 +} + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} + +Exit $exitCode +'@ + +$exitCode = 0 + +# Create a script in a public folder so that it can be accessed by all users. +$uninstallScriptPath = "${env:PUBLIC}/uninstall-$softwareName.ps1" +$taskName = "fleet-uninstall-$softwareName" +try { + Set-Content -Path $uninstallScriptPath -Value $userScript -Force + + # Task properties. The task will be started by the logged in user + $action = New-ScheduledTaskAction -Execute "PowerShell.exe" ` + -Argument "$uninstallScriptPath" + $trigger = New-ScheduledTaskTrigger -AtLogOn + $userName = Get-CimInstance -ClassName Win32_ComputerSystem | + Select-Object -expand UserName + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries + + # Create a task object with the properties defined above + $task = New-ScheduledTask -Action $action -Trigger $trigger ` + -Settings $settings + + # Register the task + Register-ScheduledTask "$taskName" -InputObject $task -User "$userName" + + # keep track of the start time to cancel if taking too long to start + $startDate = Get-Date + + # Start the task now that it is ready + Start-ScheduledTask -TaskName "$taskName" -TaskPath "\" + + # Wait for the task to be running + $state = (Get-ScheduledTask -TaskName "$taskName").State + Write-Host "ScheduledTask is '$state'" + + while ($state -ne "Running") { + Write-Host "ScheduledTask is '$state'. Waiting to uninstall..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 1 + $state = (Get-ScheduledTask -TaskName "$taskName").State + } + + # Wait for the task to be done + $state = (Get-ScheduledTask -TaskName "$taskName").State + while ($state -eq "Running") { + Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 10 + $state = (Get-ScheduledTask -TaskName "$taskName").State + } + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} finally { + # Remove task + Write-Host "Removing ScheduledTask: $taskName." + Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false + + # Remove user script + Remove-Item -Path $uninstallScriptPath -Force +} + +Exit $exitCode +``` + +## Install script for raw executables + +Raw executables without installers are less common but may be used in specific scenarios, such as when a vendor provides a standalone binary file for a lightweight application. In these cases, ensuring all necessary dependencies are in place is important. Additionally, consider cleaning up the source executable after installation to avoid leaving unnecessary files on the system. If you have a raw executable that does not come with an installer, you can use the following script to install it. This script copies the executable to Program Files, which are accessible by all users. + +```powershell +$exeFilePath = "${env:INSTALLER_PATH}" + +try { + +# extract the name of the executable to use as the sub-directory name +$exeName = [System.IO.Path]::GetFileName($exeFilePath) +$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath) + +$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir + +# check if the directory does not exist, and create it if necessary +if (-not (Test-Path -Path $destinationPath)) { + New-Item -ItemType Directory -Path $destinationPath +} + +# copy the .exe file to the new sub-directory +$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName +Copy-Item -Path $exeFilePath -Destination $destinationExePath +Exit $LASTEXITCODE + +} catch { + Write-Host "Error: $_" + Exit 1 +} +``` +## Conclusion + +EXE install scripts provide a flexible solution for installing software on Windows devices when MSI installers are unavailable. By leveraging the power of PowerShell and scheduled tasks, IT administrators can easily automate both device-scoped and user-scoped installations. Whether you're deploying software for all users on a device or targeting a specific logged-in user, the provided scripts offer a robust starting point for handling EXE installations. + +Always verify the EXE installer's specific flags for silent installation for smoother operations, ensure proper permissions are in place, and consider implementing logging for troubleshooting. While MSI installers are generally preferred for their standardized behavior, these scripts allow you to manage even the most customized EXE installs in enterprise environments. + +Following this guide will enable you to manage software deployments using EXE install scripts, improving efficiency and ensuring a seamless installation experience across your Windows devices. + + + + + + + diff --git a/articles/fleet-now-supports-ios-and-ipados-software-deployment-and-automated-patch-management.md b/articles/fleet-now-supports-ios-and-ipados-software-deployment-and-automated-patch-management.md index 50ae4e241c48..4280852e116c 100644 --- a/articles/fleet-now-supports-ios-and-ipados-software-deployment-and-automated-patch-management.md +++ b/articles/fleet-now-supports-ios-and-ipados-software-deployment-and-automated-patch-management.md @@ -63,8 +63,9 @@ As Mike McNeil, Fleet’s CEO, says: *“Our vision is to empower teams to manag To learn more about how Fleet can support your organization, visit [fleetdm.com/mdm](https://fleetdm.com/mdm). - + + diff --git a/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18.md b/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18.md new file mode 100644 index 000000000000..afde4d97339e --- /dev/null +++ b/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18.md @@ -0,0 +1,29 @@ +# Fleet supports Apple’s latest operating systems: macOS 15 Sequoia, iOS 18, and iPadOS 18 + +![Fleet supports Apple’s latest operating systems: macOS 15 Sequoia, iOS 18, and iPadOS 18](../website/assets/images/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18-1600x900@2x.jpg) + +_Photo by [aditya bhatia](https://www.pexels.com/photo/people-walking-on-a-bridge-between-trees-13809734/)_ + +Fleet is pleased to announce full support for Apple’s newest operating systems, including macOS 15 Sequoia, iOS 18, and iPadOS 18. With these updates, Fleet ensures seamless management and security capabilities for organizations adopting the latest Apple technology across their device fleet. This release enables IT administrators to confidently manage devices running these new operating systems, leveraging Fleet's robust tools for device monitoring, policy enforcement, and configuration management. + +## Noteworthy changes and known issues for macOS 15 Sequoia + +macOS 15 Sequoia brings significant changes that may impact device management workflows. Notably, firewall settings are no longer contained in a `.plist` file, resulting in potential reporting issues with osquery. Fleet is working with the osquery community to address this issue and update the `alf` table to correctly report firewall status under Sequoia. For more details on this issue, [follow the progress here](https://github.com/fleetdm/fleet/issues/21802). Additionally, manual installation of unsigned `fleet-osquery.pkg` packages now require extra steps due to changes in Apple's security settings, reflecting a heightened focus on device security. + +## Expanding Support for Declarative Device Management (DDM) + +As Apple expands the Declarative Device Management (DDM) capabilities with macOS 15 Sequoia, Fleet looks forward to implementing these new functionalities to enhance device management capabilities further. Today, administrators can send DDM JSON payloads directly to hosts, enabling more responsive and granular control over device configurations and settings. Fleet's support for DDM allows organizations to leverage this robust framework to manage devices efficiently without constant communication with the server. For more information on DDM, visit Apple’s [guide to Declarative Device Management](https://support.apple.com/guide/deployment/intro-to-declarative-device-management-depb1bab77f8/1/web/1.0). + +## Updates Across macOS, iOS, and iPadOS + +Apple has renamed "Profiles" to "Device Management" in all the new operating systems within System Settings. This change affects how administrators and users interact with device management settings on all Apple platforms. For more details on navigating these changes, visit Apple’s [support page](https://it-training.apple.com/tutorials/support/sup530/#Determining-Whether-a-Device-Is-Managed). + +Fleet remains committed to supporting Apple's latest advancements, ensuring that your organization can seamlessly integrate and manage devices running macOS 15 Sequoia, iOS 18, and iPadOS 18 with the same reliability and security you expect. Beginning with macOS 16, [Fleet will provide release-day support for major macOS releases](https://fleetdm.com/handbook/engineering#provide-0-day-support-for-major-version-macos-releases), ensuring your fleet is always prepared for the latest updates. Stay tuned for updates as we refine and enhance our support for these new platforms. [Engineering | Fleet handbook](https://fleetdm.com/handbook/engineering#provide-0-day-support-for-major-version-macos-releases) + + + + + + + + diff --git a/articles/how-to-uninstall-fleetd.md b/articles/how-to-uninstall-fleetd.md new file mode 100644 index 000000000000..1b7a524cd7d9 --- /dev/null +++ b/articles/how-to-uninstall-fleetd.md @@ -0,0 +1,34 @@ +# How to uninstall Fleet's agent (fleetd) + +This guide walks you through the steps to remove fleetd from your device. After performing these steps, the device will display as an offline host in the Fleet UI until you manually remove it. + +## On macOS: +Run the [cleanup script](https://github.com/fleetdm/fleet/blob/main/orbit/tools/cleanup/cleanup_macos.sh) found in Fleet's GitHub + +--- + +## On Windows: +Use the "Add or remove programs" dialog to remove Fleet osquery. + +![windows_uninstall](https://github.com/user-attachments/assets/4140e62b-f67a-4df6-85b0-430c2c624881) + +--- + +## On Linux: + +Using Debian package manager (Debian, Ubuntu, etc.) : + +Run ```sudo apt remove fleet-osquery -y``` + +Using yum Package Manager (RHEL, CentOS, etc.) : + +Run ```sudo rpm -e fleet-osquery-X.Y.Z.x86_64``` + +Are you having trouble uninstalling Fleetd on macOS, Windows, or Linux? Get help on Slack in the [#fleet channel](https://fleetdm.com/slack). + + + + + + + diff --git a/articles/how-to-uninstall-osquery.md b/articles/how-to-uninstall-osquery.md deleted file mode 100644 index c5c54359ab47..000000000000 --- a/articles/how-to-uninstall-osquery.md +++ /dev/null @@ -1,60 +0,0 @@ -# How to uninstall osquery - -This article walks you through the steps to remove osquery from your device. Remember that if you enrolled this device in a Fleet instance, it would display as an offline host in the Fleet UI until you manually remove it. - -## On macOS: -Open up your terminal and paste the following commands; note that `sudo` is required, and you’ll need administrator privileges to complete this process. - -``` -sudo launchctl unload /Library/LaunchDaemons/io.osquery.agent.plist -sudo rm /Library/LaunchDaemons/io.osquery.agent.plist -sudo rm -rf /private/var/log/osquery /private/var/osquery -sudo rm /usr/local/bin/osquery* -sudo pkgutil --forget io.osquery.agent -``` - -These commands stop the running osquery daemon, remove it from your device, and delete the files created by osquery. - -And that’s it; you have now removed osquery from your macOS device. - ---- - -## On Windows: -Removing osquery on Windows 10 is a simple process. To get started, open Windows settings and go to Apps. Then find “osquery” and click Uninstall. - -![Uninstall osquery](../website/assets/images/articles/how-to-uninstall-osquery-1-607x188@2x.png) - -Click Uninstall again to confirm, and osquery will be removed from your Windows device. You might need to restart your computer to complete the uninstall process fully. - ---- - -## On Linux: - -1. Open your terminal and paste the following commands to stop the running osquery service, uninstall osquery, and clean up files created by osquery. - -2. Note that `sudo` is required, and you’ll need administrative privileges to complete this process. - -3. Using Debian package manager (Debian, Ubuntu, etc.) : - -``` -sudo systemctl stop osqueryd.service -sudo apt remove osquery -rm -rf /var/osquery /var/log/osquery /etc/osquery -``` - -Using yum Package Manager (RHEL, CentOS, etc.) : - -``` -sudo systemctl stop osqueryd.service -sudo yum remove osquery -rm -rf /var/osquery /var/log/osquery /etc/osquery -``` - -Are you running into trouble uninstalling osquery on macOS, Windows, or Linux? Get help on Slack in the [#fleet channel](https://fleetdm.com/slack). - - - - - - - diff --git a/articles/macos-mdm-setup.md b/articles/macos-mdm-setup.md index bc91ee6a7206..0f32ccb09033 100644 --- a/articles/macos-mdm-setup.md +++ b/articles/macos-mdm-setup.md @@ -25,8 +25,7 @@ To connect Fleet to ABM, you have to add an ABM token to Fleet. To add an ABM to 1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. 2. Under "Automatic enrollment", click "Add ABM", and then click "Add ABM" again on the next page. Follow the instructions in the modal and upload an ABM token to Fleet. -When one of your uploaded ABM tokens has expired or is within 30 days of expiring, you will see a warning -banner at the top of page reminding you to renew your token. +When one of your uploaded ABM tokens has expired or is within 30 days of expiring, you will see a warning banner at the top of page reminding you to renew your token. To renew an ABM token: @@ -53,7 +52,50 @@ If no default team is set for a host platform (macOS, iOS, or iPadOS), then newl > A host can be transferred to a new (not default) team before it enrolls. In the Fleet UI, you can do this under **Settings** > **Teams**. -### Simple Certificate Enrollment Protocol (SCEP) +## Volume Purchasing Program (VPP) + +> Available in Fleet Premium + +To connect Fleet to Apple's VPP, head to the guide [here](https://fleetdm.com/guides/install-vpp-apps-on-macos-using-fleet). + +## Best practice + +Most organizations only need one ABM token and one VPP token to manage their macOS, iOS, and iPadOS hosts. + +These organizations may need multiple ABM and VPP tokens: + +- Managed Service Providers (MSPs) +- Enterprises that acquire new businesses and as a result inherit new hosts +- Umbrella organizations that preside over entities with separated purchasing authority (i.e. a hospital or university) + +For **MSPs**, the best practice is to have one ABM and VPP connection per client. + +The default teams in Fleet for each client's ABM token in Fleet will look like this: +- macOS: 💻 Client A - Workstations +- iOS: 📱🏢 Client A - Company-owned iPhones +- iPadOS:🔳🏢 Client A - Company-owned iPads + +Client A's VPP token will be assigned to the above teams. + +For **enterprises that acquire**, the best practice is to add a new ABM and VPP connection for each acquisition. + +These will default teams in Fleet: + +Enterprise ABM token: +- macOS: 💻 Enterprise - Workstations +- iOS: 📱🏢 Enterprise - Company-owned iPhones +- iPadOS:🔳🏢 Enterprise - Company-owned iPads + +The enterprises's VPP token will be assigned to the above teams. + +Acquisition ABM token: +- macOS: 💻 Acquisition - Workstations +- iOS: 📱🏢 Acquisition - Company-owned iPhones +- iPadOS:🔳🏢 Acquisition - Company-owned iPads + +The acquisitions's VPP token will be assigned to the above teams. + +## Simple Certificate Enrollment Protocol (SCEP) Fleet uses SCEP certificates (1 year expiry) to authenticate the requests hosts make to Fleet. Fleet renews each host's SCEP certificates automatically every 180 days. diff --git a/articles/role-based-access.md b/articles/role-based-access.md index 0b587e3d8c2a..d147796cdb58 100644 --- a/articles/role-based-access.md +++ b/articles/role-based-access.md @@ -48,7 +48,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | View all software | ✅ | ✅ | ✅ | ✅ | | | Add, edit, and delete software | | | ✅ | ✅ | ✅ | | Download added software | | | ✅ | ✅ | | -| Install software on hosts | | | ✅ | ✅ | | +| Install/uninstall software on hosts | | | ✅ | ✅ | | | Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ | | | Filter hosts by software | ✅ | ✅ | ✅ | ✅ | | | Filter software by team\* | ✅ | ✅ | ✅ | ✅ | | @@ -130,7 +130,7 @@ Users with access to multiple teams can be assigned different roles for each tea | View software | ✅ | ✅ | ✅ | ✅ | | | Add and delete software | | | ✅ | ✅ | ✅ | | Download added software | | | ✅ | ✅ | | -| Install software on hosts | | | ✅ | ✅ | | +| Install/uninstall software on hosts | | | ✅ | ✅ | | | Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ | | | Filter hosts by software | ✅ | ✅ | ✅ | ✅ | | | Filter software | ✅ | ✅ | ✅ | ✅ | | diff --git a/articles/sysadmin-diaries-gitops-a-strategic-advantage.md b/articles/sysadmin-diaries-gitops-a-strategic-advantage.md new file mode 100644 index 000000000000..51d087a410df --- /dev/null +++ b/articles/sysadmin-diaries-gitops-a-strategic-advantage.md @@ -0,0 +1,63 @@ +# Sysadmin diaries: GitOps: A strategic advantage for automation, collaboration, and cost savings + +![Sysadmin diaries: GitOps: A strategic advantage for automation, collaboration, and cost savings](../website/assets/images/articles/sysadmin-diaries-1600x900@2x.png) + +_This diary entry was originally published on [Allen's LinkedIn](https://www.linkedin.com/pulse/gitops-strategic-advantage-automation-collaboration-cost-houchins-a4luf/)._ + +![GitHub logo and GitHub Actions logo](../website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-1-312x207@2x.jpg) + +For many IT professionals, the idea of GitOps might seem daunting, if not completely foreign. I can relate to this. I’ve spent over two decades contributing at every level of IT, from individual contributor to executive leadership, and until recently, GitOps was something I had never implemented. It felt like a complex, high-barrier concept that no one adequately explained in terms I could quantify. + +As an IT leader, I often prioritized immediate business needs over initiatives that seemed more technical or abstract, especially those I didn’t fully understand. GitOps, at first glance, fell into that category. While the term GitOps might be newer, the principles behind it aren’t. Its roots lie in familiar DevOps and Engineering methodologies. GitOps has helped these groups become efficiency wizards orchestrating complex testing, builds, and deployments through centralized code repositories and automations. + +Imagine applying those same principles to your entire IT infrastructure and applications. GitOps is something you can approach gradually, finding small wins to build upon, even with the smallest steps. But before you invest any time, why should you care? + +GitOps is no longer just for DevOps teams. IT professionals, Information Security and compliance experts, people leaders, and Executives alike should consider it a cornerstone of our operations. It’s time to start thinking about GitOps capabilities as a critical factor in decision-making, whether you’re evaluating new tools or renewing existing ones. + +## Information technology + +According to *[Okta’s Businesses at Work 2024 report](https://www.okta.com/resources/whitepaper-businesses-at-work/)*, IT teams manage an average of 93 apps globally—some more, some fewer. That’s potentially 93 different systems to context-switch between, each with its own UI, change management process, and configuration quirks. To complicate matters, these systems aren’t usually managed by a single IT team. For instance, network systems might be handled by one group and productivity applications by another, while IT support often requires access across the board. + +GitOps changes that dynamic. By centralizing configuration management, GitOps offers full visibility, allowing every team to access the information they need to be more effective. Better yet, it standardizes how contributors interact with systems by using a common “code language,” empowering them to collaborate on areas they wouldn’t usually have administrator rights over. This is one of the most effective ways to break down silos and foster collaboration across traditionally separate groups. + +GitOps enables IT teams to unlock greater value through automation, allowing them to focus on more engaging, creative challenges that require human ingenuity—while leaving the repetitive, mundane tasks to the machines. For example, I want to ensure my end users’ devices are running the latest version of Google Chrome. Without GitOps, I’d need to manually monitor for updates and adjust compliance criteria whenever a new version is released. + +With GitOps, however, I can automate both the monitoring of Chrome updates and the adjustment of compliance criteria, creating a true “set it and forget it” workflow. This not only saves time but also ensures that compliance is maintained seamlessly, without manual intervention. + +![Screenshot of GitHub Actions workflow progress](../website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-2-1999x680@2x.png) + +## Information security & compliance + +The best Information Security and Compliance teams aim to protect the company’s assets and customers’ data without negatively impacting user productivity or privacy. However, achieving this balance can be tricky, as security needs often conflict with user autonomy. These teams are also responsible for accessing corporate systems to verify and audit configurations, which can sometimes create friction. + +A GitOps-centric environment can help ease this tension by fostering a culture of trust and transparency. Instead of multiple touchpoints, both security teams and end users can access a centralized repository, a single source of truth, for all configuration data. This level of visibility streamlines audits and builds trust, ensuring that security doesn’t feel like “big brother” hovering over end users. It creates an environment where operational teams and users feel empowered, with clear, mutual access to critical information. + +![GitOps workflow diagram](../website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-3-566x415@2x.png) + +Most systems offer little or no change management features. At best, they might say, “An admin made a change to this setting” and display what was updated. At worst, they might just say, “An admin made a change” without further details. Not only that, but information security and compliance teams don’t want to hunt for this data across various systems, which leads to the creation of centralized logging systems. These shortcomings also result in teams creating complex ticketing systems to propose, approve, and track changes, inevitably leading to the dreaded change management meeting—a time-consuming and often uninspiring process. They’re typically boring and take us away from more engaging, skill-appropriate tasks. However, With GitOps, every change across every system is tracked in one centralized location. This allows full visibility into each proposed change and its outcome and space for questions and comments. More importantly, anyone can propose changes or solutions, ensuring that the entire team feels involved and invested in decisions that impact them. GitOps doesn’t just streamline the process; it fosters collaboration and ownership in a way that traditional methods can’t. + +## Leadership & executives + +Let’s face it: you’re juggling competing priorities with constant pressure to increase efficiency and justify spending. While the pitch for GitOps sounds appealing, you’re a realist and know it will require an investment. It may not be strictly financial, but it will demand time and focus from your team. You’ll need to reprioritize commitments to ensure a meaningful return on the effort spent improving efficiency and meeting business demands. This is why GitOps should be introduced as a core strategy from the top down. It requires a shift in mindset and workflow, which might spark a cultural transformation within your organization. + +Beyond the tangible benefits, GitOps offers a host of unrealized gains that could have an even bigger impact on your business. Yes, it may be challenging at first, but you’ll be upskilling your team along the way. Learning something new is rarely easy, but support and investing in training will pay off. As your team implements greater automation, you’ll uncover opportunities to streamline processes and eliminate redundant systems, ultimately driving down costs. While licensing reductions are easy to quantify, it’s just as critical for leaders to communicate the story of time and efficiency savings. + +Perhaps most importantly, you’ll see a morale boost. Your teams will engage in more meaningful, stimulating work, and the skills they develop in one system will transfer across many others, multiplying their value to the business. This kind of transformation drives productivity and enables your team to become true force multipliers across the entire organization. + +## Where do you go from here? + +Some of you may already be well along your GitOps journey. If that’s the case, I encourage you to share your insights through conference presentations or by open-sourcing your solutions. By doing so, you can help others navigate similar challenges and showcase the real-world benefits of GitOps. + +For those yet to begin, [Fleet](https://fleetdm.com/) is a great place to start. Not only will you be setting up a system that provides comprehensive visibility and insights into all your devices, Fleet also provides an [easy-to-understand GitOps repository to help you get started](https://github.com/fleetdm/fleet-gitops). + +I hope this discussion has piqued your interest and given you the confidence to explore GitOps further. It should be at the top of your decision-making criteria during purchasing and renewal cycles for any software or systems you manage. For IT professionals, imagine becoming the hero of work automation and enhancing the services you provide to your customers. For InfoSec and compliance teams, picture gaining faster and deeper visibility into how devices in your environment are configured, whether on-prem or in the cloud, and having one location to run queries and enforce policies. For executives and leadership, envision a more engaged, motivated, and productive team that consistently helps you meet operational goals while driving down costs. + +Good luck on your GitOps journey, and don’t hesitate to [reach out if you have questions or want to learn more](https://fleetdm.com/support)! + + + + + + + + diff --git a/changes/18354-update-success-messages b/changes/18354-update-success-messages new file mode 100644 index 000000000000..98f582dad033 --- /dev/null +++ b/changes/18354-update-success-messages @@ -0,0 +1 @@ +* Update success messages for lock, unlock, and wipe commands in the UI. diff --git a/changes/22159-hide-severity-fleet-free b/changes/22159-hide-severity-fleet-free new file mode 100644 index 000000000000..ddb47088fa36 --- /dev/null +++ b/changes/22159-hide-severity-fleet-free @@ -0,0 +1 @@ +- Hide CVSS severity column from Fleet Free software details > vulnerabilities sections diff --git a/changes/22197-policy-auto-software-truncation b/changes/22197-policy-auto-software-truncation new file mode 100644 index 000000000000..4b4740622e6c --- /dev/null +++ b/changes/22197-policy-auto-software-truncation @@ -0,0 +1 @@ +- Fleet UI: Fix policy automation truncation when selecting software to auto-install \ No newline at end of file diff --git a/changes/22207-close-team-modal b/changes/22207-close-team-modal new file mode 100644 index 000000000000..cad918366754 --- /dev/null +++ b/changes/22207-close-team-modal @@ -0,0 +1 @@ +- Fix UI bug: Edit team name closes modal diff --git a/changes/22415-fix-vpp-migration b/changes/22415-fix-vpp-migration new file mode 100644 index 000000000000..eed21b93dc12 --- /dev/null +++ b/changes/22415-fix-vpp-migration @@ -0,0 +1 @@ +* Fixed an issue with the migration adding support for multiple VPP tokens that would happen if a token is removed prior to upgrading Fleet. diff --git a/changes/22492-msrc-fix b/changes/22492-msrc-fix new file mode 100644 index 000000000000..4bd6035faaed --- /dev/null +++ b/changes/22492-msrc-fix @@ -0,0 +1 @@ +* Fix MSRC feed pulls (for NVD release builds) in environments where GitHub access is authenticated diff --git a/cmd/msrc/generate.go b/cmd/msrc/generate.go index 75909ac4fc27..9af759eb2a72 100644 --- a/cmd/msrc/generate.go +++ b/cmd/msrc/generate.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -34,13 +35,16 @@ func main() { panicif(err) now := time.Now() - httpC := fleethttp.NewGithubClient() ctx := context.Background() - ghAPI := io.NewGitHubClient(httpC, github.NewClient(httpC).Repositories, wd) - msrcAPI := msrc.NewMSRCClient(httpC, inPath, msrc.MSRCBaseURL) - fmt.Println("Downloading existing bulletins...") + githubHttp := fleethttp.NewGithubClient() + ghAPI := io.NewGitHubClient(githubHttp, github.NewClient(githubHttp).Repositories, wd) + + msrcHttp := fleethttp.NewClient() // don't reuse the GitHub client as it has an OAuth token baked in + msrcAPI := msrc.NewMSRCClient(msrcHttp, inPath, msrc.MSRCBaseURL) + + fmt.Println("Downloading existing MSRC bulletins...") eBulletins, err := ghAPI.MSRCBulletins(ctx) panicif(err) @@ -61,7 +65,16 @@ func main() { panicif(err) } - fmt.Println("Done.") + fmt.Println("Done processing MSRC feed.") +} + +// windowsBulletinGracePeriod returns whether we are within the grace period for a MSRC monthly feed to exist. +// +// E.g. September 2024 bulletin was released on the 2nd, thus we add some grace period (5 days) +// for Microsoft to publish the current month bulletin. +func windowsBulletinGracePeriod(month time.Month, year int) bool { + now := time.Now() + return month == now.Month() && year == now.Year() && now.Day() <= 5 } func update( @@ -72,15 +85,22 @@ func update( ghClient io.GitHubAPI, ) ([]*parsed.SecurityBulletin, error) { fmt.Println("Downloading current feed...") - f, err := msrcClient.GetFeed(m, y) + currentFeed, err := msrcClient.GetFeed(m, y) if err != nil { - return nil, err + if errors.Is(err, msrc.FeedNotFound) && windowsBulletinGracePeriod(m, y) { + fmt.Printf("Current month feed %d-%d was not found, skipping...\n", y, m) + } else { + return nil, err + } } - fmt.Println("Parsing current feed...") - nBulletins, err := msrc.ParseFeed(f) - if err != nil { - return nil, err + var nBulletins map[string]*parsed.SecurityBulletin + if currentFeed != "" { + fmt.Println("Parsing current feed...") + nBulletins, err = msrc.ParseFeed(currentFeed) + if err != nil { + return nil, err + } } var bulletins []*parsed.SecurityBulletin @@ -118,6 +138,10 @@ func backfill(upToM time.Month, upToY int, client msrc.MSRCAPI) ([]*parsed.Secur fmt.Printf("Downloading feed for %d-%d...\n", d.Year(), d.Month()) f, err := client.GetFeed(d.Month(), d.Year()) if err != nil { + if errors.Is(err, msrc.FeedNotFound) && windowsBulletinGracePeriod(d.Month(), d.Year()) { + fmt.Printf("Current month feed %d-%d was not found, skipping...\n", d.Year(), d.Month()) + continue + } return nil, err } diff --git a/docs/Contributing/Building-Fleet.md b/docs/Contributing/Building-Fleet.md index 2ab34243a45a..368b68d23b46 100644 --- a/docs/Contributing/Building-Fleet.md +++ b/docs/Contributing/Building-Fleet.md @@ -38,7 +38,7 @@ sudo npm install -g yarn # Install nvm to manage node versions (apt very out of date) https://github.com/nvm-sh/nvm#install--update-script curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash # refresh your session before continuing -nvm install v19.7.0 +nvm install v20.11.1 ``` #### Windows @@ -149,7 +149,7 @@ The following assumes that you already installed [Docker](https://docs.docker.c To set up a canonical development environment via Docker, run the following from the root of the repository: ```sh -docker-compose up +docker compose up ``` > Note: you can customize the DB Docker image via the environment variables FLEET_MYSQL_IMAGE and FLEET_MYSQL_PLATFORM. For example: @@ -161,12 +161,12 @@ docker-compose up If you'd like to shut down the virtual infrastructure created by Docker, run the following from the root of the repository: ```sh -docker-compose down +docker compose down ``` ### Setting up the database tables -Once you `docker-compose up` and are running the databases, you can build the code and run the following command to create the database tables: +Once you `docker compose up` and are running the databases, you can build the code and run the following command to create the database tables: ```sh ./build/fleet prepare db --dev diff --git a/docs/Get started/tutorials-and-guides.md b/docs/Get started/tutorials-and-guides.md index f83de79f27db..aff2e5a6087c 100644 --- a/docs/Get started/tutorials-and-guides.md +++ b/docs/Get started/tutorials-and-guides.md @@ -6,7 +6,7 @@ A collection of guides to help you with Fleet. - [How to install osquery and enroll Linux devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-linux-devices-into-fleet) - [How to install osquery and enroll Windows devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-windows-devices-into-fleet) - [Sysadmin diaries: restoring fleetd](https://fleetdm.com/guides/sysadmin-diaries-restoring-fleetd) -- [How to uninstall osquery](https://fleetdm.com/guides/how-to-uninstall-osquery) +- [How to uninstall Fleet's agent (fleetd)](https://fleetdm.com/guides/how-to-uninstall-fleetd) - [Sysadmin diaries: device enrollment](https://fleetdm.com/guides/sysadmin-diaries-device-enrollment) - [Sysadmin diaries: passcode profiles](https://fleetdm.com/guides/sysadmin-diaries-passcode-profiles) - [Sysadmin diaries: lost device](https://fleetdm.com/guides/sysadmin-diaries-lost-device) diff --git a/docs/Get started/why-fleet.md b/docs/Get started/why-fleet.md index 0918248ac085..e0db75d68759 100644 --- a/docs/Get started/why-fleet.md +++ b/docs/Get started/why-fleet.md @@ -1,70 +1,26 @@ # Why Fleet -## What's it for? - -In an industry that views security and IT as a shopping spree rather than a process, it's little wonder that IT and security teams get stuck with bolt-on solutions, misconfigured tools, and disparate workflows. - -Fleet helps organizations like Fastly and Gusto reimagine the "easy button." - -By simplifying how they do device health, FIM, HIDS, posture assessment, malware detection, vulnerability management, MDM, and the rest, Fleet's API enables teams with thousands of computers to build an IT and security program that works. - -### Explore data -To see what kind of data you can use Fleet to gather, you can check out the [table reference documentation](https://fleetdm.com/tables). - -### Out-of-the-box policies -Fleet includes out-of-the-box support for all [CIS benchmarks for macOS and Windows](https://fleetdm.com/pricing), as well as many [simpler queries](https://fleetdm.com/queries). - -Take as much or as little as you need for your organization. - -### Supported platforms -Here are the platforms Fleet currently supports: +Fleet is an open-source device management platform for Linux, macOS, Windows, Chromebooks, and iOS devices (BYOD or corporate owned.) -- Linux (all distros) -- macOS -- Windows -- Chromebooks -- Amazon Web Services (AWS) -- Google Cloud (GCP) -- Azure (Microsoft cloud) -- Data centers -- Containers (kube, etc) -- Linux-based IoT devices - -## Lighter than air -Fleet is lightweight and modular. You can use it for security without using it for MDM, and vice versa. You can turn off features you are not using. - -### Open by design -Fleet is dedicated to flexibility, accessibility, and clarity. We think [everyone can contribute](https://fleetdm.com/handbook/company#openness) and that tools should be as easy as possible for everyone to understand. - -### Good neighbors -Ready-to-use, enterprise-friendly [integrations](https://fleetdm.com/integrations) exist for Snowflake, Splunk, GitHub Actions, Vanta, Elastic Jira, Zendesk, and more. - -Fleet plays well with Munki, Chef, Puppet, and Ansible, as well as with security tools like Crowdstrike and SentinelOne. For example, you can use the free version of Fleet to quickly report on what hosts are _actually_ running your EDR agent. - - -### Free as in free -The free version of Fleet will [always be free](https://fleetdm.com/pricing). Fleet is [independently backed](https://linkedin.com/company/fleetdm) and actively maintained with the help of many amazing [contributors](https://github.com/fleetdm/fleet/graphs/contributors). +## What's it for? -### Longevity -The [company behind Fleet](https://fleetdm.com/handbook/company) is founded (and majority-owned) by [true believers in open source](https://fleetdm.com/handbook/company/why-this-way#why-open-source). The company's business model is influenced by GitLab (NYSE: GTLB), with great investors, happy customers, and the capacity to become profitable at any time. +Managing computers today is getting harder. You have to juggle a mix of operating systems and devices, with a whole bunch of middleman vendors in between. -In keeping with Fleet's value of openness, [Fleet Device Management's company handbook](https://fleetdm.com/handbook/company) is public and open source. You can read about the [history of Fleet and osquery](https://fleetdm.com/handbook/company#history) and our commitment to improving the product. +Fleet makes things easier by giving you a single system to manage and secure all your computing devices. You can do MDM, patch stuff, and verify anything—all from one dashboard. It's like having a universal remote control for all your organization's computers. - +Fleet is open source, and free features will always be free. ## Is it any good? -Fleet is used in production by IT and security teams with thousands of laptops and servers. Many deployments support tens of thousands of hosts, and a few large organizations manage deployments as large as 400,000+ hosts. -## Chat -Please join us in [MacAdmins Slack](https://www.macadmins.org/) or [osquery Slack](https://fleetdm.com/slack). +Fleet is used in production by IT and security teams with thousands of laptops and servers. Many deployments support tens of thousands of hosts, and a few large organizations manage deployments as large as 400,000+ hosts. -The Fleet community is full of [kind and helpful people](https://fleetdm.com/handbook/company#empathy). Whether or not you are a paying customer, if you need help, just ask. +- **Get what you need:** Fleet lets you work directly with [data](https://fleetdm.com/integrations) and events from the native operating system. It lets you go all the way down to the bare metal. It’s also modular. (You can turn off features you are not using.) +- **Out of the box:** Ready-to-use integrations exist for the [most common tools](https://fleetdm.com/integrations). You can also build custom workflows with the REST API, webhook events, and the fleetctl command line tool. Or go all in and manage computers [with GitOps](https://fleetdm.com/handbook/company#history). +- **Good neighbors:** We think tools should be as easy as possible for everyone to understand. We helped [create osquery](https://fleetdm.com/handbook/company#history), and we are committed to improving it. +- **Free as in free:** The free version of Fleet will [always be free](https://fleetdm.com/pricing). Fleet is independently backed and actively maintained with the help of many amazing contributors. - +## Ready to get started? -## Questions? -We have answers to the most [common questions](https://fleetdm.com/docs/get-started/faq). +Deploying Fleet is easy. [Install it](https://fleetdm.com/docs/deploy/deploy-fleet) on a server, or [we can host it](https://fleetdm.com/register) for you. Enroll your devices, and you're set. diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index a2a8ccb1d1c3..7f520ae6440c 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -257,7 +257,7 @@ const PlatformWrapper = ({ Run this command with the{" "} diff --git a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx index 1935b38b1133..8b5fec069426 100644 --- a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx @@ -44,8 +44,8 @@ const DeleteScriptModal = ({

The script{" "} {scriptName} will - run on pending hosts. After the scripts runs, it's output and - exit code will appear in the activity feed. + run on pending hosts. After the script runs, its output and exit code + will appear in the activity feed.

diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/LockModal/LockModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/LockModal/LockModal.tsx index 9877e4729ac0..a527de72b338 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/LockModal/LockModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/LockModal/LockModal.tsx @@ -34,7 +34,7 @@ const LockModal = ({ try { await hostAPI.lockHost(id); onSuccess(); - renderFlash("success", "Host is locking!"); + renderFlash("success", "Locking host or will lock when it comes online."); } catch (e) { renderFlash("error", getErrorReason(e)); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/UnlockModal/UnlockModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/UnlockModal/UnlockModal.tsx index 178f21a1b8f7..5d40e09886d2 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/UnlockModal/UnlockModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/UnlockModal/UnlockModal.tsx @@ -51,7 +51,10 @@ const UnlockModal = ({ try { await hostAPI.unlockHost(id); onSuccess(); - renderFlash("success", "Host is unlocking!"); + renderFlash( + "success", + "Unlocking host or will unlock when it comes online." + ); } catch (e) { renderFlash("error", getErrorReason(e)); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/WipeModal/WipeModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/WipeModal/WipeModal.tsx index 7df40b2adb97..9e7754ac55cb 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/WipeModal/WipeModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/WipeModal/WipeModal.tsx @@ -27,7 +27,10 @@ const WipeModal = ({ id, hostName, onSuccess, onClose }: IWipeModalProps) => { try { await hostAPI.wipeHost(id); onSuccess(); - renderFlash("success", "Success! Host is wiping."); + renderFlash( + "success", + "Wiping host or will wipe when the host comes online." + ); } catch (e) { renderFlash("error", getErrorReason(e)); } diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx index 9731262c7677..483a786e62eb 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx @@ -175,7 +175,7 @@ const InstallSoftwareModal = ({
  • diff --git a/handbook/product-design/product-design.rituals.yml b/handbook/product-design/product-design.rituals.yml index b32f5406fe06..fb6af51f8d50 100644 --- a/handbook/product-design/product-design.rituals.yml +++ b/handbook/product-design/product-design.rituals.yml @@ -52,7 +52,7 @@ startedOn: "2024-03-01" frequency: "Weekly" description: "Head of Product Design checks the latest versions of relevant platforms, updates the maintenance tracker, and notifies the #g-mdm and #g-endpoint-ops Slack channel." - moreInfoUrl: + moreInfoUrl: "https://docs.google.com/spreadsheets/d/1IWfQtSkOQgm_JIQZ0i2y3A8aaK5vQW1ayWRk6-4FOp0/edit?gid=0#gid=0" dri: "noahtalerman" - task: "Product confirm and celebrate" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes diff --git a/handbook/sales/README.md b/handbook/sales/README.md index 1ddb627e25e9..be6df24ea636 100644 --- a/handbook/sales/README.md +++ b/handbook/sales/README.md @@ -145,14 +145,19 @@ To schedule an [ad hoc meeting](https://www.vocabulary.com/dictionary/ad%20hoc) ### Conduct a POV -We use the "tech eval test plan" as a guide when conducting a "POV" (Proof of Value) with a prospect. This planning helps us avoid costly detours that can take a long time, and result in folks getting lost. The tech eval test plan is the main document that will track success criteria for the tech eval. Before the Solutions Consultant (SC) creates a [tech eval issue](https://github.com/fleetdm/confidential/issues/new?assignees=dherder&labels=%23g-sales&projects=&template=technical-evaluation.md&title=Technical+evaluation%3A+___________________), the AE and SC will ask each other, at minimum, the following questions in order to enter the "Stage 3 - Requested POV" phase for the tech eval: -1. Do we have a well-defined set of technical criteria to test? +We use the "tech eval test plan" as a guide when conducting a "POV" (Proof of Value) with a prospect. This planning helps us avoid costly detours that can take a long time, and result in folks getting lost. The tech eval test plan is the main document that will track success criteria for the tech eval. + +When we have had sufficient meetings and demos, including an overview demo and a customized demo, and we have qualified the prospect, when the prospect asks to "kick the tires/do a POC/do a technical evaluation", the AE moves the opportuity to "Stage 3 - Requested POV" phase in Salesforce. Automation will generate the tech eval test plan. This doc will exist in Google Drive> Sales> Opportunities> "Account Name". + +The AE and SC will work together to scope the POV with the prospect in this stage. The AE and SC will work together to answer the following questions: + +1. Do we have a well-defined set of technical criteria to test and are we confident that Fleet can meet this criteria to achieve a technical win? 2. Do we have a timeline agreed upon? 3. What are the key business outcomes that will be verified as a result of completing the tech eval? -If the above questions cannot be answered, the opportunity should not progress to tech eval. Once the opportunity moves to the "Stage 3 - Requested POV" phase in Salesforce, automation will generate the tech eval test plan. This doc will exist in Google Drive> Sales> Opportunities> "Account Name". +If the above questions are answered successfully, the opportunity should progress to tech eval. If we cannot answer the questions above successfully, then the POV should not start unless approved by the CRO. -Once there is agreement to proceed with the tech eval and success criteria have been defined and documented, follow this process: +During Stage 4, follow this process: 1. SC creates a [tech eval issue](https://github.com/fleetdm/confidential/issues/new?assignees=dherder&labels=%23g-sales&projects=&template=technical-evaluation.md&title=Technical+evaluation%3A+___________________). 2. SC updates the issue labels to include: "~sc, :tech-eval" and the obfuscated "prospect-codename" label. See [Assign a customer a codename](https://fleetdm.com/handbook/customer-success#assign-a-customer-codename). Instead of "customer-codename", prospects are labeled "prospect-codename". When a prospect purchases Fleet, the SC will edit this label from "prospect-codename" to "customer-codename". diff --git a/pkg/download/download.go b/pkg/download/download.go index 23c0f5c152ea..b15a2cd75b79 100644 --- a/pkg/download/download.go +++ b/pkg/download/download.go @@ -4,6 +4,7 @@ package download import ( "compress/bzip2" "compress/gzip" + "errors" "fmt" "io" "net/http" @@ -20,16 +21,24 @@ import ( const backoffMaxElapsedTime = 5 * time.Minute // Download downloads a file from a URL and writes it to path. +// +// It will retry requests until it succeeds. If the server returns a 404 +// then it will not retry and return a NotFound error. func Download(client *http.Client, u *url.URL, path string) error { return download(client, u, path, false) } // DownloadAndExtract downloads and extracts a file from a URL and writes it to path. // The compression method is determined using extension from the url path. Only .gz, .bz2, or .xz extensions are supported. +// +// It will retry requests until it succeeds. If the server returns a 404 +// then it will not retry and return a NotFound error. func DownloadAndExtract(client *http.Client, u *url.URL, path string) error { return download(client, u, path, true) } +var NotFound = errors.New("resource not found") + func download(client *http.Client, u *url.URL, path string, extract bool) error { // atomically write to file dir, file := filepath.Split(path) @@ -79,7 +88,12 @@ func download(client *http.Client, u *url.URL, path string, extract bool) error } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + switch { + case resp.StatusCode == http.StatusOK: + // OK + case resp.StatusCode == http.StatusNotFound: + return &backoff.PermanentError{Err: NotFound} + default: return fmt.Errorf("unexpected status code %d", resp.StatusCode) } diff --git a/pkg/download/download_test.go b/pkg/download/download_test.go new file mode 100644 index 000000000000..9b5bf1901488 --- /dev/null +++ b/pkg/download/download_test.go @@ -0,0 +1,24 @@ +package download + +import ( + "net/url" + "path/filepath" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/pkg/fleethttp" + "github.com/stretchr/testify/require" +) + +func TestDownloadNotFoundNoRetries(t *testing.T) { + c := fleethttp.NewClient() + tmpDir := t.TempDir() + outputFile := filepath.Join(tmpDir) + url, err := url.Parse("https://github.com/fleetdm/non-existent") + require.NoError(t, err) + start := time.Now() + err = Download(c, url, outputFile) + require.Error(t, err) + require.ErrorIs(t, err, NotFound) + require.True(t, time.Since(start) < backoffMaxElapsedTime) +} diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go index 601fe313b6ab..b455b4eddd19 100644 --- a/pkg/mdm/mdmtest/apple.go +++ b/pkg/mdm/mdmtest/apple.go @@ -26,7 +26,6 @@ import ( apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" httptransport "github.com/go-kit/kit/transport/http" "github.com/go-kit/log" @@ -34,6 +33,7 @@ import ( "github.com/google/uuid" "github.com/groob/plist" "github.com/smallstep/pkcs7" + "github.com/smallstep/scep" ) // TestAppleMDMClient simulates a macOS MDM client. diff --git a/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams.go b/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams.go index ea367f59ea69..b8ef59e25ffd 100644 --- a/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams.go +++ b/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams.go @@ -3,6 +3,8 @@ package tables import ( "database/sql" "fmt" + + "github.com/pkg/errors" ) func init() { @@ -14,7 +16,11 @@ func Up_20240829170033(tx *sql.Tx) error { ALTER TABLE vpp_apps_teams ADD COLUMN vpp_token_id int(10) UNSIGNED NOT NULL` - stmtAssociate := `UPDATE vpp_apps_teams SET vpp_token_id = (SELECT id FROM vpp_tokens LIMIT 1)` + stmtFindToken := `SELECT id FROM vpp_tokens LIMIT 1` //nolint:gosec + + stmtCleanAssociations := `DELETE FROM vpp_apps_teams` + + stmtAssociate := `UPDATE vpp_apps_teams SET vpp_token_id = ?` stmtAddConstraint := ` ALTER TABLE vpp_apps_teams @@ -27,8 +33,22 @@ ALTER TABLE vpp_apps_teams // Associate apps with the first token available. If we're // migrating from single-token VPP this should be correct. - if _, err := tx.Exec(stmtAssociate); err != nil { - return fmt.Errorf("failed to associate vpp apps with first token: %w", err) + var vppTokenID uint + err := tx.QueryRow(stmtFindToken).Scan(&vppTokenID) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("get existing VPP token ID: %w", err) + } + } + + if vppTokenID > 0 { + if _, err := tx.Exec(stmtAssociate, vppTokenID); err != nil { + return fmt.Errorf("failed to associate vpp apps with first token: %w", err) + } + } else { + if _, err := tx.Exec(stmtCleanAssociations); err != nil { + return fmt.Errorf("failed clean orphaned VPP team associations: %w", err) + } } if _, err := tx.Exec(stmtAddConstraint); err != nil { diff --git a/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams_test.go b/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams_test.go new file mode 100644 index 000000000000..7329d628de93 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240829170033_AddVPPTokenIDToVppAppsTeams_test.go @@ -0,0 +1,70 @@ +package tables + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestUp_20240829170033_Existing(t *testing.T) { + db := applyUpToPrev(t) + + // insert a vpp token + vppTokenID := execNoErrLastID(t, db, "INSERT INTO vpp_tokens (organization_name, location, renew_at, token) VALUES (?, ?, ?, ?)", "org", "location", time.Now(), "token") + + // create a couple teams + tm1 := execNoErrLastID(t, db, "INSERT INTO teams (name) VALUES ('team1')") + tm2 := execNoErrLastID(t, db, "INSERT INTO teams (name) VALUES ('team2')") + + // create a couple of vpp apps + adamID1 := "123" + execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, "iOS")`, adamID1) + adamID2 := "456" + execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, "iOS")`, adamID2) + + // insert some teams with vpp apps + execNoErr(t, db, `INSERT INTO vpp_apps_teams (adam_id, team_id, global_or_team_id, platform, self_service) VALUES (?, ?, ?, ?, ?)`, adamID1, tm1, 0, "iOS", 0) + execNoErr(t, db, `INSERT INTO vpp_apps_teams (adam_id, team_id, global_or_team_id, platform, self_service) VALUES (?, ?, ?, ?, ?)`, adamID2, tm2, 0, "iOS", 0) + + // apply current migration + applyNext(t, db) + + // ensure vpp_token_id is set for all teams + var vppTokenIDs []int + err := sqlx.Select(db, &vppTokenIDs, `SELECT vpp_token_id FROM vpp_apps_teams`) + require.NoError(t, err) + require.Len(t, vppTokenIDs, 2) + for _, tokenID := range vppTokenIDs { + require.Equal(t, int(vppTokenID), tokenID) + } +} + +func TestUp_20240829170033_NoTokens(t *testing.T) { + db := applyUpToPrev(t) + + // create a couple teams + tm1 := execNoErrLastID(t, db, "INSERT INTO teams (name) VALUES ('team1')") + tm2 := execNoErrLastID(t, db, "INSERT INTO teams (name) VALUES ('team2')") + + // create a couple of vpp apps + adamID1 := "123" + execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, "iOS")`, adamID1) + adamID2 := "456" + execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, "iOS")`, adamID2) + + // insert some teams with vpp apps + execNoErr(t, db, `INSERT INTO vpp_apps_teams (adam_id, team_id, global_or_team_id, platform, self_service) VALUES (?, ?, ?, ?, ?)`, adamID1, tm1, 0, "iOS", 0) + execNoErr(t, db, `INSERT INTO vpp_apps_teams (adam_id, team_id, global_or_team_id, platform, self_service) VALUES (?, ?, ?, ?, ?)`, adamID2, tm2, 0, "iOS", 0) + + // apply current migration + applyNext(t, db) + + // ensure no rows are left in vpp_apps_teams (since there are no tokens) + var count int + err := sqlx.Get(db, &count, `SELECT COUNT(*) FROM vpp_apps_teams`) + require.NoError(t, err) + require.Zero(t, count) + +} diff --git a/server/mdm/scep/Makefile b/server/mdm/scep/Makefile index e13e4ea8e404..b035f5896272 100644 --- a/server/mdm/scep/Makefile +++ b/server/mdm/scep/Makefile @@ -20,6 +20,8 @@ SCEPSERVER=\ my: scepclient-$(OSARCH) scepserver-$(OSARCH) +win: scepclient-$(OSARCH).exe scepserver-$(OSARCH).exe + docker: scepclient-linux-amd64 scepserver-linux-amd64 $(SCEPCLIENT): @@ -48,4 +50,4 @@ test: test-race: go test -cover -race ./... -.PHONY: my docker $(SCEPCLIENT) $(SCEPSERVER) release clean test test-race +.PHONY: my mywin docker $(SCEPCLIENT) $(SCEPSERVER) release clean test test-race diff --git a/server/mdm/scep/README.md b/server/mdm/scep/README.md index 49a419c2f2ce..f932f5e25cd7 100644 --- a/server/mdm/scep/README.md +++ b/server/mdm/scep/README.md @@ -1,6 +1,7 @@ # scep > The contents of this directory were copied (in February 2024) from https://github.com/fleetdm/scep (the `remove-path-setting-on-scep-handler` branch) which was forked from https://github.com/micromdm/scep. +> They were updated in September 2024 with changes up to github.com/micromdm/scep@781f8042a79cabcf61a5e6c01affdbadcb785932 [![CI](https://github.com/micromdm/scep/workflows/CI/badge.svg)](https://github.com/micromdm/scep/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/micromdm/scep/v2.svg)](https://pkg.go.dev/github.com/micromdm/scep/v2) @@ -16,7 +17,7 @@ Binary releases are available on the [releases page](https://github.com/micromdm To compile the SCEP client and server you will need [a Go compiler](https://golang.org/dl/) as well as standard tools like git, make, etc. 1. Clone the repository and get into the source directory: `git clone https://github.com/micromdm/scep.git && cd scep` -2. Compile the client and server binaries: `make` +2. Compile the client and server binaries: `make` (for Windows: `make win`) The binaries will be compiled in the current directory and named after the architecture. I.e. `scepclient-linux-amd64` and `scepserver-linux-amd64`. @@ -177,58 +178,6 @@ docker run -it --rm -v /path/to/ca/folder:/depot micromdm/scep:latest ca -init docker run -it --rm -v /path/to/ca/folder:/depot -p 8080:8080 micromdm/scep:latest ``` -## SCEP library - -The core `scep` library can be used for both client and server operations. - -``` -go get github.com/micromdm/scep/scep -``` - -For detailed usage, see the [Go Reference](https://pkg.go.dev/github.com/micromdm/scep/v2/scep). - -Example (server): - -```go -// read a request body containing SCEP message -body, err := ioutil.ReadAll(r.Body) -if err != nil { - // handle err -} - -// parse the SCEP message -msg, err := scep.ParsePKIMessage(body) -if err != nil { - // handle err -} - -// do something with msg -fmt.Println(msg.MessageType) - -// extract encrypted pkiEnvelope -err := msg.DecryptPKIEnvelope(CAcert, CAkey) -if err != nil { - // handle err -} - -// use the CSR from decrypted PKCS request and sign -// MyCSRSigner returns an *x509.Certificate here -crt, err := MyCSRSigner(msg.CSRReqMessage.CSR) -if err != nil { - // handle err -} - -// create a CertRep message from the original -certRep, err := msg.Success(CAcert, CAkey, crt) -if err != nil { - // handle err -} - -// send response back -// w is a http.ResponseWriter -w.Write(certRep.Raw) -``` - ## Server library You can import the scep endpoint into another Go project. For an example take a look at [scepserver.go](cmd/scepserver/scepserver.go). diff --git a/server/mdm/scep/challenge/challenge.go b/server/mdm/scep/challenge/challenge.go index dc26c9bd30cd..e4c3fbeb580e 100644 --- a/server/mdm/scep/challenge/challenge.go +++ b/server/mdm/scep/challenge/challenge.go @@ -2,22 +2,31 @@ package challenge import ( + "context" "crypto/x509" "errors" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" + + "github.com/smallstep/scep" ) +// Validator validates challenge passwords. +type Validator interface { + // HasChallenge validates pw as valid. + HasChallenge(pw string) (bool, error) +} + // Store is a dynamic challenge password cache. type Store interface { + // SCEPChallenge generates a new challenge password. SCEPChallenge() (string, error) - HasChallenge(pw string) (bool, error) + Validator } -// Middleware wraps next in a CSRSigner that verifies and invalidates the challenge -func Middleware(store Store, next scepserver.CSRSigner) scepserver.CSRSignerFunc { - return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { +// Middleware wraps next in a CSRSigner that verifies and invalidates the challenge. +func Middleware(store Validator, next scepserver.CSRSignerContext) scepserver.CSRSignerContextFunc { + return func(ctx context.Context, m *scep.CSRReqMessage) (*x509.Certificate, error) { // TODO: compare challenge only for PKCSReq? valid, err := store.HasChallenge(m.ChallengePassword) if err != nil { @@ -26,6 +35,6 @@ func Middleware(store Store, next scepserver.CSRSigner) scepserver.CSRSignerFunc if !valid { return nil, errors.New("invalid challenge") } - return next.SignCSR(m) + return next.SignCSRContext(ctx, m) } } diff --git a/server/mdm/scep/challenge/challenge_bolt_test.go b/server/mdm/scep/challenge/challenge_bolt_test.go index 4f6ae7e3fe4c..5527a582d506 100644 --- a/server/mdm/scep/challenge/challenge_bolt_test.go +++ b/server/mdm/scep/challenge/challenge_bolt_test.go @@ -1,14 +1,15 @@ package challenge import ( + "context" "io/ioutil" "os" "testing" challengestore "github.com/fleetdm/fleet/v4/server/mdm/scep/challenge/bolt" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" + "github.com/smallstep/scep" bolt "go.etcd.io/bbolt" ) @@ -69,15 +70,18 @@ func TestDynamicChallenge(t *testing.T) { ChallengePassword: challengePassword, } - _, err = signer.SignCSR(csrReq) + ctx := context.Background() + + _, err = signer.SignCSRContext(ctx, csrReq) if err != nil { t.Error(err) } - _, err = signer.SignCSR(csrReq) + _, err = signer.SignCSRContext(ctx, csrReq) if err == nil { t.Error("challenge should not be valid twice") } + } func openTempBolt(prefix string) (*bolt.DB, error) { diff --git a/server/mdm/scep/cmd/scepclient/csr.go b/server/mdm/scep/cmd/scepclient/csr.go index fde14934d086..0eadcac4e355 100644 --- a/server/mdm/scep/cmd/scepclient/csr.go +++ b/server/mdm/scep/cmd/scepclient/csr.go @@ -18,8 +18,8 @@ const ( ) type csrOptions struct { - cn, org, country, ou, locality, province, challenge string - key *rsa.PrivateKey + cn, org, country, ou, locality, province, dnsName, challenge string + key *rsa.PrivateKey } func loadOrMakeCSR(path string, opts *csrOptions) (*x509.CertificateRequest, error) { @@ -44,6 +44,7 @@ func loadOrMakeCSR(path string, opts *csrOptions) (*x509.CertificateRequest, err CertificateRequest: x509.CertificateRequest{ Subject: subject, SignatureAlgorithm: x509.SHA256WithRSA, + DNSNames: subjOrNil(opts.dnsName), }, } if opts.challenge != "" { diff --git a/server/mdm/scep/cmd/scepclient/scepclient.go b/server/mdm/scep/cmd/scepclient/scepclient.go index 91175ffb816c..39d115f17ff8 100644 --- a/server/mdm/scep/cmd/scepclient/scepclient.go +++ b/server/mdm/scep/cmd/scepclient/scepclient.go @@ -18,10 +18,10 @@ import ( "time" scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/smallstep/scep" ) // version info @@ -50,6 +50,7 @@ type runCfg struct { debug bool logfmt string caCertMsg string + dnsName string } func run(cfg runCfg) error { @@ -88,6 +89,7 @@ func run(cfg runCfg) error { province: cfg.province, challenge: cfg.challenge, key: key, + dnsName: cfg.dnsName, } csr, err := loadOrMakeCSR(cfg.csrPath, opts) @@ -113,15 +115,15 @@ func run(cfg runCfg) error { if err != nil { return err } - var certs []*x509.Certificate + var caCerts []*x509.Certificate { if certNum > 1 { - certs, err = scep.CACerts(resp) + caCerts, err = scep.CACerts(resp) if err != nil { return err } } else { - certs, err = x509.ParseCertificates(resp) + caCerts, err = x509.ParseCertificates(resp) if err != nil { return err } @@ -129,7 +131,7 @@ func run(cfg runCfg) error { } if cfg.debug { - logCerts(level.Debug(logger), certs) + logCerts(level.Debug(logger), caCerts) } var signerCert *x509.Certificate @@ -153,7 +155,7 @@ func run(cfg runCfg) error { tmpl := &scep.PKIMessage{ MessageType: msgType, - Recipients: certs, + Recipients: caCerts, SignerKey: key, SignerCert: signerCert, } @@ -180,7 +182,7 @@ func run(cfg runCfg) error { return errors.Join(err, fmt.Errorf("PKIOperation for %s", msgType)) } - respMsg, err = scep.ParsePKIMessage(respBytes, scep.WithLogger(logger), scep.WithCACerts(msg.Recipients)) + respMsg, err = scep.ParsePKIMessage(respBytes, scep.WithLogger(logger), scep.WithCACerts(caCerts)) if err != nil { return errors.Join(err, fmt.Errorf("parsing pkiMessage response %s", msgType)) } @@ -251,7 +253,7 @@ func validateFingerprint(fingerprint string) (hash []byte, err error) { return } -func validateFlags(keyPath, serverURL string) error { +func validateFlags(keyPath, serverURL, caFingerprint string, useKeyEnciphermentSelector bool) error { if keyPath == "" { return errors.New("must specify private key path") } @@ -262,6 +264,9 @@ func validateFlags(keyPath, serverURL string) error { if err != nil { return fmt.Errorf("invalid server-url flag parameter %s", err) } + if caFingerprint != "" && useKeyEnciphermentSelector { + return errors.New("ca-fingerprint and key-encipherment-selector can't be used at the same time") + } return nil } @@ -280,14 +285,19 @@ func main() { flProvince = flag.String("province", "", "province for certificate") flCountry = flag.String("country", "US", "country code in certificate") flCACertMessage = flag.String("cacert-message", "", "message sent with GetCACert operation") + flDNSName = flag.String("dnsname", "", "DNS name to be included in the certificate (SAN)") // in case of multiple certificate authorities, we need to figure out who the recipient of the encrypted - // data is. - flCAFingerprint = flag.String("ca-fingerprint", "", "SHA-256 digest of CA certificate for NDES server. Note: Changed from MD5.") + // data is. This can be done using either the CA fingerprint, or based on the key usage encoded in the + // certificates returned by the authority. + flCAFingerprint = flag.String("ca-fingerprint", "", + "SHA-256 digest of CA certificate for NDES server. Note: Changed from MD5.") + flKeyEnciphermentSelector = flag.Bool("key-encipherment-selector", false, "Filter CA certificates by key encipherment usage") flDebugLogging = flag.Bool("debug", false, "enable debug logging") flLogJSON = flag.Bool("log-json", false, "use JSON for log output") ) + flag.Parse() // print version information @@ -296,19 +306,22 @@ func main() { os.Exit(0) } - if err := validateFlags(*flPKeyPath, *flServerURL); err != nil { + if err := validateFlags(*flPKeyPath, *flServerURL, *flCAFingerprint, *flKeyEnciphermentSelector); err != nil { fmt.Println(err) os.Exit(1) } caCertsSelector := scep.NopCertsSelector() - if *flCAFingerprint != "" { + switch { + case *flCAFingerprint != "": hash, err := validateFingerprint(*flCAFingerprint) if err != nil { fmt.Printf("invalid fingerprint: %s\n", err) os.Exit(1) } caCertsSelector = scep.FingerprintCertsSelector(fingerprintHashType, hash) + case *flKeyEnciphermentSelector: + caCertsSelector = scep.EnciphermentCertsSelector() } dir := filepath.Dir(*flPKeyPath) @@ -341,6 +354,7 @@ func main() { debug: *flDebugLogging, logfmt: logfmt, caCertMsg: *flCACertMessage, + dnsName: *flDNSName, } if err := run(cfg); err != nil { diff --git a/server/mdm/scep/cmd/scepserver/scepserver.go b/server/mdm/scep/cmd/scepserver/scepserver.go index 8bdb5225ffad..58e624501f07 100644 --- a/server/mdm/scep/cmd/scepserver/scepserver.go +++ b/server/mdm/scep/cmd/scepserver/scepserver.go @@ -31,7 +31,7 @@ var ( ) func main() { - caCMD := flag.NewFlagSet("ca", flag.ExitOnError) + var caCMD = flag.NewFlagSet("ca", flag.ExitOnError) { if len(os.Args) >= 2 { if os.Args[1] == "ca" { @@ -148,9 +148,9 @@ func main() { if *flSignServerAttrs { signerOpts = append(signerOpts, scepdepot.WithSeverAttrs()) } - var signer scepserver.CSRSigner = scepdepot.NewSigner(depot, signerOpts...) + var signer scepserver.CSRSignerContext = scepserver.SignCSRAdapter(scepdepot.NewSigner(depot, signerOpts...)) if *flChallengePassword != "" { - signer = scepserver.ChallengeMiddleware(*flChallengePassword, signer) + signer = scepserver.StaticChallengeMiddleware(*flChallengePassword, signer) } if csrVerifier != nil { signer = csrverifier.Middleware(csrVerifier, signer) diff --git a/server/mdm/scep/cryptoutil/cryptoutil_test.go b/server/mdm/scep/cryptoutil/cryptoutil_test.go index 1adaaf41a431..53a73ee9b36f 100644 --- a/server/mdm/scep/cryptoutil/cryptoutil_test.go +++ b/server/mdm/scep/cryptoutil/cryptoutil_test.go @@ -11,7 +11,7 @@ import ( ) func TestGenerateSubjectKeyID(t *testing.T) { - ecdsaKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + ecKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) if err != nil { t.Fatal(err) } @@ -20,7 +20,7 @@ func TestGenerateSubjectKeyID(t *testing.T) { pub crypto.PublicKey }{ {"RSA", &rsa.PublicKey{N: big.NewInt(123), E: 65537}}, - {"ECDSA", &ecdsa.PublicKey{X: ecdsaKey.X, Y: ecdsaKey.Y, Curve: elliptic.P224()}}, + {"ECDSA", ecKey.Public()}, } { test := test t.Run(test.testName, func(t *testing.T) { diff --git a/server/mdm/scep/csrverifier/csrverifier.go b/server/mdm/scep/csrverifier/csrverifier.go index 8a0dce89ebe3..8ce4eb07f63f 100644 --- a/server/mdm/scep/csrverifier/csrverifier.go +++ b/server/mdm/scep/csrverifier/csrverifier.go @@ -2,11 +2,12 @@ package csrverifier import ( + "context" "crypto/x509" "errors" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" + "github.com/smallstep/scep" ) // CSRVerifier verifies the raw decrypted CSR. @@ -15,8 +16,8 @@ type CSRVerifier interface { } // Middleware wraps next in a CSRSigner that runs verifier -func Middleware(verifier CSRVerifier, next scepserver.CSRSigner) scepserver.CSRSignerFunc { - return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { +func Middleware(verifier CSRVerifier, next scepserver.CSRSignerContext) scepserver.CSRSignerContextFunc { + return func(ctx context.Context, m *scep.CSRReqMessage) (*x509.Certificate, error) { ok, err := verifier.Verify(m.RawDecrypted) if err != nil { return nil, err @@ -24,6 +25,6 @@ func Middleware(verifier CSRVerifier, next scepserver.CSRSigner) scepserver.CSRS if !ok { return nil, errors.New("CSR verify failed") } - return next.SignCSR(m) + return next.SignCSRContext(ctx, m) } } diff --git a/server/mdm/scep/depot/bolt/depot.go b/server/mdm/scep/depot/bolt/depot.go index eb85e4ec17a9..4e6b75afe211 100644 --- a/server/mdm/scep/depot/bolt/depot.go +++ b/server/mdm/scep/depot/bolt/depot.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "math/big" + "sync" "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" @@ -18,6 +19,7 @@ import ( // https://github.com/boltdb/bolt type Depot struct { *bolt.DB + serialMu sync.RWMutex } const ( @@ -36,7 +38,7 @@ func NewBoltDepot(db *bolt.DB) (*Depot, error) { if err != nil { return nil, err } - return &Depot{db}, nil + return &Depot{DB: db}, nil } // For some read operations Bolt returns a direct memory reference to @@ -93,26 +95,28 @@ func (db *Depot) Put(cn string, crt *x509.Certificate) error { if crt == nil || crt.Raw == nil { return fmt.Errorf("%q does not specify a valid certificate for storage", cn) } - serial, err := db.Serial() - if err != nil { - return err - } - - err = db.Update(func(tx *bolt.Tx) error { + err := db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(certBucket)) if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } - name := cn + "." + serial.String() + name := cn + "." + crt.SerialNumber.String() return bucket.Put([]byte(name), crt.Raw) }) + return err +} + +func (db *Depot) Serial() (*big.Int, error) { + db.serialMu.Lock() + defer db.serialMu.Unlock() + s, err := db.readSerial() if err != nil { - return err + return nil, err } - return db.incrementSerial(serial) + return s, db.incrementSerial(s) } -func (db *Depot) Serial() (*big.Int, error) { +func (db *Depot) readSerial() (*big.Int, error) { s := big.NewInt(2) if !db.hasKey([]byte("serial")) { if err := db.writeSerial(s); err != nil { @@ -132,10 +136,7 @@ func (db *Depot) Serial() (*big.Int, error) { s = s.SetBytes(k) return nil }) - if err != nil { - return nil, err - } - return s, nil + return s, err } func (db *Depot) writeSerial(s *big.Int) error { @@ -156,7 +157,7 @@ func (db *Depot) hasKey(name []byte) bool { if bucket == nil { return fmt.Errorf("bucket %q not found!", certBucket) } - k := bucket.Get([]byte("serial")) + k := bucket.Get(name) if k != nil { present = true } @@ -166,15 +167,8 @@ func (db *Depot) hasKey(name []byte) bool { } func (db *Depot) incrementSerial(s *big.Int) error { - serial := s.Add(s, big.NewInt(1)) - err := db.Update(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(certBucket)) - if bucket == nil { - return fmt.Errorf("bucket %q not found!", certBucket) - } - return bucket.Put([]byte("serial"), serial.Bytes()) - }) - return err + serial := new(big.Int).Add(s, big.NewInt(1)) + return db.writeSerial(serial) } func (db *Depot) HasCN(cn string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) { @@ -185,8 +179,7 @@ func (db *Depot) HasCN(cn string, allowTime int, cert *x509.Certificate, revokeO } var hasCN bool err := db.View(func(tx *bolt.Tx) error { - // TODO: "scep_certificates" is internal const in micromdm/scep - curs := tx.Bucket([]byte("scep_certificates")).Cursor() + curs := tx.Bucket([]byte(certBucket)).Cursor() prefix := []byte(cert.Subject.CommonName) for k, v := curs.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = curs.Next() { if bytes.Compare(v, cert.Raw) == 0 { diff --git a/server/mdm/scep/depot/bolt/depot_test.go b/server/mdm/scep/depot/bolt/depot_test.go index 837df03310d7..aadd5b81f8d4 100644 --- a/server/mdm/scep/depot/bolt/depot_test.go +++ b/server/mdm/scep/depot/bolt/depot_test.go @@ -96,7 +96,7 @@ func TestDepot_incrementSerial(t *testing.T) { if err := db.incrementSerial(tt.args); (err != nil) != tt.wantErr { t.Errorf("%q. Depot.incrementSerial() error = %v, wantErr %v", tt.name, err, tt.wantErr) } - got, _ := db.Serial() + got, _ := db.readSerial() if !reflect.DeepEqual(got, tt.want) { t.Errorf("%q. Depot.Serial() = %v, want %v", tt.name, got, tt.want) } diff --git a/server/mdm/scep/depot/cacert.go b/server/mdm/scep/depot/cacert.go index 65d44a0c8eb7..7aba250c3115 100644 --- a/server/mdm/scep/depot/cacert.go +++ b/server/mdm/scep/depot/cacert.go @@ -27,7 +27,9 @@ func NewCACert(opts ...CACertOption) *CACert { organization: "scep-ca", organizationalUnit: "SCEP CA", years: 10, - keyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + keyUsage: x509.KeyUsageCertSign | + x509.KeyUsageCRLSign | + x509.KeyUsageDigitalSignature, } for _, opt := range opts { opt(c) diff --git a/server/mdm/scep/depot/file/depot.go b/server/mdm/scep/depot/file/depot.go index 0c5d1e65c561..20b9b0e405a5 100644 --- a/server/mdm/scep/depot/file/depot.go +++ b/server/mdm/scep/depot/file/depot.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "time" ) @@ -31,7 +32,9 @@ func NewFileDepot(path string) (*fileDepot, error) { } type fileDepot struct { - dirPath string + dirPath string + serialMu sync.Mutex + dbMu sync.Mutex } func (d *fileDepot) CA(pass []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) { @@ -75,10 +78,7 @@ func (d *fileDepot) Put(cn string, crt *x509.Certificate) error { return err } - serial, err := d.Serial() - if err != nil { - return err - } + serial := crt.SerialNumber if crt.Subject.CommonName == "" { // this means our cn was replaced by the certificate Signature @@ -103,14 +103,12 @@ func (d *fileDepot) Put(cn string, crt *x509.Certificate) error { return err } - if err := d.incrementSerial(serial); err != nil { - return err - } - return nil } func (d *fileDepot) Serial() (*big.Int, error) { + d.serialMu.Lock() + defer d.serialMu.Unlock() name := d.path("serial") s := big.NewInt(2) if err := d.check("serial"); err != nil { @@ -136,11 +134,14 @@ func (d *fileDepot) Serial() (*big.Int, error) { if !ok { return nil, errors.New("could not convert " + data + " to serial number") } + if err := d.incrementSerial(serial); err != nil { + return serial, err + } return serial, nil } func makeOpenSSLTime(t time.Time) string { - y := (t.Year() % 100) + y := t.Year() % 100 validDate := fmt.Sprintf("%02d%02d%02d%02d%02d%02dZ", y, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) return validDate } @@ -174,6 +175,7 @@ func makeDn(cert *x509.Certificate) string { // Determine if the cadb already has a valid certificate with the same name func (d *fileDepot) HasCN(_ string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) { + var addDB bytes.Buffer candidates := make(map[string]string) @@ -254,6 +256,9 @@ func (d *fileDepot) HasCN(_ string, allowTime int, cert *x509.Certificate, revok } func (d *fileDepot) writeDB(cn string, serial *big.Int, filename string, cert *x509.Certificate) error { + d.dbMu.Lock() + defer d.dbMu.Unlock() + var dbEntry bytes.Buffer // Revoke old certificate @@ -363,8 +368,9 @@ func (d *fileDepot) path(name string) string { } const ( - rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" - certificatePEMBlockType = "CERTIFICATE" + rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" + pkcs8PrivateKeyPEMBlockType = "PRIVATE KEY" + certificatePEMBlockType = "CERTIFICATE" ) // load an encrypted private key from disk @@ -373,15 +379,33 @@ func loadKey(data []byte, password []byte) (*rsa.PrivateKey, error) { if pemBlock == nil { return nil, errors.New("PEM decode failed") } - if pemBlock.Type != rsaPrivateKeyPEMBlockType { + switch pemBlock.Type { + case rsaPrivateKeyPEMBlockType: + if x509.IsEncryptedPEMBlock(pemBlock) { + b, err := x509.DecryptPEMBlock(pemBlock, password) + if err != nil { + return nil, err + } + return x509.ParsePKCS1PrivateKey(b) + } + return x509.ParsePKCS1PrivateKey(pemBlock.Bytes) + case pkcs8PrivateKeyPEMBlockType: + priv, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) + if err != nil { + return nil, err + } + switch priv := priv.(type) { + case *rsa.PrivateKey: + return priv, nil + // case *dsa.PublicKey: + // case *ecdsa.PublicKey: + // case ed25519.PublicKey: + default: + panic("unsupported type of public key. SCEP need RSA private key") + } + default: return nil, errors.New("unmatched type or headers") } - - b, err := x509.DecryptPEMBlock(pemBlock, password) - if err != nil { - return nil, err - } - return x509.ParsePKCS1PrivateKey(b) } // load an encrypted private key from disk diff --git a/server/mdm/scep/depot/signer.go b/server/mdm/scep/depot/signer.go index 3f5e3562ddd3..d2f33ae45174 100644 --- a/server/mdm/scep/depot/signer.go +++ b/server/mdm/scep/depot/signer.go @@ -3,21 +3,20 @@ package depot import ( "crypto/rand" "crypto/x509" - "sync" "time" "github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" + "github.com/smallstep/scep" ) // Signer signs x509 certificates and stores them in a Depot type Signer struct { depot Depot - mu sync.Mutex caPass string allowRenewalDays int validityDays int serverAttrs bool + signatureAlgo x509.SignatureAlgorithm } // Option customizes Signer @@ -29,6 +28,7 @@ func NewSigner(depot Depot, opts ...Option) *Signer { depot: depot, allowRenewalDays: 14, validityDays: 365, + signatureAlgo: 0, } for _, opt := range opts { opt(s) @@ -36,6 +36,15 @@ func NewSigner(depot Depot, opts ...Option) *Signer { return s } +// WithSignatureAlgorithm sets the signature algorithm to be used to sign certificates. +// When set to a non-zero value, this would take preference over the default behaviour of +// matching the signing algorithm from the x509 CSR. +func WithSignatureAlgorithm(a x509.SignatureAlgorithm) Option { + return func(s *Signer) { + s.signatureAlgo = a + } +} + // WithCAPass specifies the password to use with an encrypted CA key func WithCAPass(pass string) Option { return func(s *Signer) { @@ -70,14 +79,16 @@ func (s *Signer) SignCSR(m *scep.CSRReqMessage) (*x509.Certificate, error) { return nil, err } - s.mu.Lock() - defer s.mu.Unlock() - serial, err := s.depot.Serial() if err != nil { return nil, err } + var signatureAlgo x509.SignatureAlgorithm + if s.signatureAlgo != 0 { + signatureAlgo = s.signatureAlgo + } + // create cert template tmpl := &x509.Certificate{ SerialNumber: serial, @@ -89,7 +100,7 @@ func (s *Signer) SignCSR(m *scep.CSRReqMessage) (*x509.Certificate, error) { ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, }, - SignatureAlgorithm: m.CSR.SignatureAlgorithm, + SignatureAlgorithm: signatureAlgo, DNSNames: m.CSR.DNSNames, EmailAddresses: m.CSR.EmailAddresses, IPAddresses: m.CSR.IPAddresses, diff --git a/server/mdm/scep/scep/certs_selector.go b/server/mdm/scep/scep/certs_selector.go deleted file mode 100644 index 62d726d951b2..000000000000 --- a/server/mdm/scep/scep/certs_selector.go +++ /dev/null @@ -1,57 +0,0 @@ -package scep - -import ( - "bytes" - "crypto" - "crypto/x509" -) - -// A CertsSelector filters certificates. -type CertsSelector interface { - SelectCerts([]*x509.Certificate) []*x509.Certificate -} - -// CertsSelectorFunc is a type of function that filters certificates. -type CertsSelectorFunc func([]*x509.Certificate) []*x509.Certificate - -func (f CertsSelectorFunc) SelectCerts(certs []*x509.Certificate) []*x509.Certificate { - return f(certs) -} - -// NopCertsSelector returns a CertsSelectorFunc that does not do anything. -func NopCertsSelector() CertsSelectorFunc { - return func(certs []*x509.Certificate) []*x509.Certificate { - return certs - } -} - -// A EnciphermentCertsSelector returns a CertsSelectorFunc that selects -// certificates eligible for key encipherment. This certsSelector can be used -// to filter PKCSReq recipients. -func EnciphermentCertsSelector() CertsSelectorFunc { - return func(certs []*x509.Certificate) (selected []*x509.Certificate) { - enciphermentKeyUsages := x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment - for _, cert := range certs { - if cert.KeyUsage&enciphermentKeyUsages != 0 { - selected = append(selected, cert) - } - } - return selected - } -} - -// FingerprintCertsSelector selects a certificate that matches hash using -// hashType against the digest of the raw certificate DER bytes -func FingerprintCertsSelector(hashType crypto.Hash, hash []byte) CertsSelectorFunc { - return func(certs []*x509.Certificate) (selected []*x509.Certificate) { - for _, cert := range certs { - h := hashType.New() - h.Write(cert.Raw) - if bytes.Compare(hash, h.Sum(nil)) == 0 { - selected = append(selected, cert) - return - } - } - return - } -} diff --git a/server/mdm/scep/scep/certs_selector_test.go b/server/mdm/scep/scep/certs_selector_test.go deleted file mode 100644 index 643249a04030..000000000000 --- a/server/mdm/scep/scep/certs_selector_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package scep - -import ( - "crypto" - _ "crypto/sha256" - "crypto/x509" - "encoding/hex" - "testing" -) - -func TestFingerprintCertsSelector(t *testing.T) { - for _, test := range []struct { - testName string - hashType crypto.Hash - hash string - certRaw []byte - expectedCount int - }{ - { - "null SHA-256 hash", - crypto.SHA256, - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - nil, - 1, - }, - { - "3 byte SHA-256 hash", - crypto.SHA256, - "039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81", - []byte{1, 2, 3}, - 1, - }, - { - "mismatched hash", - crypto.SHA256, - "8db07061ebb4cd0b0cd00825b363e5fb7f8131d8ff2c1fd70d03fa4fd6dc3785", - []byte{4, 5, 6}, - 0, - }, - } { - test := test - t.Run(test.testName, func(t *testing.T) { - t.Parallel() - - fakeCerts := []*x509.Certificate{{Raw: test.certRaw}} - - hash, err := hex.DecodeString(test.hash) - if err != nil { - t.Fatal(err) - } - if want, have := test.hashType.Size(), len(hash); want != have { - t.Errorf("invalid input hash length, want: %d have: %d", want, have) - } - - selected := FingerprintCertsSelector(test.hashType, hash).SelectCerts(fakeCerts) - - if want, have := test.expectedCount, len(selected); want != have { - t.Errorf("wrong selected certs count, want: %d have: %d", want, have) - } - }) - } -} - -func TestEnciphermentCertsSelector(t *testing.T) { - for _, test := range []struct { - testName string - certs []*x509.Certificate - expectedSelectedCerts []*x509.Certificate - }{ - { - "empty certificates list", - []*x509.Certificate{}, - []*x509.Certificate{}, - }, - { - "non-empty certificates list", - []*x509.Certificate{ - {KeyUsage: x509.KeyUsageKeyEncipherment}, - {KeyUsage: x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageDigitalSignature}, - {}, - }, - []*x509.Certificate{ - {KeyUsage: x509.KeyUsageKeyEncipherment}, - {KeyUsage: x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, - }, - }, - } { - test := test - t.Run(test.testName, func(t *testing.T) { - t.Parallel() - - selected := EnciphermentCertsSelector().SelectCerts(test.certs) - if !certsKeyUsagesEq(selected, test.expectedSelectedCerts) { - t.Fatal("selected and expected certificates did not match") - } - }) - } -} - -func TestNopCertsSelector(t *testing.T) { - for _, test := range []struct { - testName string - certs []*x509.Certificate - expectedSelectedCerts []*x509.Certificate - }{ - { - "empty certificates list", - []*x509.Certificate{}, - []*x509.Certificate{}, - }, - { - "non-empty certificates list", - []*x509.Certificate{ - {KeyUsage: x509.KeyUsageKeyEncipherment}, - {KeyUsage: x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageDigitalSignature}, - {}, - }, - []*x509.Certificate{ - {KeyUsage: x509.KeyUsageKeyEncipherment}, - {KeyUsage: x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment}, - {KeyUsage: x509.KeyUsageDigitalSignature}, - {}, - }, - }, - } { - test := test - t.Run(test.testName, func(t *testing.T) { - t.Parallel() - - selected := NopCertsSelector().SelectCerts(test.certs) - if !certsKeyUsagesEq(selected, test.expectedSelectedCerts) { - t.Fatal("selected and expected certificates did not match") - } - }) - } -} - -// certsKeyUsagesEq returns true if certs in a have the same key usages -// of certs in b and in the same order. -func certsKeyUsagesEq(a []*x509.Certificate, b []*x509.Certificate) bool { - if len(a) != len(b) { - return false - } - for i, cert := range a { - if cert.KeyUsage != b[i].KeyUsage { - return false - } - } - return true -} diff --git a/server/mdm/scep/scep/scep.go b/server/mdm/scep/scep/scep.go deleted file mode 100644 index dc2bb1fcc824..000000000000 --- a/server/mdm/scep/scep/scep.go +++ /dev/null @@ -1,665 +0,0 @@ -// Package scep provides common functionality for encoding and decoding -// Simple Certificate Enrolment Protocol pki messages as defined by -// https://tools.ietf.org/html/draft-gutmann-scep-02 -package scep - -import ( - "bytes" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/asn1" - "encoding/base64" - "errors" - "fmt" - - "github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil" - "github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util" - - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "github.com/smallstep/pkcs7" -) - -// errors -var ( - errNotImplemented = errors.New("not implemented") - errUnknownMessageType = errors.New("unknown messageType") -) - -// The MessageType attribute specifies the type of operation performed -// by the transaction. This attribute MUST be included in all PKI -// messages. -// -// The following message types are defined: -type MessageType string - -// Undefined message types are treated as an error. -const ( - CertRep MessageType = "3" - RenewalReq = "17" - UpdateReq = "18" - PKCSReq = "19" - CertPoll = "20" - GetCert = "21" - GetCRL = "22" -) - -func (msg MessageType) String() string { - switch msg { - case CertRep: - return "CertRep (3)" - case RenewalReq: - return "RenewalReq (17)" - case UpdateReq: - return "UpdateReq (18)" - case PKCSReq: - return "PKCSReq (19)" - case CertPoll: - return "CertPoll (20) " - case GetCert: - return "GetCert (21)" - case GetCRL: - return "GetCRL (22)" - default: - panic("scep: unknown messageType" + msg) - } -} - -// PKIStatus is a SCEP pkiStatus attribute which holds transaction status information. -// All SCEP responses MUST include a pkiStatus. -// -// The following pkiStatuses are defined: -type PKIStatus string - -// Undefined pkiStatus attributes are treated as an error -const ( - SUCCESS PKIStatus = "0" - FAILURE = "2" - PENDING = "3" -) - -// FailInfo is a SCEP failInfo attribute -// -// The FailInfo attribute MUST contain one of the following failure -// reasons: -type FailInfo string - -const ( - BadAlg FailInfo = "0" - BadMessageCheck = "1" - BadRequest = "2" - BadTime = "3" - BadCertID = "4" -) - -func (info FailInfo) String() string { - switch info { - case BadAlg: - return "badAlg (0)" - case BadMessageCheck: - return "badMessageCheck (1)" - case BadRequest: - return "badRequest (2)" - case BadTime: - return "badTime (3)" - case BadCertID: - return "badCertID (4)" - default: - panic("scep: unknown failInfo type" + info) - } -} - -// SenderNonce is a random 16 byte number. -// A sender must include the senderNonce in each transaction to a recipient. -type SenderNonce []byte - -// The RecipientNonce MUST be copied from the SenderNonce -// and included in the reply. -type RecipientNonce []byte - -// The TransactionID is a text -// string generated by the client when starting a transaction. The -// client MUST generate a unique string as the transaction identifier, -// which MUST be used for all PKI messages exchanged for a given -// enrolment, encoded as a PrintableString. -type TransactionID string - -// SCEP OIDs -var ( - oidSCEPmessageType = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 2} - oidSCEPpkiStatus = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 3} - oidSCEPfailInfo = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 4} - oidSCEPsenderNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 5} - oidSCEPrecipientNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 6} - oidSCEPtransactionID = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 7} -) - -// WithLogger adds option logging to the SCEP operations. -func WithLogger(logger log.Logger) Option { - return func(c *config) { - c.logger = logger - } -} - -// WithCACerts adds option CA certificates to the SCEP operations. -// Note: This changes the verification behavior of PKCS #7 messages. If this -// option is specified, only caCerts will be used as expected signers. -func WithCACerts(caCerts []*x509.Certificate) Option { - return func(c *config) { - c.caCerts = caCerts - } -} - -// WithCertsSelector adds the certificates certsSelector option to the SCEP -// operations. -// This option is effective when used with NewCSRRequest function. In -// this case, only certificates selected with the certsSelector will be used -// as the PKCS #7 message recipients. -func WithCertsSelector(selector CertsSelector) Option { - return func(c *config) { - c.certsSelector = selector - } -} - -// Option specifies custom configuration for SCEP. -type Option func(*config) - -type config struct { - logger log.Logger - caCerts []*x509.Certificate // specified if CA certificates have already been retrieved - certsSelector CertsSelector -} - -// PKIMessage defines the possible SCEP message types -type PKIMessage struct { - TransactionID - MessageType - SenderNonce - *CertRepMessage - *CSRReqMessage - - // DER Encoded PKIMessage - Raw []byte - - // parsed - p7 *pkcs7.PKCS7 - - // decrypted enveloped content - pkiEnvelope []byte - - // Used to encrypt message - Recipients []*x509.Certificate - - // Signer info - SignerKey *rsa.PrivateKey - SignerCert *x509.Certificate - - logger log.Logger -} - -// CertRepMessage is a type of PKIMessage -type CertRepMessage struct { - PKIStatus - RecipientNonce - FailInfo - - Certificate *x509.Certificate - - degenerate []byte -} - -// CSRReqMessage can be of the type PKCSReq/RenewalReq/UpdateReq -// and includes a PKCS#10 CSR request. -// The content of this message is protected -// by the recipient public key(example CA) -type CSRReqMessage struct { - RawDecrypted []byte - - // PKCS#10 Certificate request inside the envelope - CSR *x509.CertificateRequest - - ChallengePassword string -} - -// ParsePKIMessage unmarshals a PKCS#7 signed data into a PKI message struct -func ParsePKIMessage(data []byte, opts ...Option) (*PKIMessage, error) { - conf := &config{logger: log.NewNopLogger()} - for _, opt := range opts { - opt(conf) - } - - // parse PKCS#7 signed data - p7, err := pkcs7.Parse(data) - if err != nil { - return nil, err - } - - if len(conf.caCerts) > 0 { - // According to RFC #2315 Section 9.1, it is valid that the server sends fewer - // certificates than necessary, if it is expected that those verifying the - // signatures have an alternate means of obtaining necessary certificates. - // In SCEP case, an alternate means is to use GetCaCert request. - // Note: The https://github.com/jscep/jscep implementation logs a warning if - // no certificates were found for signers in the PKCS #7 received from the - // server, but the certificates obtained from GetCaCert request are still - // used for decoding the message. - p7.Certificates = conf.caCerts - } - - if err := p7.Verify(); err != nil { - return nil, err - } - - var tID TransactionID - if err := p7.UnmarshalSignedAttribute(oidSCEPtransactionID, &tID); err != nil { - return nil, err - } - - var msgType MessageType - if err := p7.UnmarshalSignedAttribute(oidSCEPmessageType, &msgType); err != nil { - return nil, err - } - - msg := &PKIMessage{ - TransactionID: tID, - MessageType: msgType, - Raw: data, - p7: p7, - logger: conf.logger, - } - - // log relevant key-values when parsing a pkiMessage. - logKeyVals := []interface{}{ - "msg", "parsed scep pkiMessage", - "scep_message_type", msgType, - "transaction_id", tID, - } - level.Debug(msg.logger).Log(logKeyVals...) - - if err := msg.parseMessageType(); err != nil { - return nil, err - } - - return msg, nil -} - -func (msg *PKIMessage) parseMessageType() error { - switch msg.MessageType { - case CertRep: - var status PKIStatus - if err := msg.p7.UnmarshalSignedAttribute(oidSCEPpkiStatus, &status); err != nil { - return err - } - var rn RecipientNonce - if err := msg.p7.UnmarshalSignedAttribute(oidSCEPrecipientNonce, &rn); err != nil { - return err - } - if len(rn) == 0 { - return errors.New("scep pkiMessage must include recipientNonce attribute") - } - cr := &CertRepMessage{ - PKIStatus: status, - RecipientNonce: rn, - } - switch status { - case SUCCESS: - break - case FAILURE: - var fi FailInfo - if err := msg.p7.UnmarshalSignedAttribute(oidSCEPfailInfo, &fi); err != nil { - return err - } - if fi == "" { - return errors.New("scep pkiStatus FAILURE must have a failInfo attribute") - } - cr.FailInfo = fi - case PENDING: - break - default: - return fmt.Errorf("unknown scep pkiStatus %s", status) - } - msg.CertRepMessage = cr - return nil - case PKCSReq, UpdateReq, RenewalReq: - var sn SenderNonce - if err := msg.p7.UnmarshalSignedAttribute(oidSCEPsenderNonce, &sn); err != nil { - return err - } - if len(sn) == 0 { - return errors.New("scep pkiMessage must include senderNonce attribute") - } - msg.SenderNonce = sn - return nil - case GetCRL, GetCert, CertPoll: - return errNotImplemented - default: - return errUnknownMessageType - } -} - -// DecryptPKIEnvelope decrypts the pkcs envelopedData inside the SCEP PKIMessage -func (msg *PKIMessage) DecryptPKIEnvelope(cert *x509.Certificate, key *rsa.PrivateKey) error { - p7, err := pkcs7.Parse(msg.p7.Content) - if err != nil { - return err - } - msg.pkiEnvelope, err = p7.Decrypt(cert, key) - if err != nil { - return err - } - - logKeyVals := []interface{}{ - "msg", "decrypt pkiEnvelope", - } - defer func() { level.Debug(msg.logger).Log(logKeyVals...) }() - - switch msg.MessageType { - case CertRep: - certs, err := CACerts(msg.pkiEnvelope) - if err != nil { - return err - } - msg.CertRepMessage.Certificate = certs[0] - logKeyVals = append(logKeyVals, "ca_certs", len(certs)) - return nil - case PKCSReq, UpdateReq, RenewalReq: - csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope) - if err != nil { - return errors.Join(err, errors.New("parse CSR from pkiEnvelope")) - } - // check for challengePassword - cp, err := x509util.ParseChallengePassword(msg.pkiEnvelope) - if err != nil { - return errors.Join(err, errors.New("scep: parse challenge password in pkiEnvelope")) - } - msg.CSRReqMessage = &CSRReqMessage{ - RawDecrypted: msg.pkiEnvelope, - CSR: csr, - ChallengePassword: cp, - } - logKeyVals = append(logKeyVals, "has_challenge", cp != "") - return nil - case GetCRL, GetCert, CertPoll: - return errNotImplemented - default: - return errUnknownMessageType - } -} - -func (msg *PKIMessage) Fail(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, info FailInfo) (*PKIMessage, error) { - config := pkcs7.SignerInfoConfig{ - ExtraSignedAttributes: []pkcs7.Attribute{ - { - Type: oidSCEPtransactionID, - Value: msg.TransactionID, - }, - { - Type: oidSCEPpkiStatus, - Value: FAILURE, - }, - { - Type: oidSCEPfailInfo, - Value: info, - }, - { - Type: oidSCEPmessageType, - Value: CertRep, - }, - { - Type: oidSCEPsenderNonce, - Value: msg.SenderNonce, - }, - { - Type: oidSCEPrecipientNonce, - Value: msg.SenderNonce, - }, - }, - } - - sd, err := pkcs7.NewSignedData(nil) - if err != nil { - return nil, err - } - - // sign the attributes - if err := sd.AddSigner(crtAuth, keyAuth, config); err != nil { - return nil, err - } - - certRepBytes, err := sd.Finish() - if err != nil { - return nil, err - } - - cr := &CertRepMessage{ - PKIStatus: FAILURE, - FailInfo: BadRequest, - RecipientNonce: RecipientNonce(msg.SenderNonce), - } - - // create a CertRep message from the original - crepMsg := &PKIMessage{ - Raw: certRepBytes, - TransactionID: msg.TransactionID, - MessageType: CertRep, - CertRepMessage: cr, - } - - return crepMsg, nil -} - -// Success returns a new PKIMessage with CertRep data using an already-issued certificate -func (msg *PKIMessage) Success(crtAuth *x509.Certificate, keyAuth *rsa.PrivateKey, crt *x509.Certificate) (*PKIMessage, error) { - // check if CSRReqMessage has already been decrypted - if msg.CSRReqMessage.CSR == nil { - if err := msg.DecryptPKIEnvelope(crtAuth, keyAuth); err != nil { - return nil, err - } - } - - // create a degenerate cert structure - deg, err := DegenerateCertificates([]*x509.Certificate{crt}) - if err != nil { - return nil, err - } - - // encrypt degenerate data using the original messages recipients - e7, err := pkcs7.Encrypt(deg, msg.p7.Certificates) - if err != nil { - return nil, err - } - - // PKIMessageAttributes to be signed - config := pkcs7.SignerInfoConfig{ - ExtraSignedAttributes: []pkcs7.Attribute{ - { - Type: oidSCEPtransactionID, - Value: msg.TransactionID, - }, - { - Type: oidSCEPpkiStatus, - Value: SUCCESS, - }, - { - Type: oidSCEPmessageType, - Value: CertRep, - }, - { - Type: oidSCEPsenderNonce, - Value: msg.SenderNonce, - }, - { - Type: oidSCEPrecipientNonce, - Value: msg.SenderNonce, - }, - }, - } - - signedData, err := pkcs7.NewSignedData(e7) - if err != nil { - return nil, err - } - // add the certificate into the signed data type - // this cert must be added before the signedData because the recipient will expect it - // as the first certificate in the array - signedData.AddCertificate(crt) - // sign the attributes - if err := signedData.AddSigner(crtAuth, keyAuth, config); err != nil { - return nil, err - } - - certRepBytes, err := signedData.Finish() - if err != nil { - return nil, err - } - - cr := &CertRepMessage{ - PKIStatus: SUCCESS, - RecipientNonce: RecipientNonce(msg.SenderNonce), - Certificate: crt, - degenerate: deg, - } - - // create a CertRep message from the original - crepMsg := &PKIMessage{ - Raw: certRepBytes, - TransactionID: msg.TransactionID, - MessageType: CertRep, - CertRepMessage: cr, - } - - return crepMsg, nil -} - -// DegenerateCertificates creates degenerate certificates pkcs#7 type -func DegenerateCertificates(certs []*x509.Certificate) ([]byte, error) { - var buf bytes.Buffer - for _, cert := range certs { - buf.Write(cert.Raw) - } - degenerate, err := pkcs7.DegenerateCertificate(buf.Bytes()) - if err != nil { - return nil, err - } - return degenerate, nil -} - -// CACerts extract CA Certificate or chain from pkcs7 degenerate signed data -func CACerts(data []byte) ([]*x509.Certificate, error) { - p7, err := pkcs7.Parse(data) - if err != nil { - return nil, err - } - return p7.Certificates, nil -} - -// NewCSRRequest creates a scep PKI PKCSReq/UpdateReq message -func NewCSRRequest(csr *x509.CertificateRequest, tmpl *PKIMessage, opts ...Option) (*PKIMessage, error) { - conf := &config{logger: log.NewNopLogger(), certsSelector: NopCertsSelector()} - for _, opt := range opts { - opt(conf) - } - - derBytes := csr.Raw - recipients := conf.certsSelector.SelectCerts(tmpl.Recipients) - if len(recipients) < 1 { - if len(tmpl.Recipients) >= 1 { - // our certsSelector eliminated any CA/RA recipients - return nil, errors.New("no selected CA/RA recipients") - } - return nil, errors.New("no CA/RA recipients") - } - e7, err := pkcs7.Encrypt(derBytes, recipients) - if err != nil { - return nil, err - } - - signedData, err := pkcs7.NewSignedData(e7) - if err != nil { - return nil, err - } - - // create transaction ID from public key hash - tID, err := newTransactionID(csr.PublicKey) - if err != nil { - return nil, err - } - - sn, err := newNonce() - if err != nil { - return nil, err - } - - level.Debug(conf.logger).Log( - "msg", "creating SCEP CSR request", - "transaction_id", tID, - "signer_cn", tmpl.SignerCert.Subject.CommonName, - ) - - // PKIMessageAttributes to be signed - config := pkcs7.SignerInfoConfig{ - ExtraSignedAttributes: []pkcs7.Attribute{ - { - Type: oidSCEPtransactionID, - Value: tID, - }, - { - Type: oidSCEPmessageType, - Value: tmpl.MessageType, - }, - { - Type: oidSCEPsenderNonce, - Value: sn, - }, - }, - } - - // sign attributes - if err := signedData.AddSigner(tmpl.SignerCert, tmpl.SignerKey, config); err != nil { - return nil, err - } - - rawPKIMessage, err := signedData.Finish() - if err != nil { - return nil, err - } - - cr := &CSRReqMessage{ - CSR: csr, - } - - newMsg := &PKIMessage{ - Raw: rawPKIMessage, - MessageType: tmpl.MessageType, - TransactionID: tID, - SenderNonce: sn, - CSRReqMessage: cr, - Recipients: recipients, - logger: conf.logger, - } - - return newMsg, nil -} - -func newNonce() (SenderNonce, error) { - size := 16 - b := make([]byte, size) - _, err := rand.Read(b) - if err != nil { - return SenderNonce{}, err - } - return SenderNonce(b), nil -} - -// use public key to create a deterministric transactionID -func newTransactionID(key crypto.PublicKey) (TransactionID, error) { - id, err := cryptoutil.GenerateSubjectKeyID(key) - if err != nil { - return "", err - } - - encHash := base64.StdEncoding.EncodeToString(id) - return TransactionID(encHash), nil -} diff --git a/server/mdm/scep/scep/scep_test.go b/server/mdm/scep/scep/scep_test.go deleted file mode 100644 index 4b330beeebce..000000000000 --- a/server/mdm/scep/scep/scep_test.go +++ /dev/null @@ -1,349 +0,0 @@ -package scep_test - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "errors" - "io/ioutil" - "math/big" - "testing" - "time" - - "github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil" - "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" -) - -func testParsePKIMessage(t *testing.T, data []byte) *scep.PKIMessage { - msg, err := scep.ParsePKIMessage(data) - if err != nil { - t.Fatal(err) - } - validateParsedPKIMessage(t, msg) - return msg -} - -func validateParsedPKIMessage(t *testing.T, msg *scep.PKIMessage) { - if msg.TransactionID == "" { - t.Errorf("expected TransactionID attribute") - } - if msg.MessageType == "" { - t.Errorf("expected MessageType attribute") - } - switch msg.MessageType { - case scep.CertRep: - if len(msg.RecipientNonce) == 0 { - t.Errorf("expected RecipientNonce attribute") - } - case scep.PKCSReq, scep.UpdateReq, scep.RenewalReq: - if len(msg.SenderNonce) == 0 { - t.Errorf("expected SenderNonce attribute") - } - } -} - -// Tests the case when servers reply with PKCS #7 signed-data that contains no -// certificates assuming that the client can request CA certificates using -// GetCaCert request. -func TestParsePKIEnvelopeCert_MissingCertificatesForSigners(t *testing.T) { - certRepMissingCertificates := loadTestFile(t, "testdata/testca2/CertRep_NoCertificatesForSigners.der") - caPEM := loadTestFile(t, "testdata/testca2/ca2.pem") - - // Try to parse the PKIMessage without providing certificates for signers. - _, err := scep.ParsePKIMessage(certRepMissingCertificates) - if err == nil { - t.Fatal("parsed PKIMessage without providing signer certificates") - } - - signerCert := decodePEMCert(t, caPEM) - msg, err := scep.ParsePKIMessage(certRepMissingCertificates, scep.WithCACerts([]*x509.Certificate{signerCert})) - if err != nil { - t.Fatalf("failed to parse PKIMessage: %v", err) - } - validateParsedPKIMessage(t, msg) -} - -func TestDecryptPKIEnvelopeCSR(t *testing.T) { - pkcsReq := loadTestFile(t, "testdata/PKCSReq.der") - msg := testParsePKIMessage(t, pkcsReq) - cacert, cakey := loadCACredentials(t) - err := msg.DecryptPKIEnvelope(cacert, cakey) - if err != nil { - t.Fatal(err) - } - if msg.CSRReqMessage.CSR == nil { - t.Errorf("expected non-nil CSR field") - } -} - -func TestDecryptPKIEnvelopeCert(t *testing.T) { - certRep := loadTestFile(t, "testdata/CertRep.der") - testParsePKIMessage(t, certRep) - // clientcert, clientkey := loadClientCredentials(t) - // err = msg.DecryptPKIEnvelope(clientcert, clientkey) - // if err != nil { - // t.Fatal(err) - // } -} - -func TestSignCSR(t *testing.T) { - pkcsReq := loadTestFile(t, "testdata/PKCSReq.der") - msg := testParsePKIMessage(t, pkcsReq) - cacert, cakey := loadCACredentials(t) - err := msg.DecryptPKIEnvelope(cacert, cakey) - if err != nil { - t.Fatal(err) - } - csr := msg.CSRReqMessage.CSR - id, err := cryptoutil.GenerateSubjectKeyID(csr.PublicKey) - if err != nil { - t.Fatal(err) - } - tmpl := &x509.Certificate{ - SerialNumber: big.NewInt(4), - Subject: csr.Subject, - NotBefore: time.Now().Add(-600).UTC(), - NotAfter: time.Now().AddDate(1, 0, 0).UTC(), - SubjectKeyId: id, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageAny, - x509.ExtKeyUsageClientAuth, - }, - } - // sign the CSR creating a DER encoded cert - crtBytes, err := x509.CreateCertificate(rand.Reader, tmpl, cacert, csr.PublicKey, cakey) - if err != nil { - t.Fatal(err) - } - crt, err := x509.ParseCertificate(crtBytes) - if err != nil { - t.Fatal(err) - } - certRep, err := msg.Success(cacert, cakey, crt) - if err != nil { - t.Fatal(err) - } - testParsePKIMessage(t, certRep.Raw) -} - -func TestNewCSRRequest(t *testing.T) { - for _, test := range []struct { - testName string - keyUsage x509.KeyUsage - certsSelectorFunc scep.CertsSelectorFunc - shouldCreateCSR bool - }{ - { - "KeyEncipherment not set with NOP certificates selector", - x509.KeyUsageCertSign, - scep.NopCertsSelector(), - true, - }, - { - "KeyEncipherment is set with NOP certificates selector", - x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment, - scep.NopCertsSelector(), - true, - }, - { - "KeyEncipherment not set with Encipherment certificates selector", - x509.KeyUsageCertSign, - scep.EnciphermentCertsSelector(), - false, - }, - { - "KeyEncipherment is set with Encipherment certificates selector", - x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment, - scep.EnciphermentCertsSelector(), - true, - }, - } { - test := test - t.Run(test.testName, func(t *testing.T) { - key, err := newRSAKey(2048) - if err != nil { - t.Fatal(err) - } - derBytes, err := newCSR(key, "john.doe@example.com", "US", "com.apple.scep.2379B935-294B-4AF1-A213-9BD44A2C6688") - if err != nil { - t.Fatal(err) - } - csr, err := x509.ParseCertificateRequest(derBytes) - if err != nil { - t.Fatal(err) - } - clientcert, clientkey := loadClientCredentials(t) - cacert, cakey := createCaCertWithKeyUsage(t, test.keyUsage) - tmpl := &scep.PKIMessage{ - MessageType: scep.PKCSReq, - Recipients: []*x509.Certificate{cacert}, - SignerCert: clientcert, - SignerKey: clientkey, - } - - pkcsreq, err := scep.NewCSRRequest(csr, tmpl, scep.WithCertsSelector(test.certsSelectorFunc)) - if test.shouldCreateCSR && err != nil { - t.Fatalf("keyUsage: %d, failed creating a CSR request: %v", test.keyUsage, err) - } - if !test.shouldCreateCSR && err == nil { - t.Fatalf("keyUsage: %d, shouldn't have created a CSR: %v", test.keyUsage, err) - } - if !test.shouldCreateCSR { - return - } - msg := testParsePKIMessage(t, pkcsreq.Raw) - err = msg.DecryptPKIEnvelope(cacert, cakey) - if err != nil { - t.Fatal(err) - } - }) - } -} - -// create a new RSA private key -func newRSAKey(bits int) (*rsa.PrivateKey, error) { - private, err := rsa.GenerateKey(rand.Reader, bits) - if err != nil { - return nil, err - } - return private, nil -} - -// create a CSR using the same parameters as Keychain Access would produce -func newCSR(priv *rsa.PrivateKey, email, country, cname string) ([]byte, error) { - subj := pkix.Name{ - Country: []string{country}, - CommonName: cname, - ExtraNames: []pkix.AttributeTypeAndValue{{ - Type: []int{1, 2, 840, 113549, 1, 9, 1}, - Value: email, - }}, - } - template := &x509.CertificateRequest{ - Subject: subj, - } - return x509.CreateCertificateRequest(rand.Reader, template, priv) -} - -func loadTestFile(t *testing.T, path string) []byte { - data, err := ioutil.ReadFile(path) - if err != nil { - t.Fatal(err) - } - return data -} - -// createCaCertWithKeyUsage generates a CA key and certificate with keyUsage. -func createCaCertWithKeyUsage(t *testing.T, keyUsage x509.KeyUsage) (*x509.Certificate, *rsa.PrivateKey) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - caCert := depot.NewCACert( - depot.WithCountry("US"), - depot.WithOrganization("MICROMDM"), - depot.WithCommonName("MICROMDM SCEP CA"), - depot.WithKeyUsage(keyUsage), - ) - crtBytes, err := caCert.SelfSign(rand.Reader, &key.PublicKey, key) - if err != nil { - t.Fatal(err) - } - cert, err := x509.ParseCertificate(crtBytes) - if err != nil { - t.Fatal(err) - } - return cert, key -} - -func loadCACredentials(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) { - cert, err := loadCertFromFile("testdata/testca/ca.crt") - if err != nil { - t.Fatal(err) - } - key, err := loadKeyFromFile("testdata/testca/ca.key") - if err != nil { - t.Fatal(err) - } - return cert, key -} - -func loadClientCredentials(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) { - cert, err := loadCertFromFile("testdata/testclient/client.pem") - if err != nil { - t.Fatal(err) - } - key, err := loadKeyFromFile("testdata/testclient/client.key") - if err != nil { - t.Fatal(err) - } - return cert, key -} - -const ( - rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" - certificatePEMBlockType = "CERTIFICATE" -) - -func loadCertFromFile(path string) (*x509.Certificate, error) { - data, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - pemBlock, _ := pem.Decode(data) - if pemBlock == nil { - return nil, errors.New("PEM decode failed") - } - if pemBlock.Type != certificatePEMBlockType { - return nil, errors.New("unmatched type or headers") - } - return x509.ParseCertificate(pemBlock.Bytes) -} - -// load an encrypted private key from disk -func loadKeyFromFile(path string) (*rsa.PrivateKey, error) { - data, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - pemBlock, _ := pem.Decode(data) - if pemBlock == nil { - return nil, errors.New("PEM decode failed") - } - if pemBlock.Type != rsaPrivateKeyPEMBlockType { - return nil, errors.New("unmatched type or headers") - } - - // testca key has a password - if len(pemBlock.Headers) > 0 { - password := []byte("") - b, err := x509.DecryptPEMBlock(pemBlock, password) - if err != nil { - return nil, err - } - return x509.ParsePKCS1PrivateKey(b) - } - - return x509.ParsePKCS1PrivateKey(pemBlock.Bytes) -} - -func decodePEMCert(t *testing.T, data []byte) *x509.Certificate { - pemBlock, _ := pem.Decode(data) - if pemBlock == nil { - t.Fatal("PEM decode failed") - } - if pemBlock.Type != certificatePEMBlockType { - t.Fatal("unmatched type or headers") - } - - cert, err := x509.ParseCertificate(pemBlock.Bytes) - if err != nil { - t.Fatal(err) - } - return cert -} diff --git a/server/mdm/scep/scep/testdata/CertRep.der b/server/mdm/scep/scep/testdata/CertRep.der deleted file mode 100755 index 16ebc2bebbbb..000000000000 Binary files a/server/mdm/scep/scep/testdata/CertRep.der and /dev/null differ diff --git a/server/mdm/scep/scep/testdata/testca2/CertRep_NoCertificatesForSigners.der b/server/mdm/scep/scep/testdata/testca2/CertRep_NoCertificatesForSigners.der deleted file mode 100644 index 6ed5f52f5839..000000000000 Binary files a/server/mdm/scep/scep/testdata/testca2/CertRep_NoCertificatesForSigners.der and /dev/null differ diff --git a/server/mdm/scep/scep/testdata/testca2/ca2.pem b/server/mdm/scep/scep/testdata/testca2/ca2.pem deleted file mode 100644 index b45d4981d0ef..000000000000 --- a/server/mdm/scep/scep/testdata/testca2/ca2.pem +++ /dev/null @@ -1,101 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 47:00:00:00:02:f5:40:0d:75:85:dd:87:88:00:00:00:00:00:02 - Signature Algorithm: sha256WithRSAEncryption - Issuer: DC = org, DC = example, CN = example-CERT-PROV-CA - Validity - Not Before: Oct 30 19:07:21 2020 GMT - Not After : Oct 30 19:07:21 2022 GMT - Subject: C = US, CN = CERT-PROV-CA-MSCEP-RA - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:a1:0b:13:03:0a:fc:2f:ed:92:22:3f:a5:01:2d: - f9:46:f5:2c:fb:38:f9:0e:f1:b9:48:65:30:61:95: - 84:15:a2:15:68:16:61:5d:cb:d5:83:d4:69:c7:da: - 4a:79:a3:c4:a2:e7:5b:33:b1:bc:e0:a1:3e:4e:1b: - 96:be:ff:34:a1:a5:da:ca:cd:ee:99:5e:91:e3:dc: - 96:ba:92:0a:77:01:82:2c:cb:c9:b5:30:69:de:39: - d3:59:34:86:35:e4:ce:7f:ca:b5:7c:2a:58:14:21: - 2a:4f:d8:0d:c6:90:17:a3:29:2d:b2:1f:89:e9:53: - 10:5b:e3:36:01:af:6c:01:08:2c:e8:43:cc:89:3d: - 99:13:39:85:76:d8:18:3f:df:db:1a:4d:fd:fa:39: - fa:f6:7c:86:d9:70:1b:0f:3a:e8:6b:fa:3d:e5:e4: - 38:c1:3e:3c:d1:c5:c7:74:ca:77:74:a1:0c:f1:dd: - f4:28:9b:d9:99:7d:1e:e9:36:9f:6a:da:64:6e:90: - 59:58:d6:db:e8:e3:5c:08:41:30:bd:14:35:de:0c: - 4a:9a:9c:1c:1e:ce:86:7d:cc:be:47:37:f6:5c:c4: - 91:86:7e:9a:9f:9f:d0:de:49:e4:bd:71:b0:d1:33: - b1:1f:ca:43:fe:e7:b0:f1:48:cf:40:79:1a:2e:f8: - 30:d5 - Exponent: 65537 (0x10001) - X509v3 extensions: - 1.3.6.1.4.1.311.20.2: - .,.E.n.r.o.l.l.m.e.n.t.A.g.e.n.t.O.f.f.l.i.n.e - X509v3 Extended Key Usage: - 1.3.6.1.4.1.311.20.2.1 - X509v3 Key Usage: critical - Digital Signature - X509v3 Subject Key Identifier: - D2:DB:A3:DF:13:2B:EE:D5:88:A1:3E:8C:28:0E:2A:00:7B:C7:18:19 - X509v3 Authority Key Identifier: - keyid:4B:C5:63:29:BB:CF:68:AF:18:1E:C5:99:E8:DF:55:F7:23:9A:08:EB - - X509v3 CRL Distribution Points: - - Full Name: - URI:ldap:///CN=example-CERT-PROV-CA,CN=cert-prov-ca,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=example,DC=org?certificateRevocationList?base?objectClass=cRLDistributionPoint - - Authority Information Access: - CA Issuers - URI:ldap:///CN=example-CERT-PROV-CA,CN=AIA,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=example,DC=org?cACertificate?base?objectClass=certificationAuthority - - Signature Algorithm: sha256WithRSAEncryption - 06:c8:d1:6f:ba:9b:48:84:a3:63:8e:4b:0d:73:85:91:7d:e4: - ce:50:9b:de:09:99:91:a3:1e:e6:ce:6f:ec:bf:2b:ce:bd:8d: - 0a:6c:0e:98:3f:b2:1f:cc:ab:53:62:2a:99:61:2d:76:9d:16: - 5d:27:f4:db:b6:9d:08:91:5b:cb:0c:18:09:b0:ab:38:e8:66: - ad:7b:45:53:81:11:16:aa:b2:5f:f6:ca:58:c0:fc:3c:98:04: - a6:0b:cd:28:28:8f:74:96:c4:57:7b:d1:a6:df:c8:ac:2f:cf: - 79:69:2e:ae:7c:e4:af:ad:ef:74:6f:c9:42:f7:03:3d:fe:48: - 25:05:d5:23:96:4a:4b:ed:a2:15:cf:b6:fe:06:d9:53:72:8e: - d2:14:3f:ab:83:db:22:e1:9b:16:51:f5:b6:ec:05:13:ad:2b: - fb:a4:1c:4c:97:17:29:5e:15:b9:f9:49:fb:33:7c:6d:b5:89: - ad:3d:50:64:9d:38:59:87:4d:9c:4f:39:44:48:34:96:77:f2: - 4b:1c:ad:84:94:e9:b7:9f:f7:1b:35:8a:c7:ab:18:14:59:d1: - f2:14:93:c3:a8:8f:6b:47:53:c9:9f:e2:f5:59:00:34:e6:23: - 2f:ce:5e:84:f9:81:ad:6b:cc:b3:ef:2c:04:5c:de:16:54:ba: - eb:38:f4:3b ------BEGIN CERTIFICATE----- -MIIFVDCCBDygAwIBAgITRwAAAAL1QA11hd2HiAAAAAAAAjANBgkqhkiG9w0BAQsF -ADBNMRMwEQYKCZImiZPyLGQBGRYDb3JnMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs -ZTEdMBsGA1UEAxMUZXhhbXBsZS1DRVJULVBST1YtQ0EwHhcNMjAxMDMwMTkwNzIx -WhcNMjIxMDMwMTkwNzIxWjAtMQswCQYDVQQGEwJVUzEeMBwGA1UEAxMVQ0VSVC1Q -Uk9WLUNBLU1TQ0VQLVJBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -oQsTAwr8L+2SIj+lAS35RvUs+zj5DvG5SGUwYZWEFaIVaBZhXcvVg9Rpx9pKeaPE -oudbM7G84KE+ThuWvv80oaXays3umV6R49yWupIKdwGCLMvJtTBp3jnTWTSGNeTO -f8q1fCpYFCEqT9gNxpAXoyktsh+J6VMQW+M2Aa9sAQgs6EPMiT2ZEzmFdtgYP9/b -Gk39+jn69nyG2XAbDzroa/o95eQ4wT480cXHdMp3dKEM8d30KJvZmX0e6Tafatpk -bpBZWNbb6ONcCEEwvRQ13gxKmpwcHs6Gfcy+Rzf2XMSRhn6an5/Q3knkvXGw0TOx -H8pD/uew8UjPQHkaLvgw1QIDAQABo4ICSzCCAkcwOwYJKwYBBAGCNxQCBC4eLABF -AG4AcgBvAGwAbABtAGUAbgB0AEEAZwBlAG4AdABPAGYAZgBsAGkAbgBlMBUGA1Ud -JQQOMAwGCisGAQQBgjcUAgEwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBTS26Pf -Eyvu1YihPowoDioAe8cYGTAfBgNVHSMEGDAWgBRLxWMpu89orxgexZno31X3I5oI -6zCB1wYDVR0fBIHPMIHMMIHJoIHGoIHDhoHAbGRhcDovLy9DTj1leGFtcGxlLUNF -UlQtUFJPVi1DQSxDTj1jZXJ0LXByb3YtY2EsQ049Q0RQLENOPVB1YmxpYyUyMEtl -eSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9ZXhh -bXBsZSxEQz1vcmc/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVj -dENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIHGBggrBgEFBQcBAQSBuTCBtjCB -swYIKwYBBQUHMAKGgaZsZGFwOi8vL0NOPWV4YW1wbGUtQ0VSVC1QUk9WLUNBLENO -PUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1D -b25maWd1cmF0aW9uLERDPWV4YW1wbGUsREM9b3JnP2NBQ2VydGlmaWNhdGU/YmFz -ZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MA0GCSqGSIb3DQEB -CwUAA4IBAQAGyNFvuptIhKNjjksNc4WRfeTOUJveCZmRox7mzm/svyvOvY0KbA6Y -P7IfzKtTYiqZYS12nRZdJ/Tbtp0IkVvLDBgJsKs46Gate0VTgREWqrJf9spYwPw8 -mASmC80oKI90lsRXe9Gm38isL895aS6ufOSvre90b8lC9wM9/kglBdUjlkpL7aIV -z7b+BtlTco7SFD+rg9si4ZsWUfW27AUTrSv7pBxMlxcpXhW5+Un7M3xttYmtPVBk -nThZh02cTzlESDSWd/JLHK2ElOm3n/cbNYrHqxgUWdHyFJPDqI9rR1PJn+L1WQA0 -5iMvzl6E+YGta8yz7ywEXN4WVLrrOPQ7 ------END CERTIFICATE----- diff --git a/server/mdm/scep/scep/testdata/testclient/client.key b/server/mdm/scep/scep/testdata/testclient/client.key deleted file mode 100644 index be2febb14f83..000000000000 --- a/server/mdm/scep/scep/testdata/testclient/client.key +++ /dev/null @@ -1,19 +0,0 @@ -Bag Attributes - friendlyName: com.apple.security.scep.063D7953-1338-4BF0-8F99-913382996224 - localKeyID: A1 30 90 76 1A 30 F6 66 64 F8 5D 37 43 3D 20 65 E1 11 2B A1 -Key Attributes: ------BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDT9YGr0H8dpozAEi5l2XkWyKy2JD3yEybI9A1ZDXcK/78UPQ+C -4tBb6BTRJWDWoZFlFcHUGbZWXbySPw6ggBsLl4feF1A+hjtCjlZsRF4mnfctixkr -dP+UGl37UunsW63mn8uM6oM+7elhB2zRscZrZPBDKZx1V+Et+BFrX49xNwIDAQAB -AoGAP9bzDmfG0YxnWjZfqSd+NCGO+3EhAzdHeEEhgA/xKevrhlH5yQc9kGDvXCrw -5tRU8WhDL/nqlEq5UCcT5b2P5zp1L0PY+X6gD5C7KGEIio5SvimAnQMh2HCKftDc -KX9NA9EJLq9BUsqK9HjXdsfIGzyoqhZQpDGTrgyVlfm/zwkCQQDyuKJvxm/6tWry -GBN0eBLyA4F738MFP/3kb87zUsGTD8dh92vwjMhGv1woKp09POs2s2MnADw2wOEa -hh+v6R5zAkEA344KSwaKZZSf7E5qFayg3qG9M55uhbus9CazoAWiceYMR6ImIevA -EtnhwQczIRGI8Bp4jgUtO9gu4IgjCUHNLQJBAIJkS+cuPGP75+sMog70noDi/0GT -0MnWOcfphMzUzWb6mAr6B0Of7cuL668sTXJjcpzdO8vs5WworAU6vnUbEB8CQQCB -+Hy3fcf8otoPcs9uZnzosrPjTNsI2UIGeHG6OUxmV88P3o+47O0wiIgdx2fMc/tf -TKSGPTA9OMSYOc3U1fLJAkEArLyCHxxDzcEuhyHnmoct/dTUg5Q8CYKIQfo5oVKZ -jvtL/r0udFpxLUDxZ7590I32cTSrtfgNBJegHr54YKN9KA== ------END RSA PRIVATE KEY----- diff --git a/server/mdm/scep/scep/testdata/testclient/client.pem b/server/mdm/scep/scep/testdata/testclient/client.pem deleted file mode 100644 index 3de20270b294..000000000000 --- a/server/mdm/scep/scep/testdata/testclient/client.pem +++ /dev/null @@ -1,58 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 6f:4f:31:6a:b2:da:d4:ce:d0:fc:09:fb:b9:26:90:03:d6:09:a4:8c - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = USA, O = etcd-ca, OU = CA, CN = etcd-ca - Validity - Not Before: Feb 16 12:11:45 2021 GMT - Not After : Dec 26 12:11:45 2030 GMT - Subject: C = US, ST = USA, O = etcd-ca, OU = CA, CN = etcd-ca - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (1024 bit) - Modulus: - 00:d3:f5:81:ab:d0:7f:1d:a6:8c:c0:12:2e:65:d9: - 79:16:c8:ac:b6:24:3d:f2:13:26:c8:f4:0d:59:0d: - 77:0a:ff:bf:14:3d:0f:82:e2:d0:5b:e8:14:d1:25: - 60:d6:a1:91:65:15:c1:d4:19:b6:56:5d:bc:92:3f: - 0e:a0:80:1b:0b:97:87:de:17:50:3e:86:3b:42:8e: - 56:6c:44:5e:26:9d:f7:2d:8b:19:2b:74:ff:94:1a: - 5d:fb:52:e9:ec:5b:ad:e6:9f:cb:8c:ea:83:3e:ed: - e9:61:07:6c:d1:b1:c6:6b:64:f0:43:29:9c:75:57: - e1:2d:f8:11:6b:5f:8f:71:37 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - A1:30:90:76:1A:30:F6:66:64:F8:5D:37:43:3D:20:65:E1:11:2B:A1 - X509v3 Authority Key Identifier: - keyid:A1:30:90:76:1A:30:F6:66:64:F8:5D:37:43:3D:20:65:E1:11:2B:A1 - - X509v3 Basic Constraints: critical - CA:TRUE - Signature Algorithm: sha256WithRSAEncryption - 96:f0:7b:28:3b:7a:06:2e:cd:37:23:19:f3:98:0c:a2:d3:16: - 9e:5a:b7:56:ca:9d:9d:ca:a4:59:78:b3:29:b1:3c:18:e8:dc: - 4c:f6:64:62:84:a3:19:ca:ca:b0:34:ed:d2:6f:9b:a6:38:20: - 98:64:db:c5:cb:a4:ce:b2:9c:62:a2:0e:e2:76:cb:f4:a1:c5: - 40:ee:c5:b4:18:9d:9e:5a:bf:bd:72:29:96:f8:82:05:87:d3: - fb:84:12:91:ea:e0:86:02:b1:63:c2:59:6a:10:9a:b7:7d:e2: - be:f3:19:31:31:3e:bb:21:4d:a0:16:f9:c0:94:ba:0f:e6:3d: - 37:26 ------BEGIN CERTIFICATE----- -MIICdDCCAd2gAwIBAgIUb08xarLa1M7Q/An7uSaQA9YJpIwwDQYJKoZIhvcNAQEL -BQAwTDELMAkGA1UEBhMCVVMxDDAKBgNVBAgMA1VTQTEQMA4GA1UECgwHZXRjZC1j -YTELMAkGA1UECwwCQ0ExEDAOBgNVBAMMB2V0Y2QtY2EwHhcNMjEwMjE2MTIxMTQ1 -WhcNMzAxMjI2MTIxMTQ1WjBMMQswCQYDVQQGEwJVUzEMMAoGA1UECAwDVVNBMRAw -DgYDVQQKDAdldGNkLWNhMQswCQYDVQQLDAJDQTEQMA4GA1UEAwwHZXRjZC1jYTCB -nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0/WBq9B/HaaMwBIuZdl5FsistiQ9 -8hMmyPQNWQ13Cv+/FD0PguLQW+gU0SVg1qGRZRXB1Bm2Vl28kj8OoIAbC5eH3hdQ -PoY7Qo5WbEReJp33LYsZK3T/lBpd+1Lp7Fut5p/LjOqDPu3pYQds0bHGa2TwQymc -dVfhLfgRa1+PcTcCAwEAAaNTMFEwHQYDVR0OBBYEFKEwkHYaMPZmZPhdN0M9IGXh -ESuhMB8GA1UdIwQYMBaAFKEwkHYaMPZmZPhdN0M9IGXhESuhMA8GA1UdEwEB/wQF -MAMBAf8wDQYJKoZIhvcNAQELBQADgYEAlvB7KDt6Bi7NNyMZ85gMotMWnlq3Vsqd -ncqkWXizKbE8GOjcTPZkYoSjGcrKsDTt0m+bpjggmGTbxcukzrKcYqIO4nbL9KHF -QO7FtBidnlq/vXIplviCBYfT+4QSkerghgKxY8JZahCat33ivvMZMTE+uyFNoBb5 -wJS6D+Y9NyY= ------END CERTIFICATE----- diff --git a/server/mdm/scep/server/csrsigner.go b/server/mdm/scep/server/csrsigner.go index 7e2fdd88f7c1..bfae3101a665 100644 --- a/server/mdm/scep/server/csrsigner.go +++ b/server/mdm/scep/server/csrsigner.go @@ -1,13 +1,30 @@ package scepserver import ( + "context" "crypto/subtle" "crypto/x509" "errors" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" + "github.com/smallstep/scep" ) +// CSRSignerContext is a handler for signing CSRs by a CA/RA. +// +// SignCSRContext should take the CSR in the CSRReqMessage and return a +// Certificate signed by the CA. +type CSRSignerContext interface { + SignCSRContext(context.Context, *scep.CSRReqMessage) (*x509.Certificate, error) +} + +// CSRSignerContextFunc is an adapter for CSR signing by the CA/RA. +type CSRSignerContextFunc func(context.Context, *scep.CSRReqMessage) (*x509.Certificate, error) + +// SignCSR calls f(ctx, m). +func (f CSRSignerContextFunc) SignCSRContext(ctx context.Context, m *scep.CSRReqMessage) (*x509.Certificate, error) { + return f(ctx, m) +} + // CSRSigner is a handler for CSR signing by the CA/RA // // SignCSR should take the CSR in the CSRReqMessage and return a @@ -16,29 +33,36 @@ type CSRSigner interface { SignCSR(*scep.CSRReqMessage) (*x509.Certificate, error) } -// CSRSignerFunc is an adapter for CSR signing by the CA/RA +// CSRSignerFunc is an adapter for CSR signing by the CA/RA. type CSRSignerFunc func(*scep.CSRReqMessage) (*x509.Certificate, error) -// SignCSR calls f(m) +// SignCSR calls f(m). func (f CSRSignerFunc) SignCSR(m *scep.CSRReqMessage) (*x509.Certificate, error) { return f(m) } -// NopCSRSigner does nothing -func NopCSRSigner() CSRSignerFunc { - return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { +// NopCSRSigner does nothing. +func NopCSRSigner() CSRSignerContextFunc { + return func(_ context.Context, _ *scep.CSRReqMessage) (*x509.Certificate, error) { return nil, nil } } -// ChallengeMiddleware wraps next in a CSRSigner that validates the challenge from the CSR -func ChallengeMiddleware(challenge string, next CSRSigner) CSRSignerFunc { +// StaticChallengeMiddleware wraps next and validates the challenge from the CSR. +func StaticChallengeMiddleware(challenge string, next CSRSignerContext) CSRSignerContextFunc { challengeBytes := []byte(challenge) - return func(m *scep.CSRReqMessage) (*x509.Certificate, error) { + return func(ctx context.Context, m *scep.CSRReqMessage) (*x509.Certificate, error) { // TODO: compare challenge only for PKCSReq? if subtle.ConstantTimeCompare(challengeBytes, []byte(m.ChallengePassword)) != 1 { return nil, errors.New("invalid challenge") } + return next.SignCSRContext(ctx, m) + } +} + +// SignCSRAdapter adapts a next (i.e. no context) to a context signer. +func SignCSRAdapter(next CSRSigner) CSRSignerContextFunc { + return func(_ context.Context, m *scep.CSRReqMessage) (*x509.Certificate, error) { return next.SignCSR(m) } } diff --git a/server/mdm/scep/server/csrsigner_test.go b/server/mdm/scep/server/csrsigner_test.go index 54576d1c7b4f..5e24b8c35b68 100644 --- a/server/mdm/scep/server/csrsigner_test.go +++ b/server/mdm/scep/server/csrsigner_test.go @@ -1,25 +1,28 @@ package scepserver import ( + "context" "testing" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" + "github.com/smallstep/scep" ) func TestChallengeMiddleware(t *testing.T) { testPW := "RIGHT" - signer := ChallengeMiddleware(testPW, NopCSRSigner()) + signer := StaticChallengeMiddleware(testPW, NopCSRSigner()) csrReq := &scep.CSRReqMessage{ChallengePassword: testPW} - _, err := signer.SignCSR(csrReq) + ctx := context.Background() + + _, err := signer.SignCSRContext(ctx, csrReq) if err != nil { t.Error(err) } csrReq.ChallengePassword = "WRONG" - _, err = signer.SignCSR(csrReq) + _, err = signer.SignCSRContext(ctx, csrReq) if err == nil { t.Error("invalid challenge should generate an error") } diff --git a/server/mdm/scep/server/endpoint.go b/server/mdm/scep/server/endpoint.go index 799d4108652c..e865a3341653 100644 --- a/server/mdm/scep/server/endpoint.go +++ b/server/mdm/scep/server/endpoint.go @@ -185,6 +185,7 @@ func EndpointLoggingMiddleware(logger log.Logger) endpoint.Middleware { logger.Log(append(keyvals, "error", err, "took", time.Since(begin))...) }(time.Now()) return next(ctx, request) + } } } diff --git a/server/mdm/scep/server/service.go b/server/mdm/scep/server/service.go index 85d01910d38e..d387f3f39cce 100644 --- a/server/mdm/scep/server/service.go +++ b/server/mdm/scep/server/service.go @@ -6,9 +6,8 @@ import ( "crypto/x509" "errors" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" - - "github.com/go-kit/log" + "github.com/go-kit/kit/log" + "github.com/smallstep/scep" ) // Service is the interface for all supported SCEP server operations. @@ -47,7 +46,7 @@ type service struct { // The (chainable) CSR signing function. Intended to handle all // SCEP request functionality such as CSR & challenge checking, CA // issuance, RA proxying, etc. - signer CSRSigner + signer CSRSignerContext /// info logging is implemented in the service middleware layer. debugLogger log.Logger @@ -83,7 +82,7 @@ func (svc *service) PKIOperation(ctx context.Context, data []byte) ([]byte, erro return nil, err } - crt, err := svc.signer.SignCSR(msg.CSRReqMessage) + crt, err := svc.signer.SignCSRContext(ctx, msg.CSRReqMessage) if err == nil && crt == nil { err = errors.New("no signed certificate") } @@ -122,7 +121,7 @@ func WithAddlCA(ca *x509.Certificate) ServiceOption { } // NewService creates a new scep service -func NewService(crt *x509.Certificate, key *rsa.PrivateKey, signer CSRSigner, opts ...ServiceOption) (Service, error) { +func NewService(crt *x509.Certificate, key *rsa.PrivateKey, signer CSRSignerContext, opts ...ServiceOption) (Service, error) { s := &service{ crt: crt, key: key, diff --git a/server/mdm/scep/server/service_bolt_test.go b/server/mdm/scep/server/service_bolt_test.go index e0c8db1fcdb0..6374497cf61b 100644 --- a/server/mdm/scep/server/service_bolt_test.go +++ b/server/mdm/scep/server/service_bolt_test.go @@ -16,9 +16,9 @@ import ( scepdepot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" boltdepot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot/bolt" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" + "github.com/smallstep/scep" bolt "go.etcd.io/bbolt" ) @@ -45,7 +45,7 @@ func TestCaCert(t *testing.T) { caCert := certs[0] // SCEP service - svc, err := scepserver.NewService(caCert, key, scepdepot.NewSigner(depot)) + svc, err := scepserver.NewService(caCert, key, scepserver.SignCSRAdapter(scepdepot.NewSigner(depot))) if err != nil { t.Fatal(err) } @@ -130,6 +130,12 @@ func TestCaCert(t *testing.T) { t.Error("no established chain between issued cert and CA") } + if csr.SignatureAlgorithm != respCert.SignatureAlgorithm { + t.Fatal(fmt.Errorf("cert signature algo %s different from csr signature algo %s", + csr.SignatureAlgorithm.String(), + respCert.SignatureAlgorithm.String())) + } + // verify unique certificate serials for _, ser := range serCollector { if respCert.SerialNumber.Cmp(ser) == 0 { @@ -138,6 +144,7 @@ func TestCaCert(t *testing.T) { } serCollector = append(serCollector, respCert.SerialNumber) } + } func createDB(mode os.FileMode, options *bolt.Options) *boltdepot.Depot { diff --git a/server/mdm/scep/server/transport_test.go b/server/mdm/scep/server/transport_test.go index 05bdc7f9ee5f..becc5091d070 100644 --- a/server/mdm/scep/server/transport_test.go +++ b/server/mdm/scep/server/transport_test.go @@ -34,7 +34,7 @@ func TestCACaps(t *testing.T) { } func TestEncodePKCSReq_Request(t *testing.T) { - pkcsreq := loadTestFile(t, "../scep/testdata/PKCSReq.der") + pkcsreq := loadTestFile(t, "../testdata/PKCSReq.der") msg := scepserver.SCEPRequest{ Operation: "PKIOperation", Message: pkcsreq, @@ -64,8 +64,10 @@ func TestEncodePKCSReq_Request(t *testing.T) { t.Errorf("expected GET PKIOperation to have a non-empty message field") } } + }) } + } func TestGetCACertMessage(t *testing.T) { @@ -87,7 +89,7 @@ func TestGetCACertMessage(t *testing.T) { func TestPKIOperation(t *testing.T) { server, _, teardown := newServer(t) defer teardown() - pkcsreq := loadTestFile(t, "../scep/testdata/PKCSReq.der") + pkcsreq := loadTestFile(t, "../testdata/PKCSReq.der") body := bytes.NewReader(pkcsreq) url := server.URL + "/scep?operation=PKIOperation" resp, err := http.Post(url, "", body) //nolint:gosec @@ -102,7 +104,7 @@ func TestPKIOperation(t *testing.T) { func TestPKIOperationGET(t *testing.T) { server, _, teardown := newServer(t) defer teardown() - pkcsreq := loadTestFile(t, "../scep/testdata/PKCSReq.der") + pkcsreq := loadTestFile(t, "../testdata/PKCSReq.der") message := base64.StdEncoding.EncodeToString(pkcsreq) req, err := http.NewRequest("GET", server.URL+"/scep", nil) if err != nil { @@ -202,7 +204,7 @@ func newServer(t *testing.T, opts ...scepserver.ServiceOption) (*httptest.Server var err error var depot depot.Depot // cert storage { - depot, err = filedepot.NewFileDepot("../scep/testdata/testca") + depot, err = filedepot.NewFileDepot("../testdata/testca") if err != nil { t.Fatal(err) } @@ -227,8 +229,8 @@ func newServer(t *testing.T, opts ...scepserver.ServiceOption) (*httptest.Server server := httptest.NewServer(r) teardown := func() { server.Close() - os.Remove("../scep/testdata/testca/serial") - os.Remove("../scep/testdata/testca/index.txt") + os.Remove("../testdata/testca/serial") + os.Remove("../testdata/testca/index.txt") } return server, svc, teardown } diff --git a/server/mdm/scep/scep/testdata/PKCSReq.der b/server/mdm/scep/testdata/PKCSReq.der similarity index 100% rename from server/mdm/scep/scep/testdata/PKCSReq.der rename to server/mdm/scep/testdata/PKCSReq.der diff --git a/server/mdm/scep/scep/testdata/testca/ca.crt b/server/mdm/scep/testdata/testca/ca.crt similarity index 100% rename from server/mdm/scep/scep/testdata/testca/ca.crt rename to server/mdm/scep/testdata/testca/ca.crt diff --git a/server/mdm/scep/scep/testdata/testca/ca.crt.info b/server/mdm/scep/testdata/testca/ca.crt.info similarity index 100% rename from server/mdm/scep/scep/testdata/testca/ca.crt.info rename to server/mdm/scep/testdata/testca/ca.crt.info diff --git a/server/mdm/scep/scep/testdata/testca/ca.key b/server/mdm/scep/testdata/testca/ca.key similarity index 100% rename from server/mdm/scep/scep/testdata/testca/ca.key rename to server/mdm/scep/testdata/testca/ca.key diff --git a/server/mdm/scep/scep/testdata/testca/ca.pem b/server/mdm/scep/testdata/testca/ca.pem similarity index 100% rename from server/mdm/scep/scep/testdata/testca/ca.pem rename to server/mdm/scep/testdata/testca/ca.pem diff --git a/server/mdm/scep/scep/testdata/testca/sceptest.mobileconfig b/server/mdm/scep/testdata/testca/sceptest.mobileconfig similarity index 100% rename from server/mdm/scep/scep/testdata/testca/sceptest.mobileconfig rename to server/mdm/scep/testdata/testca/sceptest.mobileconfig diff --git a/server/service/handler.go b/server/service/handler.go index 7012393952bc..7443436b8e42 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -1096,18 +1096,18 @@ func registerSCEP( mdmStorage fleet.MDMAppleStore, logger kitlog.Logger, ) error { - var signer scepserver.CSRSigner = scep_depot.NewSigner( + var signer scepserver.CSRSignerContext = scepserver.SignCSRAdapter(scep_depot.NewSigner( scepStorage, scep_depot.WithValidityDays(scepConfig.AppleSCEPSignerValidityDays), scep_depot.WithAllowRenewalDays(scepConfig.AppleSCEPSignerAllowRenewalDays), - ) + )) assets, err := mdmStorage.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{fleet.MDMAssetSCEPChallenge}) if err != nil { return fmt.Errorf("retrieving SCEP challenge: %w", err) } scepChallenge := string(assets[fleet.MDMAssetSCEPChallenge].Value) - signer = scepserver.ChallengeMiddleware(scepChallenge, signer) + signer = scepserver.StaticChallengeMiddleware(scepChallenge, signer) scepService := NewSCEPService( mdmStorage, signer, diff --git a/server/service/mdm_scep.go b/server/service/mdm_scep.go index 2032580686a4..92f9a95ae197 100644 --- a/server/service/mdm_scep.go +++ b/server/service/mdm_scep.go @@ -8,10 +8,10 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/assets" - "github.com/fleetdm/fleet/v4/server/mdm/scep/scep" scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" "github.com/go-kit/log" + "github.com/smallstep/scep" ) var _ scepserver.Service = (*service)(nil) @@ -20,7 +20,7 @@ type service struct { // The (chainable) CSR signing function. Intended to handle all // SCEP request functionality such as CSR & challenge checking, CA // issuance, RA proxying, etc. - signer scepserver.CSRSigner + signer scepserver.CSRSignerContext /// info logging is implemented in the service middleware layer. debugLogger log.Logger @@ -64,7 +64,7 @@ func (svc *service) PKIOperation(ctx context.Context, data []byte) ([]byte, erro return nil, err } - crt, err := svc.signer.SignCSR(msg.CSRReqMessage) + crt, err := svc.signer.SignCSRContext(ctx, msg.CSRReqMessage) if err == nil && crt == nil { err = errors.New("no signed certificate") } @@ -83,7 +83,7 @@ func (svc *service) GetNextCACert(ctx context.Context) ([]byte, error) { } // NewService creates a new scep service -func NewSCEPService(ds fleet.MDMAssetRetriever, signer scepserver.CSRSigner, logger log.Logger) scepserver.Service { +func NewSCEPService(ds fleet.MDMAssetRetriever, signer scepserver.CSRSignerContext, logger log.Logger) scepserver.Service { return &service{ signer: signer, debugLogger: log.NewNopLogger(), diff --git a/server/vulnerabilities/io/github.go b/server/vulnerabilities/io/github.go index 8d696bf2240a..005a894281aa 100644 --- a/server/vulnerabilities/io/github.go +++ b/server/vulnerabilities/io/github.go @@ -114,14 +114,17 @@ func (gh GitHubClient) list(ctx context.Context, prefix string, ctor func(fileNa } results := make(map[MetadataFileName]string) - for _, e := range releases[0].Assets { - name := e.GetName() - if strings.HasPrefix(name, prefix) { - metadataFileName, err := ctor(name) - if err != nil { - return nil, err + + if len(releases) > 0 { + for _, e := range releases[0].Assets { + name := e.GetName() + if strings.HasPrefix(name, prefix) { + metadataFileName, err := ctor(name) + if err != nil { + return nil, err + } + results[metadataFileName] = e.GetBrowserDownloadURL() } - results[metadataFileName] = e.GetBrowserDownloadURL() } } return results, nil diff --git a/server/vulnerabilities/msrc/msrc_api.go b/server/vulnerabilities/msrc/msrc_api.go index 1465852b579a..c44f7356d1a5 100644 --- a/server/vulnerabilities/msrc/msrc_api.go +++ b/server/vulnerabilities/msrc/msrc_api.go @@ -23,6 +23,11 @@ type MSRCAPI interface { GetFeed(time.Month, int) (string, error) } +// FeedNotFound is returned when a MSRC feed was not found. +// +// E.g. September 2024 bulleting was released on the 2nd. +var FeedNotFound = errors.New("feed not found") + type MSRCClient struct { client *http.Client workDir string @@ -44,7 +49,7 @@ func feedName(date time.Time) string { } func (msrc MSRCClient) getURL(date time.Time) (*url.URL, error) { - return url.Parse(msrc.baseURL + "/cvrf/v2.0/document/" + feedName(date)) + return url.Parse(msrc.baseURL + "/cvrf/v3.0/document/" + feedName(date)) } // GetFeed downloads the MSRC security feed for 'month' and 'year' into 'workDir', returning the @@ -68,6 +73,9 @@ func (msrc MSRCClient) GetFeed(month time.Month, year int) (string, error) { } if err := download.Download(msrc.client, u, dst); err != nil { + if errors.Is(err, download.NotFound) { + return "", FeedNotFound + } return "", err } diff --git a/server/vulnerabilities/msrc/msrc_api_test.go b/server/vulnerabilities/msrc/msrc_api_test.go index 79ea381eb428..592977315f9c 100644 --- a/server/vulnerabilities/msrc/msrc_api_test.go +++ b/server/vulnerabilities/msrc/msrc_api_test.go @@ -50,7 +50,7 @@ func TestMSRCClient(t *testing.T) { dir := t.TempDir() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/cvrf/v2.0/document/2021-Oct" { + if r.URL.Path == "/cvrf/v3.0/document/2021-Oct" { w.WriteHeader(http.StatusOK) w.Write([]byte("some payload")) //nolint:errcheck } diff --git a/terraform/addons/byo-firehose-logging-destination/target-account/.header.md b/terraform/addons/byo-firehose-logging-destination/target-account/.header.md index 8e754c402a0f..e361e1aa6233 100644 --- a/terraform/addons/byo-firehose-logging-destination/target-account/.header.md +++ b/terraform/addons/byo-firehose-logging-destination/target-account/.header.md @@ -6,4 +6,17 @@ The reason we need a local IAM role in your account is so that we can assume rol The Firehose service is KMS encrypted, so the IAM Role we assume into needs permission to the KMS key that is being used to encrypt the data going into Firehose. Additionally, if the data is being delivered to S3, it will also be encrypted with KMS using the AWS S3 KMS key that is managed by AWS. This is because only customer managed keys can be shared across accounts, and the Firehose delivery stream is actually the one writing to S3. -This code sets up a secure and controlled environment for the Fleet application to perform its necessary actions on the Firehose service within your AWS Account. \ No newline at end of file +This code sets up a secure and controlled environment for the Fleet application to perform its necessary actions on the Firehose service within your AWS Account. + +If you wanted to make changes to the individual files to fit your environment, feel free. However, it's recommended to use a module like the example below for simplicity. + +``` +module "firehose_logging" { + source = "github.com/fleetdm/fleet//terraform/addons/byo-firehose-logging-destination/target-account" + + # Variables + osquery_logging_destination_bucket_name = {your-desired-bucket-prefix} + fleet_iam_role_arn = {supplied by Fleet} + sts_external_id = {if using} +} +``` diff --git a/terraform/addons/byo-firehose-logging-destination/target-account/README.md b/terraform/addons/byo-firehose-logging-destination/target-account/README.md index b4eb9af91bbd..f378e24fafbf 100644 --- a/terraform/addons/byo-firehose-logging-destination/target-account/README.md +++ b/terraform/addons/byo-firehose-logging-destination/target-account/README.md @@ -8,6 +8,18 @@ The Firehose service is KMS encrypted, so the IAM Role we assume into needs perm This code sets up a secure and controlled environment for the Fleet application to perform its necessary actions on the Firehose service within your AWS Account. +If you wanted to make changes to the individual files to fit your environment, feel free. However, it's recommended to use a module like the example below for simplicity. + +``` +module "firehose_logging" { + source = "github.com/fleetdm/fleet//terraform/addons/byo-firehose-logging-destination/target-account" + + # Variables + osquery_logging_destination_bucket_name = {your-desired-bucket-prefix} + fleet_iam_role_arn = {supplied by Fleet} + sts_external_id = {if using} +} +``` ## Requirements | Name | Version | diff --git a/website/assets/images/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18-1600x900@2x.jpg b/website/assets/images/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18-1600x900@2x.jpg new file mode 100644 index 000000000000..d66f91aaf53a Binary files /dev/null and b/website/assets/images/articles/fleet-supports-macos-15-sequoia-ios-18-and-ipados-18-1600x900@2x.jpg differ diff --git a/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-1-312x207@2x.jpg b/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-1-312x207@2x.jpg new file mode 100644 index 000000000000..02a8d7ad0118 Binary files /dev/null and b/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-1-312x207@2x.jpg differ diff --git a/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-2-1999x680@2x.png b/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-2-1999x680@2x.png new file mode 100644 index 000000000000..3ecc4e437f12 Binary files /dev/null and b/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-2-1999x680@2x.png differ diff --git a/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-3-566x415@2x.png b/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-3-566x415@2x.png new file mode 100644 index 000000000000..232d33010323 Binary files /dev/null and b/website/assets/images/articles/sysadmin-diaries-gitops-a-strategic-advantage-3-566x415@2x.png differ diff --git a/website/config/routes.js b/website/config/routes.js index ddbc2d07676e..92618498739c 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -487,6 +487,7 @@ module.exports.routes = { 'GET /docs/using-fleet/standard-query-library': (req,res)=> { return res.redirect(301, '/guides/standard-query-library');}, 'GET /docs/using-fleet/mdm-commands': (req,res)=> { return res.redirect(301, '/guides/mdm-commands');}, 'GET /docs/using-fleet/log-destinations': (req,res)=> { return res.redirect(301, '/guides/log-destinations');}, + 'GET /guides/how-to-uninstall-osquery': (req,res)=> { return res.redirect(301, '/guides/how-to-uninstall-fleetd');}, // ╔╦╗╦╔═╗╔═╗ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗ ┬ ╔╦╗╔═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔╦╗╔═╗ // ║║║║╚═╗║ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗ ┌┼─ ║║║ ║║║║║║║║ ║ ║╠═╣ ║║╚═╗