VYPR
Medium severity6.3NVD Advisory· Published Nov 6, 2024· Updated Apr 15, 2026

CVE-2024-9902

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.

PackageAffected versionsPatched versions
ansible-corePyPI
< 2.14.18rc12.14.18rc1
ansible-corePyPI
>= 2.15.0b1, < 2.15.13rc12.15.13rc1
ansible-corePyPI
>= 2.16.0b1, < 2.16.13rc12.16.13rc1
ansible-corePyPI
>= 2.17.0b1, < 2.17.6rc12.17.6rc1
ansible-corePyPI
>= 2.18.0b1, < 2.18.0rc22.18.0rc2

Patches

5
03daf774d0d8

[stable-2.14] user action, fix ssh-keygen issues (#84167)

https://github.com/ansible/ansibleBrian CocaOct 28, 2024via ghsa
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)

https://github.com/ansible/ansibleBrian CocaOct 28, 2024via ghsa
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)

https://github.com/ansible/ansibleBrian CocaOct 28, 2024via ghsa
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)

https://github.com/ansible/ansibleBrian CocaOct 28, 2024via ghsa
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
    
3b6de811abea

user module avoid conflicts ssh pub key (#84165) (#84171)

https://github.com/ansible/ansibleBrian CocaOct 28, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.