Skip to content

feat: create general purpose toolbox CLI#1

Draft
khuedoan wants to merge 5 commits intomasterfrom
toolbox
Draft

feat: create general purpose toolbox CLI#1
khuedoan wants to merge 5 commits intomasterfrom
toolbox

Conversation

@khuedoan
Copy link
Owner

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new general-purpose CLI tool called "toolbox" for managing infrastructure, specifically focused on managing secrets in HashiCorp Vault through SSH connections to Kubernetes clusters. The tool provides SSH tunneling, port-forwarding capabilities, and automated secret generation.

Changes:

  • Created a new Go CLI tool with Cobra framework for managing Vault secrets
  • Implemented SSH connection management with port-forwarding to Kubernetes services
  • Added support for generating random strings, SSH keypairs (ed25519/RSA), and manual secret entry

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 24 comments.

Show a summary per file
File Description
toolbox/go.mod Defines Go module with dependencies for CLI, Vault API, and SSH libraries
toolbox/go.sum Dependency checksums for reproducible builds
toolbox/main.go Main entry point that delegates to the cmd package
toolbox/cmd/root.go Root CLI command definition with persistent flags for SSH connection
toolbox/cmd/secrets.go Secrets management command with support for multiple secret types
toolbox/internal/cluster/cluster.go SSH connection and port-forwarding implementation
toolbox/internal/cluster/client.go High-level cluster client with Vault integration
toolbox/Makefile Build and test targets for the toolbox
settings.yaml Example configuration file demonstrating secret types
flake.nix Nix flake configuration to build and package the toolbox

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +229 to +254
func waitForService(ctx context.Context, addr string) error {
client := &http.Client{Timeout: 2 * time.Second}
url := "http://" + addr

for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
return nil
}

// Also try TCP connect for non-HTTP services
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err == nil {
conn.Close()
return nil
}

time.Sleep(healthCheckInterval)
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The waitForService function can loop indefinitely if the service never becomes available and the context is never cancelled. Consider adding a maximum timeout or retry limit to prevent indefinite blocking.

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +175
_ = t.session.Signal(ssh.SIGTERM)
t.session.Close()
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The session.Close() error is not captured or returned. While this is acceptable in cleanup code where you want to continue closing other resources, consider using errors.Join to accumulate errors from both Signal and Close for better error reporting.

Suggested change
_ = t.session.Signal(ssh.SIGTERM)
t.session.Close()
if err := t.session.Signal(ssh.SIGTERM); err != nil {
errs = append(errs, fmt.Errorf("signal session: %w", err))
}
if err := t.session.Close(); err != nil {
errs = append(errs, fmt.Errorf("close session: %w", err))
}

Copilot uses AI. Check for mistakes.
Comment on lines +262 to +265
algorithm := e.settings.Algorithm
if algorithm == "" {
algorithm = "ed25519"
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default SSH key algorithm is "ed25519" with a fallback to "rsa" using 4096 bits. While these are secure choices, consider documenting these defaults in the settings.yaml example or in code comments to help users understand what algorithms are available and recommended.

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +197
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
continue
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors during tunnel connection handling are silently ignored. When Accept() fails (other than net.ErrClosed), the error is discarded with 'continue'. This makes it difficult to diagnose connection issues. Consider logging these errors for operational visibility.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +114
func getVaultToken(conn *Connector) (string, error) {
cmd := fmt.Sprintf(
`kubectl get secret vault-unseal-keys -n %s -o template='{{ index .data "vault-root" }}'`,
vaultNamespace,
)

output, err := conn.RunCommand(cmd)
if err != nil {
return "", err
}

token, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(output)))
if err != nil {
return "", fmt.Errorf("decode token: %w", err)
}

return string(token), nil
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Vault root token is retrieved from a Kubernetes secret and used directly. This is a highly sensitive operation. Consider documenting that this command should only be used in trusted environments, and that proper RBAC should be enforced to prevent unauthorized access to the vault-unseal-keys secret.

Copilot uses AI. Check for mistakes.

resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP response bodies are closed immediately without reading the body. While this is correct for health checks, you should defer the Close call and check its error to ensure proper resource cleanup, even though in practice the error is unlikely to matter here.

Suggested change
resp.Body.Close()
if resp.Body != nil {
defer func() {
if cerr := resp.Body.Close(); cerr != nil {
// ignore close error for health check
}
}()
}

Copilot uses AI. Check for mistakes.
Comment on lines +302 to +309
randomBytes := make([]byte, length)

if _, err := rand.Read(randomBytes); err != nil {
return "", err
}

for i, b := range randomBytes {
result[i] = charset[int(b)%len(charset)]
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateRandomString function uses modulo bias when selecting characters from the charset. Using int(b)%len(charset) introduces a slight bias toward certain characters. For cryptographic purposes, consider using a rejection sampling approach or crypto/rand's Read with proper bounds checking to ensure uniform distribution.

Suggested change
randomBytes := make([]byte, length)
if _, err := rand.Read(randomBytes); err != nil {
return "", err
}
for i, b := range randomBytes {
result[i] = charset[int(b)%len(charset)]
charsetLen := len(charset)
if charsetLen == 0 {
return "", fmt.Errorf("charset is empty")
}
// Use rejection sampling to avoid modulo bias when mapping random bytes to charset indices.
maxMultiple := 256 / charsetLen * charsetLen
for i := 0; i < length; i++ {
for {
var b [1]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
if int(b[0]) >= maxMultiple {
// Discard values that would introduce modulo bias.
continue
}
result[i] = charset[int(b[0])%charsetLen]
break
}

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +227
done := make(chan struct{}, 2)

go func() {
io.Copy(remoteConn, localConn)
done <- struct{}{}
}()

go func() {
io.Copy(localConn, remoteConn)
done <- struct{}{}
}()

<-done
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two io.Copy goroutines can potentially leak if one completes while the other is blocked. After receiving from the done channel once, the function returns immediately, potentially leaving the other goroutine running. Consider using a sync.WaitGroup or closing both connections to ensure both goroutines terminate.

Copilot uses AI. Check for mistakes.
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ssh.InsecureIgnoreHostKey() disables host key verification, making the connection vulnerable to man-in-the-middle attacks. Consider implementing proper host key verification using ssh.FixedHostKey() with a known host key, or at least document why this security compromise is acceptable for this use case.

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +224
go func() {
io.Copy(remoteConn, localConn)
done <- struct{}{}
}()

go func() {
io.Copy(localConn, remoteConn)
done <- struct{}{}
}()
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors from io.Copy operations are ignored. If data copying between local and remote connections fails, these errors are silently discarded. This can hide network issues or connection problems. Consider logging these errors for better operational visibility and debugging.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments