diff --git a/test/ostest.py b/test/ostest.py index 59ae5e2..a6b44d7 100644 --- a/test/ostest.py +++ b/test/ostest.py @@ -20,7 +20,9 @@ import env import test.tools as tools +from tools import * import uuid +import shutil import unittest import os import walinuxagent.logger as logger @@ -51,6 +53,8 @@ class TestUserOperation(unittest.TestCase): userName)) self.assertTrue(os.path.isdir(os.path.join(CurrOS.GetHome(), userName))) + +MockSshdConfigPath=MockFunc("GetSshdConfigPath", "/tmp/sshd_config") class TestSshOperation(unittest.TestCase): def _setUp(self): logger.AddLoggerAppender(logger.AppenderConfig({ @@ -59,6 +63,16 @@ class TestSshOperation(unittest.TestCase): "console_path":"/dev/stdout" })) + @Mockup(CurrOS, "GetSshdConfigPath", MockSshdConfigPath) + def test_config_sshd(self): + shutil.copyfile(os.path.join(env.test_root, "sshd_config"), + CurrOS.GetSshdConfigPath()) + CurrOS.ConfigSshd(True) + simple_file_grep(CurrOS.GetSshdConfigPath(), + "PasswordAuthentication no") + simple_file_grep(CurrOS.GetSshdConfigPath(), + "ChallengeResponseAuthentication no") + def test_regen_ssh_host_key(self): oldKey = fileutil.GetFileContents('/etc/ssh/ssh_host_rsa_key') CurrOS.RegenerateSshHostkey('rsa') diff --git a/test/sshd_config b/test/sshd_config new file mode 100644 index 0000000..77fb290 --- /dev/null +++ b/test/sshd_config @@ -0,0 +1,90 @@ +# Package generated configuration file +# See the sshd_config(5) manpage for details + +# What ports, IPs and protocols we listen for +Port 22 +# Use these options to restrict which interfaces/protocols sshd will bind to +#ListenAddress :: +#ListenAddress 0.0.0.0 +Protocol 2 +# HostKeys for protocol version 2 +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +#Privilege Separation is turned on for security +UsePrivilegeSeparation yes + +# Lifetime and size of ephemeral version 1 server key +KeyRegenerationInterval 3600 +ServerKeyBits 1024 + +# Logging +SyslogFacility AUTH +LogLevel INFO + +# Authentication: +LoginGraceTime 120 +PermitRootLogin without-password +StrictModes yes + +RSAAuthentication yes +PubkeyAuthentication yes +#AuthorizedKeysFile %h/.ssh/authorized_keys + +# Don't read the user's ~/.rhosts and ~/.shosts files +IgnoreRhosts yes +# For this to work you will also need host keys in /etc/ssh_known_hosts +RhostsRSAAuthentication no +# similar for protocol version 2 +HostbasedAuthentication no +# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication +#IgnoreUserKnownHosts yes + +# To enable empty passwords, change to yes (NOT RECOMMENDED) +PermitEmptyPasswords no + +# Change to yes to enable challenge-response passwords (beware issues with +# some PAM modules and threads) +ChallengeResponseAuthentication no + +# Change to no to disable tunnelled clear text passwords +#PasswordAuthentication yes + +# Kerberos options +#KerberosAuthentication no +#KerberosGetAFSToken no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes + +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes +#UseLogin no + +#MaxStartups 10:30:60 +#Banner /etc/issue.net + +# Allow client to pass locale environment variables +AcceptEnv LANG LC_* + +Subsystem sftp /usr/lib/openssh/sftp-server + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin without-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +UsePAM yes + +Match group root diff --git a/walinuxagent/protocol/common.py b/walinuxagent/protocol/common.py index a7b7a89..6545e32 100644 --- a/walinuxagent/protocol/common.py +++ b/walinuxagent/protocol/common.py @@ -140,6 +140,9 @@ class OvfEnv(object): def getUserPassword(self): return self.UserPassword + def clearUserPassword(self): + self.UserPassword = None + def getCustomData(self): return self.CustomData diff --git a/walinuxagent/protocol/v1.py b/walinuxagent/protocol/v1.py index ef2b967..f48e327 100644 --- a/walinuxagent/protocol/v1.py +++ b/walinuxagent/protocol/v1.py @@ -549,15 +549,15 @@ class Certificates(object): beginCrt = True elif re.match(r'[-]+END.*KEY[-]+', line): tmpFile = self.writeToTempFile(index, 'prv', buf) - pub = self.getPubKeyFromPrv(tmpFile) + pub = CurrOS.GetPubKeyFromPrv(tmpFile) prvs[pub] = tmpFile buf = [] index += 1 beginPrv = False elif re.match(r'[-]+END.*CERTIFICATE[-]+', line): tmpFile = self.writeToTempFile(index, 'crt', buf) - pub = self.getPubKeyFromCrt(tmpFile) - thumbprint = self.getThumbprintFromCrt(tmpFile) + pub = CurrOS.GetPubKeyFromCrt(tmpFile) + thumbprint = CurrOS.GetThumbprintFromCrt(tmpFile) thumbprints[pub] = thumbprint #Rename crt with thumbprint as the file name crt = "{0}.crt".format(thumbprint) @@ -596,25 +596,6 @@ class Certificates(object): tmp.writelines(buf) return fileName - def getPubKeyFromPrv(self, fileName): - cmd = "{0} rsa -in {1} -pubout 2>/dev/null".format(self.opensslCmd, - fileName) - pub = shellutil.RunGetOutput(cmd)[1] - return pub - - def getPubKeyFromCrt(self, fileName): - cmd = "{0} x509 -in {1} -pubkey -noout".format(self.opensslCmd, - fileName) - pub = shellutil.RunGetOutput(cmd)[1] - return pub - - def getThumbprintFromCrt(self, fileName): - cmd = "{0} x509 -in {1} -fingerprint -noout".format(self.opensslCmd, - fileName) - thumbprint = shellutil.RunGetOutput(cmd)[1] - thumbprint = thumbprint.rstrip().split('=')[1].replace(':', '').upper() - return thumbprint - class ExtensionsConfig(object): """ parse ExtensionsConfig, downloading and unpacking them to /var/lib/waagent. diff --git a/walinuxagent/provision.py b/walinuxagent/provision.py index af99771..a529a02 100644 --- a/walinuxagent/provision.py +++ b/walinuxagent/provision.py @@ -21,46 +21,57 @@ import os import walinuxagent.logger as logger from walinuxagent.utils.osutil import CurrOS, CurrOSInfo import walinuxagent.utils.shellutil as shellutil -from CurrOS import LibDir, OvfMountPoint CustomDataFile="CustomData" class ProvisionHandler(object): - def __init__(self, config, protocol, libDir=LibDir): + def __init__(self, config, protocol): self.config = config self.protocol = protocol - self.libDir = libDir def provision(self): if self.config.getSwitch("Provisioning.Enabled"): return logger.Info("Provisioning image started") - ovfenv = self.protocol.copyOvf() - - self.setHostName(ovfenv.getComputerName()) - self.createUserAccount(ovfenv.getUserName(), ovfenv.getUserPassword()) - self.deploySshPublicKeys(ovfenv.getSshPublicKeys) - self.saveCustomData(ovfenv.getCustomData()) + self.ovfenv = self.protocol.copyOvf() + + password = self.ovfenv.getUserPassword() + self.ovfenv.clearUserPassword() + + self.setHostName() + self.createUserAccount(self.ovfenv.getUserName(), password) + self.deploySshPublicKeys() + self.deploySshKeyPairs() + self.saveCustomData() if config.getSwitch("Provisioning.RegenerateSshHostKeyPair"): keyPairType = config.get("Provisioning.SshHostKeyPairType", "rsa") CurrOS.RegenerateSshHostkey(keyPairType) + #TODO Wait for host name published CurrOS.RestartSshService() - self.reportSshHostkeyThumbnail() + + #TODO report provision status + self.protocol.reportProvisionStatus("") if config.getSwitch("Provisioning.DeleteRootPassword"): self.deleteRootPassword() def saveCustomData(self, customData): - CurrOS.SetFileContents(os.path.join(self.libDir, CustomDataFile), + libDir = CurrOS.GetLibDir() + CurrOS.SetFileContents(os.path.join(libDir, CustomDataFile), customData) - def deploySshPublicKeys(self, keys): - pass + def deploySshPublicKeys(self): + for thumbprint, path in self.ovfenv.getSshPublicKeys(): + CurrOS.DeploySshPublicKey(self.ovfenv.getUserName(), thumbprint, path) - def setHostName(self, hostName): + def deploySshKeyPairs(self): + for thumbprint, path in self.ovfenv.getSshKeyPairs(): + CurrOS.DeploySshKeyPair(self.ovfenv.getUserName(), thumbprint, path) + + def setHostName(self): pass def createUserAccount(self, userName, password): @@ -68,10 +79,7 @@ class ProvisionHandler(object): raise Exception("User name is empty.") if CurrOS.IsSysUser(userName): raise Exception("User:{0} is a system user.".format(userName)) - CurrOS.CreateUserAccount(userName, password) - - def reportSshHostkeyThumbnail(self): - pass + CurrOS.UpdateUserAccount(userName, password) def deleteRootPassword(self): CurrOS.DeleteRootPassword() diff --git a/walinuxagent/utils/fileutil.py b/walinuxagent/utils/fileutil.py index 29b309c..e2764d3 100644 --- a/walinuxagent/utils/fileutil.py +++ b/walinuxagent/utils/fileutil.py @@ -120,7 +120,7 @@ def GetLineStartingWith(prefix, filepath): #End File operation util functions def CreateDir(dirpath, owner, mode): - if os.path.isdir(dirpath): + if not os.path.isdir(dirpath): os.makedirs(dirpath, mode) ChangeOwner(dirpath, owner) diff --git a/walinuxagent/utils/osutil.py b/walinuxagent/utils/osutil.py index b8399e1..b7bd5d1 100644 --- a/walinuxagent/utils/osutil.py +++ b/walinuxagent/utils/osutil.py @@ -103,7 +103,7 @@ class DefaultDistro(): def ChangePassword(self, userName, password): shellutil.RunSendStdin("chpasswd", "{0}:{1}\n".format(userName, password)) - + def ConfigSudoer(self, userName, nopasswd): # for older distros create sudoers.d if not os.path.isdir('/etc/sudoers.d/'): @@ -120,21 +120,153 @@ class DefaultDistro(): sudoer = "{0} ALL = (ALL) ALL\n".format(userName) fileutil.SetFileContents('/etc/sudoers.d/waagent', sudoer) os.chmod('/etc/sudoers.d/waagent', 0440) - + + def DeleteRootPassword(self): + pass + def GetHome(self): return '/home' - def ConfigSshKey(self, userName, thumbprint): - sshDir = os.path.join(self.getHome(), userName, '.ssh') - fileutil.CreateDir(sshDir, userName, '0700') - pub = os.path.join(sshDir, 'id_rsa.pub') - prv = os.path.join(sshDir, 'id_rsa') + def GetPubKeyFromPrv(self, fileName): + cmd = "{0} rsa -in {1} -pubout 2>/dev/null".format(self.GetOpensslCmd(), + fileName) + pub = shellutil.RunGetOutput(cmd)[1] + return pub + + def GetPubKeyFromCrt(self, fileName): + cmd = "{0} x509 -in {1} -pubkey -noout".format(self.GetOpensslCmd(), + fileName) + pub = shellutil.RunGetOutput(cmd)[1] + return pub + + def NormPath(self, filepath): + home = CurrOS.GetHome() + # Expand HOME variable if present in path + path = os.path.normpath(filepath.replace("$HOME", home)) + + def GetThumbprintFromCrt(self, fileName): + cmd="{0} x509 -in {1} -fingerprint -noout".format(self.GetOpensslCmd(), + fileName) + thumbprint = shellutil.RunGetOutput(cmd)[1] + thumbprint = thumbprint.rstrip().split('=')[1].replace(':', '').upper() + return thumbprint + + def DeploySshKeyPair(self, userName, thumbprint, path): + """ + Deploy id_rsa and id_rsa.pub + """ + path = self.NormPath(path) + dirPath = os.path.dirname(path) + fileutil.CreateDir(dirPath, userName, 0700) + libDir = CurrOS.GetLibDir() + prvPath = os.path.join(libDir, thumbprint + '.prv') + if not os.path.isfile(prvPath): + logger.Error("Failed to deploy key pair, thumbprint: {0}", + thumbprint) + return + shutil.copyfile(prvPath, path) + pubPath = path + '.pub' + pub = self.GetPubKeyFromPrv(prvPath) + fileutil.SetFileContents(pubPath, pub) + self.SetSelinuxContext(path, 'unconfined_u:object_r:ssh_home_t:s') + self.SetSelinuxContext(pubPath, 'unconfined_u:object_r:ssh_home_t:s') + os.chmod(path, 0600) + os.chmod(pubPath, 0600) + + def DeploySshPublicKey(self, userName, thumbprint, path): + """ + Deploy authorized_key + """ + path = self.NormPath(path) + dirPath = os.path.dirname(path) + fileutil.CreateDir(dirPath, userName, 0700) + libDir = CurrOS.GetLibDir() + crtPath = os.path.join(libDir, thumbprint + '.crt') + if not os.path.isfile(crtPath): + logger.Error("Failed to deploy public key, thumbprint: {0}", + thumbprint) + return + pubPath = os.path.join(libDir, thumbprint + '.pub') + pub = self.GetPubKeyFromCrt(crtPath) + fileutil.SetFileContents(pubPath, pub) + self.SetSelinuxContext(pubPath, 'unconfined_u:object_r:ssh_home_t:s') + shellutil.Run("ssh-keygen -i -m PKCS8 -f {0} >> {1}", thumbprint, path) + self.SetSelinuxContext(path, 'unconfined_u:object_r:ssh_home_t:s') + os.chmod(path, 0600) + os.chmod(pubPath, 0600) + + def IsSelinuxSystem(self): + """ + Checks and sets self.selinux = True if SELinux is available on system. + """ + if self.selinux == None: + if shellutil.Run("which getenforce", chk_err=False): + self.selinux = False + else: + self.selinux = True + return self.selinux + + def IsSelinuxRunning(self): + """ + Calls shell command 'getenforce' and returns True if 'Enforcing'. + """ + if self.IsSelinuxSystem(): + output = shellutil.RunGetOutput("getenforce")[1] + return output.startswith("Enforcing") + else: + return False + + def SetSelinuxEnforce(self, state): + """ + Calls shell command 'setenforce' with 'state' + and returns resulting exit code. + """ + if self.IsSelinuxSystem(): + if state: s = '1' + else: s='0' + return shellutil.Run("setenforce "+s) + + def SetSelinuxContext(self, path, cn): + """ + Calls shell 'chcon' with 'path' and 'cn' context. + Returns exit result. + """ + if self.IsSelinuxSystem(): + return shellutil.Run('chcon ' + cn + ' ' + path) + + def GetSshdConfigPath(self): + return "/etc/ssh/sshd_config" + + def ConfigSshd(self, disablePassword): + if not disablePassword: + return + configPath = self.GetSshdConfigPath() + config = fileutil.GetFileContents(configPath).split("\n") + passwordAuthFound = False + challengeAuthFound = False + for i in range(0, len(config)): + if config[i].startswith("PasswordAuthentication"): + passwordAuthFound = True + config[i] = "PasswordAuthentication no" + elif config[i].startswith("ChallengeResponseAuthentication"): + challengeAuthFound = True + config[i] = "ChallengeResponseAuthentication no" + elif config[i].startswith("Match"): + #Match block must be put in the end of sshd config + break + + if not passwordAuthFound: + config.insert(i, "PasswordAuthentication no") + if not challengeAuthFound: + config.insert(i, "ChallengeResponseAuthentication no") + + logger.Info("Disabled SSH password-based authentication methods.") + fileutil.ReplaceFileContentsAtomic(configPath, "\n".join(config)) def RegenerateSshHostkey(self, keyPairType): shellutil.Run("rm -f /etc/ssh/ssh_host_*key*") shellutil.Run("ssh-keygen -N '' -t {0} -f /etc/ssh/ssh_host_{1}_key" .format(keyPairType, keyPairType)) - self.RestartSshService() def GetOpensslCmd(self): return '/usr/bin/openssl'