mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
parent
db9258c9d0
commit
effd3563c8
12 changed files with 178 additions and 10 deletions
1
changes/24899-software-installer-scripts-secrets
Normal file
1
changes/24899-software-installer-scripts-secrets
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Add support for fleet secret validation in software installer scripts
|
||||
|
|
@ -36,6 +36,10 @@ func (svc *Service) AddFleetMaintainedApp(
|
|||
return 0, fleet.ErrNoContext
|
||||
}
|
||||
|
||||
if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{installScript, postInstallScript, uninstallScript}); err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("script", err.Error()))
|
||||
}
|
||||
|
||||
app, err := svc.ds.GetMaintainedAppByID(ctx, appID)
|
||||
if err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, err, "getting maintained app by id")
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
// Update $PACKAGE_ID in uninstall script
|
||||
preProcessUninstallScript(payload)
|
||||
|
||||
if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{payload.InstallScript, payload.PostInstallScript, payload.UninstallScript}); err != nil {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("script", err.Error()))
|
||||
}
|
||||
|
||||
installerID, titleID, err := svc.ds.MatchOrCreateSoftwareInstaller(ctx, payload)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "matching or creating software installer")
|
||||
|
|
@ -144,6 +148,22 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
teamName = &t.Name
|
||||
}
|
||||
|
||||
var scripts []string
|
||||
|
||||
if payload.InstallScript != nil {
|
||||
scripts = append(scripts, *payload.InstallScript)
|
||||
}
|
||||
if payload.PostInstallScript != nil {
|
||||
scripts = append(scripts, *payload.PostInstallScript)
|
||||
}
|
||||
if payload.UninstallScript != nil {
|
||||
scripts = append(scripts, *payload.UninstallScript)
|
||||
}
|
||||
|
||||
if err := svc.ds.ValidateEmbeddedSecrets(ctx, scripts); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("script", err.Error()))
|
||||
}
|
||||
|
||||
// get software by ID, fail if it does not exist or does not have an existing installer
|
||||
software, err := svc.ds.SoftwareTitleByID(ctx, payload.TitleID, payload.TeamID, fleet.TeamFilter{
|
||||
User: vc.User,
|
||||
|
|
@ -1149,6 +1169,8 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
|||
return "", ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
}
|
||||
|
||||
var allScripts []string
|
||||
|
||||
// Verify payloads first, to prevent starting the download+upload process if the data is invalid.
|
||||
for _, payload := range payloads {
|
||||
if len(payload.URL) > fleet.SoftwareInstallerURLMaxLength {
|
||||
|
|
@ -1163,6 +1185,11 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
|||
fmt.Sprintf("Couldn't edit software. URL (%q) is invalid", payload.URL),
|
||||
)
|
||||
}
|
||||
allScripts = append(allScripts, payload.InstallScript, payload.PostInstallScript, payload.UninstallScript)
|
||||
}
|
||||
|
||||
if err := svc.ds.ValidateEmbeddedSecrets(ctx, allScripts); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("script", err.Error()))
|
||||
}
|
||||
|
||||
// keyExpireTime is the current maximum time supported for retrieving
|
||||
|
|
|
|||
|
|
@ -853,6 +853,24 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
|
|||
} else {
|
||||
softwarePackageSpec = resolveSoftwarePackagePaths(baseDir, item.SoftwarePackageSpec)
|
||||
}
|
||||
if softwarePackageSpec.InstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, softwarePackageSpec.InstallScript.Path); err != nil {
|
||||
multiError = multierror.Append(multiError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if softwarePackageSpec.PostInstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, softwarePackageSpec.PostInstallScript.Path); err != nil {
|
||||
multiError = multierror.Append(multiError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if softwarePackageSpec.UninstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, softwarePackageSpec.UninstallScript.Path); err != nil {
|
||||
multiError = multierror.Append(multiError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if softwarePackageSpec.URL == "" {
|
||||
multiError = multierror.Append(multiError, errors.New("software URL is required"))
|
||||
continue
|
||||
|
|
@ -867,6 +885,20 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
|
|||
return multiError
|
||||
}
|
||||
|
||||
func gatherFileSecrets(result *GitOps, filePath string) error {
|
||||
fileBytes, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
err = LookupEnvSecrets(string(fileBytes), result.FleetSecrets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup environment secrets for %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveSoftwarePackagePaths(baseDir string, softwareSpec fleet.SoftwarePackageSpec) fleet.SoftwarePackageSpec {
|
||||
if softwareSpec.PreInstallQuery.Path != "" {
|
||||
softwareSpec.PreInstallQuery.Path = resolveApplyRelativePath(baseDir, softwareSpec.PreInstallQuery.Path)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
},
|
||||
filePath: "testdata/global_config_no_paths.yml",
|
||||
},
|
||||
|
|
@ -94,6 +95,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
},
|
||||
filePath: "testdata/global_config.yml",
|
||||
},
|
||||
|
|
@ -102,6 +104,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
},
|
||||
filePath: "testdata/team_config_no_paths.yml",
|
||||
isTeam: true,
|
||||
|
|
@ -115,6 +118,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
},
|
||||
filePath: "testdata/team_config.yml",
|
||||
isTeam: true,
|
||||
|
|
@ -172,7 +176,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
require.Len(t, gitops.Software.Packages, 2)
|
||||
for _, pkg := range gitops.Software.Packages {
|
||||
if strings.Contains(pkg.URL, "MicrosoftTeams") {
|
||||
assert.Equal(t, "uninstall.sh", pkg.UninstallScript.Path)
|
||||
assert.Equal(t, "testdata/lib/uninstall.sh", pkg.UninstallScript.Path)
|
||||
} else {
|
||||
assert.Empty(t, pkg.UninstallScript.Path)
|
||||
}
|
||||
|
|
@ -236,10 +240,11 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
assert.True(t, ok, "windows_migration_enabled not found")
|
||||
_, ok = gitops.Controls.WindowsUpdates.(map[string]interface{})
|
||||
assert.True(t, ok, "windows_updates not found")
|
||||
require.Len(t, gitops.FleetSecrets, 3)
|
||||
require.Len(t, gitops.FleetSecrets, 4)
|
||||
assert.Equal(t, "fleet_secret", gitops.FleetSecrets["FLEET_SECRET_FLEET_SECRET_"])
|
||||
assert.Equal(t, "secret_name", gitops.FleetSecrets["FLEET_SECRET_NAME"])
|
||||
assert.Equal(t, "10", gitops.FleetSecrets["FLEET_SECRET_length"])
|
||||
assert.Equal(t, "bread", gitops.FleetSecrets["FLEET_SECRET_BANANA"])
|
||||
|
||||
// Check agent options
|
||||
assert.NotNil(t, gitops.AgentOptions)
|
||||
|
|
|
|||
1
pkg/spec/testdata/lib/collect-fleetd-logs.sh
vendored
1
pkg/spec/testdata/lib/collect-fleetd-logs.sh
vendored
|
|
@ -1,3 +1,4 @@
|
|||
# collect fleetd logs
|
||||
echo a${FLEET_SECRET_FLEET_SECRET_}a
|
||||
echo $NOT_FLEET_SECRET_X
|
||||
echo $FLEET_SECRET_BANANA
|
||||
|
|
|
|||
1
pkg/spec/testdata/lib/uninstall.sh
vendored
Normal file
1
pkg/spec/testdata/lib/uninstall.sh
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
echo $FLEET_SECRET_BANANA
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
url: https://statics.teams.cdn.office.net/production-osx/enterprise/webview2/lkg/MicrosoftTeams.pkg
|
||||
self_service: false
|
||||
uninstall_script:
|
||||
path: ../uninstall.sh
|
||||
path: ./lib/uninstall.sh
|
||||
|
|
|
|||
|
|
@ -70,6 +70,24 @@ func (ds *Datastore) GetSoftwareInstallDetails(ctx context.Context, executionId
|
|||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get software install details")
|
||||
}
|
||||
|
||||
expandedInstallScript, err := ds.ExpandEmbeddedSecrets(ctx, result.InstallScript)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "expanding secrets in install script")
|
||||
}
|
||||
expandedPostInstallScript, err := ds.ExpandEmbeddedSecrets(ctx, result.PostInstallScript)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "expanding secrets in post-install script")
|
||||
}
|
||||
expandedUninstallScript, err := ds.ExpandEmbeddedSecrets(ctx, result.UninstallScript)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "expanding secrets in uninstall script")
|
||||
}
|
||||
|
||||
result.InstallScript = expandedInstallScript
|
||||
result.PostInstallScript = expandedPostInstallScript
|
||||
result.UninstallScript = expandedUninstallScript
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,13 +54,29 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
err := ds.UpsertSecretVariables(ctx, []fleet.SecretVariable{
|
||||
{
|
||||
Name: "RUBBER",
|
||||
Value: "DUCKY",
|
||||
},
|
||||
{
|
||||
Name: "BIG",
|
||||
Value: "BIRD",
|
||||
},
|
||||
{
|
||||
Name: "COOKIE",
|
||||
Value: "MONSTER",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
|
||||
require.NoError(t, err)
|
||||
installerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "hello",
|
||||
InstallScript: "hello $FLEET_SECRET_RUBBER",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
PostInstallScript: "world",
|
||||
UninstallScript: "goodbye",
|
||||
PostInstallScript: "world $FLEET_SECRET_BIG",
|
||||
UninstallScript: "goodbye $FLEET_SECRET_COOKIE",
|
||||
InstallerFile: tfr1,
|
||||
StorageID: "storage1",
|
||||
Filename: "file1",
|
||||
|
|
@ -151,12 +167,12 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
|
||||
require.Equal(t, host1.ID, exec1.HostID)
|
||||
require.Equal(t, hostInstall1, exec1.ExecutionID)
|
||||
require.Equal(t, "hello", exec1.InstallScript)
|
||||
require.Equal(t, "world", exec1.PostInstallScript)
|
||||
require.Equal(t, "hello DUCKY", exec1.InstallScript)
|
||||
require.Equal(t, "world BIRD", exec1.PostInstallScript)
|
||||
require.Equal(t, installerID1, exec1.InstallerID)
|
||||
require.Equal(t, "SELECT 1", exec1.PreInstallCondition)
|
||||
require.False(t, exec1.SelfService)
|
||||
assert.Equal(t, "goodbye", exec1.UninstallScript)
|
||||
assert.Equal(t, "goodbye MONSTER", exec1.UninstallScript)
|
||||
|
||||
hostInstall6, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID3, true, nil)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ func (e MissingSecretsError) Error() string {
|
|||
if len(secretVars) > 1 {
|
||||
plural = "s"
|
||||
}
|
||||
return fmt.Sprintf("Couldn't add. Variable%s %s missing", plural, strings.Join(secretVars, ", "))
|
||||
return fmt.Sprintf("Couldn't add. Secret variable%s %s missing from database", plural, strings.Join(secretVars, ", "))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8923,6 +8923,31 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
|
|||
}
|
||||
s.uploadSoftwareInstaller(t, payloadRubyTm1, http.StatusOK, "")
|
||||
|
||||
payloadEmacsMissingSecret := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install $FLEET_SECRET_INVALID",
|
||||
Filename: "emacs.deb",
|
||||
PostInstallScript: "d",
|
||||
SelfService: true,
|
||||
}
|
||||
s.uploadSoftwareInstaller(t, payloadEmacsMissingSecret, http.StatusUnprocessableEntity, "$FLEET_SECRET_INVALID")
|
||||
|
||||
payloadEmacsMissingPostSecret := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install",
|
||||
Filename: "emacs.deb",
|
||||
PostInstallScript: "d $FLEET_SECRET_INVALID",
|
||||
SelfService: true,
|
||||
}
|
||||
s.uploadSoftwareInstaller(t, payloadEmacsMissingPostSecret, http.StatusUnprocessableEntity, "$FLEET_SECRET_INVALID")
|
||||
|
||||
payloadEmacsMissingUnSecret := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install",
|
||||
Filename: "emacs.deb",
|
||||
PostInstallScript: "d",
|
||||
UninstallScript: "delet $FLEET_SECRET_INVALID",
|
||||
SelfService: true,
|
||||
}
|
||||
s.uploadSoftwareInstaller(t, payloadEmacsMissingUnSecret, http.StatusUnprocessableEntity, "$FLEET_SECRET_INVALID")
|
||||
|
||||
payloadEmacs := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install",
|
||||
Filename: "emacs.deb",
|
||||
|
|
@ -11281,6 +11306,28 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
|
||||
softwareToInstallBadSecret := []fleet.SoftwareInstallerPayload{
|
||||
{
|
||||
URL: rubyURL,
|
||||
InstallScript: "echo $FLEET_SECRET_INVALID",
|
||||
},
|
||||
}
|
||||
resp := s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstallBadSecret}, http.StatusUnprocessableEntity, "team_name", tm.Name)
|
||||
errMsg := extractServerErrorText(resp.Body)
|
||||
require.Contains(t, errMsg, "$FLEET_SECRET_INVALID")
|
||||
|
||||
softwareToInstallBadSecret[0].InstallScript = ""
|
||||
softwareToInstallBadSecret[0].PostInstallScript = "echo $FLEET_SECRET_ALSO_INVALID"
|
||||
resp = s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstallBadSecret}, http.StatusUnprocessableEntity, "team_name", tm.Name)
|
||||
errMsg = extractServerErrorText(resp.Body)
|
||||
require.Contains(t, errMsg, "$FLEET_SECRET_ALSO_INVALID")
|
||||
|
||||
softwareToInstallBadSecret[0].PostInstallScript = ""
|
||||
softwareToInstallBadSecret[0].UninstallScript = "echo $FLEET_SECRET_THIRD_INVALID"
|
||||
resp = s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstallBadSecret}, http.StatusUnprocessableEntity, "team_name", tm.Name)
|
||||
errMsg = extractServerErrorText(resp.Body)
|
||||
require.Contains(t, errMsg, "$FLEET_SECRET_THIRD_INVALID")
|
||||
|
||||
// TODO(roberto): test with a variety of response codes
|
||||
|
||||
// check the application status
|
||||
|
|
@ -15411,6 +15458,22 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
|
|||
getMAResp.FleetMaintainedApp.UninstallScript = ""
|
||||
require.Equal(t, actualApp, *getMAResp.FleetMaintainedApp)
|
||||
|
||||
// Try adding ingested app with invalid secret
|
||||
reqInvalidSecret := &addFleetMaintainedAppRequest{
|
||||
AppID: 1,
|
||||
TeamID: &team.ID,
|
||||
SelfService: true,
|
||||
PreInstallQuery: "SELECT 1",
|
||||
InstallScript: "echo foo $FLEET_SECRET_INVALID1",
|
||||
PostInstallScript: "echo done $FLEET_SECRET_INVALID2",
|
||||
UninstallScript: "echo $FLEET_SECRET_INVALID3",
|
||||
}
|
||||
respBadSecret := s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", reqInvalidSecret, http.StatusUnprocessableEntity)
|
||||
errMsg := extractServerErrorText(respBadSecret.Body)
|
||||
require.Contains(t, errMsg, "$FLEET_SECRET_INVALID1")
|
||||
require.Contains(t, errMsg, "$FLEET_SECRET_INVALID2")
|
||||
require.Contains(t, errMsg, "$FLEET_SECRET_INVALID3")
|
||||
|
||||
// Add an ingested app to the team
|
||||
var addMAResp addFleetMaintainedAppResponse
|
||||
req := &addFleetMaintainedAppRequest{
|
||||
|
|
|
|||
Loading…
Reference in a new issue