Skip to content
140 changes: 132 additions & 8 deletions packages/cmd/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"sync/atomic"
"syscall"
"time"
Expand All @@ -27,24 +29,24 @@ var relayStartCmd = &cobra.Command{
Use: "start",
Short: "Start the Infisical relay component",
Long: "Start the Infisical relay component",
Example: "infisical relay start --type=instance --ip=<ip> --name=<name> --token=<token>",
Example: "infisical relay start --type=instance --host=<host> --name=<name> --token=<token>",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {

relayName, err := cmd.Flags().GetString("name")
relayName, err := util.GetCmdFlagOrEnv(cmd, "name", []string{gatewayv2.RELAY_NAME_ENV_NAME})
if err != nil || relayName == "" {
util.HandleError(err, "unable to get name flag")
util.HandleError(err, fmt.Sprintf("unable to get name flag or %s env", gatewayv2.RELAY_NAME_ENV_NAME))
}

host, err := cmd.Flags().GetString("host")
host, err := util.GetCmdFlagOrEnv(cmd, "host", []string{gatewayv2.RELAY_HOST_ENV_NAME})
if err != nil || host == "" {
util.HandleError(err, "unable to get host flag")
util.HandleError(err, fmt.Sprintf("unable to get host flag or %s env", gatewayv2.RELAY_HOST_ENV_NAME))
}

instanceType, err := cmd.Flags().GetString("type")
instanceType, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "type", []string{gatewayv2.RELAY_TYPE_ENV_NAME}, "org")
if err != nil {
util.HandleError(err, "unable to get type flag")
util.HandleError(err, fmt.Sprintf("unable to get type flag or %s env", gatewayv2.RELAY_TYPE_ENV_NAME))
}

relayInstance, err := relay.NewRelay(&relay.RelayConfig{
Expand Down Expand Up @@ -137,8 +139,118 @@ var relayStartCmd = &cobra.Command{
},
}

var relaySystemdCmd = &cobra.Command{
Use: "systemd",
Short: "Manage systemd service for Infisical relay",
Long: "Manage systemd service for Infisical relay. Use 'systemd install' to install and enable the service.",
Example: `sudo infisical relay systemd install --token=<token> --name=<name> --host=<host>
sudo infisical relay systemd install --type=instance --name=<name> --host=<host> --relay-auth-secret=<secret>
sudo infisical relay systemd uninstall`,
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
}

var relaySystemdInstallCmd = &cobra.Command{
Use: "install",
Short: "Install and enable systemd service for the relay (requires sudo)",
Long: "Install and enable systemd service for the relay. Must be run with sudo on Linux.",
Example: `sudo infisical relay systemd install --token=<token> --name=<name> --host=<host>
sudo infisical relay systemd install --type=instance --name=<name> --host=<host> --relay-auth-secret=<secret>`,
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
if runtime.GOOS != "linux" {
util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux"))
}

if os.Geteuid() != 0 {
util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges"))
}

token, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse token flag")
}

domain, err := cmd.Flags().GetString("domain")
if err != nil {
util.HandleError(err, "Unable to parse domain flag")
}

name, err := cmd.Flags().GetString("name")
if err != nil {
util.HandleError(err, "Unable to parse name flag")
}
if name == "" {
util.HandleError(fmt.Errorf("name flag is required"), "name is required")
}

host, err := cmd.Flags().GetString("host")
if err != nil {
util.HandleError(err, "Unable to parse host flag")
}
if host == "" {
util.HandleError(fmt.Errorf("host flag is required"), "host is required")
}

instanceType, err := cmd.Flags().GetString("type")
if err != nil {
util.HandleError(err, "Unable to parse type flag")
}
if instanceType == "" {
util.HandleError(fmt.Errorf("type flag is required"), "type is required")
}

relayAuthSecret, err := cmd.Flags().GetString("relay-auth-secret")
if err != nil {
util.HandleError(err, "Unable to parse relay-auth-secret flag")
}

if instanceType == "instance" && relayAuthSecret == "" && os.Getenv(gatewayv2.RELAY_AUTH_SECRET_ENV_NAME) == "" {
util.HandleError(fmt.Errorf("for type 'instance', --relay-auth-secret flag or %s env must be set", gatewayv2.RELAY_AUTH_SECRET_ENV_NAME))
}
if instanceType != "instance" && token == "" {
util.HandleError(fmt.Errorf("for type '%s', --token is required", instanceType))
}

if err := relay.InstallRelaySystemdService(token, domain, name, host, instanceType, relayAuthSecret); err != nil {
util.HandleError(err, "Failed to install relay systemd service")
}

enableCmd := exec.Command("systemctl", "enable", "infisical-relay")
if err := enableCmd.Run(); err != nil {
util.HandleError(err, "Failed to enable relay systemd service")
}

log.Info().Msg("Successfully installed and enabled infisical-relay service")
log.Info().Msg("To start the service, run: sudo systemctl start infisical-relay")
},
}

var relaySystemdUninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall and remove systemd service for the relay (requires sudo)",
Long: "Uninstall and remove systemd service for the relay. Must be run with sudo on Linux.",
Example: "sudo infisical relay systemd uninstall",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
if runtime.GOOS != "linux" {
util.HandleError(fmt.Errorf("systemd service uninstallation is only supported on Linux"))
}

if os.Geteuid() != 0 {
util.HandleError(fmt.Errorf("systemd service uninstallation requires root/sudo privileges"))
}

if err := relay.UninstallRelaySystemdService(); err != nil {
util.HandleError(err, "Failed to uninstall relay systemd service")
}
},
}

