Skip to content
Merged
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
143 changes: 143 additions & 0 deletions cmd/authdaemon/authdaemon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package authdaemon

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"runtime"
"syscall"

"github.com/fosrl/cli/internal/logger"
authdaemonpkg "github.com/fosrl/newt/authdaemon"
"github.com/spf13/cobra"
)

const (
defaultPort = 22123
defaultPrincipalsPath = "/var/run/auth-daemon/principals"
defaultCACertPath = "/etc/ssh/ca.pem"
)

var (
errPresharedKeyRequired = errors.New("pre-shared-key is required")
errRootRequired = errors.New("auth-daemon must be run as root (use sudo)")
)

func AuthDaemonCmd() *cobra.Command {
opts := struct {
PreSharedKey string
Port int
PrincipalsFile string
CACertPath string
}{}

cmd := &cobra.Command{
Use: "auth-daemon",
Short: "Start the auth daemon",
Long: "Start the auth daemon for remote SSH authentication",
PreRunE: func(c *cobra.Command, args []string) error {
if runtime.GOOS != "linux" {
return fmt.Errorf("auth-daemon is only supported on Linux, not %s", runtime.GOOS)
}
if opts.PreSharedKey == "" {
return errPresharedKeyRequired
}
if os.Geteuid() != 0 {
return errRootRequired
}
return nil
},
Run: func(c *cobra.Command, args []string) {
runAuthDaemon(opts)
},
}

cmd.Flags().StringVar(&opts.PreSharedKey, "pre-shared-key", "", "Preshared key required for all requests to the auth daemon (required)")
cmd.MarkFlagRequired("pre-shared-key")
cmd.Flags().IntVar(&opts.Port, "port", defaultPort, "TCP listen port for the HTTPS server")
cmd.Flags().StringVar(&opts.PrincipalsFile, "principals-file", defaultPrincipalsPath, "Path to the principals file")
cmd.Flags().StringVar(&opts.CACertPath, "ca-cert-path", defaultCACertPath, "Path to the CA certificate file")

cmd.AddCommand(PrincipalsCmd())

return cmd
}

// PrincipalsCmd returns the "principals" subcommand for use as AuthorizedPrincipalsCommand in sshd_config.
func PrincipalsCmd() *cobra.Command {
opts := struct {
PrincipalsFile string
Username string
}{}

cmd := &cobra.Command{
Use: "principals",
Short: "Output principals for a username (for AuthorizedPrincipalsCommand in sshd_config)",
Long: "Read the principals file and print principals that match the given username, one per line. Configure in sshd_config with AuthorizedPrincipalsCommand and %u for the username.",
PreRunE: func(c *cobra.Command, args []string) error {
if opts.Username == "" {
return errors.New("username is required")
}
return nil
},
Run: func(c *cobra.Command, args []string) {
path := opts.PrincipalsFile
if path == "" {
path = defaultPrincipalsPath
}
runPrincipals(path, opts.Username)
},
}

cmd.Flags().StringVar(&opts.PrincipalsFile, "principals-file", defaultPrincipalsPath, "Path to the principals file written by the auth daemon")
cmd.Flags().StringVar(&opts.Username, "username", "", "Username to look up (e.g. from sshd %u)")
cmd.MarkFlagRequired("username")

return cmd
}

func runPrincipals(principalsPath, username string) {
list, err := authdaemonpkg.GetPrincipals(principalsPath, username)
if err != nil {
logger.Error("%v", err)
os.Exit(1)
}
if len(list) == 0 {
fmt.Println("")
return
}
for _, principal := range list {
fmt.Println(principal)
}
}

