diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go
index c846db10d..8e9f5d63b 100644
--- a/cli/packages/cmd/secrets.go
+++ b/cli/packages/cmd/secrets.go
@@ -143,7 +143,15 @@ var secretsSetCmd = &cobra.Command{
Short: "Used set secrets",
Use: "set [secrets]",
DisableFlagsInUseLine: true,
- Args: cobra.MinimumNArgs(1),
+ Args: func(cmd *cobra.Command, args []string) error {
+ if cmd.Flags().Changed("file") {
+ if len(args) > 0 {
+ return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input")
+ }
+ return nil
+ }
+ return cobra.MinimumNArgs(1)(cmd, args)
+ },
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
@@ -177,13 +185,18 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse secret type")
}
+ file, err := cmd.Flags().GetString("file")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
var secretOperations []models.SecretSetOperation
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
if projectId == "" {
util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag")
}
- secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token)
+ secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file)
} else {
if projectId == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
@@ -206,7 +219,7 @@ var secretsSetCmd = &cobra.Command{
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
Type: "",
Token: loggedInUserDetails.UserCredentials.JTWToken,
- })
+ }, file)
}
if err != nil {
@@ -691,6 +704,7 @@ func init() {
secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth")
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared")
+ secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.")
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go
index 5a2a0ec24..02079d656 100644
--- a/cli/packages/util/secrets.go
+++ b/cli/packages/util/secrets.go
@@ -17,6 +17,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/zalando/go-keyring"
+ "gopkg.in/yaml.v3"
)
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) {
@@ -564,7 +565,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
}
-func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) {
+func parseSecrets(fileName string, content string) (map[string]string, error) {
+ secrets := make(map[string]string)
+
+ if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") {
+ // Handle YAML secrets
+ var yamlData map[string]interface{}
+ if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil {
+ return nil, fmt.Errorf("failed to parse YAML file: %v", err)
+ }
+
+ for key, value := range yamlData {
+ if strValue, ok := value.(string); ok {
+ secrets[key] = strValue
+ } else {
+ return nil, fmt.Errorf("YAML secret '%s' must be a string", key)
+ }
+ }
+ } else {
+ // Handle .env files
+ lines := strings.Split(content, "\n")
+
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+
+ // Ignore empty lines and comments
+ if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
+ continue
+ }
+
+ // Ensure it's a valid key=value pair
+ splitKeyValue := strings.SplitN(line, "=", 2)
+ if len(splitKeyValue) != 2 {
+ return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line)
+ }
+
+ key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1])
+
+ // Handle quoted values
+ if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
+ (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
+ value = value[1 : len(value)-1] // Remove surrounding quotes
+ }
+
+ secrets[key] = value
+ }
+ }
+
+ return secrets, nil
+}
+
+func validateSecretKey(key string) error {
+ if key == "" {
+ return errors.New("secret keys cannot be empty")
+ }
+ if unicode.IsNumber(rune(key[0])) {
+ return fmt.Errorf("secret key '%s' cannot start with a number", key)
+ }
+ if strings.Contains(key, " ") {
+ return fmt.Errorf("secret key '%s' cannot contain spaces", key)
+ }
+ return nil
+}
+
+func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) {
+ if file != "" {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ PrintErrorMessageAndExit("File does not exist")
+ }
+ return nil, fmt.Errorf("unable to process file [err=%v]", err)
+ }
+
+ parsedSecrets, err := parseSecrets(file, string(content))
+ if err != nil {
+ PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err))
+ }
+
+ // Step 2: Validate secrets
+ for key, value := range parsedSecrets {
+ if err := validateSecretKey(key); err != nil {
+ PrintErrorMessageAndExit(err.Error())
+ }
+ if strings.TrimSpace(value) == "" {
+ PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key))
+ }
+ secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value))
+ }
+
+ if len(secretArgs) == 0 {
+ PrintErrorMessageAndExit("no valid secrets found in the file")
+ }
+ }
if tokenDetails == nil {
return nil, fmt.Errorf("unable to process set secret operations, token details are missing")
diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx
index 2dff5cf7b..d1f4baa9f 100644
--- a/docs/cli/commands/secrets.mdx
+++ b/docs/cli/commands/secrets.mdx
@@ -219,6 +219,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
```
+
+ Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory.
+
+ The file should contain secrets in the following formats:
+ - `key=value` for `.env` files
+ - `key: value` for YAML files
+
+ Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing.
+
+
+ ```bash
+ # Example
+ infisical secrets set --file="./.env"
+ ```
+