Skip to content
Merged
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
43 changes: 18 additions & 25 deletions packages/cmd/agent_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ var agentProxyCmd = &cobra.Command{
}

var agentProxyConnectCmd = &cobra.Command{
Use: "connect [flags] -- [agent start command]",
Short: "Set up the environment and launch an agent behind the agent proxy",
Example: "infisical secrets agent-proxy connect --proxy=<proxy-host>:17322 --projectId=<project-id> --env=prod --path=/myapp -- claude",
Use: "connect [flags] -- [agent start command]",
Short: "Set up the environment and launch an agent behind the agent proxy",
Example: `# With flags
infisical secrets agent-proxy connect --proxy=<proxy-host>:17322 --projectId=<project-id> --env=prod --path=/myapp -- claude

# With environment variables (INFISICAL_PROJECT_ID, INFISICAL_ENVIRONMENT, INFISICAL_SECRET_PATH, INFISICAL_AGENT_PROXY_ADDRESS)
infisical secrets agent-proxy connect -- claude`,
DisableFlagsInUseLine: true,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
Expand Down Expand Up @@ -103,28 +107,17 @@ func mergeNoProxy(operatorEntries ...string) string {
}

func runAgentProxyConnect(cmd *cobra.Command, args []string) {
proxyAddr, err := cmd.Flags().GetString("proxy")
if err != nil || proxyAddr == "" {
util.HandleError(fmt.Errorf("the --proxy flag is required (e.g. --proxy=<proxy-host>:17322)"))
proxyAddr := util.ResolveAgentProxyAddress(cmd)
if proxyAddr == "" {
util.HandleError(fmt.Errorf("the agent proxy address is required; pass --proxy or set INFISICAL_AGENT_PROXY_ADDRESS (e.g. <proxy-host>:17322)"))
}

environment, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse --env")
}
if !cmd.Flags().Changed("env") {
if envFromWorkspace := util.GetEnvFromWorkspaceFile(); envFromWorkspace != "" {
environment = envFromWorkspace
}
}
environment := util.ResolveEnvironmentName(cmd)
if environment == "" {
util.HandleError(fmt.Errorf("the --env flag is required"))
util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set defaultEnvironment in .infisical.json"))
}

secretPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse --path")
}
secretPath := util.ResolveSecretPath(cmd)

projectID, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "projectId", []string{util.INFISICAL_PROJECT_ID_NAME}, "")
if err != nil {
Expand Down Expand Up @@ -156,7 +149,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) {

realSecrets := fetchAgentRealSecrets(token, projectID, environment, secretPath)

allowReadableBrokered, _ := cmd.Flags().GetBool("allow-readable-brokered-secrets")
allowReadableBrokered := util.GetBoolFlagOrEnv(cmd, "allow-readable-brokered-secrets", util.INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME)
if !allowReadableBrokered {
// static readability is derived from realSecrets we already fetch; dynamic lease-ability comes from
// the server (callerCanLease) since we don't fetch dynamic secrets here.
Expand Down Expand Up @@ -436,15 +429,15 @@ func runAgentProcess(args, env []string) error {
}

func init() {
agentProxyConnectCmd.Flags().String("proxy", "", "address of the agent proxy (host:port)")
agentProxyConnectCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from")
agentProxyConnectCmd.Flags().String("path", "/", "secret path (folder) scope")
agentProxyConnectCmd.Flags().String("proxy", "", "address of the agent proxy as host:port (falls back to INFISICAL_AGENT_PROXY_ADDRESS)")
agentProxyConnectCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from (falls back to INFISICAL_ENVIRONMENT or .infisical.json)")
agentProxyConnectCmd.Flags().String("path", "/", "secret path (folder) scope (falls back to INFISICAL_SECRET_PATH or defaultSecretPath in .infisical.json)")
agentProxyConnectCmd.Flags().String("projectId", "", "project id (falls back to INFISICAL_PROJECT_ID or .infisical.json)")
agentProxyConnectCmd.Flags().String("client-id", "", "universal auth client id for the agent machine identity")
agentProxyConnectCmd.Flags().String("client-secret", "", "universal auth client secret for the agent machine identity")
agentProxyConnectCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
agentProxyConnectCmd.Flags().String("no-proxy", "", "additional comma-separated hosts to bypass the proxy (always merged with localhost,127.0.0.1)")
agentProxyConnectCmd.Flags().Bool("allow-readable-brokered-secrets", false, "start even if the agent can read secrets that proxied services broker to it (bypasses a misconfiguration guardrail)")
agentProxyConnectCmd.Flags().Bool("allow-readable-brokered-secrets", false, "start even if the agent can read secrets that proxied services broker to it (bypasses a misconfiguration guardrail; falls back to INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS)")

agentProxyStartCmd.Flags().Int("port", 17322, "port for the agent proxy to listen on")
agentProxyStartCmd.Flags().String("unmatched-host", "allow", "policy for hosts with no proxied service: allow | block")
Expand Down
1 change: 1 addition & 0 deletions packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type WorkspaceConfigFile struct {
WorkspaceId string `json:"workspaceId"`
DefaultEnvironment string `json:"defaultEnvironment"`
GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"`
DefaultSecretPath string `json:"defaultSecretPath,omitempty"`
Domain string `json:"domain,omitempty"`
}

Expand Down
8 changes: 7 additions & 1 deletion packages/util/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ const (
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
INFISICAL_PROJECT_ID_NAME = "INFISICAL_PROJECT_ID"
INFISICAL_ENVIRONMENT_NAME = "INFISICAL_ENVIRONMENT"
INFISICAL_SECRET_PATH_NAME = "INFISICAL_SECRET_PATH"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.

// Agent proxy (connect)
INFISICAL_AGENT_PROXY_ADDRESS_NAME = "INFISICAL_AGENT_PROXY_ADDRESS"
INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME = "INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.

INFISICAL_BOOTSTRAP_EMAIL_NAME = "INFISICAL_ADMIN_EMAIL"
INFISICAL_BOOTSTRAP_PASSWORD_NAME = "INFISICAL_ADMIN_PASSWORD"
Expand Down
70 changes: 70 additions & 0 deletions packages/util/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,76 @@ func GetCmdFlagOrEnvWithDefaultValue(cmd *cobra.Command, flag string, envNames [
return value, nil
}

// ResolveEnvironmentName resolves the environment slug for `agent-proxy connect`, in order:
// the --env flag (if explicitly set) > INFISICAL_ENVIRONMENT > .infisical.json
// (git-branch mapping, then defaultEnvironment) > the flag's own default value.
// It keys off cmd.Flags().Changed rather than an empty-value check so env and workspace
// are still consulted when --env is left at its default.
func ResolveEnvironmentName(cmd *cobra.Command) string {
if cmd.Flags().Changed("env") {
value, _ := cmd.Flags().GetString("env")
return value
}
if value := strings.TrimSpace(os.Getenv(INFISICAL_ENVIRONMENT_NAME)); value != "" {
return value
}
if value := GetEnvFromWorkspaceFile(); value != "" {
return value
}
value, _ := cmd.Flags().GetString("env")
return value
}

// ResolveSecretPath resolves the secret path (folder) for a command, in order:
// the --path flag (if explicitly set) > INFISICAL_SECRET_PATH > .infisical.json
// defaultSecretPath > the flag's own default value ("/").
func ResolveSecretPath(cmd *cobra.Command) string {
if cmd.Flags().Changed("path") {
value, _ := cmd.Flags().GetString("path")
return value
}
if value := strings.TrimSpace(os.Getenv(INFISICAL_SECRET_PATH_NAME)); value != "" {
return value
}
if value := GetSecretPathFromWorkspaceFile(); value != "" {
return value
}
value, _ := cmd.Flags().GetString("path")
return value
}

// ResolveAgentProxyAddress resolves the agent proxy address for `agent-proxy connect`, in order:
// the --proxy flag (if explicitly set) > INFISICAL_AGENT_PROXY_ADDRESS > empty (the caller
// requires a non-empty result). It is deliberately NOT sourced from .infisical.json: that file
// is usually committed to a repo, so a poisoned proxy address would silently route all agent
// traffic and its auth token through an attacker-controlled host.
func ResolveAgentProxyAddress(cmd *cobra.Command) string {
if cmd.Flags().Changed("proxy") {
value, _ := cmd.Flags().GetString("proxy")
return value
}
return strings.TrimSpace(os.Getenv(INFISICAL_AGENT_PROXY_ADDRESS_NAME))
}

// GetBoolFlagOrEnv resolves a boolean flag from the flag (if explicitly set), then the given
// environment variable, then the flag's default. An unparseable env value fails closed to false.
func GetBoolFlagOrEnv(cmd *cobra.Command, flag string, envName string) bool {
if cmd.Flags().Changed(flag) {
value, _ := cmd.Flags().GetBool(flag)
return value
}
if raw := strings.TrimSpace(os.Getenv(envName)); raw != "" {
parsed, err := strconv.ParseBool(raw)
if err != nil {
log.Warn().Msgf("Ignoring %s: %q is not a valid boolean", envName, raw)
return false
}
return parsed
}
value, _ := cmd.Flags().GetBool(flag)
return value
}

func GenerateRandomString(length int) string {
b := make([]byte, length)
for i := range b {
Expand Down
180 changes: 180 additions & 0 deletions packages/util/resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package util

import (
"os"
"path/filepath"
"testing"

"github.com/spf13/cobra"
)

// newResolveTestCmd builds a command with the flags the resolvers read. Passing a non-empty
// value marks that flag as explicitly set (Changed == true), mirroring a user-passed flag.
func newResolveTestCmd(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("proxy", "", "")
cmd.Flags().StringP("env", "e", "", "")
cmd.Flags().String("path", "/", "")
Comment thread
saifsmailbox98 marked this conversation as resolved.
cmd.Flags().Bool("allow", false, "")
return cmd
}

// writeWorkspace drops a .infisical.json into an isolated cwd so file-fallback is deterministic.
func writeWorkspace(t *testing.T, contents string) {
t.Helper()
dir := t.TempDir()
t.Chdir(dir)
if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(contents), 0o600); err != nil {
t.Fatalf("write workspace: %v", err)
}
}

func TestResolveEnvironmentName(t *testing.T) {
t.Run("flag wins over env and file", func(t *testing.T) {
writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`)
t.Setenv(INFISICAL_ENVIRONMENT_NAME, "fromenv")
cmd := newResolveTestCmd(t)
_ = cmd.Flags().Set("env", "fromflag")
if got := ResolveEnvironmentName(cmd); got != "fromflag" {
t.Fatalf("got %q, want fromflag", got)
}
})

t.Run("env wins over file when flag unset", func(t *testing.T) {
writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`)
t.Setenv(INFISICAL_ENVIRONMENT_NAME, "fromenv")
if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromenv" {
t.Fatalf("got %q, want fromenv", got)
}
})

t.Run("file used when flag and env unset", func(t *testing.T) {
writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`)
t.Setenv(INFISICAL_ENVIRONMENT_NAME, "")
if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" {
t.Fatalf("got %q, want fromfile", got)
}
})

t.Run("flag default is the final fallback", func(t *testing.T) {
t.Chdir(t.TempDir()) // no workspace file
t.Setenv(INFISICAL_ENVIRONMENT_NAME, "")
if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "" {
t.Fatalf("got %q, want empty", got)
}
})
}

func TestResolveSecretPath(t *testing.T) {
t.Run("flag wins", func(t *testing.T) {
writeWorkspace(t, `{"defaultSecretPath":"/fromfile"}`)
t.Setenv(INFISICAL_SECRET_PATH_NAME, "/fromenv")
cmd := newResolveTestCmd(t)
_ = cmd.Flags().Set("path", "/fromflag")
if got := ResolveSecretPath(cmd); got != "/fromflag" {
t.Fatalf("got %q, want /fromflag", got)
}
})

t.Run("env over file", func(t *testing.T) {
writeWorkspace(t, `{"defaultSecretPath":"/fromfile"}`)
t.Setenv(INFISICAL_SECRET_PATH_NAME, "/fromenv")
if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/fromenv" {
t.Fatalf("got %q, want /fromenv", got)
}
})

t.Run("file when flag and env unset", func(t *testing.T) {
writeWorkspace(t, `{"defaultSecretPath":"/fromfile"}`)
t.Setenv(INFISICAL_SECRET_PATH_NAME, "")
if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/fromfile" {
t.Fatalf("got %q, want /fromfile", got)
}
})

t.Run("defaults to /", func(t *testing.T) {
t.Chdir(t.TempDir())
t.Setenv(INFISICAL_SECRET_PATH_NAME, "")
if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/" {
t.Fatalf("got %q, want /", got)
}
})
}

func TestResolveAgentProxyAddress(t *testing.T) {
t.Run("flag wins over env", func(t *testing.T) {
t.Setenv(INFISICAL_AGENT_PROXY_ADDRESS_NAME, "env:1")
cmd := newResolveTestCmd(t)
_ = cmd.Flags().Set("proxy", "flag:1")
if got := ResolveAgentProxyAddress(cmd); got != "flag:1" {
t.Fatalf("got %q, want flag:1", got)
}
})

t.Run("env when flag unset", func(t *testing.T) {
t.Setenv(INFISICAL_AGENT_PROXY_ADDRESS_NAME, "env:1")
if got := ResolveAgentProxyAddress(newResolveTestCmd(t)); got != "env:1" {
t.Fatalf("got %q, want env:1", got)
}
})

// The proxy address must never come from .infisical.json (a committed file), or a poisoned
// value could silently redirect all agent traffic. Prove the file is ignored.
t.Run("workspace file is not a source", func(t *testing.T) {
writeWorkspace(t, `{"agentProxyAddress":"file:1"}`)
t.Setenv(INFISICAL_AGENT_PROXY_ADDRESS_NAME, "")
if got := ResolveAgentProxyAddress(newResolveTestCmd(t)); got != "" {
t.Fatalf("got %q, want empty (.infisical.json must not set the proxy address)", got)
}
})

t.Run("empty when nothing set", func(t *testing.T) {
t.Chdir(t.TempDir())
t.Setenv(INFISICAL_AGENT_PROXY_ADDRESS_NAME, "")
if got := ResolveAgentProxyAddress(newResolveTestCmd(t)); got != "" {
t.Fatalf("got %q, want empty", got)
}
})
}

func TestGetBoolFlagOrEnv(t *testing.T) {
const env = "INFISICAL_TEST_BOOL"

t.Run("explicit flag wins", func(t *testing.T) {
t.Setenv(env, "false")
cmd := newResolveTestCmd(t)
_ = cmd.Flags().Set("allow", "true")
if !GetBoolFlagOrEnv(cmd, "allow", env) {
t.Fatal("expected true from flag")
}
})

t.Run("env true when flag unset", func(t *testing.T) {
t.Setenv(env, "true")
if !GetBoolFlagOrEnv(newResolveTestCmd(t), "allow", env) {
t.Fatal("expected true from env")
}
})

t.Run("env false when flag unset", func(t *testing.T) {
t.Setenv(env, "false")
if GetBoolFlagOrEnv(newResolveTestCmd(t), "allow", env) {
t.Fatal("expected false from env")
}
})

t.Run("unparseable env fails closed to false", func(t *testing.T) {
t.Setenv(env, "yeah")
if GetBoolFlagOrEnv(newResolveTestCmd(t), "allow", env) {
t.Fatal("expected false on unparseable env")
}
})

t.Run("flag default when nothing set", func(t *testing.T) {
t.Setenv(env, "")
if GetBoolFlagOrEnv(newResolveTestCmd(t), "allow", env) {
t.Fatal("expected false (flag default)")
}
})
}
9 changes: 9 additions & 0 deletions packages/util/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,15 @@ func GetEnvFromWorkspaceFile() string {
return workspaceFile.DefaultEnvironment
}

func GetSecretPathFromWorkspaceFile() string {
workspaceFile, err := GetWorkSpaceFromFile()
if err != nil {
log.Debug().Msgf("GetSecretPathFromWorkspaceFile: [err=%s]", err)
return ""
}
return workspaceFile.DefaultSecretPath
}

func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) string {
branch, err := getCurrentBranch()
if err != nil {
Expand Down
Loading