func runAuthDaemon(opts struct {
PreSharedKey string
Port int
PrincipalsFile string
CACertPath string
}) {
cfg := authdaemonpkg.Config{
Port: opts.Port,
PresharedKey: opts.PreSharedKey,
PrincipalsFilePath: opts.PrincipalsFile,
CACertPath: opts.CACertPath,
Force: true,
}

srv, err := authdaemonpkg.NewServer(cfg)
if err != nil {
logger.Error("%v", err)
os.Exit(1)
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

if err := srv.Run(ctx); err != nil {
logger.Error("%v", err)
os.Exit(1)
}
}
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"github.com/fosrl/cli/cmd/auth"
"github.com/fosrl/cli/cmd/auth/login"
"github.com/fosrl/cli/cmd/auth/logout"
"github.com/fosrl/cli/cmd/authdaemon"
"github.com/fosrl/cli/cmd/down"
"github.com/fosrl/cli/cmd/logs"
selectcmd "github.com/fosrl/cli/cmd/select"
"github.com/fosrl/cli/cmd/ssh"
"github.com/fosrl/cli/cmd/status"
"github.com/fosrl/cli/cmd/up"
"github.com/fosrl/cli/cmd/update"
Expand Down Expand Up @@ -42,11 +44,13 @@ func RootCommand(initResources bool) (*cobra.Command, error) {
}

cmd.AddCommand(auth.AuthCommand())
cmd.AddCommand(authdaemon.AuthDaemonCmd())
cmd.AddCommand(apply.ApplyCommand())
cmd.AddCommand(selectcmd.SelectCmd())
cmd.AddCommand(up.UpCmd())
cmd.AddCommand(down.DownCmd())
cmd.AddCommand(logs.LogsCmd())
cmd.AddCommand(ssh.SSHCmd())
cmd.AddCommand(status.StatusCmd())
cmd.AddCommand(update.UpdateCmd())
cmd.AddCommand(version.VersionCmd())
Expand Down
13 changes: 8 additions & 5 deletions cmd/select/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,16 @@ func accountMain(cmd *cobra.Command, opts *AccountCmdOpts) error {
return err
}

// Check if olmClient is running and if we need to shut it down
// Shut down running client only if it was started by this CLI
olmClient := olm.NewClient("")
if olmClient.IsRunning() {
logger.Info("Shutting down running client")
_, err := olmClient.Exit()
if err != nil {
logger.Warning("Failed to shut down OLM client: %s; you may need to do so manually.", err)
status, err := olmClient.GetStatus()
if err == nil && status != nil && status.Agent == olm.AgentName {
logger.Info("Shutting down running client")
_, err := olmClient.Exit()
if err != nil {
logger.Warning("Failed to shut down OLM client: %s; you may need to do so manually.", err)
}
}
}

Expand Down
22 changes: 5 additions & 17 deletions cmd/select/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,17 @@ func orgMain(cmd *cobra.Command, opts *OrgCmdOpts) error {
}
account.OrgID = selectedOrgID
accountStore.Accounts[userID] = account

if err := accountStore.Save(); err != nil {
logger.Error("Failed to save account to store: %v", err)
return err
}

// Switch active client if running
utils.SwitchActiveClientOrg(selectedOrgID)

// Check if olmClient is running and if we need to monitor a switch
olmClient := olm.NewClient("")
if olmClient.IsRunning() {
// Get current status - if it doesn't match the new org, monitor the switch
currentStatus, err := olmClient.GetStatus()
if err == nil && currentStatus != nil && currentStatus.OrgID != selectedOrgID {
// Switch was sent, monitor the switch process
monitorOrgSwitch(cfg.LogFile, selectedOrgID)
} else {
// Already on the correct org or no status available
logger.Success("Successfully selected organization: %s", selectedOrgID)
}
// Switch active client if running (and started by this CLI)
switched := utils.SwitchActiveClientOrg(selectedOrgID)
if switched {
monitorOrgSwitch(cfg.LogFile, selectedOrgID)
} else {
// Client not running, no switch needed
logger.Success("Successfully selected organization: %s", selectedOrgID)
}

Expand Down
72 changes: 72 additions & 0 deletions cmd/ssh/jit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ssh

import (
"fmt"
"time"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/sshkeys"
)

const (
pollInitialDelay = 250 * time.Millisecond
pollStartInterval = 250 * time.Millisecond
pollBackoffSteps = 6
)

// GenerateAndSignKey generates an Ed25519 key pair and signs the public key via the API.
func GenerateAndSignKey(client *api.Client, orgID string, resourceID string) (privPEM, pubKey, cert string, signData *api.SignSSHKeyData, err error) {
privPEM, pubKey, err = sshkeys.GenerateKeyPair()
if err != nil {
return "", "", "", nil, fmt.Errorf("generate key pair: %w", err)
}

initResp, err := client.SignSSHKey(orgID, api.SignSSHKeyRequest{
PublicKey: pubKey,
Resource: resourceID,
})
if err != nil {
return "", "", "", nil, fmt.Errorf("SSH error: %w", err)
}
messageID := initResp.MessageID
if messageID == 0 {
return "", "", "", nil, fmt.Errorf("SSH error: API did not return a message ID")
}

time.Sleep(pollInitialDelay)

interval := pollStartInterval
for i := 0; i <= pollBackoffSteps; i++ {
msg, pollErr := client.GetRoundTripMessage(messageID)
if pollErr != nil {
return "", "", "", nil, fmt.Errorf("SSH error: poll: %w", pollErr)
}
if msg.Complete {
if msg.Error != nil && *msg.Error != "" {
return "", "", "", nil, fmt.Errorf("SSH error: %s", *msg.Error)
}
return privPEM, pubKey, initResp.Certificate, initResp, nil
}
if i < pollBackoffSteps {
time.Sleep(interval)
interval *= 2
}
}
return "", "", "", nil, fmt.Errorf("SSH error: timed out waiting for round-trip message")
}

// ResolveOrgID returns orgID from the flag or the active account. Returns empty string and nil error if both are empty.
func ResolveOrgID(accountStore *config.AccountStore, flagOrgID string) (string, error) {
if flagOrgID != "" {
return flagOrgID, nil
}
active, err := accountStore.ActiveAccount()
if err != nil || active == nil {
return "", errOrgRequired
}
if active.OrgID == "" {
return "", errOrgRequired
}
return active.OrgID, nil
}
Loading
Loading