From 3e2fe7d4186eb316b2823fd8d0192b28713abb16 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 15 Sep 2026 16:46:26 -0400 Subject: [PATCH 1/8] feat(gateway): GCP enrollment Adds --enroll-method=gcp to gateway start and gateway systemd install. The gce type reads an identity token from the instance metadata server, covering Compute Engine VMs and GKE workload identity; the iam type signs a JWT through the IAM Credentials API, using ADC or a key file. Both carry the gateway ID as the token audience and re-authenticate on every start, so no credential is written to disk. --- go.mod | 4 +- packages/api/api.go | 21 ++++ packages/api/model.go | 11 +++ packages/cmd/gateway.go | 94 ++++++++++++++++-- packages/gateway-v2/constants.go | 8 ++ packages/gateway-v2/gcp_auth.go | 165 +++++++++++++++++++++++++++++++ packages/gateway-v2/systemd.go | 19 +++- 7 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 packages/gateway-v2/gcp_auth.go diff --git a/go.mod b/go.mod index 40b03466..ddf8315e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Infisical/infisical-merge go 1.25.14 require ( + cloud.google.com/go/iam v1.1.11 github.com/Azure/go-ntlmssp v0.1.1 github.com/BobuSumisu/aho-corasick v1.0.3 github.com/Masterminds/sprig/v3 v3.3.0 @@ -57,6 +58,7 @@ require ( golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 + google.golang.org/api v0.267.0 gopkg.in/ini.v1 v1.62.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -69,7 +71,6 @@ require ( cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.1.11 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.1 // indirect github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect @@ -221,7 +222,6 @@ require ( golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/api v0.267.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc v1.82.1 // indirect diff --git a/packages/api/api.go b/packages/api/api.go index 9407c8c5..fac79475 100644 --- a/packages/api/api.go +++ b/packages/api/api.go @@ -59,6 +59,7 @@ const ( operationCallEnrollGateway = "CallEnrollGateway" operationCallAwsAuthLoginGateway = "CallAwsAuthLoginGateway" operationCallKubernetesAuthLoginGateway = "CallKubernetesAuthLoginGateway" + operationCallGcpAuthLoginGateway = "CallGcpAuthLoginGateway" operationCallPAMAccess = "CallPAMAccess" operationCallPAMListAccessibleAccounts = "CallPAMListAccessibleAccounts" operationCallPAMAccessApprovalRequest = "CallPAMAccessApprovalRequest" @@ -1162,6 +1163,26 @@ func CallKubernetesAuthLoginGateway(httpClient *resty.Client, request Kubernetes return resBody, nil } +func CallGcpAuthLoginGateway(httpClient *resty.Client, request GcpAuthLoginGatewayRequest) (GcpAuthLoginGatewayResponse, error) { + var resBody GcpAuthLoginGatewayResponse + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/gateways/login", config.INFISICAL_URL)) + + if err != nil { + return GcpAuthLoginGatewayResponse{}, NewGenericRequestError(operationCallGcpAuthLoginGateway, err) + } + + if response.IsError() { + return GcpAuthLoginGatewayResponse{}, NewAPIErrorWithResponse(operationCallGcpAuthLoginGateway, response, nil) + } + + return resBody, nil +} + func CallPAMAccess(httpClient *resty.Client, request PAMAccessRequest) (PAMAccessResponse, error) { var pamAccessResponse PAMAccessResponse response, err := httpClient. diff --git a/packages/api/model.go b/packages/api/model.go index ed70ac5b..ef2891a2 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -864,6 +864,17 @@ type KubernetesAuthLoginGatewayResponse struct { TokenType string `json:"tokenType"` } +type GcpAuthLoginGatewayRequest struct { + Method string `json:"method"` + GatewayID string `json:"gatewayId"` + JWT string `json:"jwt"` +} + +type GcpAuthLoginGatewayResponse struct { + AccessToken string `json:"accessToken"` + TokenType string `json:"tokenType"` +} + type RegisterGatewayResponse struct { GatewayID string `json:"gatewayId"` DirectAddress string `json:"directAddress,omitempty"` diff --git a/packages/cmd/gateway.go b/packages/cmd/gateway.go index 3c3450c0..31fac168 100644 --- a/packages/cmd/gateway.go +++ b/packages/cmd/gateway.go @@ -233,14 +233,15 @@ var gatewayStartCmd = &cobra.Command{ enrollMethod, _ := cmd.Flags().GetString("enroll-method") // Fall back to env var for systemd-managed runs where flags aren't set. if enrollMethod == "" { - enrollMethod = os.Getenv("INFISICAL_GATEWAY_ENROLL_METHOD") + enrollMethod = os.Getenv(gatewayv2.ENROLL_METHOD_ENV_NAME) } if enrollMethod != "" && enrollMethod != gatewayv2.EnrollMethodToken && enrollMethod != gatewayv2.EnrollMethodAws && + enrollMethod != gatewayv2.EnrollMethodGcp && enrollMethod != gatewayv2.EnrollMethodKubernetes { - util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid enroll method: %s. Valid values are '%s', '%s', and '%s'", - enrollMethod, gatewayv2.EnrollMethodToken, gatewayv2.EnrollMethodAws, gatewayv2.EnrollMethodKubernetes)) + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid enroll method: %s. Valid values are '%s', '%s', '%s', and '%s'", + enrollMethod, gatewayv2.EnrollMethodToken, gatewayv2.EnrollMethodAws, gatewayv2.EnrollMethodGcp, gatewayv2.EnrollMethodKubernetes)) } var alreadyEnrolled bool var enrolledAccessToken string // set during fresh enrollment, used directly to avoid env var interference @@ -308,6 +309,56 @@ var gatewayStartCmd = &cobra.Command{ log.Info().Msg("Starting gateway...") } + // --- GCP Auth path --- + if enrollMethod == gatewayv2.EnrollMethodGcp { + gatewayID, _ := cmd.Flags().GetString("gateway-id") + if gatewayID == "" { + gatewayID = os.Getenv(gatewayv2.INFISICAL_GATEWAY_ID_KEY) + } + if gatewayID == "" { + stored, _ := gatewayv2.LoadStoredGatewayID(gatewayName) + gatewayID = stored + } + if gatewayID == "" { + util.HandleError(errors.New("--gateway-id is required when --enroll-method=gcp")) + } + + gcpAuthType, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "gcp-auth-type", []string{gatewayv2.GCP_AUTH_TYPE_ENV_NAME}, gatewayv2.GcpAuthTypeGce) + if gcpAuthType != gatewayv2.GcpAuthTypeGce && gcpAuthType != gatewayv2.GcpAuthTypeIam { + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid gcp auth type: %s. Valid values are '%s' and '%s'", + gcpAuthType, gatewayv2.GcpAuthTypeGce, gatewayv2.GcpAuthTypeIam)) + } + + serviceAccountKeyPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "unable to create HTTP client") + } + + log.Info().Msgf("Authenticating gateway via GCP Auth (%s)...", gcpAuthType) + accessTokenStr, err := gatewayv2.LoginGatewayWithGcp(cmd.Context(), httpClient, gatewayID, gcpAuthType, serviceAccountKeyPath) + if err != nil { + util.HandleError(err, "GCP Auth login failed") + } + + enrolledAccessToken = accessTokenStr + alreadyEnrolled = true + + // No SaveAccessToken here: a fresh JWT is minted on every start, so an on-disk copy + // would only ever be stale. + if err := gatewayv2.SaveGatewayID(gatewayName, gatewayID); err != nil { + util.HandleError(err, "failed to save gateway id to config") + } + + if err := gatewayv2.SaveDomain(gatewayName, config.INFISICAL_URL); err != nil { + util.HandleError(err, "failed to save domain to config") + } + + log.Info().Msgf("Gateway authenticated via GCP Auth. State saved to %s", gatewayv2.GetConfPathDisplay(gatewayName)) + log.Info().Msg("Starting gateway...") + } + // --- Kubernetes Auth path --- if enrollMethod == gatewayv2.EnrollMethodKubernetes { gatewayID, _ := cmd.Flags().GetString("gateway-id") @@ -401,6 +452,7 @@ var gatewayStartCmd = &cobra.Command{ isResourceAuth := enrollMethod == gatewayv2.EnrollMethodToken || enrollMethod == gatewayv2.EnrollMethodAws || + enrollMethod == gatewayv2.EnrollMethodGcp || enrollMethod == gatewayv2.EnrollMethodKubernetes // Only use the stored token when no explicit identity credentials are provided. @@ -752,6 +804,29 @@ var gatewaySystemdInstallCmd = &cobra.Command{ util.HandleError(installErr, "Unable to install systemd service") } installedServiceName = svcName + } else if enrollMethod == gatewayv2.EnrollMethodGcp { + // --- GCP Auth path --- + // As with AWS, the login happens on each service start rather than at install time. + gatewayID, _ := cmd.Flags().GetString("gateway-id") + if gatewayID == "" { + util.HandleError(errors.New("--gateway-id is required when --enroll-method=gcp")) + } + + gcpAuthType, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "gcp-auth-type", []string{gatewayv2.GCP_AUTH_TYPE_ENV_NAME}, gatewayv2.GcpAuthTypeGce) + if gcpAuthType != gatewayv2.GcpAuthTypeGce && gcpAuthType != gatewayv2.GcpAuthTypeIam { + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid gcp auth type: %s. Valid values are '%s' and '%s'", + gcpAuthType, gatewayv2.GcpAuthTypeGce, gatewayv2.GcpAuthTypeIam)) + } + + serviceAccountKeyPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + + relayName, _ := resolveRelayName("") + + svcName, installErr := gatewayv2.InstallGcpAuthGatewaySystemdService(gatewayID, gcpAuthType, serviceAccountKeyPath, domain, gatewayName, relayName, listenAddress, bindAddress, serviceLogFile, pkcs11ModulePath) + if installErr != nil { + util.HandleError(installErr, "Unable to install systemd service") + } + installedServiceName = svcName } else { // --- Machine identity token path --- token, tokenErr := util.GetInfisicalToken(cmd) @@ -862,9 +937,10 @@ func init() { gatewayStartCmd.Flags().String("name", "", "name of the gateway (deprecated, use positional argument instead)") _ = gatewayStartCmd.Flags().MarkDeprecated("name", "use positional argument instead: infisical gateway start ") gatewayStartCmd.Flags().String("token", "", "enrollment token or access token for authenticating with Infisical") - gatewayStartCmd.Flags().String("enroll-method", "", "gateway auth method [token, aws, kubernetes]. when set to 'token', uses --token as a one-time enrollment token. when set to 'aws', authenticates via signed STS GetCallerIdentity using --gateway-id. when set to 'kubernetes', authenticates with the pod's service account token using --gateway-id") - gatewayStartCmd.Flags().String("gateway-id", "", "gateway id (required when --enroll-method=aws or --enroll-method=kubernetes)") - gatewayStartCmd.Flags().String("domain", "", "domain of your self-hosted Infisical instance (used with --enroll-method=token, --enroll-method=aws, or --enroll-method=kubernetes)") + gatewayStartCmd.Flags().String("enroll-method", "", "gateway auth method [token, aws, gcp, kubernetes]. when set to 'token', uses --token as a one-time enrollment token. when set to 'aws', authenticates via signed STS GetCallerIdentity using --gateway-id. when set to 'gcp', authenticates with a GCP identity token using --gateway-id. when set to 'kubernetes', authenticates with the pod's service account token using --gateway-id") + gatewayStartCmd.Flags().String("gateway-id", "", "gateway id (required when --enroll-method=aws, --enroll-method=gcp or --enroll-method=kubernetes)") + gatewayStartCmd.Flags().String("gcp-auth-type", "", "how the gateway proves its GCP identity when --enroll-method=gcp [gce, iam]. 'gce' reads an identity token from the instance metadata server, which covers Compute Engine VMs and GKE workload identity. 'iam' signs a JWT through the IAM Credentials API. defaults to gce") + gatewayStartCmd.Flags().String("domain", "", "domain of your self-hosted Infisical instance (used with --enroll-method=token, --enroll-method=aws, --enroll-method=gcp, or --enroll-method=kubernetes)") gatewayStartCmd.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") gatewayStartCmd.Flags().String("organization-slug", "", "When set, this will scope the login session to the specified sub-organization the machine identity has access to. If left empty, the session defaults to the organization where the machine identity was created in.") gatewayStartCmd.Flags().String("client-id", "", "client id for universal auth") @@ -884,8 +960,10 @@ func init() { // Systemd install command flags (v2) gatewaySystemdInstallCmd.Flags().String("token", "", "enrollment token or access token for authenticating with Infisical") - gatewaySystemdInstallCmd.Flags().String("enroll-method", "", "gateway auth method [token, aws]. when set to 'token', uses --token as a one-time enrollment token. when set to 'aws', the gateway authenticates via AWS STS on each service start (requires --gateway-id). 'kubernetes' is not available here: in-cluster gateways are not managed by systemd") - gatewaySystemdInstallCmd.Flags().String("gateway-id", "", "gateway id (required when --enroll-method=aws)") + gatewaySystemdInstallCmd.Flags().String("enroll-method", "", "gateway auth method [token, aws, gcp]. when set to 'token', uses --token as a one-time enrollment token. when set to 'aws', the gateway authenticates via AWS STS on each service start (requires --gateway-id). when set to 'gcp', it authenticates with a GCP identity token on each service start (requires --gateway-id). 'kubernetes' is not available here: in-cluster gateways are not managed by systemd") + gatewaySystemdInstallCmd.Flags().String("gateway-id", "", "gateway id (required when --enroll-method=aws or --enroll-method=gcp)") + gatewaySystemdInstallCmd.Flags().String("gcp-auth-type", "", "how the gateway proves its GCP identity when --enroll-method=gcp [gce, iam]. defaults to gce") + gatewaySystemdInstallCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") gatewaySystemdInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance") gatewaySystemdInstallCmd.Flags().String("name", "", "The name of the gateway (deprecated, use positional argument instead)") _ = gatewaySystemdInstallCmd.Flags().MarkDeprecated("name", "use positional argument instead: infisical gateway systemd install ") diff --git a/packages/gateway-v2/constants.go b/packages/gateway-v2/constants.go index 84b9ed61..ed122219 100644 --- a/packages/gateway-v2/constants.go +++ b/packages/gateway-v2/constants.go @@ -16,6 +16,8 @@ const ( RELAY_HOST_ENV_NAME = "INFISICAL_RELAY_HOST" RELAY_TYPE_ENV_NAME = "INFISICAL_RELAY_TYPE" GATEWAY_NAME_ENV_NAME = "INFISICAL_GATEWAY_NAME" + ENROLL_METHOD_ENV_NAME = "INFISICAL_GATEWAY_ENROLL_METHOD" + GCP_AUTH_TYPE_ENV_NAME = "INFISICAL_GATEWAY_GCP_AUTH_TYPE" RELAY_AUTH_SECRET_ENV_NAME = "INFISICAL_RELAY_AUTH_SECRET" INFISICAL_TOKEN_ENV_NAME = "INFISICAL_TOKEN" @@ -30,8 +32,14 @@ const ( // Gateway auth-method discriminators. Used both for matching the user's --enroll-method // flag value and as the `method` field on the /v3/gateways/login request body. EnrollMethodAws = "aws" + EnrollMethodGcp = "gcp" EnrollMethodKubernetes = "kubernetes" EnrollMethodToken = "token" + + // How the gateway proves its GCP identity: an instance metadata ID token, or a JWT the + // service account signs through the IAM Credentials API. + GcpAuthTypeGce = "gce" + GcpAuthTypeIam = "iam" ) type HttpProxyAction string diff --git a/packages/gateway-v2/gcp_auth.go b/packages/gateway-v2/gcp_auth.go new file mode 100644 index 00000000..7077b459 --- /dev/null +++ b/packages/gateway-v2/gcp_auth.go @@ -0,0 +1,165 @@ +package gatewayv2 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "time" + + credentials "cloud.google.com/go/iam/credentials/apiv1" + "cloud.google.com/go/iam/credentials/apiv1/credentialspb" + "github.com/Infisical/infisical-merge/packages/api" + "github.com/go-resty/resty/v2" + "google.golang.org/api/option" +) + +const ( + gcpMetadataIdentityURL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity" + gcpMetadataEmailURL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email" + gcpMetadataTimeout = 10 * time.Second +) + +// LoginGatewayWithGcp proves the gateway's GCP identity to Infisical and exchanges the proof for a +// GATEWAY_ACCESS_TOKEN. Both token types carry the gateway ID as their audience, so a token minted +// for one gateway cannot authenticate as another. +func LoginGatewayWithGcp(ctx context.Context, httpClient *resty.Client, gatewayID string, authType string, serviceAccountKeyPath string) (string, error) { + if gatewayID == "" { + return "", errors.New("--gateway-id is required when --enroll-method=gcp") + } + + var jwt string + var err error + switch authType { + case GcpAuthTypeGce: + if serviceAccountKeyPath != "" { + return "", errors.New("--service-account-key-file-path only applies to --gcp-auth-type=iam. The gce type reads its token from the instance metadata server") + } + jwt, err = fetchGcpIdentityToken(ctx, gatewayID) + case GcpAuthTypeIam: + jwt, err = signGcpServiceAccountJwt(ctx, gatewayID, serviceAccountKeyPath) + default: + return "", fmt.Errorf("invalid --gcp-auth-type: %s. Valid values are '%s' and '%s'", authType, GcpAuthTypeGce, GcpAuthTypeIam) + } + if err != nil { + return "", err + } + + resp, err := api.CallGcpAuthLoginGateway(httpClient, api.GcpAuthLoginGatewayRequest{ + Method: EnrollMethodGcp, + GatewayID: gatewayID, + JWT: jwt, + }) + if err != nil { + return "", err + } + + return resp.AccessToken, nil +} + +func fetchGcpIdentityToken(ctx context.Context, audience string) (string, error) { + res, err := resty.New(). + SetTimeout(gcpMetadataTimeout). + R(). + SetContext(ctx). + SetHeader("Metadata-Flavor", "Google"). + SetQueryParam("audience", audience). + SetQueryParam("format", "full"). + Get(gcpMetadataIdentityURL) + + if err != nil { + return "", fmt.Errorf("unable to reach the GCP metadata server. --gcp-auth-type=gce requires the gateway to run on a Compute Engine instance or a GKE pod with workload identity: %w", err) + } + + if res.IsError() { + return "", fmt.Errorf("the GCP metadata server rejected the identity token request [status-code=%d]: %s", res.StatusCode(), res.String()) + } + + token := strings.TrimSpace(res.String()) + if token == "" { + return "", errors.New("the GCP metadata server returned an empty identity token") + } + + return token, nil +} + +func signGcpServiceAccountJwt(ctx context.Context, audience string, serviceAccountKeyPath string) (string, error) { + clientEmail, err := resolveGcpServiceAccountEmail(ctx, serviceAccountKeyPath) + if err != nil { + return "", err + } + + payload, err := json.Marshal(map[string]string{"sub": clientEmail, "aud": audience}) + if err != nil { + return "", fmt.Errorf("unable to build the GCP JWT payload: %w", err) + } + + var opts []option.ClientOption + if serviceAccountKeyPath != "" { + opts = append(opts, option.WithCredentialsFile(serviceAccountKeyPath)) + } + + client, err := credentials.NewIamCredentialsClient(ctx, opts...) //nolint:staticcheck // deprecated but no drop-in replacement available yet + if err != nil { + return "", fmt.Errorf("unable to create the GCP IAM credentials client: %w", err) + } + defer client.Close() //nolint:errcheck + + resp, err := client.SignJwt(ctx, &credentialspb.SignJwtRequest{ + Name: fmt.Sprintf("projects/-/serviceAccounts/%s", clientEmail), + Payload: string(payload), + }) + if err != nil { + return "", fmt.Errorf("unable to sign the GCP JWT as %s. Ensure the IAM Service Account Credentials API is enabled and the caller holds roles/iam.serviceAccountTokenCreator on that service account: %w", clientEmail, err) + } + + if resp.SignedJwt == "" { + return "", errors.New("GCP returned an empty signed JWT") + } + + return resp.SignedJwt, nil +} + +func resolveGcpServiceAccountEmail(ctx context.Context, serviceAccountKeyPath string) (string, error) { + if serviceAccountKeyPath != "" { + keyBytes, err := os.ReadFile(serviceAccountKeyPath) + if err != nil { + return "", fmt.Errorf("unable to read the GCP service account key at %s: %w", serviceAccountKeyPath, err) + } + + var key struct { + ClientEmail string `json:"client_email"` + } + if err := json.Unmarshal(keyBytes, &key); err != nil { + return "", fmt.Errorf("unable to parse the GCP service account key at %s: %w", serviceAccountKeyPath, err) + } + if key.ClientEmail == "" { + return "", fmt.Errorf("the GCP service account key at %s has no client_email", serviceAccountKeyPath) + } + return key.ClientEmail, nil + } + + res, err := resty.New(). + SetTimeout(gcpMetadataTimeout). + R(). + SetContext(ctx). + SetHeader("Metadata-Flavor", "Google"). + Get(gcpMetadataEmailURL) + + if err != nil { + return "", fmt.Errorf("unable to determine which GCP service account to sign as. Provide --service-account-key-file-path, or run the gateway where the metadata server is reachable: %w", err) + } + + if res.IsError() { + return "", fmt.Errorf("the GCP metadata server rejected the service account email request [status-code=%d]: %s", res.StatusCode(), res.String()) + } + + email := strings.TrimSpace(res.String()) + if email == "" { + return "", errors.New("the GCP metadata server returned an empty service account email") + } + + return email, nil +} diff --git a/packages/gateway-v2/systemd.go b/packages/gateway-v2/systemd.go index 103e62f7..b9e49963 100644 --- a/packages/gateway-v2/systemd.go +++ b/packages/gateway-v2/systemd.go @@ -222,6 +222,20 @@ func InstallEnrolledGatewaySystemdService(accessToken string, domain string, nam // (instance role, env vars, shared profile). We just persist the gateway id, domain, and name // so `gateway start` can re-authenticate. func InstallAwsAuthGatewaySystemdService(gatewayID string, domain string, name string, relayName string, listenAddress string, bindAddress string, serviceLogFile string, pkcs11ModulePath string) (string, error) { + return installResourceAuthGatewaySystemdService(EnrollMethodAws, gatewayID, nil, domain, name, relayName, listenAddress, bindAddress, serviceLogFile, pkcs11ModulePath) +} + +// InstallGcpAuthGatewaySystemdService is the GCP equivalent: the gateway mints a fresh identity +// token on each service start, so only the gateway id and the token type are persisted. +func InstallGcpAuthGatewaySystemdService(gatewayID string, gcpAuthType string, serviceAccountKeyPath string, domain string, name string, relayName string, listenAddress string, bindAddress string, serviceLogFile string, pkcs11ModulePath string) (string, error) { + extraEnv := [][2]string{{GCP_AUTH_TYPE_ENV_NAME, gcpAuthType}} + if serviceAccountKeyPath != "" { + extraEnv = append(extraEnv, [2]string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME, serviceAccountKeyPath}) + } + return installResourceAuthGatewaySystemdService(EnrollMethodGcp, gatewayID, extraEnv, domain, name, relayName, listenAddress, bindAddress, serviceLogFile, pkcs11ModulePath) +} + +func installResourceAuthGatewaySystemdService(enrollMethod string, gatewayID string, extraEnv [][2]string, domain string, name string, relayName string, listenAddress string, bindAddress string, serviceLogFile string, pkcs11ModulePath string) (string, error) { if runtime.GOOS != "linux" { log.Info().Msg("Skipping systemd service installation - not on Linux") return "", nil @@ -242,7 +256,10 @@ func InstallAwsAuthGatewaySystemdService(gatewayID string, domain string, name s } configContent := fmt.Sprintf("%s=%s\n", INFISICAL_GATEWAY_ID_KEY, gatewayID) - configContent += "INFISICAL_GATEWAY_ENROLL_METHOD=aws\n" + configContent += fmt.Sprintf("%s=%s\n", ENROLL_METHOD_ENV_NAME, enrollMethod) + for _, entry := range extraEnv { + configContent += fmt.Sprintf("%s=%s\n", entry[0], entry[1]) + } if domain != "" { configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) } From 5c9b9a22e94aafedeeaafbf804981632c6083a1e Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 15 Sep 2026 17:15:36 -0400 Subject: [PATCH 2/8] fix(gateway): bound the signed gcp jwt and validate the systemd key path Addresses PR review findings. The signed IAM JWT carried only sub and aud, so a captured login request stayed a valid proof forever. It now carries iat and a 5 minute exp, which the backend requires. A service account key path under a home directory installed fine and then failed on every service start, because the unit runs with InaccessibleDirectories=/home and no working directory. The install now rejects a relative path, a path under /home, and a missing file. --- packages/cmd/gateway.go | 15 +++++++++++++++ packages/gateway-v2/gcp_auth.go | 11 ++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/cmd/gateway.go b/packages/cmd/gateway.go index 31fac168..de1f0bcd 100644 --- a/packages/cmd/gateway.go +++ b/packages/cmd/gateway.go @@ -8,6 +8,7 @@ import ( "os/signal" "path/filepath" "runtime" + "strings" "sync/atomic" "syscall" "time" @@ -819,6 +820,20 @@ var gatewaySystemdInstallCmd = &cobra.Command{ } serviceAccountKeyPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + if serviceAccountKeyPath != "" { + // The unit runs with InaccessibleDirectories=/home and no working directory, so a key + // under a home directory or given relatively installs fine and then fails to open on + // every service start. + if !filepath.IsAbs(serviceAccountKeyPath) { + util.HandleError(fmt.Errorf("--service-account-key-file-path must be an absolute path (got %q)", serviceAccountKeyPath)) + } + if strings.HasPrefix(serviceAccountKeyPath, "/home/") { + util.HandleError(fmt.Errorf("--service-account-key-file-path must not be under /home: the systemd service cannot read it there. Move the key somewhere like /etc/infisical (got %q)", serviceAccountKeyPath)) + } + if _, statErr := os.Stat(serviceAccountKeyPath); statErr != nil { + util.HandleError(fmt.Errorf("GCP service account key not found at %q: %w", serviceAccountKeyPath, statErr)) + } + } relayName, _ := resolveRelayName("") diff --git a/packages/gateway-v2/gcp_auth.go b/packages/gateway-v2/gcp_auth.go index 7077b459..f111d6ca 100644 --- a/packages/gateway-v2/gcp_auth.go +++ b/packages/gateway-v2/gcp_auth.go @@ -20,6 +20,7 @@ const ( gcpMetadataIdentityURL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity" gcpMetadataEmailURL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email" gcpMetadataTimeout = 10 * time.Second + gcpIamTokenLifetime = 5 * time.Minute ) // LoginGatewayWithGcp proves the gateway's GCP identity to Infisical and exchanges the proof for a @@ -91,7 +92,15 @@ func signGcpServiceAccountJwt(ctx context.Context, audience string, serviceAccou return "", err } - payload, err := json.Marshal(map[string]string{"sub": clientEmail, "aud": audience}) + // A signed JWT with no expiry stays a valid proof forever, so a captured login request could be + // replayed indefinitely. The backend refuses a token without a bounded expiry. + now := time.Now() + payload, err := json.Marshal(map[string]any{ + "sub": clientEmail, + "aud": audience, + "iat": now.Unix(), + "exp": now.Add(gcpIamTokenLifetime).Unix(), + }) if err != nil { return "", fmt.Errorf("unable to build the GCP JWT payload: %w", err) } From 277c3927e07c0f46a44bc5d294384a5217176d3c Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 15 Sep 2026 17:21:56 -0400 Subject: [PATCH 3/8] chore(deps): bump grpc to v1.83.2 Clears GO-2026-6348, GO-2026-6441 and GO-2026-6443, which govulncheck flags as non-allowlisted. These predate this branch and also fail on main; the advisories were published after main last ran green. --- go.mod | 12 +++++------- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index ddf8315e..ec505841 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( ) require ( - cloud.google.com/go/auth v0.18.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.1 // indirect @@ -212,19 +212,17 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/sdk v1.44.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 09f70aff..2bb67118 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= -cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -851,8 +851,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1104,10 +1104,10 @@ google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaE google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1128,8 +1128,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 8c7f90c48a030f5f5896244e196a25e2421f2a17 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 15 Sep 2026 17:48:21 -0400 Subject: [PATCH 4/8] chore(deps): tidy the e2e module after the grpc bump The e2e tests are their own module and share the root dependency graph, so `go test` there fails with 'updates to go.mod needed' until it is tidied alongside. --- e2e/go.mod | 10 +++++----- e2e/go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/e2e/go.mod b/e2e/go.mod index 861d9d01..6c690677 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -27,7 +27,7 @@ require ( ) require ( - cloud.google.com/go/auth v0.18.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.1.11 // indirect @@ -338,7 +338,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect @@ -347,9 +347,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.48.0 // indirect google.golang.org/api v0.267.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 72a6ca30..aa2140e6 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -18,8 +18,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= -cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -1267,8 +1267,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1538,10 +1538,10 @@ google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaE google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1563,8 +1563,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 76739e82fb2a91a0acac08a628caaaeabffa4a3c Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 15 Sep 2026 22:17:31 -0400 Subject: [PATCH 5/8] chore(gateway): trim comments to one line each --- packages/cmd/gateway.go | 7 +------ packages/gateway-v2/constants.go | 2 -- packages/gateway-v2/gcp_auth.go | 8 +++----- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/cmd/gateway.go b/packages/cmd/gateway.go index de1f0bcd..461f7c2f 100644 --- a/packages/cmd/gateway.go +++ b/packages/cmd/gateway.go @@ -346,8 +346,6 @@ var gatewayStartCmd = &cobra.Command{ enrolledAccessToken = accessTokenStr alreadyEnrolled = true - // No SaveAccessToken here: a fresh JWT is minted on every start, so an on-disk copy - // would only ever be stale. if err := gatewayv2.SaveGatewayID(gatewayName, gatewayID); err != nil { util.HandleError(err, "failed to save gateway id to config") } @@ -807,7 +805,6 @@ var gatewaySystemdInstallCmd = &cobra.Command{ installedServiceName = svcName } else if enrollMethod == gatewayv2.EnrollMethodGcp { // --- GCP Auth path --- - // As with AWS, the login happens on each service start rather than at install time. gatewayID, _ := cmd.Flags().GetString("gateway-id") if gatewayID == "" { util.HandleError(errors.New("--gateway-id is required when --enroll-method=gcp")) @@ -821,9 +818,7 @@ var gatewaySystemdInstallCmd = &cobra.Command{ serviceAccountKeyPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) if serviceAccountKeyPath != "" { - // The unit runs with InaccessibleDirectories=/home and no working directory, so a key - // under a home directory or given relatively installs fine and then fails to open on - // every service start. + // The unit sets InaccessibleDirectories=/home with no working directory. if !filepath.IsAbs(serviceAccountKeyPath) { util.HandleError(fmt.Errorf("--service-account-key-file-path must be an absolute path (got %q)", serviceAccountKeyPath)) } diff --git a/packages/gateway-v2/constants.go b/packages/gateway-v2/constants.go index ed122219..df5b1763 100644 --- a/packages/gateway-v2/constants.go +++ b/packages/gateway-v2/constants.go @@ -36,8 +36,6 @@ const ( EnrollMethodKubernetes = "kubernetes" EnrollMethodToken = "token" - // How the gateway proves its GCP identity: an instance metadata ID token, or a JWT the - // service account signs through the IAM Credentials API. GcpAuthTypeGce = "gce" GcpAuthTypeIam = "iam" ) diff --git a/packages/gateway-v2/gcp_auth.go b/packages/gateway-v2/gcp_auth.go index f111d6ca..1ccceeaf 100644 --- a/packages/gateway-v2/gcp_auth.go +++ b/packages/gateway-v2/gcp_auth.go @@ -23,9 +23,8 @@ const ( gcpIamTokenLifetime = 5 * time.Minute ) -// LoginGatewayWithGcp proves the gateway's GCP identity to Infisical and exchanges the proof for a -// GATEWAY_ACCESS_TOKEN. Both token types carry the gateway ID as their audience, so a token minted -// for one gateway cannot authenticate as another. +// LoginGatewayWithGcp exchanges a GCP identity proof for a GATEWAY_ACCESS_TOKEN. Both token types +// carry the gateway ID as their audience. func LoginGatewayWithGcp(ctx context.Context, httpClient *resty.Client, gatewayID string, authType string, serviceAccountKeyPath string) (string, error) { if gatewayID == "" { return "", errors.New("--gateway-id is required when --enroll-method=gcp") @@ -92,8 +91,7 @@ func signGcpServiceAccountJwt(ctx context.Context, audience string, serviceAccou return "", err } - // A signed JWT with no expiry stays a valid proof forever, so a captured login request could be - // replayed indefinitely. The backend refuses a token without a bounded expiry. + // Without an expiry the proof is replayable forever; the backend refuses one. now := time.Now() payload, err := json.Marshal(map[string]any{ "sub": clientEmail, From d58c106424b7ce3b6af9eef8b0c1396ced14c531 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 17 Sep 2026 17:27:23 -0400 Subject: [PATCH 6/8] fix(gateway): scope the gcp key file flag to the iam type The key path was read for every gcp login, so a stray INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH left over from machine identity auth made a plain gce login fail for no visible reason. It is read only for the iam type now, and passing the flag with gce fails at install time rather than producing a service that never starts. The systemd path check also cleans the path first and covers /tmp, which the unit makes private. --- packages/cmd/gateway.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/cmd/gateway.go b/packages/cmd/gateway.go index 461f7c2f..45cf48a5 100644 --- a/packages/cmd/gateway.go +++ b/packages/cmd/gateway.go @@ -330,7 +330,12 @@ var gatewayStartCmd = &cobra.Command{ gcpAuthType, gatewayv2.GcpAuthTypeGce, gatewayv2.GcpAuthTypeIam)) } - serviceAccountKeyPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + var serviceAccountKeyPath string + if gcpAuthType == gatewayv2.GcpAuthTypeIam { + serviceAccountKeyPath, _ = util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + } else if keyPath, _ := cmd.Flags().GetString("service-account-key-file-path"); keyPath != "" { + util.PrintErrorMessageAndExit(fmt.Sprintf("--service-account-key-file-path only applies to --gcp-auth-type=%s", gatewayv2.GcpAuthTypeIam)) + } httpClient, err := util.GetRestyClientWithCustomHeaders() if err != nil { @@ -816,14 +821,22 @@ var gatewaySystemdInstallCmd = &cobra.Command{ gcpAuthType, gatewayv2.GcpAuthTypeGce, gatewayv2.GcpAuthTypeIam)) } - serviceAccountKeyPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + var serviceAccountKeyPath string + if gcpAuthType == gatewayv2.GcpAuthTypeIam { + serviceAccountKeyPath, _ = util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", []string{util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) + } else if keyPath, _ := cmd.Flags().GetString("service-account-key-file-path"); keyPath != "" { + util.PrintErrorMessageAndExit(fmt.Sprintf("--service-account-key-file-path only applies to --gcp-auth-type=%s", gatewayv2.GcpAuthTypeIam)) + } if serviceAccountKeyPath != "" { // The unit sets InaccessibleDirectories=/home with no working directory. if !filepath.IsAbs(serviceAccountKeyPath) { util.HandleError(fmt.Errorf("--service-account-key-file-path must be an absolute path (got %q)", serviceAccountKeyPath)) } - if strings.HasPrefix(serviceAccountKeyPath, "/home/") { - util.HandleError(fmt.Errorf("--service-account-key-file-path must not be under /home: the systemd service cannot read it there. Move the key somewhere like /etc/infisical (got %q)", serviceAccountKeyPath)) + cleaned := filepath.Clean(serviceAccountKeyPath) + for _, dir := range []string{"/home/", "/tmp/"} { + if strings.HasPrefix(cleaned, dir) { + util.HandleError(fmt.Errorf("--service-account-key-file-path must not be under %s: the systemd service cannot read it there. Move the key somewhere like /etc/infisical (got %q)", strings.TrimSuffix(dir, "/"), serviceAccountKeyPath)) + } } if _, statErr := os.Stat(serviceAccountKeyPath); statErr != nil { util.HandleError(fmt.Errorf("GCP service account key not found at %q: %w", serviceAccountKeyPath, statErr)) From 9bc13bc961a6f179ff5a0e770fef912dc830d115 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 17 Sep 2026 21:06:32 -0400 Subject: [PATCH 7/8] fix(gateway): name the instance scope cause when signJwt fails On a Compute Engine instance the usual cause is the default scopes omitting iamcredentials, which the error never mentioned. It pointed at the API and the IAM role instead, neither of which is the problem. --- packages/gateway-v2/gcp_auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gateway-v2/gcp_auth.go b/packages/gateway-v2/gcp_auth.go index 1ccceeaf..262b08db 100644 --- a/packages/gateway-v2/gcp_auth.go +++ b/packages/gateway-v2/gcp_auth.go @@ -119,7 +119,7 @@ func signGcpServiceAccountJwt(ctx context.Context, audience string, serviceAccou Payload: string(payload), }) if err != nil { - return "", fmt.Errorf("unable to sign the GCP JWT as %s. Ensure the IAM Service Account Credentials API is enabled and the caller holds roles/iam.serviceAccountTokenCreator on that service account: %w", clientEmail, err) + return "", fmt.Errorf("unable to sign the GCP JWT as %s. On a Compute Engine instance this is usually the instance scopes: the defaults omit iamcredentials, so the instance needs the cloud-platform scope, which can only be changed while it is stopped. Otherwise check that the IAM Service Account Credentials API is enabled and that the caller holds roles/iam.serviceAccountTokenCreator on that service account: %w", clientEmail, err) } if resp.SignedJwt == "" { From e8c4fabb5e23525d10104bd2e4c5422d23ff7fc0 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 17 Sep 2026 21:14:27 -0400 Subject: [PATCH 8/8] refactor(gateway): one response type and one call for gateway login The three login methods post to the same endpoint and get the same body back, so aws, gcp and kubernetes each had an identical response struct and a call function differing only in type names. They share GatewayLoginResponse and CallGatewayLogin now. The request types stay per-method: aws sends a signed STS request where the other two send a JWT, and each struct documents what its own method puts on the wire. --- packages/api/api.go | 52 +++----------------------- packages/api/model.go | 21 +++-------- packages/gateway-v2/aws_auth.go | 2 +- packages/gateway-v2/enroll.go | 4 +- packages/gateway-v2/gcp_auth.go | 2 +- packages/gateway-v2/kubernetes_auth.go | 2 +- 6 files changed, 16 insertions(+), 67 deletions(-) diff --git a/packages/api/api.go b/packages/api/api.go index fac79475..c53fe550 100644 --- a/packages/api/api.go +++ b/packages/api/api.go @@ -57,9 +57,7 @@ const ( operationCallRegisterGateway = "CallRegisterGateway" operationCallConnectGateway = "CallConnectGateway" operationCallEnrollGateway = "CallEnrollGateway" - operationCallAwsAuthLoginGateway = "CallAwsAuthLoginGateway" - operationCallKubernetesAuthLoginGateway = "CallKubernetesAuthLoginGateway" - operationCallGcpAuthLoginGateway = "CallGcpAuthLoginGateway" + operationCallGatewayLogin = "CallGatewayLogin" operationCallPAMAccess = "CallPAMAccess" operationCallPAMListAccessibleAccounts = "CallPAMListAccessibleAccounts" operationCallPAMAccessApprovalRequest = "CallPAMAccessApprovalRequest" @@ -1123,8 +1121,8 @@ func CallEnrollGateway(httpClient *resty.Client, request EnrollGatewayRequest) ( return resBody, nil } -func CallAwsAuthLoginGateway(httpClient *resty.Client, request AwsAuthLoginGatewayRequest) (AwsAuthLoginGatewayResponse, error) { - var resBody AwsAuthLoginGatewayResponse +func CallGatewayLogin(httpClient *resty.Client, request any) (GatewayLoginResponse, error) { + var resBody GatewayLoginResponse response, err := httpClient. R(). SetResult(&resBody). @@ -1133,51 +1131,11 @@ func CallAwsAuthLoginGateway(httpClient *resty.Client, request AwsAuthLoginGatew Post(fmt.Sprintf("%v/v3/gateways/login", config.INFISICAL_URL)) if err != nil { - return AwsAuthLoginGatewayResponse{}, NewGenericRequestError(operationCallAwsAuthLoginGateway, err) + return GatewayLoginResponse{}, NewGenericRequestError(operationCallGatewayLogin, err) } if response.IsError() { - return AwsAuthLoginGatewayResponse{}, NewAPIErrorWithResponse(operationCallAwsAuthLoginGateway, response, nil) - } - - return resBody, nil -} - -func CallKubernetesAuthLoginGateway(httpClient *resty.Client, request KubernetesAuthLoginGatewayRequest) (KubernetesAuthLoginGatewayResponse, error) { - var resBody KubernetesAuthLoginGatewayResponse - response, err := httpClient. - R(). - SetResult(&resBody). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v3/gateways/login", config.INFISICAL_URL)) - - if err != nil { - return KubernetesAuthLoginGatewayResponse{}, NewGenericRequestError(operationCallKubernetesAuthLoginGateway, err) - } - - if response.IsError() { - return KubernetesAuthLoginGatewayResponse{}, NewAPIErrorWithResponse(operationCallKubernetesAuthLoginGateway, response, nil) - } - - return resBody, nil -} - -func CallGcpAuthLoginGateway(httpClient *resty.Client, request GcpAuthLoginGatewayRequest) (GcpAuthLoginGatewayResponse, error) { - var resBody GcpAuthLoginGatewayResponse - response, err := httpClient. - R(). - SetResult(&resBody). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v3/gateways/login", config.INFISICAL_URL)) - - if err != nil { - return GcpAuthLoginGatewayResponse{}, NewGenericRequestError(operationCallGcpAuthLoginGateway, err) - } - - if response.IsError() { - return GcpAuthLoginGatewayResponse{}, NewAPIErrorWithResponse(operationCallGcpAuthLoginGateway, response, nil) + return GatewayLoginResponse{}, NewAPIErrorWithResponse(operationCallGatewayLogin, response, nil) } return resBody, nil diff --git a/packages/api/model.go b/packages/api/model.go index ef2891a2..3cb7993c 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -840,6 +840,12 @@ type EnrollGatewayResponse struct { GatewayID string `json:"gatewayId"` } +// Every gateway login method posts to the same endpoint and gets the same body back. +type GatewayLoginResponse struct { + AccessToken string `json:"accessToken"` + TokenType string `json:"tokenType"` +} + type AwsAuthLoginGatewayRequest struct { Method string `json:"method"` GatewayID string `json:"gatewayId"` @@ -848,33 +854,18 @@ type AwsAuthLoginGatewayRequest struct { IamRequestHeaders string `json:"iamRequestHeaders"` } -type AwsAuthLoginGatewayResponse struct { - AccessToken string `json:"accessToken"` - TokenType string `json:"tokenType"` -} - type KubernetesAuthLoginGatewayRequest struct { Method string `json:"method"` GatewayID string `json:"gatewayId"` JWT string `json:"jwt"` } -type KubernetesAuthLoginGatewayResponse struct { - AccessToken string `json:"accessToken"` - TokenType string `json:"tokenType"` -} - type GcpAuthLoginGatewayRequest struct { Method string `json:"method"` GatewayID string `json:"gatewayId"` JWT string `json:"jwt"` } -type GcpAuthLoginGatewayResponse struct { - AccessToken string `json:"accessToken"` - TokenType string `json:"tokenType"` -} - type RegisterGatewayResponse struct { GatewayID string `json:"gatewayId"` DirectAddress string `json:"directAddress,omitempty"` diff --git a/packages/gateway-v2/aws_auth.go b/packages/gateway-v2/aws_auth.go index d5aa34d0..58fdfdc7 100644 --- a/packages/gateway-v2/aws_auth.go +++ b/packages/gateway-v2/aws_auth.go @@ -79,7 +79,7 @@ func LoginGatewayWithAws(ctx context.Context, httpClient *resty.Client, gatewayI return "", fmt.Errorf("error marshalling headers: %w", err) } - resp, err := api.CallAwsAuthLoginGateway(httpClient, api.AwsAuthLoginGatewayRequest{ + resp, err := api.CallGatewayLogin(httpClient, api.AwsAuthLoginGatewayRequest{ Method: EnrollMethodAws, GatewayID: gatewayID, HTTPRequestMethod: req.Method, diff --git a/packages/gateway-v2/enroll.go b/packages/gateway-v2/enroll.go index 06088f2f..9b590f44 100644 --- a/packages/gateway-v2/enroll.go +++ b/packages/gateway-v2/enroll.go @@ -9,8 +9,8 @@ import ( ) const ( - INFISICAL_GATEWAY_ACCESS_TOKEN_KEY = "INFISICAL_GATEWAY_ACCESS_TOKEN" - INFISICAL_GATEWAY_DOMAIN_KEY = "INFISICAL_GATEWAY_DOMAIN" + INFISICAL_GATEWAY_ACCESS_TOKEN_KEY = "INFISICAL_GATEWAY_ACCESS_TOKEN" + INFISICAL_GATEWAY_DOMAIN_KEY = "INFISICAL_GATEWAY_DOMAIN" INFISICAL_GATEWAY_ENROLLMENT_TOKEN_KEY = "INFISICAL_GATEWAY_ENROLLMENT_TOKEN" ) diff --git a/packages/gateway-v2/gcp_auth.go b/packages/gateway-v2/gcp_auth.go index 262b08db..65ce93e3 100644 --- a/packages/gateway-v2/gcp_auth.go +++ b/packages/gateway-v2/gcp_auth.go @@ -47,7 +47,7 @@ func LoginGatewayWithGcp(ctx context.Context, httpClient *resty.Client, gatewayI return "", err } - resp, err := api.CallGcpAuthLoginGateway(httpClient, api.GcpAuthLoginGatewayRequest{ + resp, err := api.CallGatewayLogin(httpClient, api.GcpAuthLoginGatewayRequest{ Method: EnrollMethodGcp, GatewayID: gatewayID, JWT: jwt, diff --git a/packages/gateway-v2/kubernetes_auth.go b/packages/gateway-v2/kubernetes_auth.go index ca75fac5..bf57841a 100644 --- a/packages/gateway-v2/kubernetes_auth.go +++ b/packages/gateway-v2/kubernetes_auth.go @@ -34,7 +34,7 @@ func LoginGatewayWithKubernetes(httpClient *resty.Client, gatewayID string, toke return "", fmt.Errorf("the Kubernetes service account token at %s is empty", tokenPath) } - resp, err := api.CallKubernetesAuthLoginGateway(httpClient, api.KubernetesAuthLoginGatewayRequest{ + resp, err := api.CallGatewayLogin(httpClient, api.KubernetesAuthLoginGatewayRequest{ Method: EnrollMethodKubernetes, GatewayID: gatewayID, JWT: jwt,