From 8f667e84d5913117355447029ee808a3dac258ed Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sun, 20 Aug 2023 17:37:32 -0700 Subject: [PATCH] ssh authentication capture flow --- .github/workflows/build.yml | 28 +++++++++---------- Makefile | 5 ++-- README.MD | 20 +++++++------- cmd/root.go | 31 ++++++++++++++------- cmd/{action.go => run.go} | 21 ++++++++++----- go.mod | 1 - go.sum | 3 +-- internal/executecmd.go | 21 ++++++++------- internal/ssh.go | 54 +++++++++++++++++++++++-------------- 9 files changed, 107 insertions(+), 77 deletions(-) rename cmd/{action.go => run.go} (80%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b30150d..6a74de2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,12 +27,13 @@ jobs: run: sudo apt-get install build-essential devscripts debhelper dh-make shell: bash - - name: Extract version from Makefile + - name: Extract version from root.go id: extract_version run: | - VERSION=$(awk -F '=' '/^VERSION/ {print $2}' Makefile) + VERSION=$(grep -o 'Version\s*string\s*=\s*"[^"]*"' cmd/root.go | sed 's/Version\s*string\s*=\s*"//; s/"$//') echo "VERSION=$VERSION" >> $GITHUB_ENV echo "::set-output name=version::${VERSION}" + echo "VERSION=$VERSION" shell: bash - name: Build Windows binary @@ -79,9 +80,13 @@ jobs: - name: Check out code uses: actions/checkout@v3 - - name: Extract version from Makefile + - name: Extract version from root.go id: extract_version - run: echo "VERSION=$(awk -F '=' '/^VERSION/ {print $2}' Makefile)" >> $GITHUB_ENV + run: | + VERSION=$(grep -o 'Version\s*string\s*=\s*"[^"]*"' cmd/root.go | sed 's/Version\s*string\s*=\s*"//; s/"$//') + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "::set-output name=version::${VERSION}" + echo "VERSION=$VERSION" shell: bash - name: Select XCode @@ -89,7 +94,6 @@ jobs: with: version: 14.2 - - name: Import app certificate run: | security create-keychain -p ${{ secrets.APPLE_CERTPWD }} build.keychain @@ -135,10 +139,6 @@ jobs: xcrun notarytool submit ./opnsense.pkg --wait --apple-id ${{ secrets.APPLE_NOTARIZATION_USERNAME }} --password ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} --team-id ${{ secrets.APPLE_TEAM_NAME }} xcrun stapler staple ./opnsense.pkg - - - - - name: Compile .txz package in FreeBSD id: compile uses: vmactions/freebsd-vm@v0.3.1 @@ -194,17 +194,17 @@ jobs: ls -l ./bin shell: bash - - name: Remove Existing Beta Tag - run: | - git tag -d beta || true - git push origin :refs/tags/beta || true + #- name: Remove Existing Beta Tag + # run: | + # git tag -d beta || true + # git push origin :refs/tags/beta || true - name: Create Beta Release and Upload Assets id: create_release uses: softprops/action-gh-release@v1 with: prerelease: true - draft: false + draft: true tag_name: beta name: opnsense-cli ${{ env.VERSION }} files: ./bin/opnsense* diff --git a/Makefile b/Makefile index 4034bbf..407790d 100644 --- a/Makefile +++ b/Makefile @@ -39,15 +39,14 @@ deps: @echo "Downloading dependencies..." @$(GO) mod tidy -# Build the binary build: deps @echo "Building..." ifeq ($(OS),Windows_NT) @if not exist $(BUILD_DIR) mkdir $(BUILD_DIR) - @$(GO) build -ldflags "-X cmd.version=$(VERSION)" -o $(BUILD_DIR)/$(BINARY_NAME).exe . + @$(GO) build -o $(BUILD_DIR)/$(BINARY_NAME).exe . else @mkdir -p $(BUILD_DIR) - @$(GO) build -ldflags "-X cmd.version=$(VERSION)" -o $(BUILD_DIR)/$(BINARY_NAME) . + @$(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) . endif # Run tests diff --git a/README.MD b/README.MD index 69fa5f0..7994a33 100644 --- a/README.MD +++ b/README.MD @@ -22,17 +22,17 @@ Administrators typically only two options how to manage the firewall: either th ### Commands -- **`show system `**: Retrieves system information from the firewall. -- **`show config `**: Displays hierarchical segments of `config.xml`. -- **`show backup `**: Lists available backup configs or a specific backup. +- **`show system []`**: Retrieves system information from the firewall. +- **`show config []`**: Displays xpath segment of config.xml. +- **`show backup []`**: Lists available backup configs or displays a specific backup. - **`run `**: Executes commands on OPNsense. -- **`set value `**: Sets a value of a specific node in the staging config. -- **`commit`**: Moves staging config to active config. -- **`compare [] []`**: Compares staging config and active config or any two configs. -- **`discard []`**: Discards a value in the staging config or all changes in the staging config. -- **`save []`**: Creates a new backup config. -- **`load `**: Moves a specific backup config to active config. -- **`delete `**: Deletes a specific config file. +- **`set value `**: Sets a value of a specific node in staging.xml file. +- **`commit`**: Moves staging.xml to active config.xml. +- **`compare [] []`**: Compares two config files. +- **`discard []`**: Discards a value (or all changes) in the staging.xml. +- **`save []`**: Creates a new backup.xml. +- **`load []`**: Restores config.xml from a specific backup.xml. (alias: `restore``) +- **`delete `**: Deletes a specific backup.xml. ### Flags diff --git a/cmd/root.go b/cmd/root.go index 4e8e96f..749200d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,7 +9,7 @@ import ( ) var ( - Version string + Version string = "0.5.0" verbose int force bool host string @@ -33,6 +33,11 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "Accept or bypass checks and prompts") //rootCmd.PersistentFlags().StringVarP(&configfile, "config", "c", "/conf/config.xml", "path to target config.xml") + rootCmd.Flags().StringVar(&Version, "version", "", "display version of opnsense") + rootCmd.SetHelpCommand(&cobra.Command{ + Hidden: true, + }) + cobra.OnInitialize(func() { configfile = "/conf/config.xml" stagingfile = "/conf/staging.xml" @@ -43,14 +48,19 @@ func init() { } var rootCmd = &cobra.Command{ - Use: "opnsense", - Short: "opnsense is a CLI to manage and monitor OPNsense firewall configuration, check status, change settings, and execute commands.", - Long: ` -Description: - opnsense is a command-line utility for managing, configuring, and monitoring OPNsense firewall systems. - It facilitates non-GUI administration, both directly in the shell and remotely via an SSH tunnel. - All interactions with OPNsense utilize the same mechanisms as the Web GUI, - including staged modifications of config.xml and execution of available configd commands.`, + Use: "opnsense [command]", + Short: "CLI to manage and monitor OPNsense firewall systems.", + Long: `Command Line utility to interact with OPNsense firewall. + +opnsense CLI is a command-line utility for managing, configuring, and monitoring OPNsense firewall systems. +It facilitates non-GUI administration, both locally on the firewall and remotely via an SSH tunnel. +To avoid entering passwords for each remote call, use 'ssh-add' to add private key to your ssh-agent.`, + + Example: ` opnsense -t admin@192.168.1.1 show system - Show system information on remote OPNsense + opnsense show config interfaces/wan --json - Show the inerfaces/wan of config.xml in json format + opnsense show backup -d2 - Show backup details 2 levels deep + opnsense run firmware reboot -f - Reboot OPNsense, force (no confirmation) + opnsense commit - Commit staged changes`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return nil @@ -58,7 +68,8 @@ Description: Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { - fmt.Println(cmd.Long) + cmd.Help() + os.Exit(0) } }, } diff --git a/cmd/action.go b/cmd/run.go similarity index 80% rename from cmd/action.go rename to cmd/run.go index 6ba552c..b189e6c 100644 --- a/cmd/action.go +++ b/cmd/run.go @@ -11,13 +11,20 @@ import ( "github.com/spf13/cobra" ) -var actionCmd = &cobra.Command{ - Use: "action", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: +var runCmd = &cobra.Command{ + Use: "run [service] [command] [parameters]", + Short: "Executes commands on OPNsense firewall.", + Long: `Execute registered command on OPNsense. + +The 'run' command is used to execute specific command that is registered with 'configctl' on OPNsense.`, + + Example: ` opnsense run - List configd services + opnsense run dns - List commands for dns service + opnsense run dhcpd list leases - Show DHCP leases + opnsense run interface flush arp - Flush arp table + opnsense run firmware reboot - Issue a reboot +`, -Cobra is a CLI library for Go that empowers applications.`, Run: func(cmd *cobra.Command, args []string) { path := "actions" @@ -86,6 +93,6 @@ Cobra is a CLI library for Go that empowers applications.`, } func init() { - rootCmd.AddCommand(actionCmd) + rootCmd.AddCommand(runCmd) // Here you will define your flags and configuration settings. } diff --git a/go.mod b/go.mod index 02e22b2..9894d54 100644 --- a/go.mod +++ b/go.mod @@ -18,5 +18,4 @@ require ( golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/tools v0.11.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 381d7b2..18e633b 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,7 @@ golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc= golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/executecmd.go b/internal/executecmd.go index 86fbc1d..630aa29 100644 --- a/internal/executecmd.go +++ b/internal/executecmd.go @@ -2,13 +2,11 @@ package internal import ( "bytes" - "fmt" "os/exec" ) func ExecuteCmd(command, host string) (string, error) { - - Log(4,"sh -c %s",command) + Log(4, "sh -c %s", command) if host == "" { Log(5, "no target provided; executing command locally.") out, err := exec.Command("sh", "-c", command).Output() @@ -19,21 +17,24 @@ func ExecuteCmd(command, host string) (string, error) { Log(5, "received results from executed command.") return string(out), nil } + sshClient, err := getSSHClient(host) if err != nil { return "", err } - session := sshClient.Session - if err != nil { - return "", fmt.Errorf("failed to create session: %v", err) + if sshClient.Session == nil { + session, err := sshClient.Client.NewSession() + if err != nil { + Log(1, "%s", err) + return "", err + } + sshClient.Session = session } - defer session.Close() var stdoutBuf bytes.Buffer - session.Stdout = &stdoutBuf - - err = session.Run(command) + sshClient.Session.Stdout = &stdoutBuf + err = sshClient.Session.Run(command) if err != nil { Log(3, "failed to execute sh command. %s", err.Error()) return "", err diff --git a/internal/ssh.go b/internal/ssh.go index 89032d0..968c796 100644 --- a/internal/ssh.go +++ b/internal/ssh.go @@ -12,6 +12,7 @@ import ( type SSHClient struct { Session *ssh.Session + Client *ssh.Client } var ( @@ -39,40 +40,53 @@ func getSSHClient(target string) (*SSHClient, error) { host = userhost } + // try to get config.Auth from sshAgent + // if no identities, skip to password + // try sshDial to get connection + // if failed, skip to password + // try sshDial with password + + var connection *ssh.Client + if config == nil { config = &ssh.ClientConfig{ User: user, + Auth: []ssh.AuthMethod{}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } + //try to get sshAgent sshAgent, err := GetSSHAgent() if err == nil { - config.Auth = []ssh.AuthMethod{sshAgent} + config.Auth = append(config.Auth, sshAgent) + if len(config.Auth) > 0 { + connection, err = ssh.Dial("tcp", host+":"+port, config) + if err == nil { + return &SSHClient{Client: connection}, nil + } + } } - - if len(config.Auth) == 0 { - fmt.Println("No suitable SSH identities found in ssh-agent.\nFor enhanced security add SSH key to the ssh agent using ssh-add command") - fmt.Printf("Enter password for %s@%s: \n", user, host) - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println("No authorized SSH keys found in local ssh agent, reverting to password-based access.\nTo enable seamless access, use the 'ssh-add' to add the authorized key for user",user) + fmt.Printf("Enter password for %s@%s: ", user, host) + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + Log(5, "failed to read password: %v", err) + } + password := string(bytePassword) + config.Auth = []ssh.AuthMethod{ssh.Password(password)} + connection, err = ssh.Dial("tcp", host+":"+port, config) + if err != nil { fmt.Println() - if err != nil { - return nil, fmt.Errorf("failed to read password: %v", err) - } - password := string(bytePassword) - config.Auth = []ssh.AuthMethod{ssh.Password(password)} + Log(1, "%v", err) + } else { + fmt.Println() + return &SSHClient{Client: connection}, nil } } - - connection, err := ssh.Dial("tcp", host+":"+port, config) - if err != nil { - Log(1, "%v", err) - } - - session, err := connection.NewSession() + connection, err = ssh.Dial("tcp", host+":"+port, config) if err != nil { Log(1, "%v", err) } - - SshClient = &SSHClient{Session: session} + SshClient = &SSHClient{Client: connection} return SshClient, nil }