From 400157a468da8a10bbc2e3a075cd453996d7ae5f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 3 Jun 2025 21:34:24 +0400 Subject: [PATCH 01/30] feat(cli): gateway auth methods --- cli/go.mod | 4 +- cli/go.sum | 2 + cli/packages/cmd/gateway.go | 135 +++++++++++++++++++++++++++++--- cli/packages/cmd/kmip.go | 4 +- cli/packages/cmd/login.go | 135 ++++++++------------------------ cli/packages/gateway/gateway.go | 4 + cli/packages/util/auth.go | 122 +++++++++++++++++++++++++++++ cli/packages/util/constants.go | 5 +- cli/packages/util/helper.go | 9 ++- 9 files changed, 298 insertions(+), 122 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index 229e37137..1b44d9c9d 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,6 +2,8 @@ module github.com/Infisical/infisical-merge go 1.23.0 +replace github.com/infisical/go-sdk => ../../go-sdk + toolchain go1.23.5 require ( @@ -14,7 +16,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.9.1 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.5.92 + github.com/infisical/go-sdk v0.5.94 github.com/infisical/infisical-kmip v0.3.5 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a diff --git a/cli/go.sum b/cli/go.sum index d253c70d5..957fedb05 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -294,6 +294,8 @@ github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7P github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE= github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/go-sdk v0.5.94 h1:wKBj+KpJEe+ZzOJ7koXQZDR0dLL9bt0Kqgf/1q+7tG4= +github.com/infisical/go-sdk v0.5.94/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index 51565b6fd..3e6e268d9 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -7,16 +7,79 @@ import ( "os/exec" "os/signal" "runtime" + "sync/atomic" "syscall" "time" + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/gateway" "github.com/Infisical/infisical-merge/packages/util" + infisicalSdk "github.com/infisical/go-sdk" + "github.com/pkg/errors" "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) +func getInfisicalSdkInstance(cmd *cobra.Command) (infisicalSdk.InfisicalClientInterface, context.CancelFunc, error) { + + ctx, cancel := context.WithCancel(cmd.Context()) + infisicalClient := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + }) + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + cancel() + return nil, nil, err + } + + fmt.Printf("token: %+v\n", token) + + // if the --token param is set, we use it directly for authentication + if token != nil { + infisicalClient.Auth().SetAccessToken(token.Token) + fmt.Printf("token from client: %+v\n", infisicalClient.Auth().GetAccessToken()) + return infisicalClient, cancel, nil + } + + // if the --token param is not set, we use the auth-method flag to determine the authentication method, and perform the appropriate login flow based on that + authMethod, err := cmd.Flags().GetString("auth-method") + if err != nil { + cancel() + return nil, nil, err + } + + authMethodValid, strategy := util.IsAuthMethodValid(authMethod, false) + if !authMethodValid { + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", authMethod)) + } + + sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd) + + authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){ + util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin, + util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin, + util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin, + util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin, + util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin, + util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin, + util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin, + util.AuthStrategy.JWT_AUTH: sdkAuthenticator.HandleJwtAuthLogin, + } + + _, err = authStrategies[strategy]() + + if err != nil { + cancel() + return nil, nil, err + } + + return infisicalClient, cancel, nil +} + var gatewayCmd = &cobra.Command{ Use: "gateway", Short: "Run the Infisical gateway or manage its systemd service", @@ -26,13 +89,18 @@ var gatewayCmd = &cobra.Command{ DisableFlagsInUseLine: true, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse token flag") - } - if token == nil { - util.HandleError(fmt.Errorf("Token not found")) + infisicalClient, cancelSdk, err := getInfisicalSdkInstance(cmd) + if err != nil { + util.HandleError(err, "unable to get infisical client") + } + defer cancelSdk() + + var accessToken atomic.Value + accessToken.Store(infisicalClient.Auth().GetAccessToken()) + + if accessToken.Load().(string) == "" { + util.HandleError(errors.New("no access token found")) } Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION)) @@ -41,13 +109,14 @@ var gatewayCmd = &cobra.Command{ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sigStopCh := make(chan bool, 1) - ctx, cancel := context.WithCancel(cmd.Context()) - defer cancel() + ctx, cancelCmd := context.WithCancel(cmd.Context()) + defer cancelCmd() go func() { <-sigCh close(sigStopCh) - cancel() + cancelCmd() + cancelSdk() // If we get a second signal, force exit <-sigCh @@ -55,6 +124,34 @@ var gatewayCmd = &cobra.Command{ os.Exit(1) }() + var gatewayInstance *gateway.Gateway + + // Token refresh goroutine - runs every 10 seconds + go func() { + tokenRefreshTicker := time.NewTicker(10 * time.Second) + defer tokenRefreshTicker.Stop() + + for { + select { + case <-tokenRefreshTicker.C: + if ctx.Err() != nil { + return + } + + newToken := infisicalClient.Auth().GetAccessToken() + if newToken != "" && newToken != accessToken.Load().(string) { + accessToken.Store(newToken) + if gatewayInstance != nil { + gatewayInstance.UpdateIdentityAccessToken(newToken) + } + } + + case <-ctx.Done(): + return + } + } + }() + // Main gateway retry loop with proper context handling retryTicker := time.NewTicker(5 * time.Second) defer retryTicker.Stop() @@ -64,7 +161,7 @@ var gatewayCmd = &cobra.Command{ log.Info().Msg("Shutting down gateway") return } - gatewayInstance, err := gateway.NewGateway(token.Token) + gatewayInstance, err := gateway.NewGateway(accessToken.Load().(string)) if err != nil { util.HandleError(err) } @@ -126,7 +223,7 @@ var gatewayInstallCmd = &cobra.Command{ } if token == nil { - util.HandleError(fmt.Errorf("Token not found")) + util.HandleError(errors.New("Token not found")) } domain, err := cmd.Flags().GetString("domain") @@ -183,7 +280,7 @@ var gatewayRelayCmd = &cobra.Command{ } if relayConfigFilePath == "" { - util.HandleError(fmt.Errorf("Missing config file")) + util.HandleError(errors.New("Missing config file")) } gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath) @@ -198,7 +295,19 @@ var gatewayRelayCmd = &cobra.Command{ } func init() { - gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") + gatewayCmd.Flags().String("token", "", "connect with Infisical using machine identity access token. if not provided, you must set the auth-method flag") + + gatewayCmd.Flags().String("auth-method", "", "login method [universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]. if not provided, you must set the token flag") + + gatewayCmd.Flags().String("client-id", "", "client id for universal auth") + gatewayCmd.Flags().String("client-secret", "", "client secret for universal auth") + + gatewayCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") + gatewayCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") + gatewayCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") + + gatewayCmd.Flags().String("jwt", "", "JWT for jwt-based auth methods [oidc-auth, jwt-auth]") + gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance") diff --git a/cli/packages/cmd/kmip.go b/cli/packages/cmd/kmip.go index b0c397895..91335d122 100644 --- a/cli/packages/cmd/kmip.go +++ b/cli/packages/cmd/kmip.go @@ -49,13 +49,13 @@ func startKmipServer(cmd *cobra.Command, args []string) { var identityClientSecret string if strategy == util.AuthStrategy.UNIVERSAL_AUTH { - identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) + identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}) if err != nil { util.HandleError(err, "Unable to parse identity client ID") } - identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) + identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}) if err != nil { util.HandleError(err, "Unable to parse identity client secret") } diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index b0ce7564b..ef549aabe 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -49,97 +49,6 @@ type params struct { keyLength uint32 } -func handleUniversalAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - clientId, err := util.GetCmdFlagOrEnv(cmd, "client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) - - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - clientSecret, err := util.GetCmdFlagOrEnv(cmd, "client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret) -} - -func handleKubernetesAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - serviceAccountTokenPath, err := util.GetCmdFlagOrEnv(cmd, "service-account-token-path", util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath) -} - -func handleAzureAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().AzureAuthLogin(identityId, "") -} - -func handleGcpIdTokenAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().GcpIdTokenAuthLogin(identityId) -} - -func handleGcpIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - serviceAccountKeyFilePath, err := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath) -} - -func handleAwsIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().AwsIamAuthLogin(identityId) -} - -func handleOidcAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - jwt, err := util.GetCmdFlagOrEnv(cmd, "oidc-jwt", util.INFISICAL_OIDC_AUTH_JWT_NAME) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return infisicalClient.Auth().OidcAuthLogin(identityId, jwt) -} - func formatAuthMethod(authMethod string) string { return strings.ReplaceAll(authMethod, "-", " ") } @@ -154,8 +63,22 @@ var loginCmd = &cobra.Command{ Use: "login", Short: "Login into your Infisical account", DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { + // daniel: oidc-jwt is deprecated in favor of `jwt`. we backfill the `jwt` flag with the value of `oidc-jwt` if it's set. + if cmd.Flags().Changed("oidc-jwt") && !cmd.Flags().Changed("jwt") { + oidcJWT, err := cmd.Flags().GetString("oidc-jwt") + if err != nil { + return err + } + err = cmd.Flags().Set("jwt", oidcJWT) + if err != nil { + return err + } + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { presetDomain := config.INFISICAL_URL clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains") @@ -310,17 +233,19 @@ var loginCmd = &cobra.Command{ Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION)) } else { - authStrategies := map[util.AuthStrategyType]func(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error){ - util.AuthStrategy.UNIVERSAL_AUTH: handleUniversalAuthLogin, - util.AuthStrategy.KUBERNETES_AUTH: handleKubernetesAuthLogin, - util.AuthStrategy.AZURE_AUTH: handleAzureAuthLogin, - util.AuthStrategy.GCP_ID_TOKEN_AUTH: handleGcpIdTokenAuthLogin, - util.AuthStrategy.GCP_IAM_AUTH: handleGcpIamAuthLogin, - util.AuthStrategy.AWS_IAM_AUTH: handleAwsIamAuthLogin, - util.AuthStrategy.OIDC_AUTH: handleOidcAuthLogin, + sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd) + + authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){ + util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin, + util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin, + util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin, + util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin, + util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin, + util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin, + util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin, } - credential, err := authStrategies[strategy](cmd, infisicalClient) + credential, err := authStrategies[strategy]() if err != nil { euErrorMessage := "" @@ -518,14 +443,18 @@ func init() { rootCmd.AddCommand(loginCmd) loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file") loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line") - loginCmd.Flags().String("method", "user", "login method [user, universal-auth]") loginCmd.Flags().Bool("plain", false, "only output the token without any formatting") + loginCmd.Flags().String("method", "user", "login method [user, universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]") loginCmd.Flags().String("client-id", "", "client id for universal auth") loginCmd.Flags().String("client-secret", "", "client secret for universal auth") loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") - loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication") + loginCmd.Flags().String("jwt", "", "jwt for jwt-based auth methods [oidc-auth, jwt-auth]") + loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication. Deprecated, use --jwt instead") + + loginCmd.Flags().MarkDeprecated("oidc-jwt", "use --jwt instead") + } func DomainOverridePrompt() (bool, error) { diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go index d0a25ca9c..eb0c72d5d 100644 --- a/cli/packages/gateway/gateway.go +++ b/cli/packages/gateway/gateway.go @@ -54,6 +54,10 @@ func NewGateway(identityToken string) (Gateway, error) { }, nil } +func (g *Gateway) UpdateIdentityAccessToken(accessToken string) { + g.httpClient.SetAuthToken(accessToken) +} + func (g *Gateway) ConnectWithRelay() error { relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient) if err != nil { diff --git a/cli/packages/util/auth.go b/cli/packages/util/auth.go index b54bde45b..eaf7cecc1 100644 --- a/cli/packages/util/auth.go +++ b/cli/packages/util/auth.go @@ -5,7 +5,9 @@ import ( "os" "os/exec" + infisicalSdk "github.com/infisical/go-sdk" "github.com/rs/zerolog/log" + "github.com/spf13/cobra" ) type AuthStrategyType string @@ -18,6 +20,7 @@ var AuthStrategy = struct { GCP_IAM_AUTH AuthStrategyType AWS_IAM_AUTH AuthStrategyType OIDC_AUTH AuthStrategyType + JWT_AUTH AuthStrategyType }{ UNIVERSAL_AUTH: "universal-auth", KUBERNETES_AUTH: "kubernetes", @@ -26,6 +29,7 @@ var AuthStrategy = struct { GCP_IAM_AUTH: "gcp-iam", AWS_IAM_AUTH: "aws-iam", OIDC_AUTH: "oidc-auth", + JWT_AUTH: "jwt-auth", } var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{ @@ -36,6 +40,7 @@ var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{ AuthStrategy.GCP_IAM_AUTH, AuthStrategy.AWS_IAM_AUTH, AuthStrategy.OIDC_AUTH, + AuthStrategy.JWT_AUTH, } func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) { @@ -84,3 +89,120 @@ func EstablishUserLoginSession() LoggedInUserDetails { return loggedInUserDetails } + +type SdkAuthenticator struct { + infisicalClient infisicalSdk.InfisicalClientInterface + cmd *cobra.Command +} + +func NewSdkAuthenticator(infisicalClient infisicalSdk.InfisicalClientInterface, cmd *cobra.Command) *SdkAuthenticator { + return &SdkAuthenticator{ + infisicalClient: infisicalClient, + cmd: cmd, + } +} +func (a *SdkAuthenticator) HandleUniversalAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + clientId, err := GetCmdFlagOrEnv(a.cmd, "client-id", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}) + + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + clientSecret, err := GetCmdFlagOrEnv(a.cmd, "client-secret", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret) +} + +func (a *SdkAuthenticator) HandleJwtAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().JwtAuthLogin(identityId, jwt) +} + +func (a *SdkAuthenticator) HandleKubernetesAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + serviceAccountTokenPath, err := GetCmdFlagOrEnv(a.cmd, "service-account-token-path", []string{INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath) +} + +func (a *SdkAuthenticator) HandleAzureAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().AzureAuthLogin(identityId, "") +} + +func (a *SdkAuthenticator) HandleGcpIdTokenAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().GcpIdTokenAuthLogin(identityId) +} + +func (a *SdkAuthenticator) HandleGcpIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + serviceAccountKeyFilePath, err := GetCmdFlagOrEnv(a.cmd, "service-account-key-file-path", []string{INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath) +} + +func (a *SdkAuthenticator) HandleAwsIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().AwsIamAuthLogin(identityId) +} + +func (a *SdkAuthenticator) HandleOidcAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME, INFISICAL_OIDC_AUTH_JWT_NAME}) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return a.infisicalClient.Auth().OidcAuthLogin(identityId, jwt) +} diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 8b4c586e6..68fda6d50 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -24,7 +24,10 @@ const ( INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME = "INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH" // OIDC Auth - INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT" + INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT" // deprecated in favor of INFISICAL_JWT + + // JWT AUTH + INFISICAL_JWT_NAME = "INFISICAL_JWT" // Generic env variable used for auth methods that require a machine identity ID INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID" diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 153a5a281..ceb0792f1 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -292,13 +292,18 @@ func GetEnvVarOrFileContent(envName string, filePath string) (string, error) { return fileContent, nil } -func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) { +func GetCmdFlagOrEnv(cmd *cobra.Command, flag string, envNames []string) (string, error) { value, flagsErr := cmd.Flags().GetString(flag) if flagsErr != nil { return "", flagsErr } if value == "" { - value = os.Getenv(envName) + for _, env := range envNames { + value = os.Getenv(env) + if value != "" { + break + } + } } if value == "" { return "", fmt.Errorf("please provide %s flag", flag) From 6ae7b5e996cc1b7b74e4094a0076041bca7d9570 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 3 Jun 2025 22:24:27 +0400 Subject: [PATCH 02/30] cleanup --- cli/go.mod | 4 +--- cli/go.sum | 2 ++ cli/packages/cmd/gateway.go | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index 1b44d9c9d..fc7322f61 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,8 +2,6 @@ module github.com/Infisical/infisical-merge go 1.23.0 -replace github.com/infisical/go-sdk => ../../go-sdk - toolchain go1.23.5 require ( @@ -16,7 +14,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.9.1 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.5.94 + github.com/infisical/go-sdk v0.5.95 github.com/infisical/infisical-kmip v0.3.5 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a diff --git a/cli/go.sum b/cli/go.sum index 957fedb05..aa8dc1f61 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -296,6 +296,8 @@ github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8l github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/go-sdk v0.5.94 h1:wKBj+KpJEe+ZzOJ7koXQZDR0dLL9bt0Kqgf/1q+7tG4= github.com/infisical/go-sdk v0.5.94/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/go-sdk v0.5.95 h1:so0YwPofbT7j6Ao8Xcxee/o3ia33meuEVDU2vWr9yfs= +github.com/infisical/go-sdk v0.5.95/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index 3e6e268d9..90710154e 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -36,12 +36,9 @@ func getInfisicalSdkInstance(cmd *cobra.Command) (infisicalSdk.InfisicalClientIn return nil, nil, err } - fmt.Printf("token: %+v\n", token) - // if the --token param is set, we use it directly for authentication if token != nil { infisicalClient.Auth().SetAccessToken(token.Token) - fmt.Printf("token from client: %+v\n", infisicalClient.Auth().GetAccessToken()) return infisicalClient, cancel, nil } From 1101707d8b951c9f1068976a44fd72c74fd330e4 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 4 Jun 2025 15:15:12 -0700 Subject: [PATCH 03/30] improvement: add org access control no access display --- frontend/src/hooks/api/roles/mutation.tsx | 2 +- .../AccessManagementPage/AccessManagementPage.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 8d662f993..6df9aa937 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -96,7 +96,7 @@ export const useUpdateOrgRole = () => { data: { role } } = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, { ...dto, - permissions: permissions?.length ? packRules(permissions) : undefined + permissions: permissions ? packRules(permissions) : undefined }); return role; diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 61c4f4411..bb1d5010a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -5,6 +5,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; +import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan"; import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { @@ -72,6 +73,8 @@ export const AccessManagementPage = () => { } ]; + const hasNoAccess = tabSections.every((tab) => tab.isHidden); + return (
@@ -126,6 +129,7 @@ export const AccessManagementPage = () => { ))}
+ {hasNoAccess && } ); }; From 07902f7db9281607b53d25b509b9f966e9de6416 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 04:42:15 +0400 Subject: [PATCH 04/30] feat(identities/kubernetes-auth): use gateway as token reviewer --- ...entity-kubernetes-auth-gateway-reviewer.ts | 23 ++ .../db/schemas/identity-kubernetes-auths.ts | 1 + .../ee/services/audit-log/audit-log-types.ts | 3 + .../dynamic-secret/providers/kubernetes.ts | 3 +- .../dynamic-secret/providers/sql-database.ts | 3 +- backend/src/lib/api-docs/constants.ts | 4 + backend/src/lib/gateway/index.ts | 63 +++++- .../v1/identity-kubernetes-auth-router.ts | 58 ++++- .../identity-kubernetes-auth-service.ts | 187 +++++++++++++--- .../identity-kubernetes-auth-types.ts | 7 + cli/packages/gateway/connection.go | 164 ++++++++++++++ .../src/hooks/api/identities/mutations.tsx | 12 +- frontend/src/hooks/api/identities/types.ts | 8 + .../IdentityKubernetesAuthForm.tsx | 205 ++++++++++++------ 14 files changed, 615 insertions(+), 126 deletions(-) create mode 100644 backend/src/db/migrations/20250604174128_identity-kubernetes-auth-gateway-reviewer.ts diff --git a/backend/src/db/migrations/20250604174128_identity-kubernetes-auth-gateway-reviewer.ts b/backend/src/db/migrations/20250604174128_identity-kubernetes-auth-gateway-reviewer.ts new file mode 100644 index 000000000..da5493153 --- /dev/null +++ b/backend/src/db/migrations/20250604174128_identity-kubernetes-auth-gateway-reviewer.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode"); + + if (!hasTokenReviewModeColumn) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.string("tokenReviewMode").notNullable().defaultTo("api"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode"); + + if (hasTokenReviewModeColumn) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.dropColumn("tokenReviewMode"); + }); + } +} diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index 00d1fd771..880928247 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -31,6 +31,7 @@ export const IdentityKubernetesAuthsSchema = z.object({ encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), + tokenReviewMode: z.string().default("api"), accessTokenPeriod: z.coerce.number().default(0) }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index bd500b377..2d6bc7c06 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -23,6 +23,7 @@ import { ActorType } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { IdentityKubernetesAuthTokenReviewMode } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums"; @@ -853,6 +854,7 @@ interface AddIdentityKubernetesAuthEvent { metadata: { identityId: string; kubernetesHost: string; + tokenReviewMode: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces: string; allowedNames: string; accessTokenTTL: number; @@ -874,6 +876,7 @@ interface UpdateIdentityKubernetesAuthEvent { metadata: { identityId: string; kubernetesHost?: string; + tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces?: string; allowedNames?: string; accessTokenTTL?: number; diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index 8a54ba089..eeea7e4be 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -2,7 +2,7 @@ import axios from "axios"; import https from "https"; import { InternalServerError } from "@app/lib/errors"; -import { withGatewayProxy } from "@app/lib/gateway"; +import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; @@ -43,6 +43,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): return res; }, { + protocol: ProxyProtocol.Tcp, targetHost: inputs.targetHost, targetPort: inputs.targetPort, relayHost, diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index ce16a1237..f5299687b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -3,7 +3,7 @@ import handlebars from "handlebars"; import knex from "knex"; import { z } from "zod"; -import { withGatewayProxy } from "@app/lib/gateway"; +import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -185,6 +185,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await gatewayCallback("localhost", port); }, { + protocol: ProxyProtocol.Tcp, targetHost: providerInputs.host, targetPort: providerInputs.port, relayHost, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5dcc812ad..8467f7fe6 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -393,6 +393,8 @@ export const KUBERNETES_AUTH = { caCert: "The PEM-encoded CA cert for the Kubernetes API server.", tokenReviewerJwt: "Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.", + tokenReviewMode: + "The mode to use for token review. Must be one of: 'api', 'gateway'. If gateway is selected, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding.", allowedNamespaces: "The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", @@ -410,6 +412,8 @@ export const KUBERNETES_AUTH = { caCert: "The new PEM-encoded CA cert for the Kubernetes API server.", tokenReviewerJwt: "Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.", + tokenReviewMode: + "The mode to use for token review. Must be one of: 'api', 'gateway'. If gateway is selected, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding.", allowedNamespaces: "The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", diff --git a/backend/src/lib/gateway/index.ts b/backend/src/lib/gateway/index.ts index 4d6401eac..9ec6a471e 100644 --- a/backend/src/lib/gateway/index.ts +++ b/backend/src/lib/gateway/index.ts @@ -4,6 +4,7 @@ import net from "node:net"; import quicDefault, * as quicModule from "@infisical/quic"; import axios from "axios"; +import https from "https"; import { BadRequestError } from "../errors"; import { logger } from "../logger"; @@ -148,6 +149,11 @@ interface TProxyServer { getProxyError: () => string; } +export enum ProxyProtocol { + Http = "http", + Tcp = "tcp" +} + const setupProxyServer = async ({ targetPort, targetHost, @@ -155,7 +161,9 @@ const setupProxyServer = async ({ relayHost, relayPort, identityId, - orgId + orgId, + protocol = ProxyProtocol.Tcp, + httpsAgent }: { targetHost: string; targetPort: number; @@ -164,6 +172,8 @@ const setupProxyServer = async ({ tlsOptions: TTlsOption; identityId: string; orgId: string; + protocol?: ProxyProtocol; + httpsAgent?: https.Agent; }): Promise => { const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { throw new BadRequestError({ @@ -184,9 +194,42 @@ const setupProxyServer = async ({ clientConn.setNoDelay(true); const stream = quicClient.connection.newStream("bidi"); - // Send FORWARD-TCP command + const forwardWriter = stream.writable.getWriter(); - await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`)); + let command: string; + + if (protocol === ProxyProtocol.Http) { + const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http) + command = `FORWARD-HTTP ${targetUrl}`; + logger.debug(`Using HTTP proxy mode: ${command.trim()}`); + + // extract ca certificate from httpsAgent if present + if (httpsAgent && targetHost.startsWith("https://")) { + const agentOptions = httpsAgent.options; + if (agentOptions && agentOptions.ca) { + const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca; + const caB64 = Buffer.from(caCert as string).toString("base64"); + command += ` ca=${caB64}`; + + const rejectUnauthorized = agentOptions.rejectUnauthorized !== false; + command += ` verify=${rejectUnauthorized}`; + + logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`); + } + } + + command += "\n"; + } else if (protocol === ProxyProtocol.Tcp) { + // For TCP mode, send FORWARD-TCP with host:port + command = `FORWARD-TCP ${targetHost}:${targetPort}\n`; + logger.debug(`Using TCP proxy mode: ${command.trim()}`); + } else { + throw new BadRequestError({ + message: `Invalid protocol: ${protocol as string}` + }); + } + + await forwardWriter.write(Buffer.from(command)); forwardWriter.releaseLock(); // Set up bidirectional copy @@ -320,7 +363,7 @@ const setupProxyServer = async ({ return; } - logger.info("Gateway proxy started"); + logger.info(`Gateway proxy started on port ${address.port} (${protocol} mode)`); resolve({ server, port: address.port, @@ -351,13 +394,15 @@ interface ProxyOptions { tlsOptions: TTlsOption; identityId: string; orgId: string; + protocol: ProxyProtocol; + httpsAgent?: https.Agent; } export const withGatewayProxy = async ( - callback: (port: number) => Promise, + callback: (port: number, httpsAgent?: https.Agent) => Promise, options: ProxyOptions ): Promise => { - const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; + const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId, protocol, httpsAgent } = options; // Setup the proxy server const { port, cleanup, getProxyError } = await setupProxyServer({ @@ -367,12 +412,14 @@ export const withGatewayProxy = async ( relayHost, tlsOptions, identityId, - orgId + orgId, + protocol, + httpsAgent }); try { // Execute the callback with the allocated port - return await callback(port); + return await callback(port, httpsAgent); } catch (err) { const proxyErrorMessage = getProxyError(); if (proxyErrorMessage) { diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index d9ef62087..3e244717d 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -8,6 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { IdentityKubernetesAuthTokenReviewMode } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({ @@ -18,6 +19,7 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick( accessTokenTrustedIps: true, createdAt: true, updatedAt: true, + tokenReviewMode: true, identityId: true, kubernetesHost: true, allowedNamespaces: true, @@ -124,6 +126,10 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide ), caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), + tokenReviewMode: z + .nativeEnum(IdentityKubernetesAuthTokenReviewMode) + .default(IdentityKubernetesAuthTokenReviewMode.Api) + .describe(KUBERNETES_AUTH.ATTACH.tokenReviewMode), allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), @@ -157,10 +163,22 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide .default(0) .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) }) - .refine( - (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, - "Access Token TTL cannot be greater than Access Token Max TTL." - ), + .superRefine((data, ctx) => { + if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) { + ctx.addIssue({ + path: ["gatewayId"], + code: z.ZodIssueCode.custom, + message: "When token review mode is set to Gateway, a gateway must be selected" + }); + } + if (data.accessTokenTTL > data.accessTokenMaxTTL) { + ctx.addIssue({ + path: ["accessTokenTTL"], + code: z.ZodIssueCode.custom, + message: "Access Token TTL cannot be greater than Access Token Max TTL." + }); + } + }), response: { 200: z.object({ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema @@ -186,6 +204,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide metadata: { identityId: identityKubernetesAuth.identityId, kubernetesHost: identityKubernetesAuth.kubernetesHost, + tokenReviewMode: identityKubernetesAuth.tokenReviewMode, allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNames: identityKubernetesAuth.allowedNames, accessTokenTTL: identityKubernetesAuth.accessTokenTTL, @@ -247,6 +266,10 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide ), caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), + tokenReviewMode: z + .nativeEnum(IdentityKubernetesAuthTokenReviewMode) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.tokenReviewMode), allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), @@ -280,10 +303,27 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide .optional() .describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL) }) - .refine( - (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), - "Access Token TTL cannot be greater than Access Token Max TTL." - ), + .superRefine((data, ctx) => { + if ( + data.tokenReviewMode && + data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && + !data.gatewayId && + data.tokenReviewMode !== undefined + ) { + ctx.addIssue({ + path: ["gatewayId"], + code: z.ZodIssueCode.custom, + message: "When token review mode is set to Gateway, a gateway must be selected" + }); + } + if (data.accessTokenMaxTTL && data.accessTokenTTL ? data.accessTokenTTL <= data.accessTokenMaxTTL : true) { + ctx.addIssue({ + path: ["accessTokenTTL"], + code: z.ZodIssueCode.custom, + message: "Access Token TTL cannot be greater than Access Token Max TTL." + }); + } + }), response: { 200: z.object({ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema @@ -308,6 +348,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide metadata: { identityId: identityKubernetesAuth.identityId, kubernetesHost: identityKubernetesAuth.kubernetesHost, + tokenReviewMode: identityKubernetesAuth.tokenReviewMode, + tokenReviewerJwt: identityKubernetesAuth.tokenReviewerJwt, allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNames: identityKubernetesAuth.allowedNames, accessTokenTTL: identityKubernetesAuth.accessTokenTTL, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 3314d8cab..bb24ddf62 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -20,8 +20,9 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; -import { withGatewayProxy } from "@app/lib/gateway"; +import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; @@ -33,6 +34,7 @@ import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/su import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; import { + IdentityKubernetesAuthTokenReviewMode, TAttachKubernetesAuthDTO, TCreateTokenReviewResponse, TGetKubernetesAuthDTO, @@ -72,19 +74,25 @@ export const identityKubernetesAuthServiceFactory = ({ gatewayId: string; targetHost: string; targetPort: number; + caCert?: string; + reviewTokenThroughGateway: boolean; }, - gatewayCallback: (host: string, port: number) => Promise + gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const callbackResult = await withGatewayProxy( - async (port) => { - // Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server" - const res = await gatewayCallback("https://localhost", port); + async (port, httpsAgent) => { + const res = await gatewayCallback( + inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost", + port, + httpsAgent + ); return res; }, { + protocol: inputs.reviewTokenThroughGateway ? ProxyProtocol.Http : ProxyProtocol.Tcp, targetHost: inputs.targetHost, targetPort: inputs.targetPort, relayHost, @@ -95,7 +103,12 @@ export const identityKubernetesAuthServiceFactory = ({ ca: relayDetails.certChain, cert: relayDetails.certificate, key: relayDetails.privateKey.toString() - } + }, + // we always pass this, because its needed for both tcp and http protocol + httpsAgent: new https.Agent({ + ca: inputs.caCert, + rejectUnauthorized: !!inputs.caCert + }) } ); @@ -129,17 +142,30 @@ export const identityKubernetesAuthServiceFactory = ({ caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } - let tokenReviewerJwt = ""; - if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { - tokenReviewerJwt = decryptor({ - cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt - }).toString(); - } else { - // if no token reviewer is provided means the incoming token has to act as reviewer - tokenReviewerJwt = serviceAccountJwt; - } + const tokenReviewCallbackRaw = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => { + logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API"); + let tokenReviewerJwt = ""; + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); + } else { + // if no token reviewer is provided means the incoming token has to act as reviewer + tokenReviewerJwt = serviceAccountJwt; + } + + let servername = identityKubernetesAuth.kubernetesHost; + if (servername.startsWith("https://") || servername.startsWith("http://")) { + servername = new RE2("^https?:\\/\\/").replace(servername, ""); + } + // get the last colon index, if it has a port, remove it, including the colon + const lastColonIndex = servername.lastIndexOf(":"); + if (lastColonIndex !== -1) { + servername = servername.substring(0, lastColonIndex); + } + + console.log("servername", servername); - const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => { const baseUrl = port ? `${host}:${port}` : host; const res = await axios @@ -160,10 +186,10 @@ export const identityKubernetesAuthServiceFactory = ({ }, signal: AbortSignal.timeout(10000), timeout: 10000, - // if ca cert, rejectUnauthorized: true httpsAgent: new https.Agent({ ca: caCert, - rejectUnauthorized: !!caCert + rejectUnauthorized: Boolean(caCert), + servername }) } ) @@ -186,24 +212,115 @@ export const identityKubernetesAuthServiceFactory = ({ return res.data; }; - let { kubernetesHost } = identityKubernetesAuth; + const tokenReviewCallbackThroughGateway = async ( + host: string = identityKubernetesAuth.kubernetesHost, + port?: number, + httpsAgent?: https.Agent + ) => { + logger.info( + { + host, + port + }, + "tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway" + ); - if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) { - kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, ""); + const baseUrl = port ? `${host}:${port}` : host; + + const res = await axios + .post( + `${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`, + { + apiVersion: "authentication.k8s.io/v1", + kind: "TokenReview", + spec: { + token: serviceAccountJwt, + ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) + } + }, + { + headers: { + "Content-Type": "application/json", + "x-infisical-action": "inject-k8s-sa-auth-token" + }, + signal: AbortSignal.timeout(10000), + timeout: 10000, + ...(httpsAgent ? { httpsAgent } : {}) + } + ) + .catch((err) => { + if (err instanceof AxiosError) { + if (err.response) { + const { message } = err?.response?.data as unknown as { message?: string }; + + if (message) { + throw new UnauthorizedError({ + message, + name: "KubernetesTokenReviewRequestError" + }); + } + } + } + throw err; + }); + + return res.data; + }; + + let data: TCreateTokenReviewResponse | undefined; + + if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { + const { kubernetesHost } = identityKubernetesAuth; + const lastColonIndex = kubernetesHost.lastIndexOf(":"); + const k8sHost = kubernetesHost.substring(0, lastColonIndex); + const k8sPort = kubernetesHost.substring(lastColonIndex + 1); + + if (!identityKubernetesAuth.gatewayId) { + throw new BadRequestError({ + message: "Gateway ID is required when token review mode is set to Gateway" + }); + } + + data = await $gatewayProxyWrapper( + { + gatewayId: identityKubernetesAuth.gatewayId, + targetHost: k8sHost, // note(daniel): must include the protocol (https|http) + targetPort: k8sPort ? Number(k8sPort) : 443, + caCert, + reviewTokenThroughGateway: true + }, + tokenReviewCallbackThroughGateway + ); + } else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) { + let { kubernetesHost } = identityKubernetesAuth; + if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) { + kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, ""); + } + + const [k8sHost, k8sPort] = kubernetesHost.split(":"); + + data = identityKubernetesAuth.gatewayId + ? await $gatewayProxyWrapper( + { + gatewayId: identityKubernetesAuth.gatewayId, + targetHost: k8sHost, + targetPort: k8sPort ? Number(k8sPort) : 443, + reviewTokenThroughGateway: false + }, + tokenReviewCallbackRaw + ) + : await tokenReviewCallbackRaw(); + } else { + throw new BadRequestError({ + message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}` + }); } - const [k8sHost, k8sPort] = kubernetesHost.split(":"); - - const data = identityKubernetesAuth.gatewayId - ? await $gatewayProxyWrapper( - { - gatewayId: identityKubernetesAuth.gatewayId, - targetHost: k8sHost, - targetPort: k8sPort ? Number(k8sPort) : 443 - }, - tokenReviewCallback - ) - : await tokenReviewCallback(); + if (!data) { + throw new BadRequestError({ + message: "Failed to review token" + }); + } if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); @@ -298,6 +415,7 @@ export const identityKubernetesAuthServiceFactory = ({ kubernetesHost, caCert, tokenReviewerJwt, + tokenReviewMode, allowedNamespaces, allowedNames, allowedAudience, @@ -384,6 +502,7 @@ export const identityKubernetesAuthServiceFactory = ({ { identityId: identityMembershipOrg.identityId, kubernetesHost, + tokenReviewMode, allowedNamespaces, allowedNames, allowedAudience, @@ -410,6 +529,7 @@ export const identityKubernetesAuthServiceFactory = ({ kubernetesHost, caCert, tokenReviewerJwt, + tokenReviewMode, allowedNamespaces, allowedNames, allowedAudience, @@ -492,6 +612,7 @@ export const identityKubernetesAuthServiceFactory = ({ const updateQuery: TIdentityKubernetesAuthsUpdate = { kubernetesHost, + tokenReviewMode, allowedNamespaces, allowedNames, allowedAudience, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index 12edd266f..03dd7fd77 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -5,11 +5,17 @@ export type TLoginKubernetesAuthDTO = { jwt: string; }; +export enum IdentityKubernetesAuthTokenReviewMode { + Api = "api", + Gateway = "gateway" +} + export type TAttachKubernetesAuthDTO = { identityId: string; kubernetesHost: string; caCert: string; tokenReviewerJwt?: string; + tokenReviewMode: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces: string; allowedNames: string; allowedAudience: string; @@ -26,6 +32,7 @@ export type TUpdateKubernetesAuthDTO = { kubernetesHost?: string; caCert?: string; tokenReviewerJwt?: string | null; + tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces?: string; allowedNames?: string; allowedAudience?: string; diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go index 58a0503ff..78f614f62 100644 --- a/cli/packages/gateway/connection.go +++ b/cli/packages/gateway/connection.go @@ -4,11 +4,18 @@ import ( "bufio" "bytes" "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" "errors" + "fmt" "io" "net" + "net/http" + "os" "strings" "sync" + "time" "github.com/quic-go/quic-go" "github.com/rs/zerolog/log" @@ -89,6 +96,34 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { CopyDataFromQuicToTcp(stream, destTarget) log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) return + + case "FORWARD-HTTP": + argParts := bytes.Split(args, []byte(" ")) + if len(argParts) == 0 { + log.Error().Msg("FORWARD-HTTP requires target URL") + return + } + + targetURL := string(argParts[0]) + + // Parse optional parameters + var caCertB64, verifyParam string + for _, part := range argParts[1:] { + partStr := string(part) + if strings.HasPrefix(partStr, "ca=") { + caCertB64 = strings.TrimPrefix(partStr, "ca=") + } else if strings.HasPrefix(partStr, "verify=") { + verifyParam = strings.TrimPrefix(partStr, "verify=") + } + } + + log.Info().Msgf("Starting HTTP proxy to: %s", targetURL) + + if err := handleHTTPProxy(stream, reader, targetURL, caCertB64, verifyParam); err != nil { + log.Error().Msgf("HTTP proxy error: %v", err) + } + return + case "PING": if _, err := stream.Write([]byte("PONG\n")); err != nil { log.Error().Msgf("Error writing PONG response: %v", err) @@ -100,6 +135,135 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { } } } +func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, caCertB64 string, verifyParam string) error { + transport := &http.Transport{ + DisableKeepAlives: false, + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + } + + if strings.HasPrefix(targetURL, "https://") { + tlsConfig := &tls.Config{} + + if caCertB64 != "" { + caCert, err := base64.StdEncoding.DecodeString(caCertB64) + if err == nil { + caCertPool := x509.NewCertPool() + if caCertPool.AppendCertsFromPEM(caCert) { + tlsConfig.RootCAs = caCertPool + log.Info().Msg("Using provided CA certificate from gateway client") + } else { + log.Error().Msg("Failed to parse provided CA certificate") + } + } else { + log.Error().Msgf("Failed to decode CA certificate: %v", err) + } + } + + // set certificate verification based on what the gateway client sent + if verifyParam != "" { + tlsConfig.InsecureSkipVerify = verifyParam == "false" + log.Info().Msgf("TLS verification set to: %s", verifyParam) + } + + transport.TLSClientConfig = tlsConfig + } + + // read and parse the http request from the stream + req, err := http.ReadRequest(reader) + if err != nil { + return fmt.Errorf("failed to read HTTP request: %v", err) + } + + actionHeader := req.Header.Get("x-infisical-action") + if actionHeader != "" { + + if actionHeader == "inject-k8s-sa-auth-token" { + token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + + if err != nil { + stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token"))) + return fmt.Errorf("failed to read k8s sa auth token: %v", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token))) + log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL) + } + + req.Header.Del("x-infisical-action") + } + + var targetFullURL string + if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") { + baseURL := strings.TrimSuffix(targetURL, "/") + targetFullURL = baseURL + req.URL.Path + if req.URL.RawQuery != "" { + targetFullURL += "?" + req.URL.RawQuery + } + } else { + baseURL := strings.TrimSuffix("http://"+targetURL, "/") + targetFullURL = baseURL + req.URL.Path + if req.URL.RawQuery != "" { + targetFullURL += "?" + req.URL.RawQuery + } + } + + // create the request to the target + proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body) + if err != nil { + return fmt.Errorf("failed to create proxy request: %v", err) + } + + // copy headers + for name, values := range req.Header { + for _, value := range values { + proxyReq.Header.Add(name, value) + } + } + + log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL) + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + // make the request to the target + resp, err := client.Do(proxyReq) + if err != nil { + stream.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error())))) + return fmt.Errorf("failed to reach target due to networking error: %v", err) + } + defer resp.Body.Close() + + // write response to stream + statusLine := fmt.Sprintf("HTTP/1.1 %d %s\r\n", resp.StatusCode, resp.Status[4:]) + if _, err := stream.Write([]byte(statusLine)); err != nil { + return err + } + + // write headers again + for name, values := range resp.Header { + for _, value := range values { + headerLine := fmt.Sprintf("%s: %s\r\n", name, value) + if _, err := stream.Write([]byte(headerLine)); err != nil { + return err + } + } + } + + // write empty line to end headers + if _, err := stream.Write([]byte("\r\n")); err != nil { + return err + } + + _, err = io.Copy(stream, resp.Body) + return err +} + +func buildHttpInternalServerError(message string) string { + return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message) +} type CloseWrite interface { CloseWrite() error diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index c08081f11..a7bd88ce9 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -843,7 +843,8 @@ export const useAddIdentityKubernetesAuth = () => { accessTokenMaxTTL, accessTokenNumUsesLimit, accessTokenTrustedIps, - gatewayId + gatewayId, + tokenReviewMode }) => { const { data: { identityKubernetesAuth } @@ -860,7 +861,8 @@ export const useAddIdentityKubernetesAuth = () => { accessTokenMaxTTL, accessTokenNumUsesLimit, accessTokenTrustedIps, - gatewayId + gatewayId, + tokenReviewMode } ); @@ -950,7 +952,8 @@ export const useUpdateIdentityKubernetesAuth = () => { accessTokenMaxTTL, accessTokenNumUsesLimit, accessTokenTrustedIps, - gatewayId + gatewayId, + tokenReviewMode }) => { const { data: { identityKubernetesAuth } @@ -967,7 +970,8 @@ export const useUpdateIdentityKubernetesAuth = () => { accessTokenMaxTTL, accessTokenNumUsesLimit, accessTokenTrustedIps, - gatewayId + gatewayId, + tokenReviewMode } ); diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index c26466213..f5e0f1065 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -379,10 +379,16 @@ export type DeleteIdentityAzureAuthDTO = { identityId: string; }; +export enum IdentityKubernetesAuthTokenReviewMode { + Api = "api", + Gateway = "gateway" +} + export type IdentityKubernetesAuth = { identityId: string; kubernetesHost: string; tokenReviewerJwt: string; + tokenReviewMode: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces: string; allowedNames: string; allowedAudience: string; @@ -399,6 +405,7 @@ export type AddIdentityKubernetesAuthDTO = { identityId: string; kubernetesHost: string; tokenReviewerJwt?: string; + tokenReviewMode: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces: string; allowedNames: string; allowedAudience: string; @@ -417,6 +424,7 @@ export type UpdateIdentityKubernetesAuthDTO = { identityId: string; kubernetesHost?: string; tokenReviewerJwt?: string | null; + tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces?: string; allowedNames?: string; allowedAudience?: string; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 369977a04..1b55bc60e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -33,13 +33,19 @@ import { useGetIdentityKubernetesAuth, useUpdateIdentityKubernetesAuth } from "@app/hooks/api"; -import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { + IdentityKubernetesAuthTokenReviewMode, + IdentityTrustedIp +} from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityFormTab } from "./types"; const schema = z .object({ + tokenReviewMode: z + .nativeEnum(IdentityKubernetesAuthTokenReviewMode) + .default(IdentityKubernetesAuthTokenReviewMode.Api), kubernetesHost: z.string().min(1), tokenReviewerJwt: z.string().optional(), gatewayId: z.string().optional().nullable(), @@ -62,7 +68,15 @@ const schema = z ) .min(1) }) - .required(); + .superRefine((data, ctx) => { + if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) { + ctx.addIssue({ + path: ["gatewayId"], + code: z.ZodIssueCode.custom, + message: "When token review mode is set to Gateway, a gateway must be selected" + }); + } + }); export type FormData = z.infer; @@ -100,11 +114,14 @@ export const IdentityKubernetesAuthForm = ({ control, handleSubmit, reset, + watch, + setValue, formState: { isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { + tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api, kubernetesHost: "", tokenReviewerJwt: "", allowedNames: "", @@ -128,6 +145,7 @@ export const IdentityKubernetesAuthForm = ({ useEffect(() => { if (data) { reset({ + tokenReviewMode: data.tokenReviewMode, kubernetesHost: data.kubernetesHost, tokenReviewerJwt: data.tokenReviewerJwt, allowedNames: data.allowedNames, @@ -148,6 +166,7 @@ export const IdentityKubernetesAuthForm = ({ }); } else { reset({ + tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api, kubernetesHost: "", tokenReviewerJwt: "", allowedNames: "", @@ -173,6 +192,7 @@ export const IdentityKubernetesAuthForm = ({ accessTokenMaxTTL, accessTokenNumUsesLimit, gatewayId, + tokenReviewMode, accessTokenTrustedIps }: FormData) => { try { @@ -189,6 +209,7 @@ export const IdentityKubernetesAuthForm = ({ caCert, identityId, gatewayId: gatewayId || null, + tokenReviewMode, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -205,6 +226,7 @@ export const IdentityKubernetesAuthForm = ({ allowedAudience: allowedAudience || "", gatewayId: gatewayId || null, caCert: caCert || "", + tokenReviewMode, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -228,6 +250,8 @@ export const IdentityKubernetesAuthForm = ({ } }; + const tokenReviewMode = watch("tokenReviewMode"); + return (
{ @@ -235,6 +259,7 @@ export const IdentityKubernetesAuthForm = ({ [ "kubernetesHost", "tokenReviewerJwt", + "tokenReviewMode", "gatewayId", "accessTokenTTL", "accessTokenMaxTTL", @@ -269,21 +294,114 @@ export const IdentityKubernetesAuthForm = ({ )} /> - ( - +
+ - - - )} - /> + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
+
+ + ( + + + + )} + /> + + + {tokenReviewMode === "api" && ( + ( + + + + )} + /> + )} - - {(isAllowed) => ( - ( - - -
- -
-
-
- )} - /> - )} -
- Date: Thu, 5 Jun 2025 04:45:57 +0400 Subject: [PATCH 05/30] cleanup --- backend/src/db/schemas/identity-kubernetes-auths.ts | 5 +++-- .../identity-kubernetes-auth-service.ts | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index 880928247..251ae685e 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -31,8 +31,9 @@ export const IdentityKubernetesAuthsSchema = z.object({ encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), - tokenReviewMode: z.string().default("api"), - accessTokenPeriod: z.coerce.number().default(0) + accessTokenPeriod: z.coerce.number().default(0), + useGatewayAsReviewer: z.boolean().default(false), + tokenReviewMode: z.string().default("api") }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index bb24ddf62..59c062686 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -164,8 +164,6 @@ export const identityKubernetesAuthServiceFactory = ({ servername = servername.substring(0, lastColonIndex); } - console.log("servername", servername); - const baseUrl = port ? `${host}:${port}` : host; const res = await axios From f2a55da9b62afb6def5d9893512bb8a6e8c8dbe5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 04:49:50 +0400 Subject: [PATCH 06/30] Update .infisicalignore --- .infisicalignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.infisicalignore b/.infisicalignore index 02cdd4f0e..7c203945f 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -40,3 +40,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:578 cli/detect/config/gitleaks.toml:gcp-api-key:579 cli/detect/config/gitleaks.toml:gcp-api-key:581 cli/detect/config/gitleaks.toml:gcp-api-key:582 +backend/src/services/smtp/smtp-service.ts:generic-api-key:79 From c34a139b198d34ff7758aaf3b969c9fbf2f403ef Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 05:02:58 +0400 Subject: [PATCH 07/30] cleanup --- backend/src/db/schemas/identity-kubernetes-auths.ts | 1 - backend/src/ee/services/dynamic-secret/providers/vertica.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index 251ae685e..8a351014a 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -32,7 +32,6 @@ export const IdentityKubernetesAuthsSchema = z.object({ encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), accessTokenPeriod: z.coerce.number().default(0), - useGatewayAsReviewer: z.boolean().default(false), tokenReviewMode: z.string().default("api") }); diff --git a/backend/src/ee/services/dynamic-secret/providers/vertica.ts b/backend/src/ee/services/dynamic-secret/providers/vertica.ts index 9e283ab41..046b73017 100644 --- a/backend/src/ee/services/dynamic-secret/providers/vertica.ts +++ b/backend/src/ee/services/dynamic-secret/providers/vertica.ts @@ -4,7 +4,7 @@ import knex, { Knex } from "knex"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; -import { withGatewayProxy } from "@app/lib/gateway"; +import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -196,6 +196,7 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam await gatewayCallback("localhost", port); }, { + protocol: ProxyProtocol.Tcp, targetHost: providerInputs.host, targetPort: providerInputs.port, relayHost, From a4c21d85aca21f5b94d50015df90e6cfb813b330 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 05:07:58 +0400 Subject: [PATCH 08/30] Update identity-kubernetes-auth-router.ts --- backend/src/server/routes/v1/identity-kubernetes-auth-router.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 3e244717d..724880638 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -349,7 +349,6 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide identityId: identityKubernetesAuth.identityId, kubernetesHost: identityKubernetesAuth.kubernetesHost, tokenReviewMode: identityKubernetesAuth.tokenReviewMode, - tokenReviewerJwt: identityKubernetesAuth.tokenReviewerJwt, allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNames: identityKubernetesAuth.allowedNames, accessTokenTTL: identityKubernetesAuth.accessTokenTTL, From 30d7e63a67fcedf3399bb2ef346371052bf0ff49 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 4 Jun 2025 21:20:16 -0400 Subject: [PATCH 09/30] Add {{environment}} support for key schemas --- .../oci-vault/oci-vault-sync-fns.ts | 3 +- .../1password/1password-sync-fns.ts | 3 +- .../aws-parameter-store-sync-fns.ts | 4 +- .../aws-secrets-manager-sync-fns.ts | 24 ++++++--- .../azure-app-configuration-sync-fns.ts | 2 +- .../azure-key-vault-sync-fns.ts | 2 +- .../secret-sync/camunda/camunda-sync-fns.ts | 2 +- .../databricks/databricks-sync-fns.ts | 2 +- .../services/secret-sync/gcp/gcp-sync-fns.ts | 2 +- .../secret-sync/github/github-sync-fns.ts | 3 +- .../secret-sync/hc-vault/hc-vault-sync-fns.ts | 3 +- .../humanitec/humanitec-sync-fns.ts | 3 +- .../services/secret-sync/secret-sync-fns.ts | 50 ++++++++++++++----- .../secret-sync/secret-sync-schemas.ts | 27 ++++++++-- .../secret-sync/teamcity/teamcity-sync-fns.ts | 2 +- .../terraform-cloud-sync-fns.ts | 7 ++- .../secret-sync/vercel/vercel-sync-fns.ts | 5 +- .../secret-sync/windmill/windmill-sync-fns.ts | 3 +- docs/integrations/secret-syncs/1password.mdx | 2 +- .../secret-syncs/aws-parameter-store.mdx | 2 +- .../secret-syncs/aws-secrets-manager.mdx | 2 +- .../secret-syncs/azure-app-configuration.mdx | 2 +- .../secret-syncs/azure-key-vault.mdx | 2 +- docs/integrations/secret-syncs/camunda.mdx | 2 +- docs/integrations/secret-syncs/databricks.mdx | 2 +- .../secret-syncs/gcp-secret-manager.mdx | 2 +- docs/integrations/secret-syncs/github.mdx | 2 +- .../secret-syncs/hashicorp-vault.mdx | 2 +- docs/integrations/secret-syncs/humanitec.mdx | 2 +- docs/integrations/secret-syncs/oci-vault.mdx | 2 +- docs/integrations/secret-syncs/teamcity.mdx | 2 +- .../secret-syncs/terraform-cloud.mdx | 2 +- docs/integrations/secret-syncs/vercel.mdx | 2 +- docs/integrations/secret-syncs/windmill.mdx | 2 +- .../forms/schemas/base-secret-sync-schema.ts | 22 ++++++-- 35 files changed, 140 insertions(+), 61 deletions(-) diff --git a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts index 5b05b2301..5fb39a0ec 100644 --- a/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -117,6 +117,7 @@ export const OCIVaultSyncFns = { syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { compartmentOcid, vaultOcid, keyOcid } } = secretSync; @@ -213,7 +214,7 @@ export const OCIVaultSyncFns = { // Update and delete secrets for await (const [key, variable] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; // Only update / delete active secrets if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { diff --git a/backend/src/services/secret-sync/1password/1password-sync-fns.ts b/backend/src/services/secret-sync/1password/1password-sync-fns.ts index c832fbbdb..9305f2e3c 100644 --- a/backend/src/services/secret-sync/1password/1password-sync-fns.ts +++ b/backend/src/services/secret-sync/1password/1password-sync-fns.ts @@ -127,6 +127,7 @@ export const OnePassSyncFns = { syncSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { vaultId } } = secretSync; @@ -164,7 +165,7 @@ export const OnePassSyncFns = { for await (const [key, variable] of Object.entries(items)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(key in secretMap)) { try { diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index a73bc81c9..b687d81dd 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -294,7 +294,7 @@ const deleteParametersBatch = async ( export const AwsParameterStoreSyncFns = { syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, syncOptions } = secretSync; + const { destinationConfig, syncOptions, environment } = secretSync; const ssm = await getSSM(secretSync); @@ -391,7 +391,7 @@ export const AwsParameterStoreSyncFns = { const [key, parameter] = entry; // eslint-disable-next-line no-continue - if (!matchesSchema(key, syncOptions.keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", syncOptions.keySchema)) continue; if (!(key in secretMap) || !secretMap[key].value) { parametersToDelete.push(parameter); diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts index 1b1daf2ac..df73512e5 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -57,7 +57,11 @@ const sleep = async () => setTimeout(resolve, 1000); }); -const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string): Promise => { +const getSecretsRecord = async ( + client: SecretsManagerClient, + environment: string, + keySchema?: string +): Promise => { const awsSecretsRecord: TAwsSecretsRecord = {}; let hasNext = true; let nextToken: string | undefined; @@ -72,7 +76,7 @@ const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string if (output.SecretList) { output.SecretList.forEach((secretEntry) => { - if (secretEntry.Name && matchesSchema(secretEntry.Name, keySchema)) { + if (secretEntry.Name && matchesSchema(secretEntry.Name, environment, keySchema)) { awsSecretsRecord[secretEntry.Name] = secretEntry; } }); @@ -307,11 +311,11 @@ const processTags = ({ export const AwsSecretsManagerSyncFns = { syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, syncOptions } = secretSync; + const { destinationConfig, syncOptions, environment } = secretSync; const client = await getSecretsManagerClient(secretSync); - const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema); + const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema); const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); @@ -401,7 +405,7 @@ export const AwsSecretsManagerSyncFns = { for await (const secretKey of Object.keys(awsSecretsRecord)) { // eslint-disable-next-line no-continue - if (!matchesSchema(secretKey, syncOptions.keySchema)) continue; + if (!matchesSchema(secretKey, environment?.slug || "", syncOptions.keySchema)) continue; if (!(secretKey in secretMap) || !secretMap[secretKey].value) { try { @@ -468,7 +472,11 @@ export const AwsSecretsManagerSyncFns = { getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise => { const client = await getSecretsManagerClient(secretSync); - const awsSecretsRecord = await getSecretsRecord(client, secretSync.syncOptions.keySchema); + const awsSecretsRecord = await getSecretsRecord( + client, + secretSync.environment?.slug || "", + secretSync.syncOptions.keySchema + ); const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); const { destinationConfig } = secretSync; @@ -503,11 +511,11 @@ export const AwsSecretsManagerSyncFns = { } }, removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { - const { destinationConfig, syncOptions } = secretSync; + const { destinationConfig, syncOptions, environment } = secretSync; const client = await getSecretsManagerClient(secretSync); - const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema); + const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema); if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { for await (const secretKey of Object.keys(awsSecretsRecord)) { diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts index dce509fac..7aa1c16ce 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -141,7 +141,7 @@ export const azureAppConfigurationSyncFactory = ({ for await (const key of Object.keys(azureAppConfigSecrets)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; const azureSecret = azureAppConfigSecrets[key]; if ( diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts index fd1e2bd78..edc8af709 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -194,7 +194,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur for await (const deleteSecretKey of deleteSecrets.filter( (secret) => - matchesSchema(secret, secretSync.syncOptions.keySchema) && + matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) && !setSecrets.find((setSecret) => setSecret.key === secret) )) { await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts index 256ae4644..516efae10 100644 --- a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts +++ b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts @@ -118,7 +118,7 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec for await (const secret of Object.keys(camundaSecrets)) { // eslint-disable-next-line no-continue - if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(secret in secretMap) || !secretMap[secret].value) { try { diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts index 11143e24d..175901323 100644 --- a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -117,7 +117,7 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri for await (const secret of databricksSecretKeys) { // eslint-disable-next-line no-continue - if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(secret.key in secretMap)) { await deleteDatabricksSecrets({ diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index 97da66a48..6a45aab31 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -155,7 +155,7 @@ export const GcpSyncFns = { for await (const key of Object.keys(gcpSecrets)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; try { if (!(key in secretMap) || !secretMap[key].value) { diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 952f4b512..dd950692a 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -224,7 +224,8 @@ export const GithubSyncFns = { for await (const encryptedSecret of encryptedSecrets) { // eslint-disable-next-line no-continue - if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + continue; if (!(encryptedSecret.name in secretMap)) { await deleteSecret(client, secretSync, encryptedSecret); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts index 6331cd91f..724eec7be 100644 --- a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -68,6 +68,7 @@ export const HCVaultSyncFns = { syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { mount, path }, syncOptions: { disableSecretDeletion, keySchema } } = secretSync; @@ -97,7 +98,7 @@ export const HCVaultSyncFns = { for await (const [key] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; if (!(key in secretMap)) { delete variables[key]; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts index 2fcf488aa..aeef95047 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -201,7 +201,8 @@ export const HumanitecSyncFns = { for await (const humanitecSecret of humanitecSecrets) { // eslint-disable-next-line no-continue - if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(humanitecSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + continue; if (!secretMap[humanitecSecret.key]) { await deleteSecret(secretSync, humanitecSecret); diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index dbf3a3699..8e99aabb8 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -1,5 +1,5 @@ import { AxiosError } from "axios"; -import RE2 from "re2"; +import handlebars from "handlebars"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault"; @@ -68,13 +68,17 @@ type TSyncSecretDeps = { }; // Add schema to secret keys -const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => { +const addSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => { if (!schema) return unprocessedSecretMap; const processedSecretMap: TSecretMap = {}; for (const [key, value] of Object.entries(unprocessedSecretMap)) { - const newKey = new RE2("{{secretKey}}").replace(schema, key); + const newKey = handlebars.compile(schema)({ + secretKey: key, + environment + }); + processedSecretMap[newKey] = value; } @@ -103,21 +107,40 @@ const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecret }; // Checks if a key matches a schema -export const matchesSchema = (key: string, schema?: string): boolean => { +export const matchesSchema = (key: string, environment: string, schema?: string): boolean => { if (!schema) return true; - const [prefix, suffix] = schema.split("{{secretKey}}"); - if (prefix === undefined || suffix === undefined) return true; + const compiledSchemaPattern = handlebars.compile(schema)({ + secretKey: "{{secretKey}}", // Keep secretKey + environment + }); - return key.startsWith(prefix) && key.endsWith(suffix); + // This edge-case shouldn't be possible + if (!compiledSchemaPattern.includes("{{secretKey}}")) { + return key === compiledSchemaPattern; + } + + const parts = compiledSchemaPattern.split("{{secretKey}}"); + const prefix = parts[0]; + const suffix = parts[parts.length - 1]; + + if (prefix === "" && suffix === "") return true; + + // If prefix is empty, key must end with suffix + if (prefix === "") return key.endsWith(suffix); + + // If suffix is empty, key must start with prefix + if (suffix === "") return key.startsWith(prefix); + + return key.startsWith(prefix) && key.endsWith(suffix) && key.length >= prefix.length + suffix.length; }; // Filter only for secrets with keys that match the schema -const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => { +const filterForSchema = (secretMap: TSecretMap, environment: string, schema?: string): TSecretMap => { const filteredMap: TSecretMap = {}; for (const [key, value] of Object.entries(secretMap)) { - if (matchesSchema(key, schema)) { + if (matchesSchema(key, environment, schema)) { filteredMap[key] = value; } } @@ -131,7 +154,7 @@ export const SecretSyncFns = { secretMap: TSecretMap, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema); + const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); switch (secretSync.destination) { case SecretSync.AWSParameterStore: @@ -255,14 +278,17 @@ export const SecretSyncFns = { ); } - return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema); + return stripSchema( + filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema), + secretSync.syncOptions.keySchema + ); }, removeSecrets: ( secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema); + const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); switch (secretSync.destination) { case SecretSync.AWSParameterStore: diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 80e96bf8b..5b599215f 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -28,10 +28,29 @@ const BaseSyncOptionsSchema = ({ keySchema: z .string() .optional() - .refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), { - message: - "Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder." - }) + .refine( + (val) => { + if (!val) return true; + + const allowedOptionalPlaceholders = ["{{environment}}"]; + + const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders] + .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters + .join("|"); + + const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`); + const contentIsValid = allowedContentRegex.test(val); + + // Check if {{secretKey}} appears exactly once + const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length; + + return contentIsValid && secretKeyCount === 1; + }, + { + message: + "Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." + } + ) .describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema), disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) }); diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts index 0afe29beb..28ef2d0d3 100644 --- a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts @@ -127,7 +127,7 @@ export const TeamCitySyncFns = { for await (const [key, variable] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!(key in secretMap)) { try { diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts index a58ec213c..cb546ba63 100644 --- a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts @@ -232,8 +232,11 @@ export const TerraformCloudSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for (const terraformCloudVariable of terraformCloudVariables) { - // eslint-disable-next-line no-continue - if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue; + if ( + !matchesSchema(terraformCloudVariable.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) + ) + // eslint-disable-next-line no-continue + continue; if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) { await deleteVariable(secretSync, terraformCloudVariable); diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts index 90e9327e5..b5ea98265 100644 --- a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts +++ b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts @@ -291,8 +291,9 @@ export const VercelSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const vercelSecret of vercelSecrets) { - // eslint-disable-next-line no-continue - if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue; + if (!matchesSchema(vercelSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + // eslint-disable-next-line no-continue + continue; if (!secretMap[vercelSecret.key]) { await deleteSecret(secretSync, vercelSecret); diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts index a09706581..b5e11c957 100644 --- a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts +++ b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts @@ -128,6 +128,7 @@ export const WindmillSyncFns = { syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => { const { connection, + environment, destinationConfig: { path }, syncOptions: { disableSecretDeletion, keySchema } } = secretSync; @@ -171,7 +172,7 @@ export const WindmillSyncFns = { for await (const [key, variable] of Object.entries(variables)) { // eslint-disable-next-line no-continue - if (!matchesSchema(key, keySchema)) continue; + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; if (!(key in secretMap)) { try { diff --git a/docs/integrations/secret-syncs/1password.mdx b/docs/integrations/secret-syncs/1password.mdx index a33f54c8d..c259e57bf 100644 --- a/docs/integrations/secret-syncs/1password.mdx +++ b/docs/integrations/secret-syncs/1password.mdx @@ -46,7 +46,7 @@ description: "Learn how to configure a 1Password Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over 1Password when keys conflict. - **Import Secrets (Prioritize 1Password)**: Imports secrets from the destination endpoint before syncing, prioritizing values from 1Password over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 11f0c94ad..b1ceade0d 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index f7654eeae..e4c85e21b 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -43,7 +43,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index ee47504bc..cc1da4249 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -48,7 +48,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index 609ba8b8d..b676bc513 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -51,7 +51,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/camunda.mdx b/docs/integrations/secret-syncs/camunda.mdx index df57a5b7d..ee97ed6ac 100644 --- a/docs/integrations/secret-syncs/camunda.mdx +++ b/docs/integrations/secret-syncs/camunda.mdx @@ -39,7 +39,7 @@ description: "Learn how to configure a Camunda Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict. - **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx index 225bad5b1..5a303d9b0 100644 --- a/docs/integrations/secret-syncs/databricks.mdx +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -46,7 +46,7 @@ description: "Learn how to configure a Databricks Sync for Infisical." Databricks does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index ace63787d..5f1fcc326 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -42,7 +42,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict. - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 7786567cc..349680779 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -62,7 +62,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." GitHub does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/hashicorp-vault.mdx b/docs/integrations/secret-syncs/hashicorp-vault.mdx index 48e4d8dfd..143303a00 100644 --- a/docs/integrations/secret-syncs/hashicorp-vault.mdx +++ b/docs/integrations/secret-syncs/hashicorp-vault.mdx @@ -54,7 +54,7 @@ description: "Learn how to configure a Hashicorp Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Hashicorp Vault when keys conflict. - **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx index ec36bd4da..208c75df9 100644 --- a/docs/integrations/secret-syncs/humanitec.mdx +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -55,7 +55,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical." Humanitec does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 00b7120e7..db5abfb91 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -57,7 +57,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict. - **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/teamcity.mdx b/docs/integrations/secret-syncs/teamcity.mdx index 3482101ca..f5b2bf933 100644 --- a/docs/integrations/secret-syncs/teamcity.mdx +++ b/docs/integrations/secret-syncs/teamcity.mdx @@ -48,7 +48,7 @@ description: "Learn how to configure a TeamCity Sync for Infisical." Infisical only syncs secrets from within the target scope; inherited secrets will not be imported. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx index d2f762ef1..d8399db10 100644 --- a/docs/integrations/secret-syncs/terraform-cloud.mdx +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -56,7 +56,7 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical." Terraform Cloud does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/vercel.mdx b/docs/integrations/secret-syncs/vercel.mdx index c903d3faa..4553b46c6 100644 --- a/docs/integrations/secret-syncs/vercel.mdx +++ b/docs/integrations/secret-syncs/vercel.mdx @@ -43,7 +43,7 @@ description: "Learn how to configure a Vercel Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict. - **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/windmill.mdx b/docs/integrations/secret-syncs/windmill.mdx index e98a2c7b6..56dda245d 100644 --- a/docs/integrations/secret-syncs/windmill.mdx +++ b/docs/integrations/secret-syncs/windmill.mdx @@ -44,7 +44,7 @@ description: "Learn how to configure a Windmill Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Windmill when keys conflict. - **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts index 75a5b68c1..1d925eb07 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts @@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = - !val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val), + (val) => { + if (!val) return true; + + const allowedOptionalPlaceholders = ["{{environment}}"]; + + const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders] + .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters + .join("|"); + + const allowedContentRegex = new RegExp( + `^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$` + ); + const contentIsValid = allowedContentRegex.test(val); + + const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length; + + return contentIsValid && secretKeyCount === 1; + }, { message: - "Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder." + "Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." } ) }); From caa129b5653819b79d149e27ce72c7577d96d06c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 05:23:30 +0400 Subject: [PATCH 10/30] requested changes --- .../src/server/routes/v1/identity-kubernetes-auth-router.ts | 5 ++--- .../identity-kubernetes-auth-service.ts | 6 +++++- cli/packages/util/helper.go | 2 +- .../IdentitySection/IdentityKubernetesAuthForm.tsx | 1 - 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 724880638..743c5b321 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -307,8 +307,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide if ( data.tokenReviewMode && data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && - !data.gatewayId && - data.tokenReviewMode !== undefined + !data.gatewayId ) { ctx.addIssue({ path: ["gatewayId"], @@ -316,7 +315,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide message: "When token review mode is set to Gateway, a gateway must be selected" }); } - if (data.accessTokenMaxTTL && data.accessTokenTTL ? data.accessTokenTTL <= data.accessTokenMaxTTL : true) { + if (data.accessTokenMaxTTL && data.accessTokenTTL ? data.accessTokenTTL > data.accessTokenMaxTTL : false) { ctx.addIssue({ path: ["accessTokenTTL"], code: z.ZodIssueCode.custom, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 59c062686..4eb45ba14 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -249,7 +249,11 @@ export const identityKubernetesAuthServiceFactory = ({ .catch((err) => { if (err instanceof AxiosError) { if (err.response) { - const { message } = err?.response?.data as unknown as { message?: string }; + let { message } = err?.response?.data as unknown as { message?: string }; + + if (!message && typeof err.response.data === "string") { + message = err.response.data; + } if (message) { throw new UnauthorizedError({ diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index ceb0792f1..fc3f994a7 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -299,7 +299,7 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag string, envNames []string) (string } if value == "" { for _, env := range envNames { - value = os.Getenv(env) + value = strings.TrimSpace(os.Getenv(env)) if value != "" { break } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 1b55bc60e..e43c94a7e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -322,7 +322,6 @@ export const IdentityKubernetesAuthForm = ({ isDisabled={!isAllowed} value={value as string} onValueChange={(v) => { - console.log("v", v); if (v !== "") { onChange(v); } From e7fe2ea51ef4529c0c8f819611fac4e7d941ec31 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 4 Jun 2025 21:35:17 -0400 Subject: [PATCH 11/30] Fix lint issues --- backend/src/services/secret-sync/github/github-sync-fns.ts | 2 +- .../src/services/secret-sync/humanitec/humanitec-sync-fns.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index dd950692a..f06f0cfc2 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -223,8 +223,8 @@ export const GithubSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const encryptedSecret of encryptedSecrets) { - // eslint-disable-next-line no-continue if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + // eslint-disable-next-line no-continue continue; if (!(encryptedSecret.name in secretMap)) { diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts index aeef95047..ccb6ac2bc 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -200,8 +200,8 @@ export const HumanitecSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const humanitecSecret of humanitecSecrets) { - // eslint-disable-next-line no-continue if (!matchesSchema(humanitecSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + // eslint-disable-next-line no-continue continue; if (!secretMap[humanitecSecret.key]) { From 1a14c71564bf206e82ee718bf7171cd410d1f324 Mon Sep 17 00:00:00 2001 From: x032205 Date: Wed, 4 Jun 2025 21:41:21 -0400 Subject: [PATCH 12/30] Greptile review fixes --- backend/src/services/secret-sync/secret-sync-schemas.ts | 7 ++++--- docs/integrations/secret-syncs/1password.mdx | 2 +- docs/integrations/secret-syncs/aws-parameter-store.mdx | 2 +- docs/integrations/secret-syncs/aws-secrets-manager.mdx | 2 +- docs/integrations/secret-syncs/azure-app-configuration.mdx | 2 +- docs/integrations/secret-syncs/azure-key-vault.mdx | 2 +- docs/integrations/secret-syncs/camunda.mdx | 2 +- docs/integrations/secret-syncs/databricks.mdx | 2 +- docs/integrations/secret-syncs/gcp-secret-manager.mdx | 2 +- docs/integrations/secret-syncs/github.mdx | 2 +- docs/integrations/secret-syncs/hashicorp-vault.mdx | 2 +- docs/integrations/secret-syncs/humanitec.mdx | 2 +- docs/integrations/secret-syncs/oci-vault.mdx | 2 +- docs/integrations/secret-syncs/teamcity.mdx | 2 +- docs/integrations/secret-syncs/terraform-cloud.mdx | 2 +- docs/integrations/secret-syncs/vercel.mdx | 2 +- docs/integrations/secret-syncs/windmill.mdx | 2 +- 17 files changed, 20 insertions(+), 19 deletions(-) diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 5b599215f..3622ef3d0 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -41,10 +41,11 @@ const BaseSyncOptionsSchema = ({ const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`); const contentIsValid = allowedContentRegex.test(val); - // Check if {{secretKey}} appears exactly once - const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length; + // Check if {{secretKey}} is present + const secretKeyRegex = new RE2(/\{\{secretKey\}\}/); + const secretKeyIsPresent = secretKeyRegex.test(val); - return contentIsValid && secretKeyCount === 1; + return contentIsValid && secretKeyIsPresent; }, { message: diff --git a/docs/integrations/secret-syncs/1password.mdx b/docs/integrations/secret-syncs/1password.mdx index c259e57bf..6e2b96b4a 100644 --- a/docs/integrations/secret-syncs/1password.mdx +++ b/docs/integrations/secret-syncs/1password.mdx @@ -46,7 +46,7 @@ description: "Learn how to configure a 1Password Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over 1Password when keys conflict. - **Import Secrets (Prioritize 1Password)**: Imports secrets from the destination endpoint before syncing, prioritizing values from 1Password over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index b1ceade0d..abc52d971 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index e4c85e21b..91c606b0a 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -43,7 +43,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index cc1da4249..f4aaa7edd 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -48,7 +48,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index b676bc513..d19a0162e 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -51,7 +51,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/camunda.mdx b/docs/integrations/secret-syncs/camunda.mdx index ee97ed6ac..0e977aa27 100644 --- a/docs/integrations/secret-syncs/camunda.mdx +++ b/docs/integrations/secret-syncs/camunda.mdx @@ -39,7 +39,7 @@ description: "Learn how to configure a Camunda Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict. - **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx index 5a303d9b0..e11537420 100644 --- a/docs/integrations/secret-syncs/databricks.mdx +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -46,7 +46,7 @@ description: "Learn how to configure a Databricks Sync for Infisical." Databricks does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index 5f1fcc326..df490c6b5 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -42,7 +42,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict. - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 349680779..14b2d9a7f 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -62,7 +62,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." GitHub does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/hashicorp-vault.mdx b/docs/integrations/secret-syncs/hashicorp-vault.mdx index 143303a00..fae2e0962 100644 --- a/docs/integrations/secret-syncs/hashicorp-vault.mdx +++ b/docs/integrations/secret-syncs/hashicorp-vault.mdx @@ -54,7 +54,7 @@ description: "Learn how to configure a Hashicorp Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Hashicorp Vault when keys conflict. - **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx index 208c75df9..f252724fb 100644 --- a/docs/integrations/secret-syncs/humanitec.mdx +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -55,7 +55,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical." Humanitec does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index db5abfb91..396b4d13f 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -57,7 +57,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict. - **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/teamcity.mdx b/docs/integrations/secret-syncs/teamcity.mdx index f5b2bf933..52f2c1bac 100644 --- a/docs/integrations/secret-syncs/teamcity.mdx +++ b/docs/integrations/secret-syncs/teamcity.mdx @@ -48,7 +48,7 @@ description: "Learn how to configure a TeamCity Sync for Infisical." Infisical only syncs secrets from within the target scope; inherited secrets will not be imported. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx index d8399db10..c48b87609 100644 --- a/docs/integrations/secret-syncs/terraform-cloud.mdx +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -56,7 +56,7 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical." Terraform Cloud does not support importing secrets. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/vercel.mdx b/docs/integrations/secret-syncs/vercel.mdx index 4553b46c6..74cffbc11 100644 --- a/docs/integrations/secret-syncs/vercel.mdx +++ b/docs/integrations/secret-syncs/vercel.mdx @@ -43,7 +43,7 @@ description: "Learn how to configure a Vercel Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict. - **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. diff --git a/docs/integrations/secret-syncs/windmill.mdx b/docs/integrations/secret-syncs/windmill.mdx index 56dda245d..c0757ef37 100644 --- a/docs/integrations/secret-syncs/windmill.mdx +++ b/docs/integrations/secret-syncs/windmill.mdx @@ -44,7 +44,7 @@ description: "Learn how to configure a Windmill Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Windmill when keys conflict. - **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict. - - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and {{environment}} for the environment. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. From 93ba6f7b588dc66d677cb0b817a4431865f71e1b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 5 Jun 2025 01:18:21 -0400 Subject: [PATCH 13/30] add netowkring docs --- .../platform/gateways/gateway-security.mdx | 19 -- .../platform/gateways/networking.mdx | 168 ++++++++++++++++++ .../platform/gateways/overview.mdx | 2 +- docs/documentation/setup/networking.mdx | 49 ++--- docs/mint.json | 3 +- 5 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 docs/documentation/platform/gateways/networking.mdx diff --git a/docs/documentation/platform/gateways/gateway-security.mdx b/docs/documentation/platform/gateways/gateway-security.mdx index 83490fd4d..93a7f662f 100644 --- a/docs/documentation/platform/gateways/gateway-security.mdx +++ b/docs/documentation/platform/gateways/gateway-security.mdx @@ -89,22 +89,3 @@ The relay system provides secure tunneling: - Gateways only accept connections to approved resources - Each connection requires explicit project authorization - Resources remain private to their assigned organization - -## Security Measures - -### Certificate Lifecycle -- Certificates have limited validity periods -- Automatic certificate rotation -- Immediate certificate revocation capabilities - -### Monitoring and Verification -1. **Continuous Verification**: - - Regular heartbeat checks - - Certificate chain validation - - Connection state monitoring - -2. **Security Controls**: - - Automatic connection termination on verification failure - - Audit logging of all access attempts - - Machine identity based authentication - diff --git a/docs/documentation/platform/gateways/networking.mdx b/docs/documentation/platform/gateways/networking.mdx new file mode 100644 index 000000000..2e512bf8b --- /dev/null +++ b/docs/documentation/platform/gateways/networking.mdx @@ -0,0 +1,168 @@ +--- +title: "Networking" +description: "Network configuration and firewall requirements for Infisical Gateway" +--- + +The Infisical Gateway requires outbound network connectivity to establish secure communication with Infisical's relay infrastructure. +This page outlines the required ports, protocols, and firewall configurations needed for optimal gateway usage. + +## Network Architecture + +The gateway uses a relay-based architecture to establish secure connections: + +1. **Gateway** connects outbound to **Relay Servers** using UDP/QUIC protocol +2. **Relay Servers** facilitate secure communication between Gateway and Infisical Cloud +3. All traffic is end-to-end encrypted using mutual TLS over QUIC + +## Required Network Connectivity + +### Outbound Connections (Required) + +The gateway requires the following outbound connectivity: + +| Protocol | Destination | Ports | Purpose | +|----------|-------------|-------|---------| +| UDP | Relay Servers | 49152-65535 | Allocated relay communication (TLS) | +| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and relay allocation | + +### Relay Server IP Addresses + +Your firewall must allow outbound connectivity to the following Infisical relay servers on dynamically allocated ports. + + + + ``` + 54.235.197.91:49152-65535 + 18.215.196.229:49152-65535 + 3.222.120.233:49152-65535 + 34.196.115.157:49152-65535 + ``` + + + ``` + 3.125.237.40:49152-65535 + 52.28.157.98:49152-65535 + 3.125.176.90:49152-65535 + ``` + + + Please contact your Infisical account manager for dedicated relay server IP addresses. + + + + + These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice. + + +## Protocol Details + +### QUIC over UDP + +The gateway uses QUIC (Quick UDP Internet Connections) for primary communication: + +- **Port 5349**: STUN/TURN over TLS (secure relay communication) +- **Built-in features**: Connection migration, multiplexing, reduced latency +- **Encryption**: TLS 1.3 with certificate pinning + +## Understanding Firewall Behavior with UDP + +Unlike TCP connections, UDP is a stateless protocol, and depending on your organization's firewall configuration, you may need to adjust network rules accordingly. +When the gateway sends UDP packets to a relay server, the return responses need to be allowed back through the firewall. +Modern firewalls handle this through "connection tracking" (also called "stateful inspection"), but the behavior can vary depending on your firewall configuration. + + +### Connection Tracking + +Modern firewalls automatically track UDP connections and allow return responses. This is the preferred configuration as it: +- Automatically handles return responses +- Reduces firewall rule complexity +- Avoids the need for manual IP whitelisting + +In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicity define return traffic manually. + +## Common Network Scenarios + +### Corporate Firewalls + +For corporate environments with strict egress filtering: + +1. **Whitelist relay IP addresses** (listed above) +2. **Allow UDP port 5349** outbound +3. **Configure connection tracking** for UDP return traffic +4. **Allow ephemeral port range** 49152-65535 for return traffic if connection tracking is disabled + +### Cloud Environments (AWS/GCP/Azure) + +Configure security groups to allow: +- **Outbound UDP** to relay IPs on port 5349 +- **Outbound HTTPS** to api.infisical.com +- **Inbound UDP** on ephemeral ports (if not using stateful rules) + +## Frequently Asked Questions + + +The gateway is designed to handle network interruptions gracefully: + +- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers every 5 seconds if the connection is lost +- **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention +- **Multiple relay servers**: If one relay server is unavailable, the gateway can connect to alternative relay servers +- **Persistent sessions**: Existing connections are maintained where possible during brief network interruptions +- **Graceful degradation**: The gateway logs connection issues and continues attempting to restore connectivity + +No manual intervention is typically required during network interruptions. + + + +QUIC (Quick UDP Internet Connections) provides several advantages over traditional TCP for gateway communication: + +- **Faster connection establishment**: QUIC combines transport and security handshakes, reducing connection setup time +- **Built-in encryption**: TLS 1.3 is integrated into the protocol, ensuring all traffic is encrypted by default +- **Connection migration**: QUIC connections can survive IP address changes (useful for NAT rebinding) +- **Reduced head-of-line blocking**: Multiple data streams can be multiplexed without blocking each other +- **Better performance over unreliable networks**: Advanced congestion control and packet loss recovery +- **Lower latency**: Optimized for real-time communication between gateway and cloud services + +While TCP is stateful and easier for firewalls to track, QUIC's performance benefits outweigh the additional firewall configuration requirements. + + + +No inbound ports need to be opened. The gateway only makes outbound connections: + +- **Outbound UDP** to relay servers on ports 49152-65535 +- **Outbound HTTPS** to Infisical API endpoints +- **Return responses** are handled by connection tracking or explicit IP whitelisting + +This design maintains security by avoiding the need for inbound firewall rules that could expose your network to external threats. + + + +If your firewall has strict UDP restrictions: + +1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses +2. **Use explicit IP whitelisting** (Option 2) if connection tracking is disabled +3. **Consider network policy exceptions** for the gateway host +4. **Monitor firewall logs** to identify which specific rules are blocking traffic + +The gateway requires UDP connectivity to function - TCP-only configurations are not supported. + + + +The gateway connects to **one relay server at a time**: + +- **Single active connection**: Only one relay connection is established per gateway instance +- **Automatic failover**: If the current relay becomes unavailable, the gateway will connect to an alternative relay +- **Load distribution**: Different gateway instances may connect to different relay servers for load balancing +- **No manual selection**: The Infisical API automatically assigns the optimal relay server based on availability and proximity + +You should whitelist all relay IP addresses to ensure proper failover functionality. + + +No, relay servers cannot decrypt any traffic passing through them: + +- **End-to-end encryption**: All traffic between the gateway and Infisical Cloud is encrypted using mutual TLS with certificate pinning +- **Relay acts as a tunnel**: The relay server only forwards encrypted packets - it has no access to encryption keys +- **No data storage**: Relay servers do not store any traffic or network-identifiable information +- **Certificate isolation**: Each organization has its own private PKI system, ensuring complete tenant isolation + +The relay infrastructure is designed as a secure forwarding mechanism, similar to a VPN tunnel, where the relay provider cannot see the contents of the traffic flowing through it. + \ No newline at end of file diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index ae4a3c7ad..53df5993b 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -32,7 +32,7 @@ For detailed installation instructions, refer to the Infisical [CLI Installation To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway. Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable. -### Deployment process +### Get started diff --git a/docs/documentation/setup/networking.mdx b/docs/documentation/setup/networking.mdx index 4a666b73c..6de27c3c0 100644 --- a/docs/documentation/setup/networking.mdx +++ b/docs/documentation/setup/networking.mdx @@ -4,33 +4,36 @@ sidebarTitle: "Networking" description: "Network configuration details for Infisical Cloud" --- -## Overview - When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services. -## Egress IP Addresses +## Infisical IP Addresses -Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services. +Infisical Cloud operates from multiple regions. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the IP addresses that Infisical uses when making outbound requests to your services. -### US Region + + + ``` + 3.213.63.16 + 54.164.68.7 + ``` + + + + ``` + 3.77.89.19 + 3.125.209.189 + ``` + + + + For dedicated Infisical deployments, please contact your account manager for the specific IP addresses used in your dedicated environment. + + -To allow connections from Infisical US, add these IP addresses to your ingress rules: + +These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice. + -- `3.213.63.16` -- `54.164.68.7` +## What These IP Addresses Are Used For -### EU Region - -To allow connections from Infisical EU, add these IP addresses to your ingress rules: - -- `3.77.89.19` -- `3.125.209.189` - -## Common Use Cases - -You may need to allow Infisical’s egress IPs if your services require inbound connections for: - -- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials -- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services -- Secret integrations - When syncing secrets with third-party services like Azure Key Vault -- Native authentication with machine identities - When using methods like Kubernetes authentication +These IP addresses represent the source IPs you'll see when Infisical Cloud makes connections to your infrastructure. All outbound traffic from Infisical Cloud originates from these IP addresses, ensuring predictable source IP addresses for your firewall rules. diff --git a/docs/mint.json b/docs/mint.json index 67a771085..64b827f55 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -233,7 +233,8 @@ "group": "Gateway", "pages": [ "documentation/platform/gateways/overview", - "documentation/platform/gateways/gateway-security" + "documentation/platform/gateways/gateway-security", + "documentation/platform/gateways/networking" ] }, "documentation/platform/project-templates", From 9cc17452fa187254eeb9fc7d14c61ef31d4fc83a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 5 Jun 2025 01:23:28 -0400 Subject: [PATCH 14/30] address greptile --- docs/documentation/platform/gateways/networking.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/documentation/platform/gateways/networking.mdx b/docs/documentation/platform/gateways/networking.mdx index 2e512bf8b..6acdc1993 100644 --- a/docs/documentation/platform/gateways/networking.mdx +++ b/docs/documentation/platform/gateways/networking.mdx @@ -78,7 +78,7 @@ Modern firewalls automatically track UDP connections and allow return responses. - Reduces firewall rule complexity - Avoids the need for manual IP whitelisting -In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicity define return traffic manually. +In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicitly define return traffic manually. ## Common Network Scenarios @@ -95,7 +95,7 @@ For corporate environments with strict egress filtering: Configure security groups to allow: - **Outbound UDP** to relay IPs on port 5349 -- **Outbound HTTPS** to api.infisical.com +- **Outbound HTTPS** to app.infisical.com/eu.infisical.com on port 443 - **Inbound UDP** on ephemeral ports (if not using stateful rules) ## Frequently Asked Questions @@ -139,7 +139,7 @@ This design maintains security by avoiding the need for inbound firewall rules t If your firewall has strict UDP restrictions: 1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses -2. **Use explicit IP whitelisting** (Option 2) if connection tracking is disabled +2. **Use explicit IP whitelisting** if connection tracking is disabled 3. **Consider network policy exceptions** for the gateway host 4. **Monitor firewall logs** to identify which specific rules are blocking traffic From c425c039394f146a8e9b14162918f62ab9879aa3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 17:44:41 +0400 Subject: [PATCH 15/30] cleanup --- backend/src/ee/services/audit-log/audit-log-types.ts | 3 --- .../src/server/routes/v1/identity-kubernetes-auth-router.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 808a13f68..cfc01741f 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -35,7 +35,6 @@ import { ActorType } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; -import { IdentityKubernetesAuthTokenReviewMode } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums"; @@ -880,7 +879,6 @@ interface AddIdentityKubernetesAuthEvent { metadata: { identityId: string; kubernetesHost: string; - tokenReviewMode: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces: string; allowedNames: string; accessTokenTTL: number; @@ -902,7 +900,6 @@ interface UpdateIdentityKubernetesAuthEvent { metadata: { identityId: string; kubernetesHost?: string; - tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode; allowedNamespaces?: string; allowedNames?: string; accessTokenTTL?: number; diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 743c5b321..5eb0c6990 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -204,7 +204,6 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide metadata: { identityId: identityKubernetesAuth.identityId, kubernetesHost: identityKubernetesAuth.kubernetesHost, - tokenReviewMode: identityKubernetesAuth.tokenReviewMode, allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNames: identityKubernetesAuth.allowedNames, accessTokenTTL: identityKubernetesAuth.accessTokenTTL, @@ -347,7 +346,6 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide metadata: { identityId: identityKubernetesAuth.identityId, kubernetesHost: identityKubernetesAuth.kubernetesHost, - tokenReviewMode: identityKubernetesAuth.tokenReviewMode, allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNames: identityKubernetesAuth.allowedNames, accessTokenTTL: identityKubernetesAuth.accessTokenTTL, From 594445814a7b34a04d2d072d8e56c24551a78f57 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 18:40:34 +0400 Subject: [PATCH 16/30] docs(identity/kubernetes-auth): added docs for gateway as reviewer --- .../platform/identities/kubernetes-auth.mdx | 111 +++++++++++++----- ...es-kubernetes-auth-gateway-as-reviewer.png | Bin 0 -> 242582 bytes 2 files changed, 81 insertions(+), 30 deletions(-) create mode 100644 docs/images/platform/identities/identities-kubernetes-auth-gateway-as-reviewer.png diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx index 9daff1e81..e357ba75e 100644 --- a/docs/documentation/platform/identities/kubernetes-auth.mdx +++ b/docs/documentation/platform/identities/kubernetes-auth.mdx @@ -52,10 +52,11 @@ Infisical is able to authenticate and interact with the TokenReview API by using In the following steps, we explore how to create and use identities for your applications in Kubernetes to access the Infisical API using the Kubernetes Auth authentication method. + + - - + **When to use this option**: Choose this approach when you want centralized authentication management. Only one service account needs special permissions, and your application service accounts remain unchanged. @@ -126,41 +127,91 @@ In the following steps, we explore how to create and use identities for your app ``` Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. + - - + + + **When to use this option**: Choose this approach to eliminate long-lived tokens. This option simplifies Infisical configuration but requires each application service account to have elevated permissions. + - - **When to use this option**: Choose this approach to eliminate long-lived tokens. This option simplifies Infisical configuration but requires each application service account to have elevated permissions. - + The self-validation method eliminates the need for a separate long-lived reviewer JWT by using the same token for both authentication and validation. Instead of creating a dedicated reviewer service account, you'll grant the necessary permissions to each application service account. - The self-validation method eliminates the need for a separate long-lived reviewer JWT by using the same token for both authentication and validation. Instead of creating a dedicated reviewer service account, you'll grant the necessary permissions to each application service account. + For each service account that needs to authenticate with Infisical, add the `system:auth-delegator` role: - For each service account that needs to authenticate with Infisical, add the `system:auth-delegator` role: + ```yaml client-role-binding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-client-binding-[your-app-name] + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator + subjects: + - kind: ServiceAccount + name: [your-app-service-account] + namespace: [your-app-namespace] + ``` - ```yaml client-role-binding.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: - name: infisical-client-binding-[your-app-name] - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:auth-delegator - subjects: - - kind: ServiceAccount - name: [your-app-service-account] - namespace: [your-app-namespace] - ``` + ``` + kubectl apply -f client-role-binding.yaml + ``` - ``` - kubectl apply -f client-role-binding.yaml - ``` + When configuring Kubernetes Auth in Infisical, leave the **Token Reviewer JWT** field empty. Infisical will use the client's own token for validation. + + + + **When to use this option**: Choose this approach when you have a gateway deployed in your Kubernetes Cluster and wish to eliminate long-lived tokens. This approach simplifies Infisical Kubernetes Auth configuration, and only one service account will need to have the elevated `system:auth-delegator` ClusterRole binding. + - When configuring Kubernetes Auth in Infisical, leave the **Token Reviewer JWT** field empty. Infisical will use the client's own token for validation. - - - + + **Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is + available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please + contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an + enterprise license. + + + + + To deploy a gateway in your Kubernetes cluster, follow our [Gateway deployment guide using helm](/documentation/platform/gateways/overview). + + + + To grant the gateway the `system:auth-delegator` ClusterRole binding, you can use the following command: + + ```yaml gateway-role-binding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-token-reviewer-role-binding + namespace: default # Replace with your namespace if not default + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator + subjects: + - kind: ServiceAccount + name: infisical-gateway # The name of the gateway service account + namespace: default # Replace with your namespace if not default + ``` + + ```bash + kubectl apply -f gateway-role-binding.yaml + ``` + + + The gateway service account name is `infisical-gateway` by default if deployed using Helm. + + + + + To configure your Kubernetes Auth method to use the gateway as the token reviewer, set the `Review Method` to "Gateway as Reviewer", and select the gateway you want to use as the token reviewer. + + ![identities organization create kubernetes auth method](/images/platform/identities/identities-kubernetes-auth-gateway-as-reviewer.png) + + + + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. diff --git a/docs/images/platform/identities/identities-kubernetes-auth-gateway-as-reviewer.png b/docs/images/platform/identities/identities-kubernetes-auth-gateway-as-reviewer.png new file mode 100644 index 0000000000000000000000000000000000000000..30ac12545f42e2528cb0dba7d69bc3c2fd2a0413 GIT binary patch literal 242582 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�xWP*$b2(Ct@gi`XYs(#A&oSr)Sg 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%0A4#NVCWww)NT-~%qvwIfdgvM&mNueC5 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

#>r}XoVBM8_R{7DmEa|A zdg0{z#yVEJU6t}!ue67C;0xD2ziO*mba+^=ce9;(PvZ#QQ-Rofn4jrWKx>~~ zUj`!OQ)eXrDuNuLnhV9tDxIn}B_BPJIJOU33fg8uB)>Wx5S zBYP-lG6s_oBy2F;J8HPB_=Dq({Z~NJM-Z#peK!Wu=5Oz@%M7uk#rQ|mo+5)e;mN)| z=>Am~Yu`MXN^)IWs=wViImhRm^F_g#Yr_y&D=3}ICmxsw+EMSVQ+e#RiaiJ()$Wbt z5~g1D8Mcb1sI@U+Mo5N4y#+sc&CtU|EK{S4ikDR*C5qm7 zK5KODWOtIXTG5cHWY!l07ER>Zh_+po(q0&MI);WVn@&7c!??YxIsp*gNEvev-&ax3 z*RBMjf?aqON2rl8tQLF}5;qh7x_NQ(%^q2d8ntWa1TtEZuLR;!{^5(6Q$247XqsF@ zAR3$kQGb1>CZj!i7*38)vEX*f;COvzTxAJqOuyqfI9e7t2-I>LZUE@jxrPg(W33j4 zZh^w;4Dry~)XRX_c@!pI;==;A*In**DNZZa7k-2Kp=aWEQ{p6;O6v?S?%pW$la7?@ zvAua=B2?*>5E`Qc-n7aHmGw3*sgzv-XkB$dG>xmbJ0M{zSo%gB3I{l+>Z2#~ZeH0{h z)@zP(SM!mGe}2&nCNBn<3wLFg^ZD*K_un+*U-4bkr+{{790&RQ=K(I#WXkz;M4GAQ zbaSvq;M&8o8h^6LP(|NxfNhX>pXq3BxADBdvM!O+vKrX-6L>IJV|!3g1&X@@M^$sp zZ#Rt|fjpMCz~-kF?0ASHs?(-MHWs}oG7f2k{Kpn1Xj=$@)(qMTT4`TbR zwHcZ?;8O3Z(PvzRF!=>9O$DId5>Gx3n(C@mySr+e;)o_W7yZo|QpeKOGHvj2S9AZVfC z=D_v9brI62RUZ+(woP8ZDSzphWq8;XZ(-;qzQfDV%!IV4H(9qJTR>7MAm}hy5|f5s zoJ}RInyIe%U5mv@~ABp{h}_sh>^#3BOj=U zL+2}iNfxrP77@hx65Y-@k<{y`#Rl8ihP#2;gT9;7#z^~h)7+>8*W0`Gln^&z-7K}tNK3?%vUZ$_)x_gw&t=XmGOQc&R01a7= z9848Jc=GH4r2H$vY<}@145EW7{yYdT|F4y`sm#!2rVHjZoHc!mm{vqMug@zdh zd5R*=-HiHcy1$u&gxOR)MAKs*%uCNqnc5nbg-1SJQP$W$! zSxb$ZU@)5xA<)n_?F^8vZT362?vmZt9(@GHG&kwwd8HzmTOL((`zX8q$M%P zV95_!U4*9_^&QLSoc7HxlJd4P|Ilmj5>(ZHD=4*yods&GU?YnR)Xh1JL7PR7wfz#ltw}WA9nUv@(?n zhW4^0o}pDx6bo?(E5JY*NhU%CknZ1h*moP%#r60xl4ReOU8%O@BD|rNfY=!#c_#7t z&^6Vpj{BvYK}Bo`@M*3pJxTTy!Y(>?h~r< z;no~$e{}kJV}JPg>+Uwcz+wh$fiSOdD5D&*uxmfR1dZ4=42`gsx7oR;ZtO_0g~D?O z!h2?xIAP99EI``$!OQa7SFA#-523{8<)j!0<$ zZciU38|(1jfH5$Kmm-KNQMOR+8xMCK9N{m-sWL`TUtF$E+kE*&8GxrB`&k=`@*Ixf z(vUZqJjJmPCL8~R7s77BAp_pXmmmKSR8mIqgzEzt%i3IZwS!@vnmed+D}a%t?s!EH zMfeawXN0QxMn9@;E+3&CCnT(^;WRVpiPeODe}>0G8r<}H6|cJrfv;-mPt-0=N)YsD zsTt8^==`brYdlWA3cESl5{khWm1W=P-GBW=3GGoNvyQ{pi4#E-3G+i{owDYk-M`9e z4!Pz%0ET>ah1k$ouLH?ts(*xNiL}i;3_uny!}<>dsTud0p9BQ@v9NR{G)>vfDh<8^ zZh3sOLQ*`_%g;8XKmy=K*9V&4R=qtdLuosHIb{+M`> z9RikPb-$;KeVvn$s>#jarw@xJo&fyHm?zEt3Sav+m8eE6iXG>cOBG8e29(BiH2+HH zrtIOcA*>Z#+LNTK$bW7>k$VAjFDwzCgTIGp`WK|w^^<;U`}7)KgxxKdMPDj|UZ7`Z zTQfX?7y#9KaRwBtrI^2=8BzMpUk^^&E55H9F%TJ#DZkffm=NS_mN)R-r09Y%^UgKl zacLBz@^F_9jZi9Y4@~oJbFq6Iz0EVwHs*ac+Wn9|zrhSkg}H9_mzy1%s_h+n{rStr z-+8wsi^n;40~{)jB0`ZW+rKcqqwdGRJwL)|&`g&N{)kE+OYd6I&)?ACi3B0lu2~)0Z3#h0>1v|x68M|x% zfus`k07@7@E9FuY`dT)f@KPk1Hb50c&y-T$ zn99HtEB|dPCK(OuS3+@y97o7~DJobrlm!>(OJC0n9q^`Mlx*sYgcD;#t_(qDKI;i0 z0ZF7tQq|y5nZwpAO*hk5Za%Mt>DZ_;$1rDvd)$|Y9H1Mb4c0B*q)`GT>f|AQH_O4E zw@);kqZyWkX@f8O^eA--(pgS)d9{e4TGNtQ2wdeSC=g?p{HHp*v^5RdPS(H5o#HyA zquAW}bC(mdKRlN<5s=x4Gp%h|%P6UiEFL0aScfgrJQBCenbw&>6X?`{b8g8GqQ0`r z#8Bi%QF7q>BrlDirSTk1a7VAgnvP|!BCfVzLUg{&Togd>$xPv`XTnrG6={)Mf8bmC zmy`M~>^udNX?Nm1#AbJ>9VVz83ZOTH@Kx`qe3`6ow)-~V35tE;)(k%pT)q(t9{lUa zKG3#MAgx)(yW(F7rdOS$HXn8y6C4=}<7_kiHz&OX&8w5a`+T60Z2qz+v!L>Iah9G1l{bX<@!6U+ixgk~Z>@Bb)T6SAV7yiVrD8QsXV9#Z8elOGbmX zh+jqVo4|3^lAYtnxXF)MX(sXRa~SFnnG3-JQE0vqx+*xfs$@#D6qqjYnYuz@F{~XT z!sqH#;|N)18wnwDCbsz0xFV|Eb|opH%*dzgR3*!#>-q9#ADMV(**dd@d$)8MzQztm z_NkK2Fam%~dP?55&g#RV%OYtnYbTbs{jFJDd+_nn;YZhv%#A-ql$&K9;H4HdExRZ9 zN*b%Y2Bc3YHFfYCmZ4}uKyGVYZ!cO;2W#AKz%}@smbC(xVB%`i$-pv!QJ7 zQP)ENG;>--y8!5xc>uTE03R+M0!-XqE~Xf-th^SXv{pIuL zufPUPX#})o1y+=IQVb1iBM$2W0D9I;B??7sqoKv0^N$V{C8{?!8m8y@eZOK0U>8l9idVZ}Ap3 z9!zMX0V)kHp8xGasmpZ)`J)OwTp>WQqPdcUL_lHB3!}6`;qOwMpW#fJM8W|vgg+BS z8La1wYfG`nUDrCO2H{He?Hn#`LV6sePfQP1d;<@{#pP;%xz(NqU^VkWt1rqyykUy< z$++#^MwHN&iZRPjzf~IjCM_rV1J2j+h! zvr1!C7wm6Qr6hpf#~e2WH0G2s+>uE7WYsOn(%%rby71V5i`S-INtE zwaice3C9WOoF%f1j0cBbw@A3r3C(=#gDQ2xc%(d%k@rCO51XcKYzDa^Bpup&h@;>kfqFH9gPs!V5?NIAl*e&^8nbS+XI z0PJ?}^G3imowx25h3Yk!-YKB#?c$q_=nsvAjAy);ORzt&81Vk=awC_(Wec%ykwP8l z&PB62Jw1#5U8gnP4@Q5%_%RF?awjWf7!C&eB*Ie5Kw;NYA z(5T@k{IPW#xvC0VAzT~`-$2@~qz7&#du$`Kgh<+=7sxXWZ5XFVFfEVBPx&^>@FN98 z`km1?GkAO+#ZlebxQ-ChZS#d8B5C~I?;9Nsuft>8p9MH7n5MZR zR6yMb+hgtJc1I{(KjN5#yhxu3f|DP!Q6hGXni>~nT ziEA%=WuSY)dDRITk;Xei3$M|=Q>(oOVVBzS3Wfhf*(NP?Ohny<(Wd$BiwN6g?2W96 zyC;hg`bzpjL&#iS{{He1rqJYVE<36{IRjv_jhOb*~ll5qqqO-SHQO@!NCsBrEo~waC(r8dq}Hu27s^naNjPDwwqNp?X2`gs71S zu>t z-?nD~fCM2^OO7Q%5mHLE#+F8#PC)9u8_irud+>Kti?-2VVz z{$wovI7u%)No|k%n6Dq+%o4b0vc_t3b&Z^nv2}_{<=~R_+}?rQ-aL|HH?4fBMogMT z`-3`fid{v1vQi19e4dU?Z5qC|w`bxJjQGaXJ6hy7NTttS%`8tvWUPkZci}=C{%pHZ zdG{q|2R(Ux`{sxK#2=~Xzx{~6y`36|^D9z{)Ys-FIe%oHG+ZXjBqK=n-eKjl`CPcd zAjU-vYji+HwTIw@xBs{{m!Mp)mRGKzC=Pt%5D3gVp-C<|m~yx3xOrkm#^uK!ohYI6$U!%}ne^Ls?l zyK=S-`7$~)FoMX=xv1kmqE2@S8h+8O`2?HSlOyYruipCDR<-9jhV{rpuV%k4t&?23 z!O1jM1LKg;p!lPYAcwbvpOzAy;uDF=FhI#Qa9@6&Yv_FGb-68(iWx)#@7@O!uW77z zc;q83-$OtY(R1xEV*9I_`CW4hm<)P>T)b)L4h?mNEkyr`s*Ve5pvkjhFujo^G`{k< zIeXp*pBJ@Z1j>v&4lu*FvlFWKDA0{!@4rbAeBS&s3Q74l;@+2#^xjFgld(d zHD+n+l01J!+~0TyT$naK8>_~}mRE0j-m##?vRhgvk{Y zw+@X_Y;Y6mStzWT;-Ce}OrzX`A!8gF{gboN1as|I-hV;i|M0>8%7TjXkWnwXTDW^o ziujAaIO;X>OZG=t74Yau3s{WNb7ek86=uQ8Y5f+@dX+TPDor zK2efOC!vKoDbX|6t|Pjg^HZl&PkMUJbKbvxe}13${l4G#^ZmS^&->G4INOwR@gnL= z?d;FAT?nxuMD#rsuw?0gbqfVJe5ayh;SQ3~0kvDT26n1BBT&pJHbiJ_;Mqz$VPcow zPmcI~ZT-v&Q|L^z+Y4pRGV7H@k@5An^#c$3_9enq#WK^>+!KU5?gOdRo&_o$_I&tW zZo8xR*crQgJY_?=2vF2xfYW-bBu3lvrxu#8g=-oH$_YtUmw-R?B)mdz#H}$iJW9Cb zLvNwRav8hA=<2W6RpX}md*A5T*q-kK2Xq)cB*Ef#7u>btf$HukKm>(&xajGzLRnlB z=KjtL$-^|CSwPON|GYLQu)z`Jml}Mo+vc3MA8nV|G@ZLYaZbN?fP246qu@cW?Zs;N zartj;^OCu;_R?QHz&$yJuec;vj;xHd+LnfDXSj7Lop5D2#Dd2XJo?XJ-RhLV^^fpr zyh#)Lb{(%cZ^tUN1OMn#8#ehlOQr?|p9%CHnlx$z^l$7)v11XdK!+C=*+PCTWbfZt zSmv3Sm-Ll5SQ-PM;Y2LLGmdQOD)v>BnlE&uq(3wkBA=3PiQAI^vBBSxmmJeSai(gbNBSv*F#1cJMCN2jW<026fg#xJs@) zyt`WYd?^Plab9DnWYo^+m8(%d#srdb+J@c~ zGtt1VzewYYOPl!45jt&OS@)&oauH8KK!y=xbPc>cTHcyZ&arQX{`S%3V064@2v2}v z#qYH$VM5ITDkb*V@{xkO{M5oX*3U078b3-y>^cCVIt@$@GR?h~uz*Bj{RJ@85Wr4R zK`xwhR-NRX&0LwV_{1I6lxp5FZF&@uvPEeIRxnZS-E~IXS5$L)u_0Mz)bUFl#s z9S|6Swn&-ojo+V;-C-Nv|3{yuf6jaUdNR1$`1}Sz3MH_E5 zfaO5uo1LsXGJ-0~fg6JN5lkIKrphUK02U1A8gl5U>@dD=i zx$3}TaYVvbCg$g91_lwW985egfr1riXP)j%|JfSP8xTg;nY|`2Xoe{gSs79Wa3dn6 z?1tI)&dIWC6&|O@d6VNHg(f!&kWTBSlcKELw~jM4ELSQRE6!6^U%_`B7Yzo_1r9Q3 zRe?8}h+7;okC>l&*V1F*LAvtH_SZOJ&F64%{L~%xdm4ks5dqJ_;|L3Pb}7cvjlyZD zZ3oq657yx=U(s$8bq(Wj4gu|c1+(2@mj_g07+_b~!G)wL-m!q~C!@X)o~w%>L`6K9 zAG6*xO Date: Thu, 5 Jun 2025 18:40:54 +0400 Subject: [PATCH 17/30] cleanup and minor requested changes --- .../dynamic-secret/providers/kubernetes.ts | 4 +- .../dynamic-secret/providers/sql-database.ts | 4 +- .../dynamic-secret/providers/vertica.ts | 4 +- backend/src/lib/gateway/gateway.ts | 411 ++++++++++++++++ backend/src/lib/gateway/index.ts | 441 +----------------- backend/src/lib/gateway/types.ts | 42 ++ .../identity-kubernetes-auth-service.ts | 6 +- 7 files changed, 464 insertions(+), 448 deletions(-) create mode 100644 backend/src/lib/gateway/gateway.ts create mode 100644 backend/src/lib/gateway/types.ts diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index eeea7e4be..130e0fa92 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -2,7 +2,7 @@ import axios from "axios"; import https from "https"; import { InternalServerError } from "@app/lib/errors"; -import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; @@ -43,7 +43,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): return res; }, { - protocol: ProxyProtocol.Tcp, + protocol: GatewayProxyProtocol.Tcp, targetHost: inputs.targetHost, targetPort: inputs.targetPort, relayHost, diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index f5299687b..39f8dd6de 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -3,7 +3,7 @@ import handlebars from "handlebars"; import knex from "knex"; import { z } from "zod"; -import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -185,7 +185,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await gatewayCallback("localhost", port); }, { - protocol: ProxyProtocol.Tcp, + protocol: GatewayProxyProtocol.Tcp, targetHost: providerInputs.host, targetPort: providerInputs.port, relayHost, diff --git a/backend/src/ee/services/dynamic-secret/providers/vertica.ts b/backend/src/ee/services/dynamic-secret/providers/vertica.ts index 046b73017..e361ab329 100644 --- a/backend/src/ee/services/dynamic-secret/providers/vertica.ts +++ b/backend/src/ee/services/dynamic-secret/providers/vertica.ts @@ -4,7 +4,7 @@ import knex, { Knex } from "knex"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; -import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -196,7 +196,7 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam await gatewayCallback("localhost", port); }, { - protocol: ProxyProtocol.Tcp, + protocol: GatewayProxyProtocol.Tcp, targetHost: providerInputs.host, targetPort: providerInputs.port, relayHost, diff --git a/backend/src/lib/gateway/gateway.ts b/backend/src/lib/gateway/gateway.ts new file mode 100644 index 000000000..179c29fc8 --- /dev/null +++ b/backend/src/lib/gateway/gateway.ts @@ -0,0 +1,411 @@ +/* eslint-disable no-await-in-loop */ +import crypto from "node:crypto"; +import net from "node:net"; + +import quicDefault, * as quicModule from "@infisical/quic"; +import axios from "axios"; +import https from "https"; + +import { BadRequestError } from "../errors"; +import { logger } from "../logger"; +import { + GatewayProxyProtocol, + IGatewayProxyOptions, + IGatewayProxyServer, + TGatewayTlsOptions, + TPingGatewayAndVerifyDTO +} from "./types"; + +const DEFAULT_MAX_RETRIES = 3; +const DEFAULT_RETRY_DELAY = 1000; // 1 second + +const quic = quicDefault || quicModule; + +const parseSubjectDetails = (data: string) => { + const values: Record = {}; + data.split("\n").forEach((el) => { + const [key, value] = el.split("="); + values[key.trim()] = value.trim(); + }); + return values; +}; + +const createQuicConnection = async ( + relayHost: string, + relayPort: number, + tlsOptions: TGatewayTlsOptions, + identityId: string, + orgId: string +) => { + const client = await quic.QUICClient.createQUICClient({ + host: relayHost, + port: relayPort, + config: { + ca: tlsOptions.ca, + cert: tlsOptions.cert, + key: tlsOptions.key, + applicationProtos: ["infisical-gateway"], + verifyPeer: true, + verifyCallback: async (certs) => { + if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired; + const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0])); + const caCertificate = new crypto.X509Certificate(tlsOptions.ca); + const isValidServerCertificate = serverCertificate.verify(caCertificate.publicKey); + if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate; + + const subjectDetails = parseSubjectDetails(serverCertificate.subject); + if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) { + return quic.native.CryptoError.CertificateUnknown; + } + + if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) { + return quic.native.CryptoError.CertificateExpired; + } + + const formatedRelayHost = + process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost; + if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate; + }, + maxIdleTimeout: 90000, + keepAliveIntervalTime: 30000 + }, + crypto: { + ops: { + randomBytes: async (data) => { + crypto.getRandomValues(new Uint8Array(data)); + } + } + } + }); + return client; +}; + +export const pingGatewayAndVerify = async ({ + relayHost, + relayPort, + tlsOptions, + maxRetries = DEFAULT_MAX_RETRIES, + identityId, + orgId +}: TPingGatewayAndVerifyDTO) => { + let lastError: Error | null = null; + const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { + throw new BadRequestError({ + message: (err as Error)?.message, + error: err as Error + }); + }); + + for (let attempt = 1; attempt <= maxRetries; attempt += 1) { + try { + const stream = quicClient.connection.newStream("bidi"); + const pingWriter = stream.writable.getWriter(); + await pingWriter.write(Buffer.from("PING\n")); + pingWriter.releaseLock(); + + // Read PONG response + const reader = stream.readable.getReader(); + const { value, done } = await reader.read(); + + if (done) { + throw new Error("Gateway closed before receiving PONG"); + } + + const response = Buffer.from(value).toString(); + + if (response !== "PONG\n" && response !== "PONG") { + throw new Error(`Failed to Ping. Unexpected response: ${response}`); + } + + reader.releaseLock(); + return; + } catch (err) { + lastError = err as Error; + + if (attempt < maxRetries) { + await new Promise((resolve) => { + setTimeout(resolve, DEFAULT_RETRY_DELAY); + }); + } + } finally { + await quicClient.destroy(); + } + } + + logger.error(lastError); + throw new BadRequestError({ + message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}` + }); +}; + +const setupProxyServer = async ({ + targetPort, + targetHost, + tlsOptions, + relayHost, + relayPort, + identityId, + orgId, + protocol = GatewayProxyProtocol.Tcp, + httpsAgent +}: { + targetHost: string; + targetPort: number; + relayPort: number; + relayHost: string; + tlsOptions: TGatewayTlsOptions; + identityId: string; + orgId: string; + protocol?: GatewayProxyProtocol; + httpsAgent?: https.Agent; +}): Promise => { + const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { + throw new BadRequestError({ + error: err as Error + }); + }); + const proxyErrorMsg = [""]; + + return new Promise((resolve, reject) => { + const server = net.createServer(); + + let streamClosed = false; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + server.on("connection", async (clientConn) => { + try { + clientConn.setKeepAlive(true, 30000); // 30 seconds + clientConn.setNoDelay(true); + + const stream = quicClient.connection.newStream("bidi"); + + const forwardWriter = stream.writable.getWriter(); + let command: string; + + if (protocol === GatewayProxyProtocol.Http) { + const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http) + command = `FORWARD-HTTP ${targetUrl}`; + logger.debug(`Using HTTP proxy mode: ${command.trim()}`); + + // extract ca certificate from httpsAgent if present + if (httpsAgent && targetHost.startsWith("https://")) { + const agentOptions = httpsAgent.options; + if (agentOptions && agentOptions.ca) { + const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca; + const caB64 = Buffer.from(caCert as string).toString("base64"); + command += ` ca=${caB64}`; + + const rejectUnauthorized = agentOptions.rejectUnauthorized !== false; + command += ` verify=${rejectUnauthorized}`; + + logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`); + } + } + + command += "\n"; + } else if (protocol === GatewayProxyProtocol.Tcp) { + // For TCP mode, send FORWARD-TCP with host:port + command = `FORWARD-TCP ${targetHost}:${targetPort}\n`; + logger.debug(`Using TCP proxy mode: ${command.trim()}`); + } else { + throw new BadRequestError({ + message: `Invalid protocol: ${protocol as string}` + }); + } + + await forwardWriter.write(Buffer.from(command)); + forwardWriter.releaseLock(); + + // Set up bidirectional copy + const setupCopy = () => { + // Client to QUIC + // eslint-disable-next-line + (async () => { + const writer = stream.writable.getWriter(); + + // Create a handler for client data + clientConn.on("data", (chunk) => { + writer.write(chunk).catch((err) => { + proxyErrorMsg.push((err as Error)?.message); + }); + }); + + // Handle client connection close + clientConn.on("end", () => { + if (!streamClosed) { + try { + writer.close().catch((err) => { + logger.debug(err, "Error closing writer (already closed)"); + }); + } catch (error) { + logger.debug(error, "Error in writer close"); + } + } + }); + + clientConn.on("error", (clientConnErr) => { + writer.abort(clientConnErr?.message).catch((err) => { + proxyErrorMsg.push((err as Error)?.message); + }); + }); + })(); + + // QUIC to Client + void (async () => { + try { + const reader = stream.readable.getReader(); + + let reading = true; + while (reading) { + const { value, done } = await reader.read(); + + if (done) { + reading = false; + clientConn.end(); // Close client connection when QUIC stream ends + break; + } + + // Write data to TCP client + const canContinue = clientConn.write(Buffer.from(value)); + + // Handle backpressure + if (!canContinue) { + await new Promise((res) => { + clientConn.once("drain", res); + }); + } + } + } catch (err) { + proxyErrorMsg.push((err as Error)?.message); + clientConn.destroy(); + } + })(); + }; + + setupCopy(); + // Handle connection closure + clientConn.on("close", () => { + if (!streamClosed) { + streamClosed = true; + stream.destroy().catch((err) => { + logger.debug(err, "Stream already destroyed during close event"); + }); + } + }); + + const cleanup = async () => { + try { + clientConn?.destroy(); + } catch (err) { + logger.debug(err, "Error destroying client connection"); + } + + if (!streamClosed) { + streamClosed = true; + try { + await stream.destroy(); + } catch (err) { + logger.debug(err, "Error destroying stream (might be already closed)"); + } + } + }; + + clientConn.on("error", (clientConnErr) => { + logger.error(clientConnErr, "Client socket error"); + cleanup().catch((err) => { + logger.error(err, "Client conn cleanup"); + }); + }); + + clientConn.on("end", () => { + cleanup().catch((err) => { + logger.error(err, "Client conn end"); + }); + }); + } catch (err) { + logger.error(err, "Failed to establish target connection:"); + clientConn.end(); + reject(err); + } + }); + + server.on("error", (err) => { + reject(err); + }); + + server.on("close", () => { + quicClient?.destroy().catch((err) => { + logger.error(err, "Failed to destroy quic client"); + }); + }); + + server.listen(0, () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Failed to get server port")); + return; + } + + logger.info(`Gateway proxy started on port ${address.port} (${protocol} mode)`); + resolve({ + server, + port: address.port, + cleanup: async () => { + try { + server.close(); + } catch (err) { + logger.debug(err, "Error closing server"); + } + + try { + await quicClient?.destroy(); + } catch (err) { + logger.debug(err, "Error destroying QUIC client"); + } + }, + getProxyError: () => proxyErrorMsg.join(",") + }); + }); + }); +}; + +export const withGatewayProxy = async ( + callback: (port: number, httpsAgent?: https.Agent) => Promise, + options: IGatewayProxyOptions +): Promise => { + const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId, protocol, httpsAgent } = options; + + // Setup the proxy server + const { port, cleanup, getProxyError } = await setupProxyServer({ + targetHost, + targetPort, + relayPort, + relayHost, + tlsOptions, + identityId, + orgId, + protocol, + httpsAgent + }); + + try { + // Execute the callback with the allocated port + return await callback(port, httpsAgent); + } catch (err) { + const proxyErrorMessage = getProxyError(); + if (proxyErrorMessage) { + logger.error(new Error(proxyErrorMessage), "Failed to proxy"); + } + logger.error(err, "Failed to do gateway"); + let errorMessage = proxyErrorMessage || (err as Error)?.message; + if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) { + errorMessage = (err.response?.data as { message: string }).message; + } + + throw new BadRequestError({ message: errorMessage }); + } finally { + // Ensure cleanup happens regardless of success or failure + await cleanup(); + } +}; diff --git a/backend/src/lib/gateway/index.ts b/backend/src/lib/gateway/index.ts index 9ec6a471e..9292473e5 100644 --- a/backend/src/lib/gateway/index.ts +++ b/backend/src/lib/gateway/index.ts @@ -1,439 +1,2 @@ -/* eslint-disable no-await-in-loop */ -import crypto from "node:crypto"; -import net from "node:net"; - -import quicDefault, * as quicModule from "@infisical/quic"; -import axios from "axios"; -import https from "https"; - -import { BadRequestError } from "../errors"; -import { logger } from "../logger"; - -const DEFAULT_MAX_RETRIES = 3; -const DEFAULT_RETRY_DELAY = 1000; // 1 second - -const quic = quicDefault || quicModule; - -const parseSubjectDetails = (data: string) => { - const values: Record = {}; - data.split("\n").forEach((el) => { - const [key, value] = el.split("="); - values[key.trim()] = value.trim(); - }); - return values; -}; - -type TTlsOption = { ca: string; cert: string; key: string }; - -const createQuicConnection = async ( - relayHost: string, - relayPort: number, - tlsOptions: TTlsOption, - identityId: string, - orgId: string -) => { - const client = await quic.QUICClient.createQUICClient({ - host: relayHost, - port: relayPort, - config: { - ca: tlsOptions.ca, - cert: tlsOptions.cert, - key: tlsOptions.key, - applicationProtos: ["infisical-gateway"], - verifyPeer: true, - verifyCallback: async (certs) => { - if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired; - const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0])); - const caCertificate = new crypto.X509Certificate(tlsOptions.ca); - const isValidServerCertificate = serverCertificate.verify(caCertificate.publicKey); - if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate; - - const subjectDetails = parseSubjectDetails(serverCertificate.subject); - if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) { - return quic.native.CryptoError.CertificateUnknown; - } - - if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) { - return quic.native.CryptoError.CertificateExpired; - } - - const formatedRelayHost = - process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost; - if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate; - }, - maxIdleTimeout: 90000, - keepAliveIntervalTime: 30000 - }, - crypto: { - ops: { - randomBytes: async (data) => { - crypto.getRandomValues(new Uint8Array(data)); - } - } - } - }); - return client; -}; - -type TPingGatewayAndVerifyDTO = { - relayHost: string; - relayPort: number; - tlsOptions: TTlsOption; - maxRetries?: number; - identityId: string; - orgId: string; -}; - -export const pingGatewayAndVerify = async ({ - relayHost, - relayPort, - tlsOptions, - maxRetries = DEFAULT_MAX_RETRIES, - identityId, - orgId -}: TPingGatewayAndVerifyDTO) => { - let lastError: Error | null = null; - const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { - throw new BadRequestError({ - message: (err as Error)?.message, - error: err as Error - }); - }); - - for (let attempt = 1; attempt <= maxRetries; attempt += 1) { - try { - const stream = quicClient.connection.newStream("bidi"); - const pingWriter = stream.writable.getWriter(); - await pingWriter.write(Buffer.from("PING\n")); - pingWriter.releaseLock(); - - // Read PONG response - const reader = stream.readable.getReader(); - const { value, done } = await reader.read(); - - if (done) { - throw new Error("Gateway closed before receiving PONG"); - } - - const response = Buffer.from(value).toString(); - - if (response !== "PONG\n" && response !== "PONG") { - throw new Error(`Failed to Ping. Unexpected response: ${response}`); - } - - reader.releaseLock(); - return; - } catch (err) { - lastError = err as Error; - - if (attempt < maxRetries) { - await new Promise((resolve) => { - setTimeout(resolve, DEFAULT_RETRY_DELAY); - }); - } - } finally { - await quicClient.destroy(); - } - } - - logger.error(lastError); - throw new BadRequestError({ - message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}` - }); -}; - -interface TProxyServer { - server: net.Server; - port: number; - cleanup: () => Promise; - getProxyError: () => string; -} - -export enum ProxyProtocol { - Http = "http", - Tcp = "tcp" -} - -const setupProxyServer = async ({ - targetPort, - targetHost, - tlsOptions, - relayHost, - relayPort, - identityId, - orgId, - protocol = ProxyProtocol.Tcp, - httpsAgent -}: { - targetHost: string; - targetPort: number; - relayPort: number; - relayHost: string; - tlsOptions: TTlsOption; - identityId: string; - orgId: string; - protocol?: ProxyProtocol; - httpsAgent?: https.Agent; -}): Promise => { - const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { - throw new BadRequestError({ - error: err as Error - }); - }); - const proxyErrorMsg = [""]; - - return new Promise((resolve, reject) => { - const server = net.createServer(); - - let streamClosed = false; - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - server.on("connection", async (clientConn) => { - try { - clientConn.setKeepAlive(true, 30000); // 30 seconds - clientConn.setNoDelay(true); - - const stream = quicClient.connection.newStream("bidi"); - - const forwardWriter = stream.writable.getWriter(); - let command: string; - - if (protocol === ProxyProtocol.Http) { - const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http) - command = `FORWARD-HTTP ${targetUrl}`; - logger.debug(`Using HTTP proxy mode: ${command.trim()}`); - - // extract ca certificate from httpsAgent if present - if (httpsAgent && targetHost.startsWith("https://")) { - const agentOptions = httpsAgent.options; - if (agentOptions && agentOptions.ca) { - const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca; - const caB64 = Buffer.from(caCert as string).toString("base64"); - command += ` ca=${caB64}`; - - const rejectUnauthorized = agentOptions.rejectUnauthorized !== false; - command += ` verify=${rejectUnauthorized}`; - - logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`); - } - } - - command += "\n"; - } else if (protocol === ProxyProtocol.Tcp) { - // For TCP mode, send FORWARD-TCP with host:port - command = `FORWARD-TCP ${targetHost}:${targetPort}\n`; - logger.debug(`Using TCP proxy mode: ${command.trim()}`); - } else { - throw new BadRequestError({ - message: `Invalid protocol: ${protocol as string}` - }); - } - - await forwardWriter.write(Buffer.from(command)); - forwardWriter.releaseLock(); - - // Set up bidirectional copy - const setupCopy = () => { - // Client to QUIC - // eslint-disable-next-line - (async () => { - const writer = stream.writable.getWriter(); - - // Create a handler for client data - clientConn.on("data", (chunk) => { - writer.write(chunk).catch((err) => { - proxyErrorMsg.push((err as Error)?.message); - }); - }); - - // Handle client connection close - clientConn.on("end", () => { - if (!streamClosed) { - try { - writer.close().catch((err) => { - logger.debug(err, "Error closing writer (already closed)"); - }); - } catch (error) { - logger.debug(error, "Error in writer close"); - } - } - }); - - clientConn.on("error", (clientConnErr) => { - writer.abort(clientConnErr?.message).catch((err) => { - proxyErrorMsg.push((err as Error)?.message); - }); - }); - })(); - - // QUIC to Client - void (async () => { - try { - const reader = stream.readable.getReader(); - - let reading = true; - while (reading) { - const { value, done } = await reader.read(); - - if (done) { - reading = false; - clientConn.end(); // Close client connection when QUIC stream ends - break; - } - - // Write data to TCP client - const canContinue = clientConn.write(Buffer.from(value)); - - // Handle backpressure - if (!canContinue) { - await new Promise((res) => { - clientConn.once("drain", res); - }); - } - } - } catch (err) { - proxyErrorMsg.push((err as Error)?.message); - clientConn.destroy(); - } - })(); - }; - - setupCopy(); - // Handle connection closure - clientConn.on("close", () => { - if (!streamClosed) { - streamClosed = true; - stream.destroy().catch((err) => { - logger.debug(err, "Stream already destroyed during close event"); - }); - } - }); - - const cleanup = async () => { - try { - clientConn?.destroy(); - } catch (err) { - logger.debug(err, "Error destroying client connection"); - } - - if (!streamClosed) { - streamClosed = true; - try { - await stream.destroy(); - } catch (err) { - logger.debug(err, "Error destroying stream (might be already closed)"); - } - } - }; - - clientConn.on("error", (clientConnErr) => { - logger.error(clientConnErr, "Client socket error"); - cleanup().catch((err) => { - logger.error(err, "Client conn cleanup"); - }); - }); - - clientConn.on("end", () => { - cleanup().catch((err) => { - logger.error(err, "Client conn end"); - }); - }); - } catch (err) { - logger.error(err, "Failed to establish target connection:"); - clientConn.end(); - reject(err); - } - }); - - server.on("error", (err) => { - reject(err); - }); - - server.on("close", () => { - quicClient?.destroy().catch((err) => { - logger.error(err, "Failed to destroy quic client"); - }); - }); - - server.listen(0, () => { - const address = server.address(); - if (!address || typeof address === "string") { - server.close(); - reject(new Error("Failed to get server port")); - return; - } - - logger.info(`Gateway proxy started on port ${address.port} (${protocol} mode)`); - resolve({ - server, - port: address.port, - cleanup: async () => { - try { - server.close(); - } catch (err) { - logger.debug(err, "Error closing server"); - } - - try { - await quicClient?.destroy(); - } catch (err) { - logger.debug(err, "Error destroying QUIC client"); - } - }, - getProxyError: () => proxyErrorMsg.join(",") - }); - }); - }); -}; - -interface ProxyOptions { - targetHost: string; - targetPort: number; - relayHost: string; - relayPort: number; - tlsOptions: TTlsOption; - identityId: string; - orgId: string; - protocol: ProxyProtocol; - httpsAgent?: https.Agent; -} - -export const withGatewayProxy = async ( - callback: (port: number, httpsAgent?: https.Agent) => Promise, - options: ProxyOptions -): Promise => { - const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId, protocol, httpsAgent } = options; - - // Setup the proxy server - const { port, cleanup, getProxyError } = await setupProxyServer({ - targetHost, - targetPort, - relayPort, - relayHost, - tlsOptions, - identityId, - orgId, - protocol, - httpsAgent - }); - - try { - // Execute the callback with the allocated port - return await callback(port, httpsAgent); - } catch (err) { - const proxyErrorMessage = getProxyError(); - if (proxyErrorMessage) { - logger.error(new Error(proxyErrorMessage), "Failed to proxy"); - } - logger.error(err, "Failed to do gateway"); - let errorMessage = proxyErrorMessage || (err as Error)?.message; - if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) { - errorMessage = (err.response?.data as { message: string }).message; - } - - throw new BadRequestError({ message: errorMessage }); - } finally { - // Ensure cleanup happens regardless of success or failure - await cleanup(); - } -}; +export { pingGatewayAndVerify, withGatewayProxy } from "./gateway"; +export { GatewayHttpProxyActions, GatewayProxyProtocol } from "./types"; diff --git a/backend/src/lib/gateway/types.ts b/backend/src/lib/gateway/types.ts new file mode 100644 index 000000000..5d0ac8237 --- /dev/null +++ b/backend/src/lib/gateway/types.ts @@ -0,0 +1,42 @@ +import net from "node:net"; + +import https from "https"; + +export type TGatewayTlsOptions = { ca: string; cert: string; key: string }; + +export enum GatewayProxyProtocol { + Http = "http", + Tcp = "tcp" +} + +export enum GatewayHttpProxyActions { + InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token" +} + +export interface IGatewayProxyOptions { + targetHost: string; + targetPort: number; + relayHost: string; + relayPort: number; + tlsOptions: TGatewayTlsOptions; + identityId: string; + orgId: string; + protocol: GatewayProxyProtocol; + httpsAgent?: https.Agent; +} + +export type TPingGatewayAndVerifyDTO = { + relayHost: string; + relayPort: number; + tlsOptions: TGatewayTlsOptions; + maxRetries?: number; + identityId: string; + orgId: string; +}; + +export interface IGatewayProxyServer { + server: net.Server; + port: number; + cleanup: () => Promise; + getProxyError: () => string; +} diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 4eb45ba14..3501c8d28 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -20,7 +20,7 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; -import { ProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -92,7 +92,7 @@ export const identityKubernetesAuthServiceFactory = ({ return res; }, { - protocol: inputs.reviewTokenThroughGateway ? ProxyProtocol.Http : ProxyProtocol.Tcp, + protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp, targetHost: inputs.targetHost, targetPort: inputs.targetPort, relayHost, @@ -239,7 +239,7 @@ export const identityKubernetesAuthServiceFactory = ({ { headers: { "Content-Type": "application/json", - "x-infisical-action": "inject-k8s-sa-auth-token" + "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }, signal: AbortSignal.timeout(10000), timeout: 10000, From 78c4c3e847430c7f4e296bb4d10e40e9ec02149f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 18:43:46 +0400 Subject: [PATCH 18/30] Update overview.mdx --- docs/documentation/platform/gateways/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index ae4a3c7ad..30f9641c2 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -128,7 +128,7 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t - + For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: ```bash infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) From e40b4a0a4b3f516860990a3e2a33d45e350eafc6 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 6 Jun 2025 00:31:21 +0530 Subject: [PATCH 19/30] feat: added lock for project create --- backend/src/keystore/keystore.ts | 3 ++- .../src/services/project/project-service.ts | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index a7eec1078..c6ec5dccb 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -10,7 +10,8 @@ export const PgSqlLock = { KmsRootKeyInit: 2025, OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`), OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`), - SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`) + SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`), + CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`) } as const; export type TKeyStoreFactory = ReturnType; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 6b132f654..1ca5eb754 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -30,7 +30,7 @@ import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh- import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -259,16 +259,17 @@ export const projectServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); - const plan = await licenseService.getPlan(organization.id); - if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) { - // case: limit imposed on number of workspaces allowed - // case: number of workspaces used exceeds the number of workspaces allowed - throw new BadRequestError({ - message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." - }); - } - const results = await (trx || projectDAL).transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]); + + const plan = await licenseService.getPlan(organization.id); + if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) { + // case: limit imposed on number of workspaces allowed + // case: number of workspaces used exceeds the number of workspaces allowed + throw new BadRequestError({ + message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." + }); + } const ghostUser = await orgService.addGhostUser(organization.id, tx); if (kmsKeyId) { From ed4882dfac37715c1422c22e890a4494d38e65be Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 5 Jun 2025 23:50:46 +0400 Subject: [PATCH 20/30] fix: simplify gateway http copy logic --- cli/packages/gateway/connection.go | 42 ++++++++++-------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go index 78f614f62..9274086a1 100644 --- a/cli/packages/gateway/connection.go +++ b/cli/packages/gateway/connection.go @@ -210,17 +210,11 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, // create the request to the target proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body) + proxyReq.Header = req.Header.Clone() if err != nil { return fmt.Errorf("failed to create proxy request: %v", err) } - // copy headers - for name, values := range req.Header { - for _, value := range values { - proxyReq.Header.Add(name, value) - } - } - log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL) client := &http.Client{ @@ -236,29 +230,21 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, } defer resp.Body.Close() - // write response to stream - statusLine := fmt.Sprintf("HTTP/1.1 %d %s\r\n", resp.StatusCode, resp.Status[4:]) - if _, err := stream.Write([]byte(statusLine)); err != nil { - return err + // Write the entire response (status line, headers, body) to the stream + // http.Response.Write handles this for "Connection: close" correctly. + // For other connection tokens, manual removal might be needed if they cause issues with QUIC. + // For a simple proxy, this is generally sufficient. + resp.Header.Del("Connection") // Good practice for proxies + + log.Info().Msgf("Writing response to stream: %s", resp.Status) + if err := resp.Write(stream); err != nil { + // If writing the response fails, the connection to the client might be broken. + // Logging the error is important. The original error will be returned. + log.Error().Err(err).Msg("Failed to write response to stream") + return fmt.Errorf("failed to write response to stream: %w", err) } - // write headers again - for name, values := range resp.Header { - for _, value := range values { - headerLine := fmt.Sprintf("%s: %s\r\n", name, value) - if _, err := stream.Write([]byte(headerLine)); err != nil { - return err - } - } - } - - // write empty line to end headers - if _, err := stream.Write([]byte("\r\n")); err != nil { - return err - } - - _, err = io.Copy(stream, resp.Body) - return err + return nil } func buildHttpInternalServerError(message string) string { From 9d24eb15dc97b2fd4440a2f6a1cdbf19ab0a2e58 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 5 Jun 2025 16:01:56 -0400 Subject: [PATCH 21/30] Feedback --- .../services/secret-sync/secret-sync-fns.ts | 18 ++++++++++----- docs/integrations/secret-syncs/overview.mdx | 4 ++++ .../SecretSyncOptionsFields.tsx | 22 ++++++++++++++++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 8e99aabb8..ba3a79c50 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -86,10 +86,17 @@ const addSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema }; // Strip schema from secret keys -const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => { +const stripSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => { if (!schema) return unprocessedSecretMap; - const [prefix, suffix] = schema.split("{{secretKey}}"); + const compiledSchemaPattern = handlebars.compile(schema)({ + secretKey: "{{secretKey}}", // Keep secretKey + environment + }); + + const parts = compiledSchemaPattern.split("{{secretKey}}"); + const prefix = parts[0]; + const suffix = parts[parts.length - 1]; const strippedMap: TSecretMap = {}; @@ -278,10 +285,9 @@ export const SecretSyncFns = { ); } - return stripSchema( - filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema), - secretSync.syncOptions.keySchema - ); + const filtered = filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); + const stripped = stripSchema(filtered, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema); + return stripped; }, removeSecrets: ( secretSync: TSecretSyncWithCredentials, diff --git a/docs/integrations/secret-syncs/overview.mdx b/docs/integrations/secret-syncs/overview.mdx index 65ec4e6cf..937c8d826 100644 --- a/docs/integrations/secret-syncs/overview.mdx +++ b/docs/integrations/secret-syncs/overview.mdx @@ -101,6 +101,10 @@ Key Schemas transform your secret keys by applying a prefix, suffix, or format p Any destination secrets which do not match the schema will not get deleted or updated by Infisical. +Key Schemas use handlebars syntax to define dynamic values. Here's a full list of available variables: +- `{{secretKey}}` - The key of the secret +- `{{environment}}` - The environment which the secret is in (e.g. dev, staging, prod) + **Example:** - Infisical key: `SECRET_1` - Schema: `INFISICAL_{{secretKey}}` diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 6050cab21..7536d1192 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -131,7 +131,27 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { render={({ field: { value, onChange }, fieldState: { error } }) => ( + + When a secret is synced, values will be injected into the key schema before it + reaches the destination. This is useful for organization. + + +

+ Available keys: +
    +
  • + {`{{secretKey}}`} - The key of the secret +
  • +
  • + {`{{environment}}`} - The environment which the secret is in + (e.g. dev, staging, prod) +
  • +
+
+ + } isError={Boolean(error)} isOptional errorText={error?.message} From 9a935c91778b9c74a521f6805038bf6dfe4a9787 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 5 Jun 2025 16:07:00 -0400 Subject: [PATCH 22/30] Lint --- .../forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 7536d1192..30f0b250d 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -142,10 +142,10 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { Available keys:
  • - {`{{secretKey}}`} - The key of the secret + {"{{secretKey}}"} - The key of the secret
  • - {`{{environment}}`} - The environment which the secret is in + {"{{environment}}"} - The environment which the secret is in (e.g. dev, staging, prod)
From 1f80ff040d3fdf302b332a4fb12acaafb0ef65e1 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 6 Jun 2025 01:45:01 +0530 Subject: [PATCH 23/30] feat: added invalidate function to lock --- backend/src/ee/services/license/license-service.ts | 5 +++++ backend/src/services/project/project-service.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index e2cf09bb1..80e58815e 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -709,6 +709,10 @@ export const licenseServiceFactory = ({ return licenses; }; + const invalidateGetPlan = async (orgId: string) => { + await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); + }; + return { generateOrgCustomerId, removeOrgCustomer, @@ -723,6 +727,7 @@ export const licenseServiceFactory = ({ return onPremFeatures; }, getPlan, + invalidateGetPlan, updateSubscriptionOrgMemberCount, refreshPlan, getOrgPlan, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 1ca5eb754..d8eee188a 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -165,7 +165,7 @@ type TProjectServiceFactoryDep = { sshHostGroupDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; - licenseService: Pick; + licenseService: Pick; queueService: Pick; smtpService: Pick; orgDAL: Pick; @@ -494,6 +494,10 @@ export const projectServiceFactory = ({ ); } + // no need to invalidate if there was no limit + if (plan.workspaceLimit) { + await licenseService.invalidateGetPlan(organization.id); + } return { ...project, environments: envs, From eb7c533261a2ed0ae8161b23294ec1c3ad58e19c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Jun 2025 00:26:01 +0400 Subject: [PATCH 24/30] Update identity-kubernetes-auth-service.ts --- .../identity-kubernetes-auth-service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 3501c8d28..df72b428e 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -107,7 +107,7 @@ export const identityKubernetesAuthServiceFactory = ({ // we always pass this, because its needed for both tcp and http protocol httpsAgent: new https.Agent({ ca: inputs.caCert, - rejectUnauthorized: !!inputs.caCert + rejectUnauthorized: Boolean(inputs.caCert) }) } ); @@ -158,6 +158,7 @@ export const identityKubernetesAuthServiceFactory = ({ if (servername.startsWith("https://") || servername.startsWith("http://")) { servername = new RE2("^https?:\\/\\/").replace(servername, ""); } + // get the last colon index, if it has a port, remove it, including the colon const lastColonIndex = servername.lastIndexOf(":"); if (lastColonIndex !== -1) { From cb8e36ae159631ede10c112716bbf414e56600dc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Jun 2025 00:41:35 +0400 Subject: [PATCH 25/30] helm(infisical-gateway): bump CLI image version to latest --- helm-charts/infisical-gateway/CHANGELOG.md | 5 +++++ helm-charts/infisical-gateway/Chart.yaml | 4 ++-- helm-charts/infisical-gateway/values.yaml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/helm-charts/infisical-gateway/CHANGELOG.md b/helm-charts/infisical-gateway/CHANGELOG.md index 832423082..57bb3b031 100644 --- a/helm-charts/infisical-gateway/CHANGELOG.md +++ b/helm-charts/infisical-gateway/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.2 (June 6, 2025) + +* Bumped default CLI image version from 0.41.1 -> 0.41.8. + * This new image version supports using the gateway as a token reviewer for the Identity Kubernetes Auth method. + ## 0.0.1 (May 1, 2025) * Initial helm release \ No newline at end of file diff --git a/helm-charts/infisical-gateway/Chart.yaml b/helm-charts/infisical-gateway/Chart.yaml index 382acd3e1..a53258f68 100644 --- a/helm-charts/infisical-gateway/Chart.yaml +++ b/helm-charts/infisical-gateway/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.1 +version: 0.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.1" +appVersion: "0.0.2" diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml index b1d55c0fc..fef55daa7 100644 --- a/helm-charts/infisical-gateway/values.yaml +++ b/helm-charts/infisical-gateway/values.yaml @@ -1,6 +1,6 @@ image: pullPolicy: IfNotPresent - tag: "0.41.1" + tag: "0.41.8" secret: # The secret that contains the environment variables to be used by the gateway, such as INFISICAL_API_URL and TOKEN From 635f0277527ea05612cb1f4e206436f6c7e94012 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Thu, 5 Jun 2025 20:47:29 -0300 Subject: [PATCH 26/30] Fix for inviteUserToOrganization for usernames with no email formats --- backend/src/services/org/org-service.ts | 26 +++++++++++++++---------- backend/src/services/user/user-dal.ts | 6 +++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index bfd24e639..4a72f12e8 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -835,16 +835,22 @@ export const orgServiceFactory = ({ // if the user doesn't exist we create the user with the email if (!inviteeUser) { - inviteeUser = await userDAL.create( - { - isAccepted: false, - email: inviteeEmail, - username: inviteeEmail, - authMethods: [AuthMethod.EMAIL], - isGhost: false - }, - tx - ); + // TODO(carlos): will be removed once the function receives usernames instead of emails + const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx); + if (usersByEmail?.length === 1) { + [inviteeUser] = usersByEmail; + } else { + inviteeUser = await userDAL.create( + { + isAccepted: false, + email: inviteeEmail, + username: inviteeEmail, + authMethods: [AuthMethod.EMAIL], + isGhost: false + }, + tx + ); + } } const inviteeUserId = inviteeUser?.id; diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index b5a29fc8c..add1b4532 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -21,6 +21,9 @@ export const userDALFactory = (db: TDbClient) => { const findUserByUsername = async (username: string, tx?: Knex) => (tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() }); + const findUserByEmail = async (email: string, tx?: Knex) => + (tx || db)(TableName.Users).whereRaw('lower("email") = :email', { email: email.toLowerCase() }); + const getUsersByFilter = async ({ limit, offset, @@ -234,6 +237,7 @@ export const userDALFactory = (db: TDbClient) => { findOneUserAction, createUserAction, getUsersByFilter, - findAllMyAccounts + findAllMyAccounts, + findUserByEmail }; }; From 5d35ce6c6c28cb8234982327f13162c396ef4fb2 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Thu, 5 Jun 2025 20:59:12 -0300 Subject: [PATCH 27/30] Add isEmailVerified to findUserByEmail --- backend/src/services/user/user-dal.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index add1b4532..0f623dff1 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -22,7 +22,9 @@ export const userDALFactory = (db: TDbClient) => { (tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() }); const findUserByEmail = async (email: string, tx?: Knex) => - (tx || db)(TableName.Users).whereRaw('lower("email") = :email', { email: email.toLowerCase() }); + (tx || db)(TableName.Users).whereRaw('lower("email") = :email', { email: email.toLowerCase() }).where({ + isEmailVerified: true + }); const getUsersByFilter = async ({ limit, From 022ecf75e1ff15dd6dde2a36d76208c6a0fbb55d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Jun 2025 04:02:24 +0400 Subject: [PATCH 28/30] fix(gateway): handle malformed URL's --- .../identity-kubernetes-auth-service.ts | 26 ++++++++++++++++--- cli/packages/gateway/connection.go | 11 ++++++++ helm-charts/infisical-gateway/CHANGELOG.md | 4 +++ helm-charts/infisical-gateway/Chart.yaml | 4 +-- helm-charts/infisical-gateway/values.yaml | 2 +- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index df72b428e..b15a2ed4f 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -274,9 +274,27 @@ export const identityKubernetesAuthServiceFactory = ({ if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { const { kubernetesHost } = identityKubernetesAuth; - const lastColonIndex = kubernetesHost.lastIndexOf(":"); - const k8sHost = kubernetesHost.substring(0, lastColonIndex); - const k8sPort = kubernetesHost.substring(lastColonIndex + 1); + + let urlString = kubernetesHost; + if (!kubernetesHost.startsWith("http://") && !kubernetesHost.startsWith("https://")) { + urlString = `https://${kubernetesHost}`; + } + + const url = new URL(urlString); + let { port: k8sPort } = url; + const { protocol, hostname: k8sHost } = url; + + const cleanedProtocol = new RE2(/[^a-zA-Z0-9]/g).replace(protocol, "").toLowerCase(); + + if (!["https", "http"].includes(cleanedProtocol)) { + throw new BadRequestError({ + message: "Invalid Kubernetes host URL, must start with http:// or https://" + }); + } + + if (!k8sPort) { + k8sPort = cleanedProtocol === "https" ? "443" : "80"; + } if (!identityKubernetesAuth.gatewayId) { throw new BadRequestError({ @@ -287,7 +305,7 @@ export const identityKubernetesAuthServiceFactory = ({ data = await $gatewayProxyWrapper( { gatewayId: identityKubernetesAuth.gatewayId, - targetHost: k8sHost, // note(daniel): must include the protocol (https|http) + targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http) targetPort: k8sPort ? Number(k8sPort) : 443, caCert, reviewTokenThroughGateway: true diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go index 9274086a1..46d194c96 100644 --- a/cli/packages/gateway/connection.go +++ b/cli/packages/gateway/connection.go @@ -12,6 +12,7 @@ import ( "io" "net" "net/http" + "net/url" "os" "strings" "sync" @@ -106,6 +107,11 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) { targetURL := string(argParts[0]) + if !isValidURL(targetURL) { + log.Error().Msgf("Invalid target URL: %s", targetURL) + return + } + // Parse optional parameters var caCertB64, verifyParam string for _, part := range argParts[1:] { @@ -255,6 +261,11 @@ type CloseWrite interface { CloseWrite() error } +func isValidURL(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) { // Create a WaitGroup to wait for both copy operations var wg sync.WaitGroup diff --git a/helm-charts/infisical-gateway/CHANGELOG.md b/helm-charts/infisical-gateway/CHANGELOG.md index 57bb3b031..06ff25bf7 100644 --- a/helm-charts/infisical-gateway/CHANGELOG.md +++ b/helm-charts/infisical-gateway/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.3 (June 6, 2025) + +* Minor fix for handling malformed URL's for HTTP forwarding + ## 0.0.2 (June 6, 2025) * Bumped default CLI image version from 0.41.1 -> 0.41.8. diff --git a/helm-charts/infisical-gateway/Chart.yaml b/helm-charts/infisical-gateway/Chart.yaml index a53258f68..8d9d4dac3 100644 --- a/helm-charts/infisical-gateway/Chart.yaml +++ b/helm-charts/infisical-gateway/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.2 +version: 0.0.3 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.2" +appVersion: "0.0.3" diff --git a/helm-charts/infisical-gateway/values.yaml b/helm-charts/infisical-gateway/values.yaml index fef55daa7..9e897f461 100644 --- a/helm-charts/infisical-gateway/values.yaml +++ b/helm-charts/infisical-gateway/values.yaml @@ -1,6 +1,6 @@ image: pullPolicy: IfNotPresent - tag: "0.41.8" + tag: "0.41.81" secret: # The secret that contains the environment variables to be used by the gateway, such as INFISICAL_API_URL and TOKEN From 4bd1eb6f70eabd0b0a421cb317c7d5773d2c5d19 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 6 Jun 2025 04:12:04 +0400 Subject: [PATCH 29/30] Update helm-charts/infisical-gateway/CHANGELOG.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- helm-charts/infisical-gateway/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm-charts/infisical-gateway/CHANGELOG.md b/helm-charts/infisical-gateway/CHANGELOG.md index 06ff25bf7..9e9921577 100644 --- a/helm-charts/infisical-gateway/CHANGELOG.md +++ b/helm-charts/infisical-gateway/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.0.3 (June 6, 2025) -* Minor fix for handling malformed URL's for HTTP forwarding +* Minor fix for handling malformed URLs for HTTP forwarding ## 0.0.2 (June 6, 2025) From c8be5a637abe4408a9d6f1878740be2ba258b2ff Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 6 Jun 2025 20:08:21 +0530 Subject: [PATCH 30/30] feat: updated dynamic secret,secret import to support glob in environment --- .../services/permission/project-permission.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 34cffcd98..d1ae69574 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -376,7 +376,8 @@ const DynamicSecretConditionV2Schema = z .object({ [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], - [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] }) .partial() ]), @@ -404,6 +405,23 @@ const DynamicSecretConditionV2Schema = z }) .partial(); +const SecretImportConditionSchema = z + .object({ + environment: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA + }) + .partial(); + const SecretConditionV2Schema = z .object({ environment: z.union([ @@ -741,7 +759,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), - conditions: SecretConditionV1Schema.describe( + conditions: SecretImportConditionSchema.describe( "When specified, only matching conditions will be allowed to access given resource." ).optional() }),