func init() {
relayStartCmd.Flags().String("type", "org", "The type of relay to run. Must be either 'instance' or 'org'")
relayStartCmd.Flags().String("type", "", "The type of relay to run. Defaults to 'org'")
relayStartCmd.Flags().String("host", "", "The IP or hostname for the relay")
relayStartCmd.Flags().String("name", "", "The name of the relay")
relayStartCmd.Flags().String("token", "", "connect with Infisical using machine identity access token. if not provided, you must set the auth-method flag")
Expand All @@ -150,7 +262,19 @@ func init() {
relayStartCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
relayStartCmd.Flags().String("jwt", "", "JWT for jwt-based auth methods [oidc-auth, jwt-auth]")

// systemd install command flags
relaySystemdInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token (org type)")
relaySystemdInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")
relaySystemdInstallCmd.Flags().String("name", "", "The name of the relay")
relaySystemdInstallCmd.Flags().String("host", "", "The IP or hostname for the relay")
relaySystemdInstallCmd.Flags().String("type", "org", "The type of relay to run. Defaults to 'org'")
relaySystemdInstallCmd.Flags().String("relay-auth-secret", "", "Relay auth secret (required for type=instance if env not set)")

relaySystemdCmd.AddCommand(relaySystemdInstallCmd)
relaySystemdCmd.AddCommand(relaySystemdUninstallCmd)

relayCmd.AddCommand(relayStartCmd)
relayCmd.AddCommand(relaySystemdCmd)

rootCmd.AddCommand(relayCmd)
}
2 changes: 2 additions & 0 deletions packages/gateway-v2/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const (
KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"

RELAY_NAME_ENV_NAME = "INFISICAL_RELAY_NAME"
RELAY_HOST_ENV_NAME = "INFISICAL_RELAY_HOST"
RELAY_TYPE_ENV_NAME = "INFISICAL_RELAY_TYPE"
GATEWAY_NAME_ENV_NAME = "INFISICAL_GATEWAY_NAME"

RELAY_AUTH_SECRET_ENV_NAME = "INFISICAL_RELAY_AUTH_SECRET"
Expand Down
87 changes: 66 additions & 21 deletions packages/gateway-v2/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,35 +115,53 @@ func NewGateway(config *GatewayConfig) (*Gateway, error) {
}

func (g *Gateway) registerHeartBeat(ctx context.Context, errCh chan error) {
sendHeartbeat := func() {
sendHeartbeat := func() error {
if err := api.CallGatewayHeartBeatV2(g.httpClient); err != nil {
log.Warn().Msgf("Heartbeat failed: %v", err)
select {
case errCh <- err:
default:
log.Warn().Msg("Error channel full, skipping heartbeat error report")
}
return err
} else {
log.Info().Msg("Gateway is reachable by Infisical")
return nil
}
}

go func() {
select {
case <-ctx.Done():
return
case <-time.After(10 * time.Second):
sendHeartbeat()
}
defer func() {
log.Debug().Msg("Heartbeat goroutine exiting")
}()

ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
// Phase 1: Keep trying every 10 seconds until first success
func() {
retryTicker := time.NewTicker(10 * time.Second)
defer retryTicker.Stop()

for {
select {
case <-ctx.Done():
return
case <-retryTicker.C:
if err := sendHeartbeat(); err == nil {
// First success! Exit retry phase
return
}
}
}
}()

// Phase 2: Regular heartbeat every 30 minutes
regularTicker := time.NewTicker(30 * time.Minute)
defer regularTicker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
case <-regularTicker.C:
sendHeartbeat()
}
}
Expand Down Expand Up @@ -218,20 +236,47 @@ func (g *Gateway) connectAndServe() error {
return fmt.Errorf("failed to register gateway: %v", err)
}

// Create SSH client config
sshConfig, err := g.createSSHConfig()
if err != nil {
return fmt.Errorf("failed to create SSH config: %v", err)
}
return g.connectWithRetry()
}

// Connect to Relay server
log.Info().Msgf("Connecting to relay server %s on %s:%d...", g.config.RelayName, g.certificates.RelayHost, g.config.SSHPort)
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", g.certificates.RelayHost, g.config.SSHPort), sshConfig)
if err != nil {
return fmt.Errorf("failed to connect to SSH server: %v", err)
func (g *Gateway) connectWithRetry() error {
for attempt := 1; attempt <= 6; attempt++ {
// Re-register after 5 failed attempts to handle potential relay IP change
if attempt == 6 {
log.Info().Msg("Re-registering gateway to handle potential relay IP change...")
if err := g.registerGateway(); err != nil {
return fmt.Errorf("failed to re-register gateway: %v", err)
}
}

// Create SSH client config
sshConfig, err := g.createSSHConfig()
if err != nil {
return fmt.Errorf("failed to create SSH config: %v", err)
}

// Connect to Relay server
log.Info().Msgf("Connecting to relay server %s on %s:%d... (attempt %d/6)", g.config.RelayName, g.certificates.RelayHost, g.config.SSHPort, attempt)
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", g.certificates.RelayHost, g.config.SSHPort), sshConfig)
if err != nil {
log.Warn().Msgf("SSH connection attempt %d/6 failed: %v", attempt, err)
if attempt < 6 {
retryDelay := time.Duration(attempt) * 2 * time.Second
log.Info().Msgf("Retrying in %v...", retryDelay)
time.Sleep(retryDelay)
continue
}
return fmt.Errorf("failed to connect to SSH server after 6 attempts: %v", err)
}

log.Info().Msgf("Relay connection established for gateway")
return g.handleConnection(client)
}
log.Info().Msgf("Relay connection established for gateway")

return fmt.Errorf("unexpected end of retry loop")
}

func (g *Gateway) handleConnection(client *ssh.Client) error {
g.mu.Lock()
g.sshClient = client
g.isConnected = true
Expand Down
Loading
Loading