loadSSHKeys returns SSH signers from all available sources in priority order: 1. SSH agent (if SSH_AUTH_SOCK is set) — supports encrypted and hardware keys 2. Explicit keyPath if provided 3. Standard key paths (~/.ssh/id_ed25519, id_rsa, id_ecdsa) as fallback
(keyPath string)
| 208 | // 2. Explicit keyPath if provided |
| 209 | // 3. Standard key paths (~/.ssh/id_ed25519, id_rsa, id_ecdsa) as fallback |
| 210 | func (c *SSHClientImpl) loadSSHKeys(keyPath string) ([]ssh.Signer, error) { |
| 211 | var signers []ssh.Signer |
| 212 | |
| 213 | // 1. SSH agent |
| 214 | if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" { |
| 215 | // nolint:gosec // G704: sock is sourced from SSH_AUTH_SOCK env var, not user input |
| 216 | if conn, err := net.Dial("unix", sock); err == nil { |
| 217 | agentSigners, err := agent.NewClient(conn).Signers() |
| 218 | if err == nil && len(agentSigners) > 0 { |
| 219 | crSSHLogger().Debug("SSH auth: loaded %d signer(s) from SSH agent", len(agentSigners)) |
| 220 | signers = append(signers, agentSigners...) |
| 221 | } |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | // 2. Explicit keyfile or standard paths |
| 226 | homeDir, err := os.UserHomeDir() |
| 227 | if err != nil { |
| 228 | if len(signers) > 0 { |
| 229 | return signers, nil |
| 230 | } |
| 231 | return nil, fmt.Errorf("failed to get home directory: %w", err) |
| 232 | } |
| 233 | |
| 234 | var keyPaths []string |
| 235 | if keyPath != "" { |
| 236 | keyPaths = []string{keyPath} |
| 237 | } else { |
| 238 | keyPaths = []string{ |
| 239 | filepath.Join(homeDir, ".ssh", "id_ed25519"), |
| 240 | filepath.Join(homeDir, ".ssh", "id_rsa"), |
| 241 | filepath.Join(homeDir, ".ssh", "id_ecdsa"), |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | for _, candidate := range keyPaths { |
| 246 | // nolint:gosec // G304: Reading SSH keys from configured/standard paths is expected behavior |
| 247 | keyBytes, err := os.ReadFile(candidate) |
| 248 | if err != nil { |
| 249 | continue // key doesn't exist at this path |
| 250 | } |
| 251 | signer, err := ssh.ParsePrivateKey(keyBytes) |
| 252 | if err != nil { |
| 253 | crSSHLogger().Debug("SSH auth: skipping key %s (encrypted or unsupported format)", candidate) |
| 254 | continue // encrypted key — agent should cover this case |
| 255 | } |
| 256 | crSSHLogger().Debug("SSH auth: loaded key from %s", candidate) |
| 257 | signers = append(signers, signer) |
| 258 | } |
| 259 | |
| 260 | if len(signers) == 0 { |
| 261 | return nil, fmt.Errorf("no SSH authentication available: SSH_AUTH_SOCK not set and no readable keys found in %v", keyPaths) |
| 262 | } |
| 263 | |
| 264 | return signers, nil |
| 265 | } |
| 266 | |
| 267 | // ExecuteContainerCommand executes a command in an LXC container via SSH to the host. |