Compare commits

...

5 Commits

Author SHA1 Message Date
Pim van Pelt
57fc8d3630 Release v1.3.2 2025-07-13 22:23:20 +02:00
Pim van Pelt
64212fce8c Twiddle ssh auth, use password before --key-file flag before homedir before agent 2025-07-13 22:21:27 +02:00
Pim van Pelt
83797aaa34 Release v1.3.1 2025-07-07 09:10:08 +02:00
Pim van Pelt
3da4de7711 Fix lint errors, ensure errors start with 'hostname:' 2025-07-07 09:06:20 +02:00
Pim van Pelt
9a2264e867 Remove old comments; Count auth mechanisms independently 2025-07-07 09:02:41 +02:00
4 changed files with 65 additions and 36 deletions

15
debian/changelog vendored
View File

@@ -1,3 +1,18 @@
ipng-router-backup (1.3.2) stable; urgency=low
* Fix --key-file authentication priority issue
* Prioritize explicit key file over SSH agent authentication
-- Pim van Pelt <pim@ipng.ch> Sun, 13 Jul 2025 23:30:00 +0100
ipng-router-backup (1.3.1) stable; urgency=low
* Fix golangci-lint issues, replace deprecated io/ioutil
* Add SSH key error messages with hostname prefix
* Independently validate sshkey, agent auth and password methods
-- Pim van Pelt <pim@ipng.ch> Sun, 07 Jul 2025 23:30:00 +0100
ipng-router-backup (1.3.0) stable; urgency=low ipng-router-backup (1.3.0) stable; urgency=low
* Add --parallel flag for concurrent device processing (default: 10) * Add --parallel flag for concurrent device processing (default: 10)

View File

