Skip to content

Commit 6e634b7

Browse files
feat: add SSH port forwarding test and enable commands
Add SSH port forwarding testing and enablement capabilities with new flags and commands. Remove unused imports and fix duplicate struct fields. Signed-off-by: cybermonkey <git@cybermonkey.net.au>
1 parent 154b7b1 commit 6e634b7

53 files changed

Lines changed: 1460 additions & 167 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/debug/ssh.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ import (
1818
)
1919

2020
var (
21-
sshDebugFormat string
22-
sshDebugClientOnly bool
23-
sshDebugKeyPath string
24-
sshDebugSanitize bool
21+
sshDebugFormat string
22+
sshDebugClientOnly bool
23+
sshDebugKeyPath string
24+
sshDebugSanitize bool
25+
sshDebugTestForwarding bool
26+
sshDebugHost string
27+
sshDebugUser string
28+
sshDebugPort string
29+
sshDebugPassword string
30+
sshDebugForwardTestTarget string
2531
)
2632

2733
var sshDebugCmd = &cobra.Command{
@@ -65,6 +71,9 @@ USAGE EXAMPLES:
6571
# Sanitize sensitive data before sharing
6672
eos debug ssh user@host --sanitize
6773
74+
# Test that port forwarding is allowed on the target
75+
eos debug ssh --test-forwarding --host vhost1 --user henry
76+
6877
TROUBLESHOOTING COMMON ISSUES:
6978
7079
1. "Permission denied (publickey)" errors:
@@ -92,11 +101,21 @@ func init() {
92101
sshDebugCmd.Flags().BoolVar(&sshDebugClientOnly, "client-only", false, "Run client-side diagnostics only (no server connection)")
93102
sshDebugCmd.Flags().StringVar(&sshDebugKeyPath, "key", "", "Path to SSH private key (auto-detected if not specified)")
94103
sshDebugCmd.Flags().BoolVar(&sshDebugSanitize, "sanitize", false, "Redact sensitive information (keys, paths)")
104+
sshDebugCmd.Flags().BoolVar(&sshDebugTestForwarding, "test-forwarding", false, "Test whether SSH port forwarding is permitted on the target")
105+
sshDebugCmd.Flags().StringVar(&sshDebugHost, "host", "", "Target host for --test-forwarding (e.g., vhost1 or user@vhost1)")
106+
sshDebugCmd.Flags().StringVar(&sshDebugUser, "user", "", "SSH username for --test-forwarding")
107+
sshDebugCmd.Flags().StringVar(&sshDebugPort, "port", "22", "SSH port for --test-forwarding")
108+
sshDebugCmd.Flags().StringVar(&sshDebugPassword, "password", "", "SSH password for --test-forwarding (optional if key auth works)")
109+
sshDebugCmd.Flags().StringVar(&sshDebugForwardTestTarget, "forward-target", "127.0.0.1:22", "Destination to probe through the forwarded connection")
95110

96111
debugCmd.AddCommand(sshDebugCmd)
97112
}
98113

99114
func runSSHDebug(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
115+
if sshDebugTestForwarding {
116+
return runSSHForwardingTest(rc)
117+
}
118+
100119
logger := otelzap.Ctx(rc.Ctx)
101120

102121
// Determine target (if provided)
@@ -170,6 +189,40 @@ func runSSHDebug(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) e
170189
return nil
171190
}
172191

192+
func runSSHForwardingTest(rc *eos_io.RuntimeContext) error {
193+
logger := otelzap.Ctx(rc.Ctx)
194+
195+
if sshDebugHost == "" {
196+
return fmt.Errorf("--host is required when using --test-forwarding")
197+
}
198+
199+
connCfg, err := ssh.BuildConnectionConfig(sshDebugHost, sshDebugUser, sshDebugPort, sshDebugKeyPath, sshDebugPassword, "")
200+
if err != nil {
201+
return err
202+
}
203+
204+
result, err := ssh.TestForwarding(rc, connCfg, sshDebugForwardTestTarget)
205+
if err != nil {
206+
return err
207+
}
208+
209+
if result.Success {
210+
logger.Info("SSH port forwarding test passed",
211+
zap.String("host", connCfg.Host),
212+
zap.String("user", connCfg.User),
213+
zap.String("target", result.Target),
214+
zap.String("message", result.Message))
215+
return nil
216+
}
217+
218+
logger.Warn("SSH port forwarding test failed",
219+
zap.String("host", connCfg.Host),
220+
zap.String("user", connCfg.User),
221+
zap.String("target", result.Target),
222+
zap.String("message", result.Message))
223+
return fmt.Errorf("port forwarding blocked: %s", result.Message)
224+
}
225+
173226
// formatSSHDiagnosticReport formats the diagnostic report based on the requested format
174227
func formatSSHDiagnosticReport(report *ssh.SSHDiagnosticReport, format string, sanitize bool) (string, error) {
175228
switch format {

cmd/read/ssh.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package read
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
7+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
8+
"github.com/CodeMonkeyCybersecurity/eos/pkg/ssh"
9+
"github.com/spf13/cobra"
10+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
11+
"go.uber.org/zap"
12+
)
13+
14+
var (
15+
sshReadHost string
16+
sshReadUser string
17+
sshReadPort string
18+
sshReadKey string
19+
sshReadPassword string
20+
sshReadSudoPass string
21+
)
22+
23+
// readSSHCmd reports SSH forwarding-related configuration for a remote host.
24+
var readSSHCmd = &cobra.Command{
25+
Use: "ssh",
26+
Short: "Inspect SSH forwarding configuration on a host",
27+
Long: `Shows forwarding-related sshd_config directives and SSH service status.
28+
29+
Examples:
30+
eos read ssh --host vhost1
31+
eos read ssh --host user@vhost1 --key ~/.ssh/id_ed25519
32+
eos read ssh --host vhost1 --user henry --password '...' --sudo-pass '...'`,
33+
RunE: eos_cli.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
34+
if sshReadHost == "" {
35+
return fmt.Errorf("--host is required (e.g., vhost1 or user@vhost1)")
36+
}
37+
38+
connCfg, err := ssh.BuildConnectionConfig(sshReadHost, sshReadUser, sshReadPort, sshReadKey, sshReadPassword, sshReadSudoPass)
39+
if err != nil {
40+
return err
41+
}
42+
43+
logger := otelzap.Ctx(rc.Ctx)
44+
logger.Info("Connecting to host for SSH forwarding status",
45+
zap.String("host", connCfg.Host),
46+
zap.String("user", connCfg.User),
47+
zap.String("port", connCfg.Port))
48+
49+
client, err := ssh.ConnectSSHClient(rc, connCfg)
50+
if err != nil {
51+
return fmt.Errorf("failed to connect to %s: %w", connCfg.Host, err)
52+
}
53+
defer func() { _ = client.Close() }()
54+
55+
status, err := ssh.ReadForwardingStatus(rc, client)
56+
if err != nil {
57+
return err
58+
}
59+
60+
logger.Info("SSH forwarding configuration",
61+
zap.String("allow_tcp_forwarding", status.AllowTcpForwarding),
62+
zap.String("allow_stream_local_forwarding", status.AllowStreamLocalForwarding),
63+
zap.Strings("permit_open", status.PermitOpen),
64+
zap.String("service_status", status.ServiceStatus))
65+
66+
return nil
67+
}),
68+
}
69+
70+
func init() {
71+
ReadCmd.AddCommand(readSSHCmd)
72+
73+
readSSHCmd.Flags().StringVar(&sshReadHost, "host", "", "Target host (e.g., vhost1 or user@vhost1)")
74+
readSSHCmd.Flags().StringVar(&sshReadUser, "user", "", "SSH username override (defaults to current user)")
75+
readSSHCmd.Flags().StringVar(&sshReadPort, "port", "22", "SSH port (default 22)")
76+
readSSHCmd.Flags().StringVar(&sshReadKey, "key", "", "Path to SSH private key (defaults to first available)")
77+
readSSHCmd.Flags().StringVar(&sshReadPassword, "password", "", "SSH password (optional if key-based auth works)")
78+
readSSHCmd.Flags().StringVar(&sshReadSudoPass, "sudo-pass", "", "Sudo password for reading sshd_config (defaults to --password)")
79+
}

