CVE-2024-9902
Description
A flaw was found in Ansible. The ansible-core user module can allow an unprivileged user to silently create or replace the contents of any file on any system path and take ownership of it when a privileged user executes the user module against the unprivileged user's home directory. If the unprivileged user has traversal permissions on the directory containing the exploited target file, they retain full control over the contents of the file as its owner.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansible-corePyPI | < 2.14.18rc1 | 2.14.18rc1 |
ansible-corePyPI | >= 2.15.0b1, < 2.15.13rc1 | 2.15.13rc1 |
ansible-corePyPI | >= 2.16.0b1, < 2.16.13rc1 | 2.16.13rc1 |
ansible-corePyPI | >= 2.17.0b1, < 2.17.6rc1 | 2.17.6rc1 |
ansible-corePyPI | >= 2.18.0b1, < 2.18.0rc2 | 2.18.0rc2 |
Patches
503daf774d0d8[stable-2.14] user action, fix ssh-keygen issues (#84167)
5 files changed · +131 −4
changelogs/fragments/user_ssh_fix.yml+4 −0 added@@ -0,0 +1,4 @@ +bugfixes: + - user action will now require O(force) to overwrite the public part of an ssh key when generating ssh keys, as was already the case for the private part. +security_fixes: + - user action won't allow ssh-keygen, chown and chmod to run on existing ssh public key file, avoiding traversal on existing symlinks (CVE-2024-9902).
lib/ansible/modules/user.py+15 −2 modified@@ -1152,9 +1152,11 @@ def ssh_key_gen(self): overwrite = None try: ssh_key_file = self.get_ssh_key_path() + pub_file = '%s.pub' % ssh_key_file except Exception as e: return (1, '', to_native(e)) ssh_dir = os.path.dirname(ssh_key_file) + if not os.path.exists(ssh_dir): if self.module.check_mode: return (0, '', '') @@ -1163,12 +1165,23 @@ def ssh_key_gen(self): os.chown(ssh_dir, info[2], info[3]) except OSError as e: return (1, '', 'Failed to create %s: %s' % (ssh_dir, to_native(e))) + if os.path.exists(ssh_key_file): if self.force: - # ssh-keygen doesn't support overwriting the key interactively, so send 'y' to confirm + self.module.warn('Overwriting existing ssh key private file "%s"' % ssh_key_file) overwrite = 'y' else: + self.module.warn('Found existing ssh key private file "%s", no force, so skipping ssh-keygen generation' % ssh_key_file) return (None, 'Key already exists, use "force: yes" to overwrite', '') + + if os.path.exists(pub_file): + if self.force: + self.module.warn('Overwriting existing ssh key public file "%s"' % pub_file) + os.unlink(pub_file) + else: + self.module.warn('Found existing ssh key public file "%s", no force, so skipping ssh-keygen generation' % pub_file) + return (None, 'Public key already exists, use "force: yes" to overwrite', '') + cmd = [self.module.get_bin_path('ssh-keygen', True)] cmd.append('-t') cmd.append(self.ssh_type) @@ -1235,7 +1248,7 @@ def ssh_key_gen(self): # If the keys were successfully created, we should be able # to tweak ownership. os.chown(ssh_key_file, info[2], info[3]) - os.chown('%s.pub' % ssh_key_file, info[2], info[3]) + os.chown(pub_file, info[2], info[3]) return (rc, out, err) def ssh_key_fingerprint(self):
test/integration/targets/user/tasks/main.yml+3 −2 modified@@ -36,7 +36,8 @@ - import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_password_lock.yml - import_tasks: test_password_lock_new_user.yml -- import_tasks: test_local.yml +- include_tasks: test_local.yml when: not (ansible_distribution == 'openSUSE Leap' and ansible_distribution_version is version('15.4', '>=')) -- import_tasks: test_umask.yml +- include_tasks: test_umask.yml when: ansible_facts.system == 'Linux' +- import_tasks: ssh_keygen.yml
test/integration/targets/user/tasks/ssh_keygen.yml+100 −0 added@@ -0,0 +1,100 @@ +- name: user generating ssh keys tests + become: true + vars: + home: "{{ (ansible_facts['os_family'] == 'Darwin')|ternary('/Users/ansibulluser/', '/home/ansibulluser/')}}" + ssh_key_file: .ssh/ansible_test_rsa + pub_file: '{{ssh_key_file}}.pub' + key_files: + - '{{ssh_key_file}}' + - '{{pub_file}}' + block: + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Test creating ssh key creation + block: + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file}}' + + - name: check files exist + stat: + path: '{{home ~ item}}' + register: stat_keys + loop: '{{ key_files }}' + + - name: ensure they exist + assert: + that: + - stat_keys.results[item].stat.exists + loop: [0, 1] + + always: + - name: Clean ssh keys + file: path={{ home ~ item }} state=absent + loop: '{{ key_files }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Ensure we don't break on conflicts + block: + - name: flag file for test + tempfile: + register: flagfile + + - name: precreate public .ssh + file: path={{home ~ '.ssh'}} state=directory + + - name: setup public key linked to flag file + file: path={{home ~ pub_file}} src={{flagfile.path}} state=link + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + ignore_errors: true + register: user_no_force + + - stat: path={{home ~ pub_file}} + register: check_pub + + - name: ensure we didn't overwrite + assert: + that: + - check_pub.stat.exists + - check_pub.stat.islnk + - check_pub.stat.uid == 0 + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + force: true + ignore_errors: true + register: user_force + + - stat: path={{home ~ pub_file}} + register: check_pub2 + + - name: ensure we failed since we didn't force overwrite + assert: + that: + - user_force is success + - check_pub2.stat.exists + - not check_pub2.stat.islnk + - check_pub2.stat.uid != 0 + always: + - name: Clean up files + file: path={{ home ~ item }} state=absent + loop: '{{ key_files + [flagfile.path] }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent
test/integration/targets/user/tasks/test_local.yml+9 −0 modified@@ -39,6 +39,15 @@ tags: - user_test_local_mode +- name: Ensure no local_ansibulluser + user: + name: local_ansibulluser + state: absent + local: yes + remove: true + tags: + - user_test_local_mode + - name: Create local_ansibulluser user: name: local_ansibulluser
03794735d370[stable-2.15] user action, fix ssh-keygen issues (#84168)
5 files changed · +131 −4
changelogs/fragments/user_ssh_fix.yml+4 −0 added@@ -0,0 +1,4 @@ +bugfixes: + - user action will now require O(force) to overwrite the public part of an ssh key when generating ssh keys, as was already the case for the private part. +security_fixes: + - user action won't allow ssh-keygen, chown and chmod to run on existing ssh public key file, avoiding traversal on existing symlinks (CVE-2024-9902).
lib/ansible/modules/user.py+15 −2 modified@@ -1148,9 +1148,11 @@ def ssh_key_gen(self): overwrite = None try: ssh_key_file = self.get_ssh_key_path() + pub_file = '%s.pub' % ssh_key_file except Exception as e: return (1, '', to_native(e)) ssh_dir = os.path.dirname(ssh_key_file) + if not os.path.exists(ssh_dir): if self.module.check_mode: return (0, '', '') @@ -1159,12 +1161,23 @@ def ssh_key_gen(self): os.chown(ssh_dir, info[2], info[3]) except OSError as e: return (1, '', 'Failed to create %s: %s' % (ssh_dir, to_native(e))) + if os.path.exists(ssh_key_file): if self.force: - # ssh-keygen doesn't support overwriting the key interactively, so send 'y' to confirm + self.module.warn('Overwriting existing ssh key private file "%s"' % ssh_key_file) overwrite = 'y' else: + self.module.warn('Found existing ssh key private file "%s", no force, so skipping ssh-keygen generation' % ssh_key_file) return (None, 'Key already exists, use "force: yes" to overwrite', '') + + if os.path.exists(pub_file): + if self.force: + self.module.warn('Overwriting existing ssh key public file "%s"' % pub_file) + os.unlink(pub_file) + else: + self.module.warn('Found existing ssh key public file "%s", no force, so skipping ssh-keygen generation' % pub_file) + return (None, 'Public key already exists, use "force: yes" to overwrite', '') + cmd = [self.module.get_bin_path('ssh-keygen', True)] cmd.append('-t') cmd.append(self.ssh_type) @@ -1231,7 +1244,7 @@ def ssh_key_gen(self): # If the keys were successfully created, we should be able # to tweak ownership. os.chown(ssh_key_file, info[2], info[3]) - os.chown('%s.pub' % ssh_key_file, info[2], info[3]) + os.chown(pub_file, info[2], info[3]) return (rc, out, err) def ssh_key_fingerprint(self):
test/integration/targets/user/tasks/main.yml+3 −2 modified@@ -36,7 +36,8 @@ - import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_password_lock.yml - import_tasks: test_password_lock_new_user.yml -- import_tasks: test_local.yml +- include_tasks: test_local.yml when: not (ansible_distribution == 'openSUSE Leap' and ansible_distribution_version is version('15.4', '>=')) -- import_tasks: test_umask.yml +- include_tasks: test_umask.yml when: ansible_facts.system == 'Linux' +- import_tasks: ssh_keygen.yml
test/integration/targets/user/tasks/ssh_keygen.yml+100 −0 added@@ -0,0 +1,100 @@ +- name: user generating ssh keys tests + become: true + vars: + home: "{{ (ansible_facts['os_family'] == 'Darwin')|ternary('/Users/ansibulluser/', '/home/ansibulluser/')}}" + ssh_key_file: .ssh/ansible_test_rsa + pub_file: '{{ssh_key_file}}.pub' + key_files: + - '{{ssh_key_file}}' + - '{{pub_file}}' + block: + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Test creating ssh key creation + block: + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file}}' + + - name: check files exist + stat: + path: '{{home ~ item}}' + register: stat_keys + loop: '{{ key_files }}' + + - name: ensure they exist + assert: + that: + - stat_keys.results[item].stat.exists + loop: [0, 1] + + always: + - name: Clean ssh keys + file: path={{ home ~ item }} state=absent + loop: '{{ key_files }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Ensure we don't break on conflicts + block: + - name: flag file for test + tempfile: + register: flagfile + + - name: precreate public .ssh + file: path={{home ~ '.ssh'}} state=directory + + - name: setup public key linked to flag file + file: path={{home ~ pub_file}} src={{flagfile.path}} state=link + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + ignore_errors: true + register: user_no_force + + - stat: path={{home ~ pub_file}} + register: check_pub + + - name: ensure we didn't overwrite + assert: + that: + - check_pub.stat.exists + - check_pub.stat.islnk + - check_pub.stat.uid == 0 + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + force: true + ignore_errors: true + register: user_force + + - stat: path={{home ~ pub_file}} + register: check_pub2 + + - name: ensure we failed since we didn't force overwrite + assert: + that: + - user_force is success + - check_pub2.stat.exists + - not check_pub2.stat.islnk + - check_pub2.stat.uid != 0 + always: + - name: Clean up files + file: path={{ home ~ item }} state=absent + loop: '{{ key_files + [flagfile.path] }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent
test/integration/targets/user/tasks/test_local.yml+9 −0 modified@@ -39,6 +39,15 @@ tags: - user_test_local_mode +- name: Ensure no local_ansibulluser + user: + name: local_ansibulluser + state: absent + local: yes + remove: true + tags: + - user_test_local_mode + - name: Create local_ansibulluser user: name: local_ansibulluser
9d7312f69563[stable-2.16] user action, fix ssh-keygen issues (#84169)
5 files changed · +131 −4
changelogs/fragments/user_ssh_fix.yml+4 −0 added@@ -0,0 +1,4 @@ +bugfixes: + - user action will now require O(force) to overwrite the public part of an ssh key when generating ssh keys, as was already the case for the private part. +security_fixes: + - user action won't allow ssh-keygen, chown and chmod to run on existing ssh public key file, avoiding traversal on existing symlinks (CVE-2024-9902).
lib/ansible/modules/user.py+15 −2 modified@@ -1160,9 +1160,11 @@ def ssh_key_gen(self): overwrite = None try: ssh_key_file = self.get_ssh_key_path() + pub_file = '%s.pub' % ssh_key_file except Exception as e: return (1, '', to_native(e)) ssh_dir = os.path.dirname(ssh_key_file) + if not os.path.exists(ssh_dir): if self.module.check_mode: return (0, '', '') @@ -1171,12 +1173,23 @@ def ssh_key_gen(self): os.chown(ssh_dir, info[2], info[3]) except OSError as e: return (1, '', 'Failed to create %s: %s' % (ssh_dir, to_native(e))) + if os.path.exists(ssh_key_file): if self.force: - # ssh-keygen doesn't support overwriting the key interactively, so send 'y' to confirm + self.module.warn('Overwriting existing ssh key private file "%s"' % ssh_key_file) overwrite = 'y' else: + self.module.warn('Found existing ssh key private file "%s", no force, so skipping ssh-keygen generation' % ssh_key_file) return (None, 'Key already exists, use "force: yes" to overwrite', '') + + if os.path.exists(pub_file): + if self.force: + self.module.warn('Overwriting existing ssh key public file "%s"' % pub_file) + os.unlink(pub_file) + else: + self.module.warn('Found existing ssh key public file "%s", no force, so skipping ssh-keygen generation' % pub_file) + return (None, 'Public key already exists, use "force: yes" to overwrite', '') + cmd = [self.module.get_bin_path('ssh-keygen', True)] cmd.append('-t') cmd.append(self.ssh_type) @@ -1243,7 +1256,7 @@ def ssh_key_gen(self): # If the keys were successfully created, we should be able # to tweak ownership. os.chown(ssh_key_file, info[2], info[3]) - os.chown('%s.pub' % ssh_key_file, info[2], info[3]) + os.chown(pub_file, info[2], info[3]) return (rc, out, err) def ssh_key_fingerprint(self):
test/integration/targets/user/tasks/main.yml+3 −2 modified@@ -38,7 +38,8 @@ - import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_password_lock.yml - import_tasks: test_password_lock_new_user.yml -- import_tasks: test_local.yml +- include_tasks: test_local.yml when: not (ansible_distribution == 'openSUSE Leap' and ansible_distribution_version is version('15.4', '>=')) -- import_tasks: test_umask.yml +- include_tasks: test_umask.yml when: ansible_facts.system == 'Linux' +- import_tasks: ssh_keygen.yml
test/integration/targets/user/tasks/ssh_keygen.yml+100 −0 added@@ -0,0 +1,100 @@ +- name: user generating ssh keys tests + become: true + vars: + home: "{{ (ansible_facts['os_family'] == 'Darwin')|ternary('/Users/ansibulluser/', '/home/ansibulluser/')}}" + ssh_key_file: .ssh/ansible_test_rsa + pub_file: '{{ssh_key_file}}.pub' + key_files: + - '{{ssh_key_file}}' + - '{{pub_file}}' + block: + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Test creating ssh key creation + block: + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file}}' + + - name: check files exist + stat: + path: '{{home ~ item}}' + register: stat_keys + loop: '{{ key_files }}' + + - name: ensure they exist + assert: + that: + - stat_keys.results[item].stat.exists + loop: [0, 1] + + always: + - name: Clean ssh keys + file: path={{ home ~ item }} state=absent + loop: '{{ key_files }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Ensure we don't break on conflicts + block: + - name: flag file for test + tempfile: + register: flagfile + + - name: precreate public .ssh + file: path={{home ~ '.ssh'}} state=directory + + - name: setup public key linked to flag file + file: path={{home ~ pub_file}} src={{flagfile.path}} state=link + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + ignore_errors: true + register: user_no_force + + - stat: path={{home ~ pub_file}} + register: check_pub + + - name: ensure we didn't overwrite + assert: + that: + - check_pub.stat.exists + - check_pub.stat.islnk + - check_pub.stat.uid == 0 + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + force: true + ignore_errors: true + register: user_force + + - stat: path={{home ~ pub_file}} + register: check_pub2 + + - name: ensure we failed since we didn't force overwrite + assert: + that: + - user_force is success + - check_pub2.stat.exists + - not check_pub2.stat.islnk + - check_pub2.stat.uid != 0 + always: + - name: Clean up files + file: path={{ home ~ item }} state=absent + loop: '{{ key_files + [flagfile.path] }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent
test/integration/targets/user/tasks/test_local.yml+9 −0 modified@@ -39,6 +39,15 @@ tags: - user_test_local_mode +- name: Ensure no local_ansibulluser + user: + name: local_ansibulluser + state: absent + local: yes + remove: true + tags: + - user_test_local_mode + - name: Create local_ansibulluser user: name: local_ansibulluser
f7be90626da3[stable-2.17] user action, fix ssh-keygen issues (#84170)
5 files changed · +131 −4
changelogs/fragments/user_ssh_fix.yml+4 −0 added@@ -0,0 +1,4 @@ +bugfixes: + - user action will now require O(force) to overwrite the public part of an ssh key when generating ssh keys, as was already the case for the private part. +security_fixes: + - user action won't allow ssh-keygen, chown and chmod to run on existing ssh public key file, avoiding traversal on existing symlinks (CVE-2024-9902).
lib/ansible/modules/user.py+15 −2 modified@@ -1167,9 +1167,11 @@ def ssh_key_gen(self): overwrite = None try: ssh_key_file = self.get_ssh_key_path() + pub_file = f'{ssh_key_file}.pub' except Exception as e: return (1, '', to_native(e)) ssh_dir = os.path.dirname(ssh_key_file) + if not os.path.exists(ssh_dir): if self.module.check_mode: return (0, '', '') @@ -1178,12 +1180,23 @@ def ssh_key_gen(self): os.chown(ssh_dir, info[2], info[3]) except OSError as e: return (1, '', 'Failed to create %s: %s' % (ssh_dir, to_native(e))) + if os.path.exists(ssh_key_file): if self.force: - # ssh-keygen doesn't support overwriting the key interactively, so send 'y' to confirm + self.module.warn(f'Overwriting existing ssh key private file "{ssh_key_file}"') overwrite = 'y' else: + self.module.warn(f'Found existing ssh key private file "{ssh_key_file}", no force, so skipping ssh-keygen generation') return (None, 'Key already exists, use "force: yes" to overwrite', '') + + if os.path.exists(pub_file): + if self.force: + self.module.warn(f'Overwriting existing ssh key public file "{pub_file}"') + os.unlink(pub_file) + else: + self.module.warn(f'Found existing ssh key public file "{pub_file}", no force, so skipping ssh-keygen generation') + return (None, 'Public key already exists, use "force: yes" to overwrite', '') + cmd = [self.module.get_bin_path('ssh-keygen', True)] cmd.append('-t') cmd.append(self.ssh_type) @@ -1250,7 +1263,7 @@ def ssh_key_gen(self): # If the keys were successfully created, we should be able # to tweak ownership. os.chown(ssh_key_file, info[2], info[3]) - os.chown('%s.pub' % ssh_key_file, info[2], info[3]) + os.chown(pub_file, info[2], info[3]) return (rc, out, err) def ssh_key_fingerprint(self):
test/integration/targets/user/tasks/main.yml+3 −2 modified@@ -38,7 +38,8 @@ - import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_password_lock.yml - import_tasks: test_password_lock_new_user.yml -- import_tasks: test_local.yml +- include_tasks: test_local.yml when: not (ansible_distribution == 'openSUSE Leap' and ansible_distribution_version is version('15.4', '>=')) -- import_tasks: test_umask.yml +- include_tasks: test_umask.yml when: ansible_facts.system == 'Linux' +- import_tasks: ssh_keygen.yml
test/integration/targets/user/tasks/ssh_keygen.yml+100 −0 added@@ -0,0 +1,100 @@ +- name: user generating ssh keys tests + become: true + vars: + home: "{{ (ansible_facts['os_family'] == 'Darwin')|ternary('/Users/ansibulluser/', '/home/ansibulluser/')}}" + ssh_key_file: .ssh/ansible_test_rsa + pub_file: '{{ssh_key_file}}.pub' + key_files: + - '{{ssh_key_file}}' + - '{{pub_file}}' + block: + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Test creating ssh key creation + block: + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file}}' + + - name: check files exist + stat: + path: '{{home ~ item}}' + register: stat_keys + loop: '{{ key_files }}' + + - name: ensure they exist + assert: + that: + - stat_keys.results[item].stat.exists + loop: [0, 1] + + always: + - name: Clean ssh keys + file: path={{ home ~ item }} state=absent + loop: '{{ key_files }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Ensure we don't break on conflicts + block: + - name: flag file for test + tempfile: + register: flagfile + + - name: precreate public .ssh + file: path={{home ~ '.ssh'}} state=directory + + - name: setup public key linked to flag file + file: path={{home ~ pub_file}} src={{flagfile.path}} state=link + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + ignore_errors: true + register: user_no_force + + - stat: path={{home ~ pub_file}} + register: check_pub + + - name: ensure we didn't overwrite + assert: + that: + - check_pub.stat.exists + - check_pub.stat.islnk + - check_pub.stat.uid == 0 + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + force: true + ignore_errors: true + register: user_force + + - stat: path={{home ~ pub_file}} + register: check_pub2 + + - name: ensure we failed since we didn't force overwrite + assert: + that: + - user_force is success + - check_pub2.stat.exists + - not check_pub2.stat.islnk + - check_pub2.stat.uid != 0 + always: + - name: Clean up files + file: path={{ home ~ item }} state=absent + loop: '{{ key_files + [flagfile.path] }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent
test/integration/targets/user/tasks/test_local.yml+9 −0 modified@@ -39,6 +39,15 @@ tags: - user_test_local_mode +- name: Ensure no local_ansibulluser + user: + name: local_ansibulluser + state: absent + local: yes + remove: true + tags: + - user_test_local_mode + - name: Create local_ansibulluser user: name: local_ansibulluser
3b6de811abeauser module avoid conflicts ssh pub key (#84165) (#84171)
5 files changed · +132 −5
changelogs/fragments/user_ssh_fix.yml+4 −0 added@@ -0,0 +1,4 @@ +bugfixes: + - user action will now require O(force) to overwrite the public part of an ssh key when generating ssh keys, as was already the case for the private part. +security_fixes: + - user action won't allow ssh-keygen, chown and chmod to run on existing ssh public key file, avoiding traversal on existing symlinks (CVE-2024-9902).
lib/ansible/modules/user.py+15 −2 modified@@ -1220,9 +1220,11 @@ def ssh_key_gen(self): overwrite = None try: ssh_key_file = self.get_ssh_key_path() + pub_file = f'{ssh_key_file}.pub' except Exception as e: return (1, '', to_native(e)) ssh_dir = os.path.dirname(ssh_key_file) + if not os.path.exists(ssh_dir): if self.module.check_mode: return (0, '', '') @@ -1231,12 +1233,23 @@ def ssh_key_gen(self): os.chown(ssh_dir, info[2], info[3]) except OSError as e: return (1, '', 'Failed to create %s: %s' % (ssh_dir, to_native(e))) + if os.path.exists(ssh_key_file): if self.force: - # ssh-keygen doesn't support overwriting the key interactively, so send 'y' to confirm + self.module.warn(f'Overwriting existing ssh key private file "{ssh_key_file}"') overwrite = 'y' else: + self.module.warn(f'Found existing ssh key private file "{ssh_key_file}", no force, so skipping ssh-keygen generation') return (None, 'Key already exists, use "force: yes" to overwrite', '') + + if os.path.exists(pub_file): + if self.force: + self.module.warn(f'Overwriting existing ssh key public file "{pub_file}"') + os.unlink(pub_file) + else: + self.module.warn(f'Found existing ssh key public file "{pub_file}", no force, so skipping ssh-keygen generation') + return (None, 'Public key already exists, use "force: yes" to overwrite', '') + cmd = [self.module.get_bin_path('ssh-keygen', True)] cmd.append('-t') cmd.append(self.ssh_type) @@ -1303,7 +1316,7 @@ def ssh_key_gen(self): # If the keys were successfully created, we should be able # to tweak ownership. os.chown(ssh_key_file, info[2], info[3]) - os.chown('%s.pub' % ssh_key_file, info[2], info[3]) + os.chown(pub_file, info[2], info[3]) return (rc, out, err) def ssh_key_fingerprint(self):
test/integration/targets/user/tasks/main.yml+4 −3 modified@@ -38,10 +38,11 @@ - import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_password_lock.yml - import_tasks: test_password_lock_new_user.yml -- import_tasks: test_local.yml +- include_tasks: test_local.yml when: not (ansible_distribution == 'openSUSE Leap' and ansible_distribution_version is version('15.4', '>=')) -- import_tasks: test_umask.yml +- include_tasks: test_umask.yml when: ansible_facts.system == 'Linux' - import_tasks: test_inactive_new_account.yml -- import_tasks: test_create_user_min_max.yml +- include_tasks: test_create_user_min_max.yml when: ansible_facts.system == 'Linux' +- import_tasks: ssh_keygen.yml
test/integration/targets/user/tasks/ssh_keygen.yml+100 −0 added@@ -0,0 +1,100 @@ +- name: user generating ssh keys tests + become: true + vars: + home: "{{ (ansible_facts['os_family'] == 'Darwin')|ternary('/Users/ansibulluser/', '/home/ansibulluser/')}}" + ssh_key_file: .ssh/ansible_test_rsa + pub_file: '{{ssh_key_file}}.pub' + key_files: + - '{{ssh_key_file}}' + - '{{pub_file}}' + block: + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Test creating ssh key creation + block: + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file}}' + + - name: check files exist + stat: + path: '{{home ~ item}}' + register: stat_keys + loop: '{{ key_files }}' + + - name: ensure they exist + assert: + that: + - stat_keys.results[item].stat.exists + loop: [0, 1] + + always: + - name: Clean ssh keys + file: path={{ home ~ item }} state=absent + loop: '{{ key_files }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent + + - name: Ensure we don't break on conflicts + block: + - name: flag file for test + tempfile: + register: flagfile + + - name: precreate public .ssh + file: path={{home ~ '.ssh'}} state=directory + + - name: setup public key linked to flag file + file: path={{home ~ pub_file}} src={{flagfile.path}} state=link + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + ignore_errors: true + register: user_no_force + + - stat: path={{home ~ pub_file}} + register: check_pub + + - name: ensure we didn't overwrite + assert: + that: + - check_pub.stat.exists + - check_pub.stat.islnk + - check_pub.stat.uid == 0 + + - name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + ssh_key_file: '{{ ssh_key_file }}' + force: true + ignore_errors: true + register: user_force + + - stat: path={{home ~ pub_file}} + register: check_pub2 + + - name: ensure we failed since we didn't force overwrite + assert: + that: + - user_force is success + - check_pub2.stat.exists + - not check_pub2.stat.islnk + - check_pub2.stat.uid != 0 + always: + - name: Clean up files + file: path={{ home ~ item }} state=absent + loop: '{{ key_files + [flagfile.path] }}' + + - name: Ensure clean/non existsing ansibulluser + user: name=ansibulluser state=absent
test/integration/targets/user/tasks/test_local.yml+9 −0 modified@@ -39,6 +39,15 @@ tags: - user_test_local_mode +- name: Ensure no local_ansibulluser + user: + name: local_ansibulluser + state: absent + local: yes + remove: true + tags: + - user_test_local_mode + - name: Create local_ansibulluser user: name: local_ansibulluser
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
14- github.com/advisories/GHSA-32p4-gm2c-wmchghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-9902ghsaADVISORY
- access.redhat.com/errata/RHSA-2024:10762nvdWEB
- access.redhat.com/errata/RHSA-2024:8969nvdWEB
- access.redhat.com/errata/RHSA-2024:9894nvdWEB
- access.redhat.com/errata/RHSA-2025:1861nvdWEB
- access.redhat.com/security/cve/CVE-2024-9902nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/ansible/ansible/commit/03794735d370db98a5ec2ad514fab2b0dd22d6beghsaWEB
- github.com/ansible/ansible/commit/03daf774d0d80fb7235910ed1c2b4fbcaebdfe65ghsaWEB
- github.com/ansible/ansible/commit/3b6de811abea0a811e03e3029222a7e459922892ghsaWEB
- github.com/ansible/ansible/commit/9d7312f695639e804d2caeb1d0f51c716a9ac7ddghsaWEB
- github.com/ansible/ansible/commit/f7be90626da3035c697623dcf9c90b7a0bc91c92ghsaWEB
- lists.debian.org/debian-lts-announce/2024/11/msg00021.htmlnvdWEB
News mentions
0No linked articles in our index yet.