@@ -4,7 +4,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"dario.cat/mergo" "dario.cat/mergo"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -29,7 +29,7 @@ type Device struct {
} }
func readYAMLFile(path string) (map[string]interface{}, error) { func readYAMLFile(path string) (map[string]interface{}, error) {
data, err := ioutil.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -12,17 +12,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const Version = "1.3.0" const Version = "1.3.2"
// Config and SSH types are now in separate packages
// SSH connection methods are now in ssh.go
// YAML processing is now handled by the config package
// SSH helper functions are now in ssh.go
// processDevice handles backup processing for a single device
func processDevice(hostname string, deviceConfig Device, commands []string, excludePatterns []string, password, keyFile string, port int, outputDir string) bool { func processDevice(hostname string, deviceConfig Device, commands []string, excludePatterns []string, password, keyFile string, port int, outputDir string) bool {
// Create backup instance // Create backup instance
backup := NewRouterBackup(hostname, deviceConfig.Address, deviceConfig.User, password, keyFile, port) backup := NewRouterBackup(hostname, deviceConfig.Address, deviceConfig.User, password, keyFile, port)
@@ -82,17 +73,27 @@ func main() {
} }
// Check authentication setup // Check authentication setup
if password == "" && keyFile == "" { hasAuth := 0
if os.Getenv("SSH_AUTH_SOCK") != "" { if os.Getenv("SSH_AUTH_SOCK") != "" {
fmt.Println("Using SSH agent for authentication") fmt.Println("Using SSH agent for authentication")
} else { hasAuth++
keyFile = findDefaultSSHKey() }
if keyFile == "" { if keyFile == "" {
log.Fatal("No SSH key found and no password provided") keyFile = findDefaultSSHKey()
} else { if keyFile != "" {
fmt.Printf("Using SSH key: %s\n", keyFile) fmt.Printf("Using SSH key: %s\n", keyFile)
} hasAuth++
} }
} else {
fmt.Printf("Using specified SSH key: %s\n", keyFile)
hasAuth++
}
if password != "" {
fmt.Println("Using --password for authentication")
hasAuth++
}
if hasAuth == 0 {
log.Fatal("No authentication mechanisms found.")
} }
// Process devices // Process devices
@@ -219,7 +220,9 @@ func main() {
rootCmd.Flags().StringSliceVar(&hostFilter, "host", []string{}, "Specific host(s) to process (can be repeated, processes all if not specified)") rootCmd.Flags().StringSliceVar(&hostFilter, "host", []string{}, "Specific host(s) to process (can be repeated, processes all if not specified)")
rootCmd.Flags().IntVar(&parallel, "parallel", 10, "Maximum number of devices to process in parallel") rootCmd.Flags().IntVar(&parallel, "parallel", 10, "Maximum number of devices to process in parallel")
rootCmd.MarkFlagRequired("yaml") if err := rootCmd.MarkFlagRequired("yaml"); err != nil {
log.Fatal(err)
}
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)

View File

@@ -4,7 +4,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
@@ -104,11 +103,6 @@ func (rb *RouterBackup) Connect() error {
config.KeyExchanges = finalAlgorithms config.KeyExchanges = finalAlgorithms
} }
// Note: Cipher overrides disabled - Go SSH library defaults work better
// if ciphers := ssh_config.Get(rb.hostname, "Ciphers"); ciphers != "" {
// config.Ciphers = ...
// }
if macs := ssh_config.Get(rb.hostname, "MACs"); macs != "" { if macs := ssh_config.Get(rb.hostname, "MACs"); macs != "" {
macList := strings.Split(macs, ",") macList := strings.Split(macs, ",")
for i, mac := range macList { for i, mac := range macList {
@@ -127,15 +121,19 @@ func (rb *RouterBackup) Connect() error {
config.HostKeyAlgorithms = finalAlgorithms config.HostKeyAlgorithms = finalAlgorithms
} }
// Try SSH agent first if available // If explicit key file is provided, prioritize it over SSH agent
var keyFileAuth ssh.AuthMethod
var agentAuth ssh.AuthMethod
// Try SSH agent if available (but don't add to config.Auth yet)
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" { if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
if conn, err := net.Dial("unix", sshAuthSock); err == nil { if conn, err := net.Dial("unix", sshAuthSock); err == nil {
agentClient := agent.NewClient(conn) agentClient := agent.NewClient(conn)
config.Auth = []ssh.AuthMethod{ssh.PublicKeysCallback(agentClient.Signers)} agentAuth = ssh.PublicKeysCallback(agentClient.Signers)
} }
} }
// If SSH agent didn't work, try key file // Try key file
if keyFile != "" { if keyFile != "" {
// Expand ~ in keyFile path // Expand ~ in keyFile path
if strings.HasPrefix(keyFile, "~/") { if strings.HasPrefix(keyFile, "~/") {
@@ -145,17 +143,27 @@ func (rb *RouterBackup) Connect() error {
} }
} }
key, err := ioutil.ReadFile(keyFile) key, err := os.ReadFile(keyFile)
if err == nil { if err == nil {
signer, err := ssh.ParsePrivateKey(key) signer, err := ssh.ParsePrivateKey(key)
if err != nil { if err != nil {
fmt.Errorf("unable to parse private key: %v", err) fmt.Printf("%s: Unable to parse private key: %v\n", rb.hostname, err)
} else { } else {
config.Auth = append(config.Auth, ssh.PublicKeys(signer)) keyFileAuth = ssh.PublicKeys(signer)
} }
} }
} }
// Prioritize auth methods: explicit key file first, then SSH agent
if keyFileAuth != nil {
config.Auth = []ssh.AuthMethod{keyFileAuth}
if agentAuth != nil {
config.Auth = append(config.Auth, agentAuth)
}
} else if agentAuth != nil {
config.Auth = []ssh.AuthMethod{agentAuth}
}
// Fall back to password if available // Fall back to password if available
if rb.password != "" { if rb.password != "" {
config.Auth = append(config.Auth, ssh.Password(rb.password)) config.Auth = append(config.Auth, ssh.Password(rb.password))
@@ -268,7 +276,10 @@ func (rb *RouterBackup) BackupCommands(commands []string, excludePatterns []stri
fmt.Fprintf(file, "## COMMAND: %s\n", command) fmt.Fprintf(file, "## COMMAND: %s\n", command)
filteredOutput := filterOutput(output, excludePatterns) filteredOutput := filterOutput(output, excludePatterns)
file.WriteString(filteredOutput) if _, err := file.WriteString(filteredOutput); err != nil {
fmt.Printf("%s: Failed to write output: %v\n", rb.hostname, err)
hasErrors = true
}
file.Close() file.Close()
successCount++ successCount++