diff --git a/README.md b/README.md index 68b6ed80..5e56dc2f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ See [SECURITY.md](docs/SECURITY.md) for complete security specifications and imp - **Operating System**: Currently supports Ubuntu 22 only - **Architecture**: Designed for GPU-accelerated compute workloads - **Access Method**: Requires SSH server and SSH key-based authentication +- **System Requirements**: Requires systemd to be running and accessible --- diff --git a/docs/SECURITY.md b/docs/SECURITY.md index caec7711..c91aa6bb 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -16,6 +16,7 @@ This document outlines the security requirements and best practices for implemen **Implementation Requirements:** - SSH server (OpenSSH or equivalent) must be installed and running on all instances +- systemd must be running and accessible via systemctl command - SSH key pairs must be supported for authentication - Public keys must be injectable during instance provisioning - SSH access must be available through the configured firewall rules @@ -136,4 +137,4 @@ For security issues, vulnerabilities, or questions: --- -**Note**: This document is a living document and will be updated as security requirements evolve. All cloud integrations must comply with these requirements to ensure the security and integrity of the Brev Compute SDK ecosystem. \ No newline at end of file +**Note**: This document is a living document and will be updated as security requirements evolve. All cloud integrations must comply with these requirements to ensure the security and integrity of the Brev Compute SDK ecosystem. \ No newline at end of file diff --git a/pkg/v1/image.go b/pkg/v1/image.go index 9683262b..60778e18 100644 --- a/pkg/v1/image.go +++ b/pkg/v1/image.go @@ -30,84 +30,125 @@ type Image struct { } func ValidateInstanceImage(ctx context.Context, instance Instance, privateKey string) error { - // First ensure the instance is running and SSH accessible - sshUser := instance.SSHUser - sshPort := instance.SSHPort - publicIP := instance.PublicIP + sshClient, err := connectToInstance(ctx, instance, privateKey) + if err != nil { + return err + } + defer func() { + if closeErr := sshClient.Close(); closeErr != nil { + fmt.Printf("warning: failed to close SSH connection: %v\n", closeErr) + } + }() + + arch, err := validateArchitecture(ctx, sshClient) + if err != nil { + return err + } + + osVersion, err := validateOSVersion(ctx, sshClient) + if err != nil { + return err + } + + homeDir, err := validateHomeDirectory(ctx, sshClient, instance.SSHUser) + if err != nil { + return err + } + + systemdStatus, err := validateSystemd(ctx, sshClient) + if err != nil { + return err + } - // Validate that we have the required SSH connection details - if sshUser == "" { - return fmt.Errorf("SSH user is not set for instance %s", instance.CloudID) + fmt.Printf("Instance image validation passed for %s: architecture=%s, os=%s, home=%s, systemd=%s\n", + instance.CloudID, arch, osVersion, homeDir, systemdStatus) + + return nil +} + +func connectToInstance(ctx context.Context, instance Instance, privateKey string) (*ssh.Client, error) { + if instance.SSHUser == "" { + return nil, fmt.Errorf("SSH user is not set for instance %s", instance.CloudID) } - if sshPort == 0 { - return fmt.Errorf("SSH port is not set for instance %s", instance.CloudID) + if instance.SSHPort == 0 { + return nil, fmt.Errorf("SSH port is not set for instance %s", instance.CloudID) } - if publicIP == "" { - return fmt.Errorf("public IP is not available for instance %s", instance.CloudID) + if instance.PublicIP == "" { + return nil, fmt.Errorf("public IP is not available for instance %s", instance.CloudID) } - // Connect to the instance via SSH sshClient, err := ssh.ConnectToHost(ctx, ssh.ConnectionConfig{ - User: sshUser, - HostPort: fmt.Sprintf("%s:%d", publicIP, sshPort), + User: instance.SSHUser, + HostPort: fmt.Sprintf("%s:%d", instance.PublicIP, instance.SSHPort), PrivKey: privateKey, }) if err != nil { - return fmt.Errorf("failed to connect to instance via SSH: %w", err) + return nil, fmt.Errorf("failed to connect to instance via SSH: %w", err) } - defer func() { - if closeErr := sshClient.Close(); closeErr != nil { - // Log close error but don't return it as it's not the primary error - fmt.Printf("warning: failed to close SSH connection: %v\n", closeErr) - } - }() + return sshClient, nil +} - // Check 1: Verify x86_64 architecture +func validateArchitecture(ctx context.Context, sshClient *ssh.Client) (string, error) { stdout, stderr, err := sshClient.RunCommand(ctx, "uname -m") if err != nil { - return fmt.Errorf("failed to check architecture: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return "", fmt.Errorf("failed to check architecture: %w, stdout: %s, stderr: %s", err, stdout, stderr) } - if !strings.Contains(strings.TrimSpace(stdout), "x86_64") { - return fmt.Errorf("expected x86_64 architecture, got: %s", strings.TrimSpace(stdout)) + arch := strings.TrimSpace(stdout) + if !strings.Contains(arch, "x86_64") { + return "", fmt.Errorf("expected x86_64 architecture, got: %s", arch) } + return "x86_64", nil +} - // Check 2: Verify Ubuntu 20.04 or 22.04 - stdout, stderr, err = sshClient.RunCommand(ctx, "cat /etc/os-release | grep PRETTY_NAME") +func validateOSVersion(ctx context.Context, sshClient *ssh.Client) (string, error) { + stdout, stderr, err := sshClient.RunCommand(ctx, "cat /etc/os-release | grep PRETTY_NAME") if err != nil { - return fmt.Errorf("failed to check OS version: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return "", fmt.Errorf("failed to check OS version: %w, stdout: %s, stderr: %s", err, stdout, stderr) } parts := strings.Split(strings.TrimSpace(stdout), "=") if len(parts) != 2 { - return fmt.Errorf("error: os pretty name not in format PRETTY_NAME=\"Ubuntu\": %s", stdout) + return "", fmt.Errorf("error: os pretty name not in format PRETTY_NAME=\"Ubuntu\": %s", stdout) } - // Remove quotes from the value osVersion := strings.Trim(parts[1], "\"") ubuntuRegex := regexp.MustCompile(`Ubuntu 20\.04|22\.04`) if !ubuntuRegex.MatchString(osVersion) { - return fmt.Errorf("expected Ubuntu 20.04 or 22.04, got: %s", osVersion) + return "", fmt.Errorf("expected Ubuntu 20.04 or 22.04, got: %s", osVersion) } + return osVersion, nil +} - // Check 3: Verify home directory - stdout, stderr, err = sshClient.RunCommand(ctx, "cd ~ && pwd") +func validateHomeDirectory(ctx context.Context, sshClient *ssh.Client, sshUser string) (string, error) { + stdout, stderr, err := sshClient.RunCommand(ctx, "cd ~ && pwd") if err != nil { - return fmt.Errorf("failed to check home directory: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return "", fmt.Errorf("failed to check home directory: %w, stdout: %s, stderr: %s", err, stdout, stderr) } homeDir := strings.TrimSpace(stdout) if sshUser == "ubuntu" { if !strings.Contains(homeDir, "/home/ubuntu") { - return fmt.Errorf("expected ubuntu user home directory to contain /home/ubuntu, got: %s", homeDir) + return "", fmt.Errorf("expected ubuntu user home directory to contain /home/ubuntu, got: %s", homeDir) } } else { if !strings.Contains(homeDir, "/root") { - return fmt.Errorf("expected non-ubuntu user home directory to contain /root, got: %s", homeDir) + return "", fmt.Errorf("expected non-ubuntu user home directory to contain /root, got: %s", homeDir) } } + return homeDir, nil +} - fmt.Printf("Instance image validation passed for %s: architecture=%s, os=%s, home=%s\n", - instance.CloudID, "x86_64", osVersion, homeDir) +func validateSystemd(ctx context.Context, sshClient *ssh.Client) (string, error) { + stdout, stderr, err := sshClient.RunCommand(ctx, "systemctl is-system-running") - return nil + systemdStatus := strings.TrimSpace(stdout) + if systemdStatus == "running" || systemdStatus == "degraded" { + return systemdStatus, nil + } + + if err != nil { + return "", fmt.Errorf("failed to check systemd status: %w, stdout: %s, stderr: %s", err, stdout, stderr) + } + + return "", fmt.Errorf("expected systemd to be running or degraded, got: %s", systemdStatus) }