lib/ec2macosinit/systemconfig.go (342 lines of code) (raw):
package ec2macosinit
import (
"bufio"
_ "embed"
"fmt"
"log"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
const (
// ConfigurationManagementWarning is a header warning for sshd_config
ConfigurationManagementWarning = "### This file is managed by EC2 macOS Init, changes will be applied on every boot. To disable set secureSSHDConfig = false in /usr/local/aws/ec2-macos-init/init.toml ###"
// InlineWarning is a warning line for each entry to help encourage users to avoid doing the risky configuration change
InlineWarning = "# EC2 Configuration: The follow setting is recommended by EC2 and set on boot. Set secureSSHDConfig = false in /usr/local/aws/ec2-macos-init/init.toml to disable.\n"
// DefaultsCmd is the path to the script edit macOS defaults
DefaultsCmd = "/usr/bin/defaults"
// DefaultsRead is the command to read from a plist
DefaultsRead = "read"
// DefaultsReadType is the command to read the type of a parameter from a plist
DefaultsReadType = "read-type"
// DefaultsWrite is the command to write a value of a parameter to a plist
DefaultsWrite = "write"
// sshdConfigFile is the default path for the SSHD configuration file
sshdConfigFile = "/etc/ssh/sshd_config"
// ec2SSHDConfigFile is the ssh configs file path
ec2SSHDConfigFile = "/etc/ssh/sshd_config.d/050-ec2-macos.conf"
// macOSSSHDConfigDir is Apple's custom ssh configs
macOSSSHDConfigDir = "/etc/ssh/sshd_config.d"
)
//go:embed assets/ec2-macos-ssh.txt
var ec2SSHData string
var (
// numberOfBytesInCustomSSHFile is the number of bytes in assets/ec2-macos-ssh.txt
numberOfBytesInCustomSSHFile = len(ec2SSHData)
)
// ModifySysctl contains sysctl values we want to modify
type ModifySysctl struct {
Value string `toml:"value"`
}
// ModifyDefaults contains the necessary values to change a parameter in a given plist
type ModifyDefaults struct {
Plist string `toml:"plist"`
Parameter string `toml:"parameter"`
Type string `toml:"type"`
Value string `toml:"value"`
}
// SystemConfigModule contains all necessary configuration fields for running a System Configuration module.
type SystemConfigModule struct {
SecureSSHDConfig *bool `toml:"secureSSHDConfig"`
ModifySysctl []ModifySysctl `toml:"Sysctl"`
ModifyDefaults []ModifyDefaults `toml:"Defaults"`
}
// Do for the SystemConfigModule modifies system configuration such as sysctl, plist defaults, and secures the SSHD
// configuration file.
func (c *SystemConfigModule) Do(ctx *ModuleContext) (message string, err error) {
wg := sync.WaitGroup{}
// Secure SSHD configuration
var sshdConfigChanges, sshdUnchanged, sshdErrors int32
if c.SecureSSHDConfig != nil && *c.SecureSSHDConfig {
wg.Add(1)
go func() {
err := writeEC2SSHConfigs()
if err != nil {
ctx.Logger.Errorf("Error writing ec2 custom ssh configs: %s", err)
}
wg.Done()
}()
wg.Add(1)
go func() {
changes, err := c.configureSSHD(ctx)
if err != nil {
atomic.AddInt32(&sshdErrors, 1)
ctx.Logger.Errorf("Error while attempting to correct SSHD configuration: %s", err)
}
if changes {
// Add change for messaging
atomic.AddInt32(&sshdConfigChanges, 1)
} else {
// No changes made
atomic.AddInt32(&sshdUnchanged, 1)
}
wg.Done()
}()
}
// Modifications using sysctl
var sysctlChanged, sysctlUnchanged, sysctlErrors int32
for _, m := range c.ModifySysctl {
wg.Add(1)
go func(val string) {
changed, err := modifySysctl(val)
if err != nil {
atomic.AddInt32(&sysctlErrors, 1)
ctx.Logger.Errorf("Error while attempting to modify sysctl property [%s]: %s", val, err)
}
if changed { // changed a property
atomic.AddInt32(&sysctlChanged, 1)
ctx.Logger.Infof("Modified sysctl property [%s]", val)
} else { // did not change a property
atomic.AddInt32(&sysctlUnchanged, 1)
ctx.Logger.Infof("Did not modify sysctl property [%s]", val)
}
wg.Done()
}(m.Value)
}
// Modifications using defaults
var defaultsChanged, defaultsUnchanged, defaultsErrors int32
for _, m := range c.ModifyDefaults {
wg.Add(1)
go func(modifyDefault ModifyDefaults) {
changed, err := modifyDefaults(modifyDefault)
if err != nil {
atomic.AddInt32(&defaultsErrors, 1)
ctx.Logger.Errorf("Error while attempting to modify default [%s]: %s", modifyDefault.Parameter, err)
}
if changed { // changed a property
atomic.AddInt32(&defaultsChanged, 1)
ctx.Logger.Infof("Modified default [%s]", modifyDefault.Parameter)
} else { // did not change a property
atomic.AddInt32(&defaultsUnchanged, 1)
ctx.Logger.Infof("Did not modify default [%s]", modifyDefault.Parameter)
}
wg.Done()
}(m)
}
// Wait for everything to finish
wg.Wait()
// Craft output message
totalChanged := sysctlChanged + defaultsChanged + sshdConfigChanges
totalUnchanged := sysctlUnchanged + defaultsUnchanged + sshdUnchanged
totalErrors := sysctlErrors + defaultsErrors + sshdErrors
baseMessage := fmt.Sprintf("[%d changed / %d unchanged / %d error(s)] out of %d requested changes",
totalChanged, totalUnchanged, totalErrors, totalChanged+totalUnchanged)
if totalErrors > 0 {
return "", fmt.Errorf("one or more system configuration changes were unsuccessful: %s", baseMessage)
}
return "system configuration completed with " + baseMessage, nil
}
// writeEC2SSHConfigs writes custom ec2 ssh configs file
func writeEC2SSHConfigs() (err error) {
err = os.MkdirAll(macOSSSHDConfigDir, 0755)
if err != nil {
return fmt.Errorf("error while attempting to create %s dir: %s", macOSSSHDConfigDir, err)
}
f, err := os.OpenFile(ec2SSHDConfigFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("error while attempting to create %s file: %s", ec2SSHDConfigFile, err)
}
defer f.Close()
n, err := f.WriteString(ec2SSHData)
if err != nil {
return fmt.Errorf("error while writing ec2-macos ssh data on file: %s. %s", ec2SSHDConfigFile, err)
}
if n != numberOfBytesInCustomSSHFile {
return fmt.Errorf("error while writing ec2-macos ssh data on file: %s. %d should equal %d", ec2SSHDConfigFile, n, numberOfBytesInCustomSSHFile)
}
return nil
}
// modifySysctl modifies a sysctl parameter, if necessary.
func modifySysctl(value string) (changed bool, err error) {
// Separate parameter
inputSplit := strings.Split(value, "=")
if len(inputSplit) != 2 {
return false, fmt.Errorf("ec2macosinit: unable to split input sysctl value: %s", value)
}
param := inputSplit[0]
// Check current value
output, err := executeCommand([]string{"sysctl", "-e", param}, "", []string{})
if err != nil {
return false, fmt.Errorf("ec2macosinit: unable to get current value from sysctl: %s", err)
}
if strings.TrimSpace(output.stdout) == value {
return false, nil // Exit early if value is already set
}
// Attempt to set the value five times, with 100ms in between each attempt
err = retry(5, 100*time.Millisecond, func() (err error) {
// Set value
_, err = executeCommand([]string{"sysctl", value}, "", []string{})
if err != nil {
return fmt.Errorf("ec2macosinit: unable to set desired value using sysctl: %s", err)
}
// Validate new value
output, err = executeCommand([]string{"sysctl", "-e", param}, "", []string{})
if err != nil {
return fmt.Errorf("ec2macosinit: unable to get current value from sysctl: %s", err)
}
if strings.TrimSpace(output.stdout) != value {
return fmt.Errorf("ec2macosinit: error setting new value using sysctl: %s", output.stdout)
}
return nil
})
if err != nil {
return false, err
}
return true, nil
}
// modifyDefaults modifies a default, if necessary.
func modifyDefaults(modifyDefault ModifyDefaults) (changed bool, err error) {
// Check to see if current value already matches
err = checkDefaultsValue(modifyDefault)
if err == nil {
return false, err // Exit early if value is already set correctly, otherwise attempt to update value
}
// If the values did not match, update value in the plist
err = updateDefaultsValue(modifyDefault)
if err != nil {
return false, fmt.Errorf("ec2macosinit: unable to update value for plist %s, parameter %s to value %s", modifyDefault.Plist, modifyDefault.Parameter, modifyDefault.Value)
}
// Validate new value
err = checkDefaultsValue(modifyDefault)
if err != nil {
return false, fmt.Errorf("ec2macosinit: verification failed for updating value for plist %s, parameter %s", modifyDefault.Plist, modifyDefault.Parameter)
}
return true, nil
}
// checkDefaultsValue checks the value for a given parameter in a plist.
func checkDefaultsValue(modifyDefault ModifyDefaults) (err error) {
// Check value of current parameter in plist
readCmd := []string{DefaultsCmd, DefaultsRead, modifyDefault.Plist, modifyDefault.Parameter}
out, err := executeCommand(readCmd, "", []string{})
if err != nil {
return err
}
// Get value by trimming whitespace
actualValue := strings.TrimSpace(out.stdout)
// Run comparisons depending on the parameter's type
switch modifyDefault.Type {
// Only implemented for bool[ean] now, more types to be implemented later
case "bool", "boolean":
return checkBoolean(modifyDefault.Value, actualValue)
}
return nil
}
// updateDefaultsValue updates the value of a parameter in a given plist.
func updateDefaultsValue(modifyDefault ModifyDefaults) (err error) {
// Update the value, specifying its type
writeCmd := []string{DefaultsCmd, DefaultsWrite, modifyDefault.Plist, modifyDefault.Parameter, "-" + modifyDefault.Type, modifyDefault.Value}
_, err = executeCommand(writeCmd, "", []string{})
return err
}
// checkBoolean is designed to convert both inputs into a boolean and compare.
func checkBoolean(expectedValue, actualValue string) (err error) {
// Convert our expected value into a boolean
expectedOutput, err := strconv.ParseBool(expectedValue)
if err != nil {
return err
}
// Convert our actual value into a boolean
actualOutput, err := strconv.ParseBool(actualValue)
if err != nil {
return err
}
if expectedOutput != actualOutput {
return fmt.Errorf("ec2macosinit: boolean values did not match - expected: %v, actual: %v", expectedOutput, actualOutput)
} else {
return nil
}
}
// checkSSHDReturn uses launchctl to find the exit code for ssh.plist and returns if it was successful
func (c *SystemConfigModule) checkSSHDReturn() (success bool, err error) {
// Launchd can provide status on processes running, this gets that output to be parsed
out, _ := executeCommand([]string{"launchctl", "list"}, "", []string{})
// Start a line by line scanner
scanner := bufio.NewScanner(strings.NewReader(out.stdout))
for scanner.Scan() {
// Fetch the next line
line := scanner.Text()
// If the line contains "sshd." then the real SSHD is started, not just the dummy sshd wrapper
if strings.Contains(line, "sshd.") {
// Strip the newline, then split on tabs to get fields
launchctlFields := strings.Split(strings.Replace(line, "\n", "", -1), "\t")
// Take the second field which is the process exit code on start
retValue, err := strconv.ParseBool(launchctlFields[1])
if err != nil {
return false, fmt.Errorf("ec2macosinit: failed to get sshd exit code: %s", err)
}
// Return true for zero (good exit) otherwise false
return !retValue, nil
}
}
// If all of "launchctl list" output doesn't have a status, simply return false since its not running
return false, nil
}
// checkAndWriteWarning is a helper function to write out the warning if not present
func checkAndWriteWarning(lastLine string, tempSSHDFile *os.File) (err error) {
if !strings.Contains(lastLine, "EC2 Configuration") && lastLine != InlineWarning {
_, err := tempSSHDFile.WriteString(InlineWarning)
if err != nil {
return fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
}
return nil
}
// configureSSHD scans the SSHConfigFile and writes to a temporary file if changes are detected. If changes are detected
// it replaces the SSHConfigFile. If SSHD is detected as running, it restarts it.
func (c *SystemConfigModule) configureSSHD(ctx *ModuleContext) (configChanges bool, err error) {
// Look for each thing and fix them if found
sshdFile, err := os.Open(sshdConfigFile)
if err != nil {
log.Fatal(err)
}
defer sshdFile.Close()
// Create scanner for the SSHD file
scanner := bufio.NewScanner(sshdFile)
// Create a new temporary file, if changes are detected, it will be moved over the existing file
tempSSHDFile, err := os.CreateTemp("", "sshd_config_fixed.*")
if err != nil {
return false, fmt.Errorf("ec2macosinit: error creating %s", tempSSHDFile.Name())
}
defer tempSSHDFile.Close()
// Keep track of line number simply for confirming warning header
var lineNumber int
// Track the last line for adding in warning when needed
var lastLine string
// Iterate over every line in the file
for scanner.Scan() {
lineNumber++
currentLine := scanner.Text()
// If this is the first line in the file, look for the warning header and add if missing
if lineNumber == 1 && currentLine != ConfigurationManagementWarning {
_, err = tempSSHDFile.WriteString(ConfigurationManagementWarning + "\n")
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
configChanges = true
lastLine = ConfigurationManagementWarning
}
switch {
// Check if PasswordAuthentication is enabled, if so put in warning and change the config
// PasswordAuthentication allows SSHD to respond to user password brute force attacks and can result in lowered
// security, especially if a simple password is set. In EC2, this is undesired and therefore turned off by default
case strings.Contains(currentLine, "PasswordAuthentication yes"):
err = checkAndWriteWarning(lastLine, tempSSHDFile)
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
// Overwrite with desired configuration line
_, err = tempSSHDFile.WriteString("PasswordAuthentication no\n")
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
// Changes detected so this will enforce updating the file later
configChanges = true
// Check if PAM is enabled, if so, put in warning and change the config
// PAM authentication enables challenge-response authentication which can allow brute force attacks on SSHD
// In EC2, this is undesired and therefore turned off by default
case strings.TrimSpace(currentLine) == "UsePAM yes":
err = checkAndWriteWarning(lastLine, tempSSHDFile)
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
// Overwrite with desired configuration line
_, err = tempSSHDFile.WriteString("UsePAM no\n")
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
// Changes detected so this will enforce updating the file later
configChanges = true
// Check if Challenge-response is enabled, if so put in warning and change the config
// Challenge-response authentication via SSHD can allow brute force attacks for SSHD. In EC2, this is undesired
// and therefore turned off by default
case strings.Contains(currentLine, "ChallengeResponseAuthentication yes"):
err = checkAndWriteWarning(lastLine, tempSSHDFile)
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
// Overwrite with desired configuration line
_, err = tempSSHDFile.WriteString("ChallengeResponseAuthentication no\n")
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
// Changes detected so this will enforce updating the file later
configChanges = true
default:
// Otherwise write the line as is to the temp file without modification
_, err = tempSSHDFile.WriteString(currentLine + "\n")
if err != nil {
return false, fmt.Errorf("ec2macosinit: error writing to %s", tempSSHDFile.Name())
}
}
// Rotate the current line to the last line so that comments can be inserted above rewritten lines
lastLine = currentLine
}
if err := scanner.Err(); err != nil {
return false, fmt.Errorf("ec2macosinit: error reading %s: %s", sshdConfigFile, err)
}
// If there was a change detected, then copy the file and restart sshd
if configChanges {
// Get the current status of SSHD, if its not running, then it should not be started
sshdRunning, err := c.checkSSHDReturn()
if err != nil {
ctx.Logger.Errorf("ec2macosinit: unable to get SSHD status: %s", err)
}
// Move the temporary file to the SSHDConfigFile
err = os.Rename(tempSSHDFile.Name(), sshdConfigFile)
if err != nil {
return false, fmt.Errorf("ec2macosinit: unable to save updated configuration to %s", sshdConfigFile)
}
// Temporary files have different permissions by design, correct the permissions for SSHDConfigFile
err = os.Chmod(sshdConfigFile, 0644)
if err != nil {
return false, fmt.Errorf("ec2macosinit: unable to set correct permssions of %s", sshdConfigFile)
}
// If SSHD was detected as running, then a restart must happen, if it was not running, the work is complete
if sshdRunning {
// Unload and load SSHD, the launchctl method for re-loading SSHD with new configuration
_, err = executeCommand([]string{"/bin/zsh", "-c", "launchctl unload /System/Library/LaunchDaemons/ssh.plist"}, "", []string{})
if err != nil {
ctx.Logger.Errorf("ec2macosinit: unable to stop SSHD %s", err)
return false, fmt.Errorf("ec2macosinit: unable to stop SSHD %s", err)
}
_, err = executeCommand([]string{"/bin/zsh", "-c", "launchctl load -w /System/Library/LaunchDaemons/ssh.plist"}, "", []string{})
if err != nil {
ctx.Logger.Errorf("ec2macosinit: unable to restart SSHD %s", err)
return false, fmt.Errorf("ec2macosinit: unable to restart SSHD %s", err)
}
// Add the message to state that config was modified and SSHD was correctly restarted
ctx.Logger.Info("Modified SSHD configuration and restarted SSHD for new configuration")
} else {
// Since SSHD was not running, only change the configuration but no restarting is desired
ctx.Logger.Info("Modified SSHD configuration, did not restart SSHD since it was not running")
}
} else {
// There were no changes detected from desired state, simply exit and let the temp file be
ctx.Logger.Info("Did not modify SSHD configuration")
}
// Return the message to caller for logging
return configChanges, nil
}