diff --git a/aif/scripts/post/sshsecure.py b/aif/scripts/post/sshsecure.py index e38e4ea..f68a073 100644 --- a/aif/scripts/post/sshsecure.py +++ b/aif/scripts/post/sshsecure.py @@ -100,35 +100,69 @@ import subprocess # REMOVE WHEN SWITCHING TO PURE PYTHON # from cryptography.hazmat.backends import default_backend as crypto_default_backend # +# TODO: associate various config directives with version, too. +# For now, we use this for primarily CentOS 6.x, which doesn't support ED25519 and probably some of the MACs. +# Bastards. +# https://ssh-comparison.quendi.de/comparison/cipher.html at some point in the future... +# TODO: maybe implement some parsing of the ssh -Q stuff? https://superuser.com/a/869005/984616 +# If you encounter a version incompatibility, please let me know! +magic_ver = 6.5 +ssh_ver = subprocess.run(['ssh', '-V'], stderr = subprocess.PIPE).stderr.decode('utf-8').strip().split()[0] +ssh_ver = float(re.sub('^(Open|Sun_)SSH_([0-9\.]+)(p[0-9]+)?,.*$', '\g<2>', ssh_ver)) +if ssh_ver >= magic_ver: + has_ed25519 = True + supported_keys = ('ed25519', 'rsa') +else: + has_ed25519 = False + supported_keys = ('rsa', ) + + conf_options = {} -conf_options['sshd'] = {'KexAlgorithms': 'curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256', +conf_options['sshd'] = {'KexAlgorithms': 'diffie-hellman-group-exchange-sha256', 'Protocol': '2', - 'HostKey': ['/etc/ssh/ssh_host_ed25519_key', - '/etc/ssh/ssh_host_rsa_key'], - 'PermitRootLogin': 'prohibit-password', + 'HostKey': ['/etc/ssh/ssh_host_rsa_key'], + #'PermitRootLogin': 'prohibit-password', + 'PermitRootLogin': 'without-password', 'PasswordAuthentication': 'no', 'ChallengeResponseAuthentication': 'no', 'PubkeyAuthentication': 'yes', - 'Ciphers': 'chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr', - 'MACs': ('hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,' + - 'hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com')} + 'Ciphers': 'aes256-ctr,aes192-ctr,aes128-ctr', + 'MACs': ('hmac-sha2-512,hmac-sha2-256')} +if has_ed25519: + conf_options['sshd']['HostKey'].append('/etc/ssh/ssh_host_ed25519_key') + conf_options['sshd']['KexAlgorithms'] = ','.join(('curve25519-sha256@libssh.org', + conf_options['sshd']['KexAlgorithms'])) + conf_options['sshd']['Ciphers'] = ','.join((('chacha20-poly1305@openssh.com,' + 'aes256-gcm@openssh.com,' + 'aes128-gcm@openssh.com'), + conf_options['sshd']['Ciphers'])) + conf_options['sshd']['MACs'] = ','.join((('hmac-sha2-512-etm@openssh.com,' + 'hmac-sha2-256-etm@openssh.com,' + 'umac-128-etm@openssh.com'), + conf_options['sshd']['MACs'], + 'umac-128@openssh.com')) # Uncomment if this is further configured #conf_options['sshd']['AllowGroups'] = 'ssh-user' -conf_options['ssh'] = {'Host': {'*': {'KexAlgorithms': 'curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256', +conf_options['ssh'] = {'Host': {'*': {'KexAlgorithms': 'diffie-hellman-group-exchange-sha256', 'PubkeyAuthentication': 'yes', - 'HostKeyAlgorithms': 'ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ssh-rsa'}}} -# Uncomment below if Github still needs diffie-hellman-group-exchange-sha1 sometimes. -# For what it's worth, it doesn't seem to. -#conf_options['ssh']['Host']['github.com'] = {'KexAlgorithms': 'curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256,' + -# 'diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1'} + 'HostKeyAlgorithms': 'ssh-rsa'}}} +if has_ed25519: + conf_options['ssh']['Host']['*']['KexAlgorithms'] = ','.join(('curve25519-sha256@libssh.org', + conf_options['ssh']['Host']['*']['KexAlgorithms'])) + conf_options['ssh']['Host']['*']['HostKeyAlgorithms'] = ','.join( + (('ssh-ed25519-cert-v01@openssh.com,' + 'ssh-rsa-cert-v01@openssh.com,' + 'ssh-ed25519'), + conf_options['ssh']['Host']['*']['HostKeyAlgorithms'])) def hostKeys(buildmoduli): # Starting haveged should help lessen the time load a non-negligible amount, especially on virtual platforms. if os.path.lexists('/usr/bin/haveged'): # We could use psutil here, but then that's a python dependency we don't need. - # We could parse the /proc directory, but that's quite unnecessary. pgrep's installed by default on Arch. + # We could parse the /proc directory, but that's quite unnecessary. pgrep's installed by default on + # most distros. with open(os.devnull, 'wb') as devnull: if subprocess.run(['pgrep', 'haveged'], stdout = devnull).returncode != 0: subprocess.run(['haveged'], stdout = devnull) @@ -143,11 +177,12 @@ def hostKeys(buildmoduli): for suffix in ('', '.pub'): for k in glob.glob('/etc/ssh/ssh_host_*key{0}'.format(suffix)): os.rename(k, '{0}.old.{1}'.format(k, int(datetime.datetime.utcnow().timestamp()))) - subprocess.run(['ssh-keygen', '-t', 'ed25519', '-f', '/etc/ssh/ssh_host_ed25519_key', '-q', '-N', '']) + if has_ed25519: + subprocess.run(['ssh-keygen', '-t', 'ed25519', '-f', '/etc/ssh/ssh_host_ed25519_key', '-q', '-N', '']) subprocess.run(['ssh-keygen', '-t', 'rsa', '-b', '4096', '-f', '/etc/ssh/ssh_host_rsa_key', '-q', '-N', '']) # We currently don't use this, but for simplicity's sake let's return the host keys. hostkeys = {} - for k in ('ed25519', 'rsa'): + for k in supported_keys: with open('/etc/ssh/ssh_host_{0}_key.pub'.format(k), 'r') as f: hostkeys[k] = f.read() return(hostkeys) @@ -158,15 +193,19 @@ def config(opts, t): special['sshd']['opts'] = ['Match'] special['sshd']['filters'] = ['User', 'Group', 'Host', 'LocalAddress', 'LocalPort', 'Address'] # These are arguments supported by each of the special options. We'll use this to verify entries. - special['sshd']['args'] = ['AcceptEnv', 'AllowAgentForwarding', 'AllowGroups', 'AllowStreamLocalForwarding', 'AllowTcpForwarding', - 'AllowUsers', 'AuthenticationMethods', 'AuthorizedKeysCommand', 'AuthorizedKeysCommandUser', - 'AuthorizedKeysFile', 'AuthorizedPrincipalsCommand', 'AuthorizedPrincipalsCommandUser', 'AuthorizedPrincipalsFile', - 'Banner', 'ChrootDirectory', 'ClientAliveCountMax', 'ClientAliveInterval', 'DenyGroups', 'DenyUsers', 'ForceCommand', - 'GatewayPorts', 'GSSAPIAuthentication', 'HostbasedAcceptedKeyTypes', 'HostbasedAuthentication', - 'HostbasedUsesNameFromPacketOnly', 'IPQoS', 'KbdInteractiveAuthentication', 'KerberosAuthentication', 'MaxAuthTries', - 'MaxSessions', 'PasswordAuthentication', 'PermitEmptyPasswords', 'PermitOpen', 'PermitRootLogin', 'PermitTTY', - 'PermitTunnel', 'PermitUserRC', 'PubkeyAcceptedKeyTypes', 'PubkeyAuthentication', 'RekeyLimit', 'RevokedKeys', - 'StreamLocalBindMask', 'StreamLocalBindUnlink', 'TrustedUserCAKeys', 'X11DisplayOffset', 'X11Forwarding', 'X11UseLocalHost'] + special['sshd']['args'] = ['AcceptEnv', 'AllowAgentForwarding', 'AllowGroups', 'AllowStreamLocalForwarding', + 'AllowTcpForwarding', 'AllowUsers', 'AuthenticationMethods', 'AuthorizedKeysCommand', + 'AuthorizedKeysCommandUser', 'AuthorizedKeysFile', 'AuthorizedPrincipalsCommand', + 'AuthorizedPrincipalsCommandUser', 'AuthorizedPrincipalsFile', 'Banner', + 'ChrootDirectory', 'ClientAliveCountMax', 'ClientAliveInterval', 'DenyGroups', + 'DenyUsers', 'ForceCommand', 'GatewayPorts', 'GSSAPIAuthentication', + 'HostbasedAcceptedKeyTypes', 'HostbasedAuthentication', + 'HostbasedUsesNameFromPacketOnly', 'IPQoS', 'KbdInteractiveAuthentication', + 'KerberosAuthentication', 'MaxAuthTries', 'MaxSessions', 'PasswordAuthentication', + 'PermitEmptyPasswords', 'PermitOpen', 'PermitRootLogin', 'PermitTTY', 'PermitTunnel', + 'PermitUserRC', 'PubkeyAcceptedKeyTypes', 'PubkeyAuthentication', 'RekeyLimit', + 'RevokedKeys', 'StreamLocalBindMask', 'StreamLocalBindUnlink', 'TrustedUserCAKeys', + 'X11DisplayOffset', 'X11Forwarding', 'X11UseLocalHost'] special['ssh']['opts'] = ['Host', 'Match'] special['ssh']['args'] = ['canonical', 'exec', 'host', 'originalhost', 'user', 'localuser'] cf = '/etc/ssh/{0}_config'.format(t) @@ -181,7 +220,6 @@ def config(opts, t): if opt: if not re.match('^(#.*|\s+.*)$', opt[0]): confopts.append(opt[0]) - #print(confopts) # We also need to modify the config file- comment out starting with the first occurrence of the # specopts, if it exists. This is why we make a backup. commentidx = None @@ -203,16 +241,11 @@ def config(opts, t): # Since the config should be explicit, we remove any existing entries specified that we find. else: if o in confopts: - #print('commenting out old {0}'.format(o)) # If I was more worried about recursion, or if I was appending here, I should use conf[:]. # But I'm not. So I won't. for idx, opt in enumerate(conf): if re.match('^{0}(\s.*)?\n$'.format(o), opt): - #l = opt.split() - #conf[idx] = '#{0} {1}'.format(l[0].strip, l[1].strip) - #print('old {0}: {1}'.format(o, conf[idx])) conf[idx] = '#{0}'.format(opt) - #print('new {0}: {1}'.format(o, conf[idx])) # Here we handle the "multiple-specifying" options- notably, HostKey. if isinstance(opts[o], list): for l in opts[o]: @@ -247,9 +280,11 @@ def clientKeys(user = 'root'): homedir = os.path.expanduser('~{0}'.format(user)) sshdir = '{0}/.ssh'.format(homedir) os.makedirs(sshdir, mode = 0o700, exist_ok = True) - if not os.path.lexists('{0}/id_ed25519'.format(sshdir)) and not os.path.lexists('{0}/id_ed25519.pub'.format(sshdir)): - subprocess.run(['ssh-keygen', '-t', 'ed25519', '-o', '-a', '100', - '-f', '{0}/id_ed25519'.format(sshdir), '-q', '-N', '']) + if has_ed25519: + if not os.path.lexists('{0}/id_ed25519'.format(sshdir)) \ + and not os.path.lexists('{0}/id_ed25519.pub'.format(sshdir)): + subprocess.run(['ssh-keygen', '-t', 'ed25519', '-o', '-a', '100', + '-f', '{0}/id_ed25519'.format(sshdir), '-q', '-N', '']) if not os.path.lexists('{0}/id_rsa'.format(sshdir)) and not os.path.lexists('{0}/id_rsa.pub'.format(sshdir)): subprocess.run(['ssh-keygen', '-t', 'rsa', '-b', '4096', '-o', '-a', '100', '-f', '{0}/id_rsa'.format(sshdir), '-q', '-N', '']) @@ -262,7 +297,7 @@ def clientKeys(user = 'root'): if 'pubkeys' not in globals(): pubkeys = {} pubkeys[user] = {} - for k in ('ed25519', 'rsa'): + for k in supported_keys: with open('{0}/id_{1}.pub'.format(sshdir, k), 'r') as f: pubkeys[user][k] = f.read() return(pubkeys) @@ -277,8 +312,8 @@ def main(): config(conf_options[t], t) clientKeys() with open(_chkfile, 'w') as f: - f.write(('ssh, sshd, and hostkey configurations/keys have been ' + - 'modified by sshsecure.py from OpTools.\nhttps://git.square-r00t.net/OpTools/\n')) + f.write(('ssh, sshd, and hostkey configurations/keys have been modified by sshsecure.py from OpTools.\n' + 'https://git.square-r00t.net/OpTools/\n')) return() if __name__ == '__main__': diff --git a/centos/extract_files_package.py b/centos/extract_files_package.py index f9a6bf4..2e13e20 100755 --- a/centos/extract_files_package.py +++ b/centos/extract_files_package.py @@ -105,6 +105,7 @@ class FileExtractor(object): return() def extractFiles(self): + # TODO: globbing or regex on self.paths? # If we have yum, we can, TECHNICALLY, do this with: # http://yum.baseurl.org/api/yum/rpmUtils/miscutils.html#rpmUtils.miscutils.rpm2cpio # But nope. We can't selectively decompress members based on path with rpm2cpio-like funcs.