Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const (
operationCallConnectGateway = "CallConnectGateway"
operationCallEnrollGateway = "CallEnrollGateway"
operationCallAwsAuthLoginGateway = "CallAwsAuthLoginGateway"
operationCallKubernetesAuthLoginGateway = "CallKubernetesAuthLoginGateway"
operationCallPAMAccess = "CallPAMAccess"
operationCallPAMAccessApprovalRequest = "CallPAMAccessApprovalRequest"
operationCallPAMCreateAccessRequest = "CallPAMCreateAccessRequest"
Expand Down Expand Up @@ -1116,6 +1117,26 @@ func CallAwsAuthLoginGateway(httpClient *resty.Client, request AwsAuthLoginGatew
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 CallPAMAccess(httpClient *resty.Client, request PAMAccessRequest) (PAMAccessResponse, error) {
var pamAccessResponse PAMAccessResponse
response, err := httpClient.
Expand Down
11 changes: 11 additions & 0 deletions packages/api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,17 @@ type AwsAuthLoginGatewayResponse struct {
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 RegisterGatewayResponse struct {
GatewayID string `json:"gatewayId"`
RelayHost string `json:"relayHost"`
Expand Down
77 changes: 67 additions & 10 deletions packages/cmd/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,11 @@ var gatewayCmd = &cobra.Command{
}

var gatewayStartCmd = &cobra.Command{
Use: "start [name]",
Short: "Start the new Infisical gateway",
Long: "Start the new Infisical gateway component.",
Example: "infisical gateway start my-gateway --token=<token>",
Use: "start [name]",
Short: "Start the new Infisical gateway",
Long: "Start the new Infisical gateway component.",
Example: `infisical gateway start my-gateway --token=<token>
infisical gateway start my-gateway --enroll-method=kubernetes --gateway-id=<gateway-id>`,
DisableFlagsInUseLine: true,
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -234,6 +235,13 @@ var gatewayStartCmd = &cobra.Command{
if enrollMethod == "" {
enrollMethod = os.Getenv("INFISICAL_GATEWAY_ENROLL_METHOD")
}
if enrollMethod != "" &&
enrollMethod != gatewayv2.EnrollMethodToken &&
enrollMethod != gatewayv2.EnrollMethodAws &&
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))
}
var alreadyEnrolled bool
var enrolledAccessToken string // set during fresh enrollment, used directly to avoid env var interference

Expand Down Expand Up @@ -300,6 +308,50 @@ var gatewayStartCmd = &cobra.Command{
log.Info().Msg("Starting gateway...")
}

// --- Kubernetes Auth path ---
if enrollMethod == gatewayv2.EnrollMethodKubernetes {
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=kubernetes"))
}

tokenPath, _ := util.GetCmdFlagOrEnv(cmd, "service-account-token-path", []string{util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME})

httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
util.HandleError(err, "unable to create HTTP client")
}

log.Info().Msg("Authenticating gateway via Kubernetes Auth (service account token review)...")
accessTokenStr, err := gatewayv2.LoginGatewayWithKubernetes(httpClient, gatewayID, tokenPath)
if err != nil {
util.HandleError(err, "Kubernetes Auth login failed")
}

enrolledAccessToken = accessTokenStr
alreadyEnrolled = true // skip the stored-token branch below; we have a fresh one in hand

// 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 Kubernetes Auth. State saved to %s", gatewayv2.GetConfPathDisplay(gatewayName))
log.Info().Msg("Starting gateway...")
}

// --- Enrollment token path ---
if enrollMethod == gatewayv2.EnrollMethodToken {
enrollToken, err := cmd.Flags().GetString("token")
Expand Down Expand Up @@ -347,7 +399,9 @@ var gatewayStartCmd = &cobra.Command{
log.Info().Msg("Starting gateway...")
}

isResourceAuth := enrollMethod == gatewayv2.EnrollMethodToken || enrollMethod == gatewayv2.EnrollMethodAws
isResourceAuth := enrollMethod == gatewayv2.EnrollMethodToken ||
enrollMethod == gatewayv2.EnrollMethodAws ||
enrollMethod == gatewayv2.EnrollMethodKubernetes

// Only use the stored token when no explicit identity credentials are provided.
// If --token or --auth-method is set, the user wants the identity-based path.
Expand Down Expand Up @@ -621,6 +675,9 @@ var gatewaySystemdInstallCmd = &cobra.Command{
}

enrollMethod, _ := cmd.Flags().GetString("enroll-method")
if enrollMethod == gatewayv2.EnrollMethodKubernetes {
util.HandleError(errors.New("--enroll-method=kubernetes is only supported for in-cluster gateways, which are not managed by systemd. Deploy the gateway as a Kubernetes workload and run 'infisical gateway start' with --enroll-method=kubernetes instead"))
}

pkcs11ModulePath, _ := cmd.Flags().GetString("pkcs11-module")
if pkcs11ModulePath != "" && !filepath.IsAbs(pkcs11ModulePath) {
Expand Down Expand Up @@ -783,15 +840,15 @@ 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]. when set to 'token', uses --token as a one-time enrollment token. when set to 'aws', authenticates via signed STS GetCallerIdentity using --gateway-id")
gatewayStartCmd.Flags().String("gateway-id", "", "gateway id (required when --enroll-method=aws)")
gatewayStartCmd.Flags().String("domain", "", "domain of your self-hosted Infisical instance (used with --enroll-method=token or --enroll-method=aws)")
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("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")
gatewayStartCmd.Flags().String("client-secret", "", "client secret for universal auth")
gatewayStartCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods")
gatewayStartCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth")
gatewayStartCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth (defaults to /var/run/secrets/kubernetes.io/serviceaccount/token)")
gatewayStartCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
gatewayStartCmd.Flags().String("jwt", "", "JWT for jwt-based auth methods [oidc-auth, jwt-auth]")
gatewayStartCmd.Flags().String("pam-session-recording-path", "", "directory path for PAM session recordings (defaults to /var/lib/infisical/session_recordings)")
Expand All @@ -803,7 +860,7 @@ 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)")
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("domain", "", "Domain of your self-hosted Infisical instance")
gatewaySystemdInstallCmd.Flags().String("name", "", "The name of the gateway (deprecated, use positional argument instead)")
Expand Down
5 changes: 3 additions & 2 deletions packages/gateway-v2/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ 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"
EnrollMethodToken = "token"
EnrollMethodAws = "aws"
EnrollMethodKubernetes = "kubernetes"
EnrollMethodToken = "token"
)

type HttpProxyAction string
Expand Down
47 changes: 47 additions & 0 deletions packages/gateway-v2/kubernetes_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package gatewayv2

import (
"errors"
"fmt"
"os"
"strings"

"github.com/Infisical/infisical-merge/packages/api"
"github.com/go-resty/resty/v2"
)

// LoginGatewayWithKubernetes exchanges the pod's projected service account token for a
// GATEWAY_ACCESS_TOKEN. tokenPath defaults to the standard projected-token mount when empty.
func LoginGatewayWithKubernetes(httpClient *resty.Client, gatewayID string, tokenPath string) (string, error) {
if gatewayID == "" {
return "", errors.New("--gateway-id is required when --enroll-method=kubernetes")
}

if tokenPath == "" {
tokenPath = KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH
}

tokenBytes, err := os.ReadFile(tokenPath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("no Kubernetes service account token found at %s. --enroll-method=kubernetes requires the gateway to run inside a Kubernetes pod, or --service-account-token-path pointing at a projected token", tokenPath)
}
return "", fmt.Errorf("unable to read Kubernetes service account token at %s: %w", tokenPath, err)
}

jwt := strings.TrimSpace(string(tokenBytes))
if jwt == "" {
return "", fmt.Errorf("the Kubernetes service account token at %s is empty", tokenPath)
}

resp, err := api.CallKubernetesAuthLoginGateway(httpClient, api.KubernetesAuthLoginGatewayRequest{
Method: EnrollMethodKubernetes,
GatewayID: gatewayID,
JWT: jwt,
})
if err != nil {
return "", err
}

return resp.AccessToken, nil
}
Loading