Skip to content
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ 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
github.com/alessio/shellescape v1.4.1
github.com/awnumar/memguard v0.23.0
github.com/aws/aws-sdk-go-v2 v1.27.2
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
Expand Down Expand Up @@ -60,6 +60,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
Expand All @@ -72,12 +73,12 @@ require (
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
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/awnumar/memcall v0.4.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect
Expand Down Expand Up @@ -220,7 +221,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-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.83.2 // indirect
Expand Down
31 changes: 5 additions & 26 deletions packages/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ const (
operationCallRegisterGateway = "CallRegisterGateway"
operationCallConnectGateway = "CallConnectGateway"
operationCallEnrollGateway = "CallEnrollGateway"
operationCallAwsAuthLoginGateway = "CallAwsAuthLoginGateway"
operationCallKubernetesAuthLoginGateway = "CallKubernetesAuthLoginGateway"
operationCallGatewayLogin = "CallGatewayLogin"
operationCallPAMAccess = "CallPAMAccess"
operationCallPAMListAccessibleAccounts = "CallPAMListAccessibleAccounts"
operationCallPAMAccessApprovalRequest = "CallPAMAccessApprovalRequest"
Expand Down Expand Up @@ -1122,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).
Expand All @@ -1132,31 +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 GatewayLoginResponse{}, NewAPIErrorWithResponse(operationCallGatewayLogin, response, nil)
}

return resBody, nil
Expand Down
18 changes: 10 additions & 8 deletions packages/api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -848,20 +854,16 @@ 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 {
Comment thread
bernie-g marked this conversation as resolved.
Method string `json:"method"`
GatewayID string `json:"gatewayId"`
JWT string `json:"jwt"`
}

type RegisterGatewayResponse struct {
Expand Down
117 changes: 109 additions & 8 deletions packages/cmd/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"syscall"
"time"
Expand Down Expand Up @@ -233,14 +234,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
Expand Down Expand Up @@ -308,6 +310,59 @@ 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))
}

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 {
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

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")
Expand Down Expand Up @@ -401,6 +456,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.
Expand Down Expand Up @@ -752,6 +808,48 @@ var gatewaySystemdInstallCmd = &cobra.Command{
util.HandleError(installErr, "Unable to install systemd service")
}
installedServiceName = svcName
} else if enrollMethod == gatewayv2.EnrollMethodGcp {
// --- GCP Auth path ---
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))
}

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))
}
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))
}
}

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)
Expand Down Expand Up @@ -862,9 +960,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 <name>")
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")
Expand All @@ -884,8 +983,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 <name>")
Expand Down
2 changes: 1 addition & 1 deletion packages/gateway-v2/aws_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions packages/gateway-v2/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -30,8 +32,12 @@ 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"

GcpAuthTypeGce = "gce"
GcpAuthTypeIam = "iam"
)

type HttpProxyAction string
Expand Down
4 changes: 2 additions & 2 deletions packages/gateway-v2/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
Loading
Loading