diff --git a/proposals/Fleet-Installers-4-Sandbox.md b/proposals/Fleet-Installers-4-Sandbox.md deleted file mode 100644 index 7043c05fe987..000000000000 --- a/proposals/Fleet-Installers-4-Sandbox.md +++ /dev/null @@ -1,50 +0,0 @@ -# Fleet Sandbox & Pre-Packaged Fleet-osquery Installers - -## Goals - -1. Improve UX on Fleet Sandbox by offering pre-packaged Fleet-osquery installers. -2. Add the "Pre-Packaged installers" feature to "Fleet Sandbox" as soon as possible (i.e. not block on having a fully functional "Fleet Packager" service). - -## Fleet Sandbox Assumptions - -- We will limit number of teams to T. -- Sandbox has good root CA trusted certificates -- Users won't be allowed to change enroll secrets. - -## Pre-Packaged Installers Plan - -We will need some changes to fleetctl, the pre-provisioner, the Fleet server and UI. - -### fleetctl - -Make all functionality in `fleetctl package` to run in linux. (This change will be needed for the Packager service anyways.) - -PS: Abstract in its own package so that it can be used by Packager service in a next iteration. - -### Pre-provisioner - -Following are the pre-provisioner steps to generate the pre-packaged installers: - -1. Generate T+1 random enroll secrets. - -2. Run `fleetctl package --type={pkg|rpm|deb|msi}` with T+1 enroll secrets (i.e. one for Global and one for each team). -PS: There's some complexity in storing/handling credentials for macOS Signing and Notarization of the packages. - -3. The generated packages will be stored in a S3 bucket accessible by the Fleet server with the following object name format -`$INSTALLERS_DIR/$ENROLL_SECRET/fleet-osquery.$TYPE`, e.g. `/fleet-installers/FzRCZWTlEY2kqzIwk1BE9fru5KuhrlYP/fleet-osquery.pkg`. -We propose using S3 to support multiple Fleet instances serving the requests. - -4. Set comma-separated `FLEET_ENROLL_POOL` environment variable to Fleet server config (Fleet would use those secrets instead of randomly generating one). -The Fleet server will only serve the installers with enroll secret listed in this variable (security check). - -### Fleet Server and UI - -- Fleet server new configuration and new functionality: - - `FLEET_MAX_TEAMS`: Maximum number of teams to allow in the deployment (default 0, disabled). - - `FLEET_DISABLE_ENROLL_CHANGE`: Disallow users from changing enroll secrets (default false). - - `FLEET_PACKAGES_S3_*`: S3 configuration for the retrieval of the pre-packaged installers (default empty). - - `FLEET_ENROLL_POOL`: comma-separated enrolls to use when needed (default empty), must have equals to FLEET_MAX_TEAMS+1 items (default empty). - -- Fleet will serve a new authenticated API (for Sandbox-only): `{GET|HEAD} /api/v1/fleet/download_installer/{enroll}/{type}`, e.g. `GET /api/v1/fleet/download_installer/FzRCZWTlEY2kqzIwk1BE9fru5KuhrlYP/rpm`. - - The UI can make a `HEAD` request to check if an installer exists, if so, then it can display a download button for it, (if not, "show the current UI"? TBD with UI team) - - The API looks for the installer corresponding to the Global/Team the user is looking at, and returns it for download. diff --git a/proposals/Fleet-Installers.md b/proposals/Fleet-Installers.md deleted file mode 100644 index f6604e36c431..000000000000 --- a/proposals/Fleet-Installers.md +++ /dev/null @@ -1,469 +0,0 @@ -# Fleet Installers - -## Goal - -[#5757](https://github.com/fleetdm/fleet/issues/5757) - -``` -As a user, I want to be able to download a Fleet-osquery installer (aka Orbit) in the Fleet UI -so that I can add hosts to Fleet without having to know how to successfully generate an -installer with the `fleetctl package` command. - -Figma wireframes: https://www.figma.com/file/hdALBDsrti77QuDNSzLdkx/?node-id=6740%3A267448 -``` - -## Command `fleetctl package` - -Currently users use the `fleetctl package` command to generate Fleet-osquery packages. - -The `fleetctl package` command has the following *required* configuration that is specific to a "Fleet Deployment": -- `--fleet-url`: The URL that the hosts will use to connect to the Fleet server. -- `--enroll-secret`: Global or team enroll secret to use when enrolling the host to Fleet. - -As the goal states, we would like to provide functionality in Fleet to automatically generate and download such packages from the UI. - -## How - -We can implement such functionality in two ways: - -- Option A. Fleet Server to generate such packages itself. -- Option B. Separate "Packager" service. - -There's a lot of platform specific logic and tooling involved in packaging, and one of Fleet's primary goals is to keep deployment/infrastructure simple for On-Prem deployments. -To that end, we believe the best option is Option B, implementing the functionality as a separate service. - -## Packager Service - -The "Fleet Packager" service will implement all the package generation logic. Think of it like offering the `fleetctl package` command functionality but as a REST service. - -```mermaid -graph LR; - A[User-Agent/
Browser]-- Fleet API -->B[Fleet
UI/Server] - A-- Packager
API -->C - subgraph FleetDM Hosted Infrastructure - direction LR - subgraph
pkg.fleetctl.com - direction TB; - C[Packager
Service] - packages[(Generated
packages)] - end - direction TB; - D[tuf.fleetctl.com] - C-- Fetch
Targets -->D; - end - E[Apple's
Notary
Server] - C-- Notary
API ---->E -``` - -Configurations: -- The `Fleet UI/Server` will allow configuring the "Fleet Packager" URL (default value being FleetDM Hosted Packager service, something like `pkg.fleetctl.com`). -- The `Packager Service` will allow configuring an alternative TUF server URL and TUF roots via an environmental variable (default value being FleetDM Hosted TUF, `tuf.fleetctl.com`). - -Both of these configurations will allow users to deploy their own `Packager Service`. - -### Storage - -The generated packages should be stored on encrypted disk and will expire (will be deleted) after a configurable amount of time (default 30m). -There are two reasons we want to expire generated packages soon: -1. To not store user credentials for too long (URL and enroll secret). -2. To free up space. - -We can use "Storage Optimized" instances (see https://aws.amazon.com/ec2/instance-types/). - -#### Notes - -- As a possible future optimization, we could use "Memory Optimized" instances and store packages in RAM instead of using hard disk. -- S3 could also be used to store such packages, but disk storage is needed to generate the packages in the first place. -So hard-disk will be a dependency anyways (and ideally we would like these packages with sensitive credentials to be stored in one location). -- From Roberto: "sounds like the main bottleneck here will be transferring the data over the network to the user doing the request.". -In other words, we should apply all optimizations on the network (like caching, reducing package size, etc.), that will be our main bottleneck -(not CPU or hard-disk access). - -#### Back of the Envelope - -- `.pkg`s use ~70MB of storage. -- `.msi`s use ~20MB of storage. -- `.deb`s and `.rpm`s use ~75MB of storage. - -Assuming the worst case of ~75 MB for each package: -If we have a ~30TB hard disk, it would allow storing ~400_000 packages simultaneously. - -### Network & Credentials - -The service will require network access to (URLs provided via config): - -- TUF server. -- Apple's Notary Server (for generating `.pkg`). - -The packaging service will need the following credentials (provided via config): - -- Apple credentials: - - Codesign identity. - - Username and Password for notarization. -- TUF server update roots (default will be the hardcoded one for FleetDM's hosted TUF server, tuf.fleetctl.com). - -### Packager REST API - -For the MVF (Minimum Viable Feature) we'll need three APIs: one for creation/submission, one for checking status and another one for the actual download of the package. -All the APIs must be rate-limited to prevent abuse of the system. - -#### 1. Package creation - -`POST /create` - -This endpoint will perform the following operations: -1. Check if a `package_id` already exists (and hasn't been expired) with the exact same arguments, if so, return HTTP 200 with the `package_id`. -2. Generate a [random](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)) `package_id`. -3. Dispatch the creation of a package with ID set to `package_id` and the given request parameters. -4. Return HTTP 200 with the `package_id`. - -This endpoint, which is the entrypoint, should be rate-limited by IP. - -##### Request Fields - -| Name | Type | In | Description | -| ----------------- | ------- | ---- | -------------------------------------------------------------------------------------- | -| type | string | body | **Required.** One of the following values "pkg", "msi", "deb", "rpm" | -| fleet_url | string | body | **Required.** The URL that the hosts will use to connect to the Fleet server | -| enroll_secret | string | body | **Required.** Global or team enroll secret to use when enrolling the host to Fleet | -| retry | boolean | body | Retry a failed package generation (default: false) | -| fleet-certificate | string | body | Server certificate chain | -| insecure | boolean | body | Disable TLS certificate verification (default: false) | -| osqueryd-channel | string | body | Update channel of osqueryd to use (default: "stable") | -| desktop-channel | string | body | Update channel of desktop to use (default: "stable") | -| orbit-channel | string | body | Update channel of Orbit to use (default: "stable") | -| disable-updates | boolean | body | Disable auto updates on the generated package (default: false) | -| debug | boolean | body | Enable debug logging in orbit (default: false) | -| fleet-desktop | boolean | body | Include the Fleet Desktop Application in the package (default: false) | -| update-interval | string | body | Interval that Orbit will use to check for new updates (10s, 1h, etc.) (default: 15m0s) | -| osquery-flagfile | string | body | Flagfile to package and provide to osquery (default: empty) | -| service | boolean | body | Install with a persistence service (launchd, systemd, etc.) (default: true) | - -##### Error in Package Generation - -When the set of arguments correspond to a `package_id` that failed to generate, then: -- If `retry` is `false` (default) it will return such `package_id`. -- If `retry` is set to `true`, the service will dispatch a new package build and return a new `package_id`. - -##### Response Fields - -| Name | Type | In | Description | -| ----------------- | ------- | ---- | -------------------------------- | -| package_id | string | body | ID of the package being created. | - -#### 2. Package Status Check - -`GET /status` - -This endpoint allows checking the status of a package being created. -Clients can poll for the status of a package using this API. - -This endpoint should be rate-limited by `package_id`. - -##### Request Fields - -| Name | Type | In | Description | -| ----------------- | ------- | ----- | ------------------------------------------------------------------------ | -| package_id | string | query | **Required.** ID of the package created with `POST /create` | - -##### Response Fields - -| Name | Type | In | Description | -| ----------------- | ------- | ---- | ---------------------------------------------------------------------------- | -| status | string | body | One of the following values "success", "fail", "in-progress", "expired" | -| download_url | string | body | Set to the download URL if status field is "success" | -| stage | string | body | Set to a "stage" string if status field is "in-progress" (e.g. "notarizing") | -| logs | string | body | Contains logs if status is "failed" | - -#### 3. Package Download - -`GET /download/{package_id}` - -This is the API to download the generated package. - -This endpoint should be rate-limited by `package_id`. - -## Sequence Diagram - -Following is the sequence diagram for the happy-path. - -```mermaid -sequenceDiagram - User-Agent/Browser->>Fleet: GET /api/v1/fleet/config - Fleet-->>User-Agent/Browser: packager_url - User-Agent/Browser->>Packager: POST /create - Packager-->>User-Agent/Browser: package_id - Packager-->>TUF Server: Fetch targets - loop - User-Agent/Browser->>Packager: GET /status - Packager-->>User-Agent/Browser: status == "in-progress" - Note over User-Agent/Browser: Retry every ~10 seconds,
until status is "success" - TUF Server->>Packager: Targets - Note over Packager: Build package - Note over Packager: Sign package - rect rgb(128, 128, 128) - Note right of Packager: (When generating macOS pkgs) - Packager-->>Apple's Notary Server: New SubmissionRequest (upload) - Apple's Notary Server->>Packager: NewSubmissionResponse - Note over Packager: Upload to S3
(see Apple docs) - loop Every ~30 seconds, until status is "Accepted" - Packager->>Apple's Notary Server: Get submission status - Apple's Notary Server-->>Packager: status - end - end - end - User-Agent/Browser->>Packager: GET /download/{package_id} - Packager-->>User-Agent/Browser: Package File -``` - -## Package Generation Time - -Some back of the envelope calculations for the time the user clicks Download to the time the installer is downloaded fully. - -### macOS - -Test running `time fleetctl package --type=pkg --fleet-url=... --enroll-secret=... --fleet-desktop` (from Argentina). - -1. Download packages from TUF and generate raw `.pkg` (16 seconds). -2. Signing (assuming this is negligible). -3. Notarization (~3 minutes, from Zach's tests mentioned below). -4. Download from Packager service (~15 seconds to download a 100 MB file). - -Total: ~4 minutes - -### Windows - -Test running `time fleetctl package --type=msi --fleet-url=... --enroll-secret=... --fleet-desktop` (from Argentina). - -1. Download packages from TUF and generate raw `.msi` (~18 seconds). -2. Download from Packager service (~15 seconds to download a 100 MB file). - -Total: ~30 seconds. - -### Linux - -Test running `time fleetctl package --type={deb|rpm} --fleet-url=... --enroll-secret=... --fleet-desktop` (from Argentina). - -1. Download packages from TUF and generate raw `.{deb|rpm}` (~30 seconds). -2. Download from Packager service (~30 seconds to download a 200 MB file). - -Total: ~1 minute. - -### Possible Download Time Optimization - -- The Packager service could cache the TUF targets for a couple of minutes (given that they usually change ~ every three weeks). -This would reduce 15s-30s the time it takes to generate the packages. - -## Platform Specifics - -### macOS - -There are two operations **required** to have a proper macOS installer package (`.pkg`): - -1. Code signing: Used by Gatekeeper to verify the author of the package (identified by Developer ID) -2. Notarization: "Gives users more confidence that the Developer ID-signed software you distribute has been checked by Apple for malicious components." - -The packages are composed by three TUF targets: osquery, Orbit and Fleet Desktop. - -All the TUF targets served by FleetDM's TUF server are signed and notarized: - -- osquery: signed and notarized .app (by Osquery) -- Orbit: signed and notarized executable (by Fleet DM) -- fleet-desktop: signed and notarized .app (by Fleet DM) - -Even if all targets are signed and notarized we must still sign and notarize the `.pkg` installer as a whole, see [#122045](https://developer.apple.com/forums/thread/122045). - -#### Notarization - -The Notarization process can be summarized to the following steps: - -1. Submit/Upload the **signed** package to the Notary Server. -2. Notary server performs automated security checks. -3. Notary generates a ticket, publishes ticket online (so that Gatekeeper can find it) and returns the ticket. -4. Ticket can be stapled to your software to let Gatekeeper know it has been notarized. - -##### Notarization Limitations - -- Notarization completes for most software within 5 minutes, and for 98 percent of software within 15 minutes. Thus our workflow must support users leaving the "Add hosts" page and returning later. -- To help avoid long response times they suggest: to "limit notarizations to 75 per day." So it doesn't look like a hard limit, just something that would help reduce upload response times. -Source: https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow -Though Zach has ran the following test with no issues: -> I was able to Notarize ~200 packages in under 12 hours yesterday with no apparent limiting. -> Note this was using the older Notarization API (slower, about 3 mins per notarization). - -#### Future Optimizations for Notarization - -This is the current structure of a `.pkg` generated with `fleetctl package` (unsigned and unnotarized): -```sh -pkg_expanded -├── Distribution -└── base - ├── Bom - ├── Library - │   └── LaunchDaemons - │   └── com.fleetdm.orbit.plist - ├── PackageInfo - ├── Payload - ├── Scripts - │   └── postinstall - └── opt - └── orbit - ├── bin - │   ├── desktop - │   │   └── macos - │   │   └── stable - │   │   ├── Fleet Desktop.app - │   │   │   └── Contents - │   │   │   ├── CodeResources - │   │   │   ├── Info.plist - │   │   │   ├── MacOS - │   │   │   │   └── fleet-desktop - │   │   │   └── _CodeSignature - │   │   │   └── CodeResources - │   │   └── desktop.app.tar.gz - │   ├── orbit - │   │   └── macos - │   │   └── stable - │   │   └── orbit - │   └── osqueryd - │   └── macos-app - │   └── stable - │   ├── osquery.app - │   │   └── Contents - │   │   ├── Info.plist - │   │   ├── MacOS - │   │   │   └── osqueryd - │   │   ├── PkgInfo - │   │   ├── Resources - │   │   │   └── osqueryctl - │   │   ├── _CodeSignature - │   │   │   └── CodeResources - │   │   └── embedded.provisionprofile - │   └── osqueryd.app.tar.gz - ├── certs.pem - ├── osquery.flags - ├── secret.txt - ├── staging - └── tuf-metadata.json -``` - -##### Remove Unnecessary Files - -The `osqueryd.app.tar.gz` and `desktop.app.tar.gz` files are used by Orbit to compare with remote targets when checking for updates. -A future optimization could replace those `.app.tar.gz` files with a txt file with the hash of such file -(would reduce ~30MB of uncompressed size reduction for the `.pkg`). - -##### Notarized Package without Configs - -The only files that really change when users generate a pkg are: -- Library/LaunchDaemons/com.fleetdm.orbit.plist (contains the configuration, like URLs, update channels, etc.) -- opt/orbit/secret.txt (enroll secret) - -Another idea to consider could be packaging and notarizing code and scripts, but leave specific configs out of the `.pkg`. -Problem: We would need to solve how to apply the additional configuration (the two files above) to installed packages. - -From [#122512](https://developer.apple.com/forums/thread/122512): - -> I thought there might be some apple approved way of adding extra information to a package. - ->> No. The notarisation ticket covers the code signature of the package, and the code signature of the package covers the ->> effective contents of that package. This is pretty much required. For example, one of your goals is to tweak the install scripts, ->> but such scripts execute with enormous privileges and thus must be covered by the code signature, and thus covered by the notarisation ticket. - ---- - -> Is there a Apple recommended method to provide custom packages for different customers? -> Some way to add additional parameters to a package without breaking the notarizartion/signing of it? - ->> Option 2) Change your distribution strategy to distribute a static executable. ->> Download any customer-specific resources and store them in /Library/Application Support. - -### Windows - -Currently we don't support signing of the MSI installers in `fleetctl package`. But this functionality could be added both to the command and the service. -From Zach: -> When we decide to support this (which we should do soon), we can use https://github.com/mtrojnar/osslsigncode. - -We should also research https://github.com/sassoftware/relic (it mentions Windows signature support) - -## Service Implementation - -It looks like we will be able to implement the first iteration of the Packager Service as a Go service running on a Linux server. -The only limitation will be macOS stapling (see below). - -### Scale - -MVP of the service will support running one instance of the service. Probably ok as the majority of the load will be IO, network and disk. - -Nothing prevent us from horizontal scaling by sharding requests by `package_id` (or a `client_id`) in the future. - -### State storing - -For the first iteration, we should pick one of the following simple options: -1. Store state in-memory. Simplest, but not resilient to crashes/restarts. -2. Store state in an embedded disk database, such as: - - https://github.com/dgraph-io/badger (Pure Go) - - https://github.com/etcd-io/bbolt (Pure Go) - - Sqlite3 (Cgo 🙈), see https://www.youtube.com/watch?v=XcAYkriuQ1o - -- We already need disk storage for storing the packages, so option (2) is feasible to do from the get-go. - -### macOS requirements - -- Code Signing: Looks like https://github.com/sassoftware/relic would allow us to sign a `.pkg` in pure Go. -- Notarization: We can implement a Go package that uses the new [Notary API](https://developer.apple.com/documentation/notaryapi). -Limitation: Such API does not offer a way to "staple" the package, thus we would depend on the `stapler` tool for this last step (such tool is only available on macOS). -So if we run the Packager service on a Linux server, we won't be able to support stapling. However, it seems stapling is recommended but not a must, see [#116812](https://developer.apple.com/forums/thread/116812). -> If Gatekeeper can’t find a notarisation ticket stapled to the item, it attempts to get that ticket from the Apple notarisation servers. -> Assuming the Mac is online, this typically works and thus the Gatekeeper check succeeds. - -### Windows requirements - -We need Docker to generate the MSI for Windows (the `fleetctl package` command uses the `fleetdm/wix:latest` docker image to generate them). -We will need to pre-fetch such Docker Image during initialization (to not delay the first request indefinitely). - -PS: We could alternatively try a PoC that uses Wine+WiX on Linux without Docker? - -### Lambda? - -Depending on dependencies we could implement the service as a Lambda Function. Though we could make this a full service to not be tied to a specific provider. - -## Security / Threat model - -What could go wrong? - -### Fleet Credentials - -The MVP of the service will have access to: -- Fleet's Developer Signing Key (for `.pkg` signing). -- Fleet's Apple Connect Username and Password (for `.pkg` notarization). - -From Guillaume: -> If someone manages to compromise this service, they could potentially sign AND notarize malware under Fleet's identity. - -Remediation: Fleet credentials should be stored on a secrets manager, e.g. [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). - -### User Credentials - -The Fleet-URL and enroll secret are stored within the generated packages. -If the unexpired installers are leaked, users' Fleet URLs and enroll secrets would be compromised. -Attackers could enroll their devices to users' Fleet deployment. - -An attacker with access to an enroll secret can perform the following attacks: -- Feed a Fleet server with fake data (possibly DoS the service). -- Leak the queries configured in Fleet. - -Remediation: All generated packages should be securely deleted when they expire. - -## Sandbox/Demo & Cloud - -The design supports the following deployments: - -- Fleet On-Prem running with FleetDM's Packager and TUF. -- Fleet On-Prem running with On-Prem Packager with FleetDM's TUF server. -- Fleet On-Prem running with On-Prem Packager with On-Prem TUF server. -- Fleet Sandbox/Demo & Cloud (basically all hosted by FleetDM). - -## New Fleetctl Command - -We could add new flags (e.g. `--remote`) to `fleetctl package` to generate the packages using a Packager Service instead of building locally. \ No newline at end of file diff --git a/server/service/installer_test.go b/server/service/installer_test.go deleted file mode 100644 index c66ae79ded2c..000000000000 --- a/server/service/installer_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package service - -import ( - "context" - "io" - "strings" - "testing" - - "github.com/fleetdm/fleet/v4/server/authz" - "github.com/fleetdm/fleet/v4/server/config" - "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/mock" - "github.com/fleetdm/fleet/v4/server/test" - "github.com/stretchr/testify/require" -) - -func setup(t *testing.T) (context.Context, *mock.Store, *mock.InstallerStore, fleet.Service) { - ds := new(mock.Store) - is := new(mock.InstallerStore) - cfg := config.TestConfig() - cfg.Server.SandboxEnabled = true - svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{Is: is, FleetConfig: &cfg}) - ctx = test.UserContext(ctx, test.UserAdmin) - ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) { - return &fleet.EnrollSecret{Secret: "xyz"}, nil - - } - return ctx, ds, is, svc -} - -func TestGetInstaller(t *testing.T) { - t.Run("unauthorized access is not allowed", func(t *testing.T) { - _, _, _, svc := setup(t) - _, _, err := svc.GetInstaller(context.Background(), fleet.Installer{}) - require.Error(t, err) - require.Contains(t, err.Error(), authz.ForbiddenErrorMessage) - }) - - t.Run("errors if store is not configured", func(t *testing.T) { - ctx, ds, _, _ := setup(t) - cfg := config.TestConfig() - cfg.Server.SandboxEnabled = true - svc, _ := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{Is: nil, FleetConfig: &cfg}) - _, _, err := svc.GetInstaller(ctx, fleet.Installer{}) - require.Error(t, err) - require.ErrorContains(t, err, "installer storage has not been configured") - }) - - t.Run("errors if the provided enroll secret cannot be found", func(t *testing.T) { - ctx, ds, _, svc := setup(t) - ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) { - return nil, newNotFoundError() - } - _, _, err := svc.GetInstaller(ctx, fleet.Installer{}) - require.Error(t, err) - var nfe *notFoundError - require.ErrorAs(t, err, &nfe) - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - }) - - t.Run("errors if there's a problem verifying the enroll secret", func(t *testing.T) { - ctx, ds, _, svc := setup(t) - ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) { - return nil, ctxerr.New(ctx, "test error") - } - _, _, err := svc.GetInstaller(ctx, fleet.Installer{}) - require.Error(t, err) - require.ErrorContains(t, err, "test error") - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - }) - - t.Run("errors if there's a problem checking the blob storage", func(t *testing.T) { - ctx, ds, is, svc := setup(t) - is.GetFunc = func(ctx context.Context, installer fleet.Installer) (io.ReadCloser, int64, error) { - return nil, int64(0), ctxerr.New(ctx, "test error") - } - _, _, err := svc.GetInstaller(ctx, fleet.Installer{}) - require.Error(t, err) - require.ErrorContains(t, err, "test error") - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - require.True(t, is.GetFuncInvoked) - }) - - t.Run("returns binary data with the installer", func(t *testing.T) { - ctx, ds, is, svc := setup(t) - is.GetFunc = func(ctx context.Context, installer fleet.Installer) (io.ReadCloser, int64, error) { - str := "test" - length := int64(len(str)) - reader := io.NopCloser(strings.NewReader(str)) - return reader, length, nil - } - blob, length, err := svc.GetInstaller(ctx, fleet.Installer{}) - require.NoError(t, err) - body, err := io.ReadAll(blob) - require.Equal(t, "test", string(body)) - require.EqualValues(t, length, len(body)) - require.NoError(t, err) - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - require.True(t, is.GetFuncInvoked) - }) -} -func TestCheckInstallerExistence(t *testing.T) { - t.Run("unauthorized access is not allowed", func(t *testing.T) { - _, _, _, svc := setup(t) - err := svc.CheckInstallerExistence(context.Background(), fleet.Installer{}) - require.Error(t, err) - require.Contains(t, err.Error(), authz.ForbiddenErrorMessage) - }) - - t.Run("errors if store is not configured", func(t *testing.T) { - ctx, ds, _, _ := setup(t) - cfg := config.TestConfig() - cfg.Server.SandboxEnabled = true - svc, _ := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{Is: nil, FleetConfig: &cfg}) - err := svc.CheckInstallerExistence(ctx, fleet.Installer{}) - require.Error(t, err) - require.ErrorContains(t, err, "installer storage has not been configured") - }) - - t.Run("errors if the provided enroll secret cannot be found", func(t *testing.T) { - ctx, ds, _, svc := setup(t) - ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) { - return nil, newNotFoundError() - } - err := svc.CheckInstallerExistence(ctx, fleet.Installer{}) - require.Error(t, err) - var nfe *notFoundError - require.ErrorAs(t, err, &nfe) - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - }) - - t.Run("errors if there's a problem verifying the enroll secret", func(t *testing.T) { - ctx, ds, _, svc := setup(t) - ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) { - return nil, ctxerr.New(ctx, "test error") - } - err := svc.CheckInstallerExistence(ctx, fleet.Installer{}) - require.Error(t, err) - require.ErrorContains(t, err, "test error") - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - }) - - t.Run("errors if there's a problem checking the blob storage", func(t *testing.T) { - ctx, ds, is, svc := setup(t) - is.ExistsFunc = func(ctx context.Context, installer fleet.Installer) (bool, error) { - return false, ctxerr.New(ctx, "test error") - } - err := svc.CheckInstallerExistence(ctx, fleet.Installer{}) - require.Error(t, err) - require.ErrorContains(t, err, "test error") - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - require.True(t, is.ExistsFuncInvoked) - }) - - t.Run("errors with not found if the installer is not in the storage", func(t *testing.T) { - ctx, ds, is, svc := setup(t) - is.ExistsFunc = func(ctx context.Context, installer fleet.Installer) (bool, error) { - return false, nil - } - err := svc.CheckInstallerExistence(ctx, fleet.Installer{}) - require.Error(t, err) - var nfe *notFoundError - require.ErrorAs(t, err, &nfe) - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - require.True(t, is.ExistsFuncInvoked) - }) - - t.Run("returns no errors if the installer exists", func(t *testing.T) { - ctx, ds, is, svc := setup(t) - is.ExistsFunc = func(ctx context.Context, installer fleet.Installer) (bool, error) { - return true, nil - } - err := svc.CheckInstallerExistence(ctx, fleet.Installer{}) - require.NoError(t, err) - require.True(t, ds.VerifyEnrollSecretFuncInvoked) - require.True(t, is.ExistsFuncInvoked) - }) -} diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 8b1f84820506..8332e65eaf94 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8310,24 +8310,6 @@ func (s *integrationTestSuite) TestSSODisabled() { require.Contains(t, string(body), "/login?status=org_disabled") // html contains a script that redirects to this path } -func (s *integrationTestSuite) TestSandboxEndpoints() { - t := s.T() - validEmail := testUsers["user1"].Email - validPwd := testUsers["user1"].PlaintextPassword - hdrs := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} - - // demo login endpoint always fails - formBody := make(url.Values) - formBody.Set("email", validEmail) - formBody.Set("password", validPwd) - res := s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusInternalServerError, hdrs) - require.NotEqual(t, http.StatusOK, res.StatusCode) - - // installers endpoint is not enabled - url, installersBody := installerPOSTReq(enrollSecret, "pkg", s.token, false) - s.DoRaw("POST", url, installersBody, http.StatusInternalServerError) -} - func (s *integrationTestSuite) TestGetHostBatteries() { t := s.T() diff --git a/server/service/integration_sandbox_test.go b/server/service/integration_sandbox_test.go deleted file mode 100644 index 9016da8880db..000000000000 --- a/server/service/integration_sandbox_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package service - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "os" - "testing" - - "github.com/fleetdm/fleet/v4/server/config" - "github.com/fleetdm/fleet/v4/server/datastore/s3" - "github.com/fleetdm/fleet/v4/server/fleet" - kitlog "github.com/go-kit/log" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -const enrollSecret = "xyz/abc$@" - -type integrationSandboxTestSuite struct { - suite.Suite - withServer - installers []fleet.Installer -} - -func (s *integrationSandboxTestSuite) SetupSuite() { - s.withDS.SetupSuite("integrationSandboxTestSuite") - t := s.T() - - // make sure sandbox is enabled - cfg := config.TestConfig() - cfg.Server.SandboxEnabled = true - - is := s3.SetupTestInstallerStore(t, "integration-tests", "") - opts := &TestServerOpts{FleetConfig: &cfg, Is: is} - if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" { - opts.Logger = kitlog.NewNopLogger() - } - users, server := RunServerForTestsWithDS(t, s.ds, opts) - s.server = server - s.users = users - s.token = s.getTestAdminToken() - s.installers = s3.SeedTestInstallerStore(t, is, enrollSecret) - - err := s.ds.ApplyEnrollSecrets(context.TODO(), nil, []*fleet.EnrollSecret{{Secret: enrollSecret}}) - require.NoError(t, err) -} - -func TestIntegrationsSandbox(t *testing.T) { - testingSuite := new(integrationSandboxTestSuite) - testingSuite.s = &testingSuite.Suite - suite.Run(t, testingSuite) -} - -func (s *integrationSandboxTestSuite) TestDemoLogin() { - t := s.T() - - validEmail := testUsers["user1"].Email - validPwd := testUsers["user1"].PlaintextPassword - wrongPwd := "nope" - hdrs := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} - - formBody := make(url.Values) - formBody.Set("email", validEmail) - formBody.Set("password", wrongPwd) - res := s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusUnauthorized, hdrs) - require.Equal(t, http.StatusUnauthorized, res.StatusCode) - - formBody.Set("email", validEmail) - formBody.Set("password", validPwd) - res = s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusOK, hdrs) - resBody, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(resBody), `window.location = "/"`) - require.Regexp(t, `window.localStorage.setItem\('FLEET::auth_token', '[^']+'\)`, string(resBody)) -} - -func (s *integrationSandboxTestSuite) TestInstallerGet() { - t := s.T() - - validURL, formBody := installerPOSTReq(enrollSecret, "pkg", s.token, false) - - r := s.DoRaw("POST", validURL, formBody, http.StatusOK) - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - require.Equal(t, "mock", string(body)) - require.Equal(t, "application/octet-stream", r.Header.Get("Content-Type")) - require.Equal(t, "4", r.Header.Get("Content-Length")) - require.Equal(t, `attachment;filename="fleet-osquery.pkg"`, r.Header.Get("Content-Disposition")) - require.Equal(t, `nosniff`, r.Header.Get("X-Content-Type-Options")) - - // unauthorized requests - s.DoRawNoAuth("POST", validURL, nil, http.StatusUnauthorized) - s.token = "invalid" - s.Do("POST", validURL, nil, http.StatusUnauthorized) - s.token = s.cachedAdminToken - - // wrong enroll secret - wrongURL, wrongFormBody := installerPOSTReq("wrong-enroll", "pkg", s.token, false) - s.Do("POST", wrongURL, wrongFormBody, http.StatusNotFound) - - // non-existent package - wrongURL, wrongFormBody = installerPOSTReq(enrollSecret, "exe", s.token, false) - s.Do("POST", wrongURL, wrongFormBody, http.StatusNotFound) -} - -func (s *integrationSandboxTestSuite) TestInstallerHeadCheck() { - validURL := installerURL(enrollSecret, "pkg", false) - s.DoRaw("HEAD", validURL, nil, http.StatusOK) - - // unauthorized requests - s.DoRawNoAuth("HEAD", validURL, nil, http.StatusUnauthorized) - s.token = "invalid" - s.DoRaw("HEAD", validURL, nil, http.StatusUnauthorized) - s.token = s.cachedAdminToken - - // wrong enroll secret - invalidURL := installerURL("wrong-enroll", "pkg", false) - s.DoRaw("HEAD", invalidURL, nil, http.StatusNotFound) - - // non-existent package - invalidURL = installerURL(enrollSecret, "exe", false) - s.DoRaw("HEAD", invalidURL, nil, http.StatusNotFound) -} - -func installerURL(secret, kind string, desktop bool) string { - path := fmt.Sprintf("/api/latest/fleet/download_installer/%s?enroll_secret=%s", kind, secret) - if desktop { - path += "&desktop=1" - } - return path -} - -func installerPOSTReq(secret, kind, token string, desktop bool) (string, []byte) { - path := installerURL(secret, kind, desktop) - d := "0" - if desktop { - d = "1" - } - formBody := make(url.Values) - formBody.Set("token", token) - formBody.Set("enroll_secret", secret) - formBody.Set("desktop", d) - return path, []byte(formBody.Encode()) -}