Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ __debug_bin*
/pro_impl/
/go.work.x
/go.work
/go.work.sum
/go.work.sum*.log
config.json
database.boltdb
admin
213 changes: 184 additions & 29 deletions services/tasks/LocalJob.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package tasks

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/semaphoreui/semaphore/pkg/ssh"
"maps"
"os"
"os/exec"
"strings"

"path"
Expand Down Expand Up @@ -35,7 +37,10 @@ type LocalJob struct {
becomeKeyInstallation ssh.AccessKeyInstallation
vaultFileInstallations map[string]ssh.AccessKeyInstallation

KeyInstaller db_lib.AccessKeyInstaller
KeyInstaller db_lib.AccessKeyInstaller
secretVarFile string // temporary file for secret variables (Ansible)
secretVaultPassword string // vault password for encrypting secret files
secretVaultPasswordFile string // temporary file containing vault password
}

func (t *LocalJob) IsKilled() bool {
Expand Down Expand Up @@ -106,25 +111,21 @@ func (t *LocalJob) getEnvironmentExtraVars(username string, incomingVersion *str
return
}

// getEnvironmentExtraVarsJSON returns JSON for public extra vars only, secrets are handled separately
func (t *LocalJob) getEnvironmentExtraVarsJSON(username string, incomingVersion *string) (str string, err error) {
extraVars := make(map[string]any)
extraSecretVars := make(map[string]any)

// Only include public variables from Environment.JSON
if t.Environment.JSON != "" {
err = json.Unmarshal([]byte(t.Environment.JSON), &extraVars)
if err != nil {
return
}
}
if t.Secret != "" {
err = json.Unmarshal([]byte(t.Secret), &extraSecretVars)
if err != nil {
return
}
}
t.Secret = "{}"

maps.Copy(extraVars, extraSecretVars)
// Do not include secrets from t.Secret - they will be handled in separate temp file
// Clear the secret to avoid reprocessing
t.Secret = "{}"
Comment on lines +114 to +128

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Clearing t.Secret drops survey secrets for Ansible runs

Inside getEnvironmentExtraVarsJSON the code now sets t.Secret = "{}" to keep secrets out of public --extra-vars. getPlaybookArgs calls this method before creating the encrypted secret file and before Run injects secrets into the environment, and both later steps read from t.Secret. Because the field is replaced with an empty object here, any survey-provided secrets are silently discarded – they are not written to the vault-encrypted temp file and never show up in the environment variables, so Ansible playbooks lose access to these values. The sanitization should avoid mutating t.Secret or should copy its contents before clearing.

Useful? React with 👍 / 👎.


taskDetails := make(map[string]any)

Expand Down Expand Up @@ -185,6 +186,148 @@ func (t *LocalJob) getEnvironmentENV() (res []string, err error) {
return
}

// getSecretEnvironmentVars returns environment variables for secrets, avoiding command line exposure
func (t *LocalJob) getSecretEnvironmentVars(prefix string) (secretEnvVars []string, err error) {
// Add environment secrets
for _, secret := range t.Environment.Secrets {
if secret.Type == db.EnvironmentSecretVar {
secretEnvVars = append(secretEnvVars, fmt.Sprintf("%s%s=%s", prefix, secret.Name, secret.Secret))
}
}

// Add survey secrets from t.Secret
if t.Secret != "" {
var extraSecretVars map[string]any
err = json.Unmarshal([]byte(t.Secret), &extraSecretVars)
if err != nil {
return
}
for name, value := range extraSecretVars {
if strValue, ok := value.(string); ok {
secretEnvVars = append(secretEnvVars, fmt.Sprintf("%s%s=%s", prefix, name, strValue))
}
}
}

return
}

// generateVaultPassword creates a secure random vault password for encrypting secret files
func (t *LocalJob) generateVaultPassword() (string, error) {
// Generate 32 bytes of random data and encode as hex
bytes := make([]byte, 32)
_, err := rand.Read(bytes)
if err != nil {
return "", fmt.Errorf("failed to generate random vault password: %w", err)
}
return hex.EncodeToString(bytes), nil
}

// createVaultPasswordFile creates a temporary file containing the vault password
func (t *LocalJob) createVaultPasswordFile() (string, error) {
if t.secretVaultPassword == "" {
var err error
t.secretVaultPassword, err = t.generateVaultPassword()
if err != nil {
return "", err
}
}

tmpDir := util.Config.GetProjectTmpDir(t.Template.ProjectID)
vaultPasswordFile := path.Join(tmpDir, fmt.Sprintf("vault_pass_%d", t.Task.ID))

err := os.WriteFile(vaultPasswordFile, []byte(t.secretVaultPassword), 0600)
if err != nil {
return "", fmt.Errorf("failed to create vault password file: %w", err)
}

t.secretVaultPasswordFile = vaultPasswordFile
return vaultPasswordFile, nil
}

// createSecretExtraVarsFile creates a temporary file with secret variables for Ansible, encrypted with ansible-vault
func (t *LocalJob) createSecretExtraVarsFile() (tempFile string, err error) {
secretVars := make(map[string]any)

// Add environment secrets
for _, secret := range t.Environment.Secrets {
if secret.Type == db.EnvironmentSecretVar {
secretVars[secret.Name] = secret.Secret
}
}

// Add survey secrets from t.Secret (before it's cleared)
if t.Secret != "" {
var extraSecretVars map[string]any
err = json.Unmarshal([]byte(t.Secret), &extraSecretVars)
if err != nil {
return
}
for name, value := range extraSecretVars {
secretVars[name] = value
}
}

// If no secrets, don't create file
if len(secretVars) == 0 {
return "", nil
}

// Create temporary file
tmpDir := util.Config.GetProjectTmpDir(t.Template.ProjectID)
tempFile = path.Join(tmpDir, fmt.Sprintf("secret_vars_%d.json", t.Task.ID))

jsonData, err := json.Marshal(secretVars)
if err != nil {
return
}

// Write unencrypted data first
err = os.WriteFile(tempFile, jsonData, 0600)
if err != nil {
return
}

// Create vault password file for encryption
vaultPasswordFile, err := t.createVaultPasswordFile()
if err != nil {
// Clean up the secret file if vault password creation fails
os.Remove(tempFile)
return "", fmt.Errorf("failed to create vault password file: %w", err)
}

// Encrypt the file using ansible-vault
cmd := exec.Command("ansible-vault", "encrypt", "--vault-password-file", vaultPasswordFile, tempFile)
output, err := cmd.CombinedOutput()
if err != nil {
// Clean up files if encryption fails
os.Remove(tempFile)
os.Remove(vaultPasswordFile)
return "", fmt.Errorf("failed to encrypt secret vars file with ansible-vault: %w, output: %s", err, string(output))
}

// Store for cleanup
t.secretVarFile = tempFile

return
}

// cleanupSecretFile removes the temporary secret file and vault password file if they exist
func (t *LocalJob) cleanupSecretFile() {
if t.secretVarFile != "" {
if err := os.Remove(t.secretVarFile); err != nil {
t.Log("Warning: Could not remove secret vars file: " + err.Error())
}
t.secretVarFile = ""
}
if t.secretVaultPasswordFile != "" {
if err := os.Remove(t.secretVaultPasswordFile); err != nil {
t.Log("Warning: Could not remove vault password file: " + err.Error())
}
t.secretVaultPasswordFile = ""
}
}

// nolint: gocyclo
func (t *LocalJob) getShellArgs(username string, incomingVersion *string) (args []string, err error) {
extraVars, err := t.getEnvironmentExtraVars(username, incomingVersion)
Expand All @@ -204,13 +347,6 @@ func (t *LocalJob) getShellArgs(username string, incomingVersion *string) (args
// Script to run
args = append(args, t.Template.Playbook)

// Include Environment Secret Vars
for _, secret := range t.Environment.Secrets {
if secret.Type == db.EnvironmentSecretVar {
args = append(args, fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
}
}

// Include extra args from template
args = append(args, templateArgs...)

Expand Down Expand Up @@ -266,13 +402,6 @@ func (t *LocalJob) getTerraformArgs(username string, incomingVersion *string) (a
args = append(args, templateArgs...)
args = append(args, taskArgs...)

for _, secret := range t.Environment.Secrets {
if secret.Type != db.EnvironmentSecretVar {
continue
}
args = append(args, "-var", fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
}

return
}

Expand Down Expand Up @@ -396,11 +525,16 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar
args = append(args, "--extra-vars", extraVars)
}

for _, secret := range t.Environment.Secrets {
if secret.Type != db.EnvironmentSecretVar {
continue
// Create temporary file for secrets to avoid exposing them in command line
secretFile, err := t.createSecretExtraVarsFile()
if err != nil {
t.Log("Warning: Could not create secret vars file: " + err.Error())
} else if secretFile != "" {
// Add vault-id for decrypting the secret vars file
if t.secretVaultPasswordFile != "" {
args = append(args, fmt.Sprintf("--vault-id=secrets@%s", t.secretVaultPasswordFile))
}
args = append(args, "--extra-vars", fmt.Sprintf("%s=%s", secret.Name, secret.Secret))
args = append(args, "--extra-vars", "@"+secretFile)
}

templateArgs, taskArgs, err := t.getCLIArgs()
Expand Down Expand Up @@ -533,6 +667,7 @@ func (t *LocalJob) Run(username string, incomingVersion *string, alias string) (
defer func() {
t.destroyKeys()
t.destroyInventoryFile()
t.cleanupSecretFile()
t.App.Clear()
}()

Expand Down Expand Up @@ -584,6 +719,26 @@ func (t *LocalJob) Run(username string, incomingVersion *string, alias string) (
return
}

// Add secret environment variables to avoid exposing them in command line arguments
switch t.Template.App {
case db.AppTerraform, db.AppTofu, db.AppTerragrunt:
// For Terraform, use TF_VAR_ prefix for variables
secretEnvVars, secretErr := t.getSecretEnvironmentVars("TF_VAR_")
if secretErr != nil {
t.Log("Warning: Failed to process secret environment variables: " + secretErr.Error())
} else {
environmentVariables = append(environmentVariables, secretEnvVars...)
}
default:
// For Ansible and Shell, add secrets as regular environment variables
secretEnvVars, secretErr := t.getSecretEnvironmentVars("")
if secretErr != nil {
t.Log("Warning: Failed to process secret environment variables: " + secretErr.Error())
} else {
environmentVariables = append(environmentVariables, secretEnvVars...)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Survey Secrets Lost Before Encryption

Survey secrets are lost because t.Secret is cleared prematurely by getEnvironmentExtraVarsJSON. This occurs before createSecretExtraVarsFile and getSecretEnvironmentVars can read them, preventing survey secrets from being included in encrypted files or environment variables for tasks.

Additional Locations (2)

Fix in Cursor Fix in Web


if t.Inventory.SSHKey.Type == db.AccessKeySSH && t.Inventory.SSHKeyID != nil {
environmentVariables = append(environmentVariables, fmt.Sprintf("SSH_AUTH_SOCK=%s", t.sshKeyInstallation.SSHAgent.SocketFile))
}
Expand Down
Loading
Loading