From 84b019a1c081714e0f3ce051cac45b2cea8c36c0 Mon Sep 17 00:00:00 2001 From: Tre Dubrava Date: Mon, 8 Jun 2026 19:53:59 -0500 Subject: [PATCH 1/2] feature(eng-5200): add domain support in .infisical.json and INFISICAL_DOMAIN env var - Add `domain` field to .infisical.json, resolved when --domain is not passed - Add INFISICAL_DOMAIN env var; keep INFISICAL_API_URL as legacy alias - Precedence: --domain flag > INFISICAL_DOMAIN/INFISICAL_API_URL env > .infisical.json domain > default - Centralize env precedence in util.DomainEnvNames, used by GetEnvDomain and GetCmdFlagOrEnvWithDefaultValue - Move domain resolution to PersistentPreRun so the parsed flag is honored - Warn and ignore a malformed domain field; print the source when read from .infisical.json - Table-driven tests for domain parsing and env precedence --- packages/cmd/bootstrap.go | 8 +-- packages/cmd/kmip.go | 2 +- packages/cmd/login.go | 4 +- packages/cmd/login_status.go | 4 +- packages/cmd/root.go | 40 ++++++++--- packages/models/cli.go | 1 + packages/util/config_test.go | 72 +++++++++++++++++++ packages/util/constants.go | 3 +- packages/util/helper.go | 15 ++++ .../util/testdata/infisical-with-domain.json | 6 ++ 10 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 packages/util/config_test.go create mode 100644 packages/util/testdata/infisical-with-domain.json diff --git a/packages/cmd/bootstrap.go b/packages/cmd/bootstrap.go index 40b5b6e0..48af8239 100644 --- a/packages/cmd/bootstrap.go +++ b/packages/cmd/bootstrap.go @@ -171,11 +171,9 @@ var bootstrapCmd = &cobra.Command{ return } - domain, _ := cmd.Flags().GetString("domain") - if domain == "" { - if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok { - domain = envDomain - } + domain, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "domain", util.DomainEnvNames, "") + if err != nil { + util.HandleError(err, "Unable to parse domain") } if domain == "" { diff --git a/packages/cmd/kmip.go b/packages/cmd/kmip.go index cc1851f7..019370c3 100644 --- a/packages/cmd/kmip.go +++ b/packages/cmd/kmip.go @@ -138,7 +138,7 @@ var kmipSystemdInstallCmd = &cobra.Command{ util.HandleError(err, "Unable to parse identity client secret") } - domain, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "domain", []string{util.INFISICAL_API_URL_ENV_NAME}, "") + domain, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "domain", util.DomainEnvNames, "") if err != nil { util.HandleError(err, "Unable to parse domain") } diff --git a/packages/cmd/login.go b/packages/cmd/login.go index 979f50d3..873efce5 100644 --- a/packages/cmd/login.go +++ b/packages/cmd/login.go @@ -439,7 +439,7 @@ func DomainOverridePrompt() (bool, error) { //trim the '/' from the end of the domain url config.INFISICAL_URL_MANUAL_OVERRIDE = strings.TrimRight(config.INFISICAL_URL_MANUAL_OVERRIDE, "/") optionsPrompt := promptui.Select{ - Label: fmt.Sprintf("Current INFISICAL_API_URL Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), + Label: fmt.Sprintf("Current Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), Items: options, Size: 2, } @@ -491,7 +491,7 @@ func usePresetDomain(presetDomain string, domainFlagExplicitlySet bool, shouldPr boldWhite := whilte.Add(color.Bold) time.Sleep(time.Second * 1) if shouldPrintInfo { - boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain) + boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_DOMAIN environment variable\n", parsedDomain) } return true, nil diff --git a/packages/cmd/login_status.go b/packages/cmd/login_status.go index 62bda7ac..9558abac 100644 --- a/packages/cmd/login_status.go +++ b/packages/cmd/login_status.go @@ -69,8 +69,8 @@ func runLoginStatus(cmd *cobra.Command, args []string) { flagToken = strings.TrimSpace(flagToken) if flagToken != "" { if !cmd.Flags().Changed("domain") { - if _, envSet := os.LookupEnv("INFISICAL_API_URL"); !envSet { - util.PrintErrorMessageAndExit("--token requires --domain (or INFISICAL_API_URL) to be set so the status reflects the correct Infisical instance") + if _, envSet := util.GetEnvDomain(); !envSet { + util.PrintErrorMessageAndExit("--token requires --domain (or INFISICAL_DOMAIN) to be set so the status reflects the correct Infisical instance") } } ctx, err := buildContextFromToken(flagToken, "--token flag", envDomain) diff --git a/packages/cmd/root.go b/packages/cmd/root.go index 1a8bfa62..05cd5422 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -88,13 +88,41 @@ func Execute() { } } +// resolveDomain picks the domain by precedence: --domain flag > env > .infisical.json > default (flagValue). +// Must run after flag parsing (PersistentPreRun, not init) so cmd.Flags().Changed is reliable. +func resolveDomain(cmd *cobra.Command, flagValue string, silent bool) string { + if cmd.Flags().Changed("domain") { + return flagValue + } + + if envDomain, ok := util.GetEnvDomain(); ok { + return envDomain + } + + workspaceConfig, err := util.GetWorkSpaceFromFile() + if err != nil || workspaceConfig.Domain == "" { + return flagValue + } + + domain := workspaceConfig.Domain + if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") { + util.PrintWarningWithWriter("The 'domain' field in .infisical.json is not a valid URL (must start with http:// or https://). It will be ignored.", cmd.ErrOrStderr()) + return flagValue + } + + if !silent && !isStructuredOutputRequested(cmd) { + fmt.Fprintf(cmd.ErrOrStderr(), "[INFO] Using domain '%s' from .infisical.json\n", domain) + } + return domain +} + func init() { util.GetStderrWriter = RootCmdStderrWriter util.GetStdoutWriter = RootCmdStdoutWriter cobra.OnInitialize(initLog) RootCmd.PersistentFlags().StringP("log-level", "l", "", "log level (trace, debug, info, warn, error, fatal)") RootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary") - RootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your Infisical instance (e.g., https://eu.infisical.com for EU Cloud, or https://your-instance.com for self-hosted). Can also set via INFISICAL_API_URL environment variable. Required for non-US Cloud users.") + RootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your Infisical instance (e.g., https://eu.infisical.com for EU Cloud, or https://your-instance.com for self-hosted). Can also set via INFISICAL_DOMAIN environment variable or the 'domain' field in .infisical.json. Required for non-US Cloud users.") RootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") RootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { silent, err := cmd.Flags().GetBool("silent") @@ -102,7 +130,7 @@ func init() { util.HandleError(err) } - config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) + config.INFISICAL_URL = util.AppendAPIEndpoint(resolveDomain(cmd, config.INFISICAL_URL, silent)) // util.DisplayAptInstallationChangeBannerWithWriter(silent, cmd.ErrOrStderr()) if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) { @@ -121,14 +149,6 @@ func init() { } - // if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment - // this is used to allow overrides of the default value - if !RootCmd.Flag("domain").Changed { - if envInfisicalBackendUrl, ok := os.LookupEnv("INFISICAL_API_URL"); ok { - config.INFISICAL_URL = util.AppendAPIEndpoint(envInfisicalBackendUrl) - } - } - isTelemetryOn, _ := RootCmd.PersistentFlags().GetBool("telemetry") Telemetry = telemetry.NewTelemetry(isTelemetryOn) } diff --git a/packages/models/cli.go b/packages/models/cli.go index ea874088..16aed48a 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -112,6 +112,7 @@ type WorkspaceConfigFile struct { WorkspaceId string `json:"workspaceId"` DefaultEnvironment string `json:"defaultEnvironment"` GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"` + Domain string `json:"domain,omitempty"` } type SymmetricEncryptionResult struct { diff --git a/packages/util/config_test.go b/packages/util/config_test.go new file mode 100644 index 00000000..cc7e6af8 --- /dev/null +++ b/packages/util/config_test.go @@ -0,0 +1,72 @@ +package util + +import ( + "os" + "testing" +) + +func TestWorkspaceConfigDomain(t *testing.T) { + cases := []struct { + name string + path string + wantDomain string + }{ + {"domain field is parsed", "testdata/infisical-with-domain.json", "https://custom.infisical.com"}, + {"existing config without a domain field parses to empty", "testdata/infisical-default-env.json", ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg, err := GetWorkspaceConfigByPath(tc.path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Domain != tc.wantDomain { + t.Errorf("Domain = %q, want %q", cfg.Domain, tc.wantDomain) + } + }) + } +} + +func TestGetEnvDomain(t *testing.T) { + const unset = "\x00" // sentinel: leave the env var unset for this case + + cases := []struct { + name string + domain string // INFISICAL_DOMAIN + apiURL string // INFISICAL_API_URL (legacy) + wantVal string + wantOk bool + }{ + {"prefers INFISICAL_DOMAIN over legacy", "https://domain.infisical.com", "https://apiurl.infisical.com", "https://domain.infisical.com", true}, + {"falls back to legacy INFISICAL_API_URL", unset, "https://apiurl.infisical.com", "https://apiurl.infisical.com", true}, + {"blank INFISICAL_DOMAIN falls through to legacy", " ", "https://apiurl.infisical.com", "https://apiurl.infisical.com", true}, + {"neither set", unset, unset, "", false}, + {"both blank are treated as unset", " ", " ", "", false}, + } + + setOrUnset := func(t *testing.T, key, val string) { + t.Helper() + t.Setenv(key, "") // register restore-on-cleanup, then mutate freely below + if val == unset { + os.Unsetenv(key) + return + } + os.Setenv(key, val) + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + setOrUnset(t, INFISICAL_DOMAIN_ENV_NAME, tc.domain) + setOrUnset(t, LEGACY_INFISICAL_API_URL_ENV_NAME, tc.apiURL) + + got, ok := GetEnvDomain() + if ok != tc.wantOk { + t.Fatalf("ok = %v, want %v", ok, tc.wantOk) + } + if got != tc.wantVal { + t.Errorf("value = %q, want %q", got, tc.wantVal) + } + }) + } +} diff --git a/packages/util/constants.go b/packages/util/constants.go index 4bf0c9e7..22bdf606 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -49,7 +49,8 @@ const ( // Generic env variable used for auth methods that require a machine identity ID INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID" - INFISICAL_API_URL_ENV_NAME = "INFISICAL_API_URL" + INFISICAL_DOMAIN_ENV_NAME = "INFISICAL_DOMAIN" + LEGACY_INFISICAL_API_URL_ENV_NAME = "INFISICAL_API_URL" // superseded by INFISICAL_DOMAIN; kept for backwards compatibility SECRET_TYPE_PERSONAL = "personal" SECRET_TYPE_SHARED = "shared" diff --git a/packages/util/helper.go b/packages/util/helper.go index ced0dbcc..1babadef 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -456,6 +456,21 @@ func getCurrentBranch() (string, error) { return path.Base(strings.TrimSpace(out.String())), nil } +// DomainEnvNames lists the env vars that configure the Infisical domain, in +// precedence order: INFISICAL_DOMAIN first, then the legacy INFISICAL_API_URL. +var DomainEnvNames = []string{INFISICAL_DOMAIN_ENV_NAME, LEGACY_INFISICAL_API_URL_ENV_NAME} + +// GetEnvDomain returns the Infisical domain configured via environment +// variables, preferring INFISICAL_DOMAIN over the legacy INFISICAL_API_URL. +func GetEnvDomain() (string, bool) { + for _, env := range DomainEnvNames { + if domain := strings.TrimSpace(os.Getenv(env)); domain != "" { + return domain, true + } + } + return "", false +} + func AppendAPIEndpoint(address string) string { // if it's empty return as it is // Ensure the address does not already end with "/api" diff --git a/packages/util/testdata/infisical-with-domain.json b/packages/util/testdata/infisical-with-domain.json new file mode 100644 index 00000000..679747bb --- /dev/null +++ b/packages/util/testdata/infisical-with-domain.json @@ -0,0 +1,6 @@ +{ + "workspaceId": "test-workspace-id", + "defaultEnvironment": "dev", + "gitBranchToEnvironmentMapping": null, + "domain": "https://custom.infisical.com" +} From 2bf803986d5861719999acafc072f7dc38e6e6b3 Mon Sep 17 00:00:00 2001 From: Tre Dubrava Date: Mon, 8 Jun 2026 20:18:35 -0500 Subject: [PATCH 2/2] fix(eng-5200): address review on file-sourced domain - login status --token now accepts a domain from .infisical.json (was a false error) - Always warn (even under --silent) when the domain comes from .infisical.json, since a committed config could redirect requests and credentials --- packages/cmd/login_status.go | 6 ++++-- packages/cmd/root.go | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cmd/login_status.go b/packages/cmd/login_status.go index 9558abac..1c269f6a 100644 --- a/packages/cmd/login_status.go +++ b/packages/cmd/login_status.go @@ -69,8 +69,10 @@ func runLoginStatus(cmd *cobra.Command, args []string) { flagToken = strings.TrimSpace(flagToken) if flagToken != "" { if !cmd.Flags().Changed("domain") { - if _, envSet := util.GetEnvDomain(); !envSet { - util.PrintErrorMessageAndExit("--token requires --domain (or INFISICAL_DOMAIN) to be set so the status reflects the correct Infisical instance") + _, envSet := util.GetEnvDomain() + workspaceConfig, _ := util.GetWorkSpaceFromFile() + if !envSet && workspaceConfig.Domain == "" { + util.PrintErrorMessageAndExit("--token requires the Infisical instance to be set via --domain, the INFISICAL_DOMAIN env var, or the 'domain' field in .infisical.json so the status reflects the correct instance") } } ctx, err := buildContextFromToken(flagToken, "--token flag", envDomain) diff --git a/packages/cmd/root.go b/packages/cmd/root.go index 05cd5422..de1b0b82 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -90,7 +90,7 @@ func Execute() { // resolveDomain picks the domain by precedence: --domain flag > env > .infisical.json > default (flagValue). // Must run after flag parsing (PersistentPreRun, not init) so cmd.Flags().Changed is reliable. -func resolveDomain(cmd *cobra.Command, flagValue string, silent bool) string { +func resolveDomain(cmd *cobra.Command, flagValue string) string { if cmd.Flags().Changed("domain") { return flagValue } @@ -110,9 +110,9 @@ func resolveDomain(cmd *cobra.Command, flagValue string, silent bool) string { return flagValue } - if !silent && !isStructuredOutputRequested(cmd) { - fmt.Fprintf(cmd.ErrOrStderr(), "[INFO] Using domain '%s' from .infisical.json\n", domain) - } + // A .infisical.json is usually committed to the repo, so a malicious one could redirect requests + // and credentials. Always surface where traffic is going (even under --silent); it goes to stderr. + util.PrintWarningWithWriter(fmt.Sprintf("Using domain '%s' from .infisical.json; all requests and credentials will be sent there.", domain), cmd.ErrOrStderr()) return domain } @@ -130,7 +130,7 @@ func init() { util.HandleError(err) } - config.INFISICAL_URL = util.AppendAPIEndpoint(resolveDomain(cmd, config.INFISICAL_URL, silent)) + config.INFISICAL_URL = util.AppendAPIEndpoint(resolveDomain(cmd, config.INFISICAL_URL)) // util.DisplayAptInstallationChangeBannerWithWriter(silent, cmd.ErrOrStderr()) if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) {