Skip to content

Commit

Permalink
Merge pull request #29 from adfinis/fix-28-nss-passwd-backends
Browse files Browse the repository at this point in the history
fix(10-042): Limit user lookups to "local" NSS passwd databases
  • Loading branch information
s3lph authored Feb 16, 2023
2 parents 874acda + b9197a1 commit 35feb8b
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 13 deletions.
84 changes: 71 additions & 13 deletions plugins/modules/audit_ssh_authorizedkeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
required: false
default: []
type: list
limit_nss_backends:
description: Only retrieve users from these NSS backends, and emit a warning if other backends are configured.
required: false
default: [files, compat, db, systemd]
type: list
ignore_nss_backends:
description: Consider these NSS backends as "safe" and don't emit a warning if they are not present in limit_nss_backends.
required: false
default: []
type: list
config:
description: Path to the sshd config fille
required: false
Expand Down Expand Up @@ -73,6 +83,20 @@
allowed:
- 'from="2001:db8::42/128" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBIpR/ccV9KAL5eoyPaT0frG1+moHO2nM2TsRKrdANU [email protected]'
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICZWKDPix+uTd+P+ZdoD3AkrD8cfikji9JKzvrfhczMA'
- name: The same, but also check users from sssd (use with caution if your domain contains a large number of users)
adfinis.maintenance.audit_ssh_authorizedkeys:
allowed:
- 'from="2001:db8::42/128" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBIpR/ccV9KAL5eoyPaT0frG1+moHO2nM2TsRKrdANU [email protected]'
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICZWKDPix+uTd+P+ZdoD3AkrD8cfikji9JKzvrfhczMA'
limit_nss_backends: [files, compat, db, systemd, sss]
- name: Silence the warning that sss users are not audited
adfinis.maintenance.audit_ssh_authorizedkeys:
allowed:
- 'from="2001:db8::42/128" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBIpR/ccV9KAL5eoyPaT0frG1+moHO2nM2TsRKrdANU [email protected]'
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICZWKDPix+uTd+P+ZdoD3AkrD8cfikji9JKzvrfhczMA'
ignore_nss_backends: [sss]
'''


Expand All @@ -89,12 +113,17 @@

from ansible.module_utils.basic import AnsibleModule

import collections
import os
import pwd
import subprocess
import shlex


# pwdent class conforming to https://docs.python.org/3/library/pwd.html
GetentPwdEnt = collections.namedtuple('pwdent', ['pw_name', 'pw_passwd', 'pw_uid', 'pw_gid', 'pw_gecos', 'pw_dir', 'pw_shell'])


def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
Expand All @@ -105,6 +134,8 @@ def run_module():
required=dict(type='list', required=False, default=[]),
allowed=dict(type='list', required=False, default=[]),
forbidden=dict(type='list', required=False, default=[]),
limit_nss_backends=dict(type='list', required=False, default=['files', 'compat', 'db', 'systemd']),
ignore_nss_backends=dict(type='list', required=False, default=[]),
)

# seed the result dict in the object
Expand All @@ -125,18 +156,43 @@ def run_module():
supports_check_mode=True,
)

warnings = []

getent_backends = []
# Check NSS passwd db backends against list of limited backends, and emit warnings if additional backends are present
with open('/etc/nsswitch.conf', 'r') as nssf:
for line in nssf.readlines():
line = line.split('#', 1)[0].strip()
if not line:
continue
tokens = line.split()
db, backends = tokens[0], tokens[1:]
if db != 'passwd:':
continue
for backend in backends:
if backend in module.params['limit_nss_backends']:
getent_backends.append(backend)
elif backend not in module.params['ignore_nss_backends']:
msg = 'Users from the NSS passwd backend "{}" are excluded from this check. '.format(backend) + \
'Please audit manually or include the backend in either limit_nss_backends or ignore_nss_backends'
warnings.append(msg)

# Get user homes
users = []
users = set()
if module.params['user'] is not None:
user = module.params['user']
try:
pwdent = pwd.getpwnam(user)
users.append(pwdent)
users.add(pwdent)
except KeyError:
module.fail_json(msg='User {} does not exist'.format(user), **result)
else:
for pwdent in pwd.getpwall():
users.append(pwdent)
# getpwnam/getpwall don't allow filtering by backends, need to user getent
for backend in getent_backends:
getent = subprocess.Popen(['/usr/bin/getent', 'passwd', '-s', backend], stdout=subprocess.PIPE)
getent_stdout, _ = getent.communicate()
for line in getent_stdout.decode().splitlines():
users.add(GetentPwdEnt(*line.split(':', 6)))

# Read the acutal ssh authorized_keys
result['authorized_keys'] = {}
Expand All @@ -148,18 +204,20 @@ def run_module():
authorized_keys_paths = [module.params['file']]
else:
ufilter = 'host=,addr=,user=' + pwdent.pw_name # host and addr are required by some implementations
sshd_cmdline = [module.params['sshd'], '-C', ufilter , '-T', '-f', module.params['config']]
sshd_cmdline = [module.params['sshd'], '-C', ufilter, '-T', '-f', module.params['config']]
sshd_configtest = subprocess.Popen(sshd_cmdline, stdout=subprocess.PIPE)
sshd_stdout, _ = sshd_configtest.communicate()
if sshd_configtest.returncode != 0:
module.fail_json(msg='SSHD configuration invalid (or insufficient privileges, try become_user=root become=yes)', **result)

for cline in sshd_stdout.decode().splitlines():
conf = cline.split()
if conf[0] != 'authorizedkeysfile':
continue
authorized_keys_paths = conf[1:]

conf = cline.split(None, 1)
if conf[0] == 'authorizedkeyscommand' and conf[1] != 'none':
msg = 'AuthorizedKeysCommand is configured: "{}". Keys returned by this command are not audited.'.format(conf[1])
warnings.append(msg)
elif conf[0] == 'authorizedkeysfile':
authorized_keys_paths = conf[1].split()

if authorized_keys_paths is None:
authorized_keys_paths = []

Expand Down Expand Up @@ -227,13 +285,13 @@ def run_module():
'after_header': 'authorized_keys ({})'.format(user),
})

if len(violations) > 0:
if len(violations) > 0 or len(warnings) > 0:
result['changed'] = True
if not module.check_mode:
module.fail_json(msg=violations, **result)
module.fail_json(warnings=warnings, msg=violations, **result)
# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)
module.exit_json(warnings=warnings, **result)


def main():
Expand Down
7 changes: 7 additions & 0 deletions roles/maintenance_10_linux/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ linux_allowed_ssh_authorized_keys: []

linux_additional_ssh_authorized_keys: []

linux_allowed_ssh_nss_backends:
- files
- compat
- db
- systemd
linux_allowed_ssh_ignored_nss_backends: []

linux_allowed_login_users:
- root

Expand Down
2 changes: 2 additions & 0 deletions roles/maintenance_10_linux/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@
name: "Security: SSH keys: Check for unknown or outdated keys for root and all users"
adfinis.maintenance.audit_ssh_authorizedkeys:
allowed: "{{ linux_allowed_ssh_authorized_keys + linux_additional_ssh_authorized_keys }}"
limit_nss_backends: "{{ linux_allowed_ssh_nss_backends }}"
ignore_nss_backends: "{{ linux_allowed_ssh_ignored_nss_backends }}"
check_mode: yes

- <<: *task
Expand Down

0 comments on commit 35feb8b

Please sign in to comment.