cmd/self/integration_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010

1111
"github.com/CodeMonkeyCybersecurity/eos/pkg/backup"
1212
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
13-
"github.com/CodeMonkeyCybersecurity/eos/pkg/verify"
1413
"github.com/CodeMonkeyCybersecurity/eos/pkg/patterns"
1514
"github.com/spf13/cobra"
1615
"github.com/stretchr/testify/assert"

cmd/update/moni.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99

1010
"github.com/CodeMonkeyCybersecurity/eos/pkg/bionicgpt"
11-
"github.com/CodeMonkeyCybersecurity/eos/pkg/bionicgpt/apikeys"
1211
"github.com/CodeMonkeyCybersecurity/eos/pkg/bionicgpt/postinstall"
1312
"github.com/CodeMonkeyCybersecurity/eos/pkg/bionicgpt/refresh"
1413
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
@@ -294,34 +293,6 @@ func runMoniPostInstall(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []st
294293
return nil
295294
}
296295

297-
// runMoniRotateAPIKeys handles the API key rotation
298-
// Orchestration layer: delegates to pkg/bionicgpt/apikeys for business logic
299-
func runMoniRotateAPIKeys(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
300-
logger := otelzap.Ctx(rc.Ctx)
301-
302-
logger.Info("Starting Moni API key rotation",
303-
zap.String("install_dir", moniInstallDir))
304-
305-
// Build configuration
306-
config := &apikeys.Config{
307-
InstallDir: moniInstallDir,
308-
}
309-
310-
// Validate configuration
311-
if err := config.Validate(); err != nil {
312-
return fmt.Errorf("invalid configuration: %w", err)
313-
}
314-
315-
// Execute API key rotation
316-
if err := apikeys.Execute(rc, config); err != nil {
317-
logger.Error("API key rotation failed", zap.Error(err))
318-
return fmt.Errorf("API key rotation failed: %w", err)
319-
}
320-
321-
logger.Info("API key rotation completed successfully")
322-
return nil
323-
}
324-
325296
// runMoniRefresh handles the refresh operation
326297
// Orchestration layer: delegates to pkg/bionicgpt/refresh for business logic
327298
func runMoniRefresh(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
@@ -404,6 +375,9 @@ func runMoniRotateAPIKeys(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []
404375
logger.Info(" docker compose -f /opt/bionicgpt/docker-compose.yml logs -f app litellm-proxy")
405376
logger.Info("")
406377

378+
return nil
379+
}
380+
407381
// runMoniInit handles the Moni initialization worker
408382
// Orchestration layer: delegates to pkg/moni for business logic
409383
func runMoniInit(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {

cmd/update/ssh.go

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ import (
1515

1616
// TODO move to pkg/ to DRY up this code base but putting it with other similar functions
1717
var (
18-
sshHost string
19-
sshKeyPath string
20-
sshHosts string
21-
sshUsername string
18+
sshHost string
19+
sshKeyPath string
20+
sshHosts string
21+
sshUsername string
22+
sshEnableForwarding bool
23+
sshForwardUser string
24+
sshForwardPort string
25+
sshForwardPassword string
26+
sshForwardSudoPassword string
2227
)
2328

2429
var SecureSSHCmd = &cobra.Command{
@@ -35,6 +40,10 @@ Examples:
3540
eos secure ssh --host user@hostname --key ~/.ssh/id_rsa
3641
eos secure ssh # Interactive mode`,
3742
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
43+
if sshEnableForwarding {
44+
return runEnableSSHForwarding(rc)
45+
}
46+
3847
otelzap.Ctx(rc.Ctx).Info("Starting SSH security diagnostics")
3948

4049
// Get SSH host if not provided
@@ -259,4 +268,58 @@ func init() {
259268
// Add flags for copy-keys command
260269
CopyKeysCmd.Flags().StringVar(&sshHosts, "hosts", "", "Comma-separated list of hosts to copy keys to")
261270
CopyKeysCmd.Flags().StringVar(&sshUsername, "user", "", "SSH username for remote hosts")
271+
272+
SecureSSHCmd.Flags().BoolVar(&sshEnableForwarding, "enable-forwarding", false, "Enable SSH port forwarding (AllowTcpForwarding/AllowStreamLocalForwarding yes, remove PermitOpen)")
273+
SecureSSHCmd.Flags().StringVar(&sshForwardUser, "user", "", "SSH username for --enable-forwarding")
274+
SecureSSHCmd.Flags().StringVar(&sshForwardPort, "port", "22", "SSH port for --enable-forwarding")
275+
SecureSSHCmd.Flags().StringVar(&sshForwardPassword, "password", "", "SSH password for --enable-forwarding (optional if key auth works)")
276+
SecureSSHCmd.Flags().StringVar(&sshForwardSudoPassword, "sudo-pass", "", "Sudo password for remote host (defaults to --password)")
277+
}
278+
279+
func runEnableSSHForwarding(rc *eos_io.RuntimeContext) error {
280+
logger := otelzap.Ctx(rc.Ctx)
281+
282+
if sshHost == "" {
283+
var err error
284+
sshHost, err = interaction.PromptUser(rc, "Enter SSH host (user@hostname[:port]): ")
285+
if err != nil {
286+
return fmt.Errorf("failed to get SSH host: %w", err)
287+
}
288+
}
289+
290+
connCfg, err := ssh.BuildConnectionConfig(sshHost, sshForwardUser, sshForwardPort, sshKeyPath, sshForwardPassword, sshForwardSudoPassword)
291+
if err != nil {
292+
return err
293+
}
294+
295+
logger.Info("Enabling SSH forwarding on host",
296+
zap.String("host", connCfg.Host),
297+
zap.String("user", connCfg.User),
298+
zap.String("port", connCfg.Port))
299+
300+
client, err := ssh.ConnectSSHClient(rc, connCfg)
301+
if err != nil {
302+
return fmt.Errorf("failed to connect to %s: %w", connCfg.Host, err)
303+
}
304+
defer func() { _ = client.Close() }()
305+
306+
result, err := ssh.EnableSSHForwarding(rc, client)
307+
if err != nil {
308+
return err
309+
}
310+
311+
logger.Info("SSH forwarding enabled",
312+
zap.String("backup", result.BackupPath),
313+
zap.String("restart_command", result.RestartCommand),
314+
zap.String("validation", result.ValidationOutput))
315+
316+
if result.Status != nil {
317+
logger.Info("Forwarding status",
318+
zap.String("allow_tcp_forwarding", result.Status.AllowTcpForwarding),
319+
zap.String("allow_stream_local_forwarding", result.Status.AllowStreamLocalForwarding),
320+
zap.Strings("permit_open", result.Status.PermitOpen),
321+
zap.String("service_status", result.Status.ServiceStatus))
322+
}
323+
324+
return nil
262325
}

0 commit comments

Comments
 (0)