diff --git a/README.md b/README.md index 2163a69..cd62ea6 100644 --- a/README.md +++ b/README.md @@ -30,53 +30,149 @@ Provided with this role is an example Kickstart that can be used as a start to b Role Variables -------------- | variable | default | required | description | -| :--------------------------------- | :--------------------------- | :------- | :----------------------------------------------------------------------------- | -| `api_token` | unset | true | API token to use to authenticate against the Red Hat Customer Portal | -| `boot_cat_path` | `isolinux/boot.cat` | false | relative path from `temporary_work_dir_source_files_path` to the boot.cat file | -| `checksum` | unset | true | checksum of the ISO to download | -| `cleanup_iso` | `false` | false | whether to clean up the downloaded ISO | -| `cleanup_work_dir` | `false` | false | whether to clean up the work directory defined in `temporary_work_dir_path` | -| `custom_iso_group` | `root` | false | group of the custom ISO to apply | -| `custom_iso_mode` | `0600` | false | owner of the custom ISO to apply | -| `custom_iso_owner` | `root` | false | chmod of the custom ISO to apply | -| `dest_dir_path` | `{{ playbook_dir }}` | false | destination directory for the custom ISO | -| `download_directory` | `{{ playbook_dir }}` | false | directory to store the downloaded ISO in | -| `download_directory_group` | `root` | false | group of the downloaded ISO to apply | -| `download_directory_mode` | `0600` | false | owner of the downloaded ISO to apply | -| `download_directory_owner` | `root` | false | chmod of the downloaded ISO to apply | -| `download_timeout` | `3600` | false | timeout for the download (in seconds) | -| `force_recreate_custom_iso` | `true` | false | whether to delete the custom ISO before recreating it | -| `grub_cfg_path` | `isolinux/grub.conf` | false | relative path from `temporary_work_dir_source_files_path` to the grub.conf file| -| `grub_menu_selection_timeout` | `5` | false | defines how long to wait in the GRUB menu before using the default boot option | -| `implantisomd5_package_name` | `isomd5sum` | false | package name that provides the `implantisomd5` command | -| `implant_md5` | `true` | false | whether to implant an MD5 into the custom ISO that can be checked | -| `iso_group` | `root` | false | group of the downloaded ISO to apply | -| `isolinux_bin_path` | `isolinux/isolinux.bin` | false | relative path from `temporary_work_dir_source_files_path` to isolinux.bin file | -| `iso_mode` | `0600` | false | chmod of the downloaded ISO to apply | -| `iso_owner` | `root` | false | owner of the downloaded ISO to apply | -| `kickstart_path` | unset | false | path to the kickstart file to put into the ISO | -| `kickstart_root_password` | unset | false | root password to set to in the provided kickstart | -| `ksvalidator_package_name` | `pykickstart` | false | name of the package that provides the command `ksvalidator` | -| `pxelinux_cfg_path` | `isolinux/isolinux.cfg` | false | relative path from `temporary_work_dir_source_files_path` to `isolinux.cfg` | -| `quiet_assert` | `true` | false | whether to quiet asserts | -| `redhat_portal_auth_url` | Check in `defaults/main.yml` | false | URL to the Red Hat Portal to authenticate against | -| `redhat_portal_download_base_url` | Check in `defaults/main.yml` | false | base URL for image downloading from the Red Hat Customer Portal | -| `temporary_mount_path` | `/mnt` | false | path to a temporary (empty) mount point to mount the downloaded ISO to | -| `temporary_work_dir_path` | `{{ playbook_dir }}/workdir` | false | temporary directory which will be used to extract the ISO files to | -| `temporary_work_dir_path_group` | `root` | false | group of the temporary directory to apply | -| `temporary_work_dir_path_mode` | `0755` | false | chmod of the temporary directory to apply | -| `temporary_work_dir_path_owner` | `root` | false | owner of the temporary directory to apply | -| `temporary_work_dir_source_files_path` | `src` | false | relative path from the temp dir which will contain the files added to the ISO | -| `temporary_work_dir_source_files_path_group` | `root` | false | group of `temporary_work_dir_source_files_path` to apply | -| `temporary_work_dir_source_files_path_mode` | `0755` | false | chmod of `temporary_work_dir_source_files_path` to apply | -| `temporary_work_dir_source_files_path_owner` | `root` | false | owner of `temporary_work_dir_source_files_path` to apply | -| `validate_kickstart` | `true` | false | whether to validate the provided kickstart (if provided) | +| :--------------------------------- | :--------------------------- | :------- | :----------------------------------------------------------------------------- | +| `api_token` | unset | true | API token to use to authenticate against the Red Hat Customer Portal | +| `boot_cat_path` | `isolinux/boot.cat` | false | relative path from `temporary_work_dir_source_files_path` to the boot.cat file | +| `checksum` | unset | true | checksum of the ISO to download | +| `cleanup_iso` | `false` | false | whether to clean up the downloaded ISO | +| `cleanup_work_dir` | `false` | false | whether to clean up the work directory defined in `temporary_work_dir_path` | +| `create_uefi_image` | `false` | false | whether an UEFI image should be created | +| `custom_iso_group` | `root` | false | group of the custom ISO to apply | +| `custom_iso_mode` | `0600` | false | owner of the custom ISO to apply | +| `custom_iso_owner` | `root` | false | chmod of the custom ISO to apply | +| `dest_dir_path` | `{{ playbook_dir }}` | false | destination directory for the custom ISO | +| `download_directory` | `{{ playbook_dir }}` | false | directory to store the downloaded ISO in | +| `download_directory_group` | `root` | false | group of the downloaded ISO to apply | +| `download_directory_mode` | `0600` | false | owner of the downloaded ISO to apply | +| `download_directory_owner` | `root` | false | chmod of the downloaded ISO to apply | +| `download_timeout` | `3600` | false | timeout for the download (in seconds) | +| `enable_fips` | `false` | false | whether to enable FIPS compliant cryptography | +| `force_recreate_custom_iso` | `true` | false | whether to delete the custom ISO before recreating it | +| `grub_cfg_path` | `isolinux/grub.conf` | false | relative path from `temporary_work_dir_source_files_path` to the grub.conf file| +| `grub_menu_selection_timeout` | `5` | false | defines how long to wait in the GRUB menu before using the default boot option | +| `implantisomd5_package_name` | `isomd5sum` | false | package name that provides the `implantisomd5` command | +| `implant_md5` | `true` | false | whether to implant an MD5 into the custom ISO that can be checked | +| `iso_group` | `root` | false | group of the downloaded ISO to apply | +| `isolinux_bin_path` | `isolinux/isolinux.bin` | false | relative path from `temporary_work_dir_source_files_path` to isolinux.bin file | +| `iso_mode` | `0600` | false | chmod of the downloaded ISO to apply | +| `iso_owner` | `root` | false | owner of the downloaded ISO to apply | +| `kickstart_path` | unset | false | path to the kickstart file to put into the ISO | +| `kickstart_root_password` | unset | false | root password to set to in the provided kickstart | +| `ksvalidator_package_name` | `pykickstart` | false | name of the package that provides the command `ksvalidator` | +| `post_sections` | Check in `defaults/main.yml` | false | List of post sections to include into the kickstart | +| `pxelinux_cfg_path` | `isolinux/isolinux.cfg` | false | relative path from `temporary_work_dir_source_files_path` to `isolinux.cfg` | +| `quiet_assert` | `true` | false | whether to quiet asserts | +| `redhat_portal_auth_url` | Check in `defaults/main.yml` | false | URL to the Red Hat Portal to authenticate against | +| `redhat_portal_download_base_url` | Check in `defaults/main.yml` | false | base URL for image downloading from the Red Hat Customer Portal | +| `temporary_mount_path` | `/mnt` | false | path to a temporary (empty) mount point to mount the downloaded ISO to | +| `temporary_work_dir_path` | `{{ playbook_dir }}/workdir` | false | temporary directory which will be used to extract the ISO files to | +| `temporary_work_dir_path_group` | `root` | false | group of the temporary directory to apply | +| `temporary_work_dir_path_mode` | `0755` | false | chmod of the temporary directory to apply | +| `temporary_work_dir_path_owner` | `root` | false | owner of the temporary directory to apply | +| `temporary_work_dir_source_files_path` | `src` | false | relative path from the temp dir which will contain the files added to the ISO | +| `temporary_work_dir_source_files_path_group` | `root` | false | group of `temporary_work_dir_source_files_path` to apply | +| `temporary_work_dir_source_files_path_mode` | `0755` | false | chmod of `temporary_work_dir_source_files_path` to apply | +| `temporary_work_dir_source_files_path_owner` | `root` | false | owner of `temporary_work_dir_source_files_path` to apply | +| `uefi_image_path` | `images/efiboot.img` | false | relative path from `temporary_work_dir_source_files_path` to the UEFI image | +| `users` | unset | false | list of users to create during kickstart | +| `validate_kickstart` | `true` | false | whether to validate the provided kickstart (if provided) | | `xorriso_package_name` | `xorriso` | false | name of the package that provides the command `xorriso` | +## Notes + A note on `force_recreate_custom_iso`: This variable defines whether to delete the custom ISO before recreating it. Once the custom ISO file exists, it won't be recreated, even if there are changes. That's because creating the ISO makes use of the command module and thus the operation is not idempotent, nor can it be checked whether the ISO should be recreated due to changes. +Note on `users` and `post_sections`: +The `users` variable can be used to deploy users during kickstart. If those users have one or more `authorized_keys` set, and the default value of `post_section` is kept +or extended, these users will have the set authorized keys deployed in `home` (also a variable for the `users` list) within the `.ssh/authorized_keys` file. + +Please also note, that by default, all plays in a task that could reveal a secret value have the `no_log: true` option set to ensure no sensitive data is logged anywhere. If you +debug the playbook, please comment the `no_log: true` portion of that specific task. + +## Variables `users` and `post_sections` + +An extended example of only the `users` variable is illustrated down below: +``` +users: + - name: 'ansible-user' # name of the user (required) + gid: 2000 # ID of the user group to create (with the same name as the user name) + uid: 2000 # ID of the user + gecos: 'Ansible User' # user's description/comment + create_user_group: true # whether to create a user group (with a specific gid or without) + groups: # additional groups to add the user to - these need to *exist* + - 'wheel' + shell: '/bin/bash' # login shell to use + home: '/home/remote-ansible' # home directory + privileged: true # enables creation of a sudoers.d file which grants sudo privileges without a password (created by the post section template 'post__users.j2') + lock: false # whether to lock the user + password: !vault | # the user's password. If not specified, the user account is locked by default! + $ANSIBLE_VAULT;1.1;AES256 + [..] + authorized_keys: # SSH public keys to add to the user's authorized_keys file (~/.ssh/authorized_keys) + - !vault | + $ANSIBLE_VAULT;1.1;AES256 + [..] + - !vault | + $ANSIBLE_VAULT;1.1;AES256 + [..] +``` +The only required option for a user is the `name`. Everything else can be mixed and matched. + +An example of the `post_section` (which represents the default value): +``` +post_sections: + - name: 'User creation' # will be used by Ansible as the 'beginning- and end marker' (see ansible.builtin.blockinfile) + template: 'post__users.j2' # the actual template to use + + - name: 'FIPS' + template: 'post__fips.j2' + + - name: 'Autorelabel' + template: 'post__autorelabel.j2' +``` + +It is important to understand that `%post` sections in Kickstart are evaluated top to bottom. So `User Creation` is the first `%post`-section and `Autorelabel` the last. +To extend the current default with your own `%post sections` you could specify the following in e.g. your `host_vars`: +``` +post_sections: > + {{ + _def_post_sections + + [ + { + 'name': 'Custom %post section' + 'template: 'custom_post.j2' + }, + { + 'name': 'Another %post section' + 'template: 'another_post.j2' + } + ] + }} +``` + +If you want to make it more readable and don't mind duplicating the definition of `_def_post_sections`: +``` +post_sections: + - name: 'User creation' + template: 'post__users.j2' + + - name: 'FIPS' + template: 'post__fips.j2' + + - name: 'Autorelabel' + template: 'post__autorelabel.j2' + + - name: 'Custom %post section' + template: 'custom_post.j2' + + - name: 'Another %post section' + template: 'another_post.j2' +``` + + + Depending on the value of `implant_md5`, different menu entries are selected when booting from the ISO: * When `implant_md5` is set to `false` the ISO will be booted without checking the MD5 (which would fail) * When `implant_md5` is set to `true` the ISO will be booted with `rd.live.check` which will calculate the MD5 checksum of the ISO and compare it against the implanted one @@ -95,11 +191,13 @@ Further, implanting a MD5 checksum into the custom ISO requires the package `iso Example Playbook ---------------- -Complex example +### Complex example ``` --- - hosts: 'localhost' gather_facts: false + roles: + - name: 'sscheib.rhel_iso_kickstart' vars: # the checksum of a RHEL 8.8 ISO to download from the Red Hat Customer Portal checksum: '517abcc67ee3b7212f57e180f5d30be3e8269e7a99e127a3399b7935c7e00a09' @@ -139,6 +237,44 @@ Complex example implant_md5: false implantisomd5_package_name: 'isomd5sum' quiet_assert: false + enable_fips: true + post_sections: > + {{ + _def_post_sections + + [ + { + 'name': 'Custom %post section', + 'template': 'custom_post.j2' + }, + { + 'name': 'Another %post section', + 'template': 'another_post.j2' + } + ] + }} + users: + - name: 'ansible-user' + gid: 2000 + uid: 2000 + gecos: 'Ansible User' + create_user_group: true + groups: + - 'wheel' + shell: '/bin/bash' + home: '/home/remote-ansible' + privileged: true + lock: false + password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + [..] + authorized_keys: + - !vault | + $ANSIBLE_VAULT;1.1;AES256 + [..] + + - !vault | + $ANSIBLE_VAULT;1.1;AES256 + [..] kickstart_root_password: !vault | $ANSIBLE_VAULT;1.1;AES256 [..] @@ -147,11 +283,13 @@ Complex example [..] ``` -Download ISO only +### Download ISO only ``` - hosts: 'localhost' gather_facts: false + roles: + - name: 'sscheib.rhel_iso_kickstart' vars: # the checksum of a RHEL 8.8 ISO to download from the Red Hat Customer Portal checksum: '517abcc67ee3b7212f57e180f5d30be3e8269e7a99e127a3399b7935c7e00a09' @@ -168,6 +306,44 @@ Download ISO only [..] ``` +### Download ISO and enable FIPS mode + +``` +- hosts: 'localhost' + gather_facts: false + roles: + - name: 'sscheib.rhel_iso_kickstart' + vars: + # the checksum of a RHEL 8.8 ISO to download from the Red Hat Customer Portal + checksum: '517abcc67ee3b7212f57e180f5d30be3e8269e7a99e127a3399b7935c7e00a09' + download_directory: '/home/steffen/workdir' + download_directory_owner: 'root' + download_directory_group: 'root' + download_directory_mode: '0755' + iso_owner: 'root' + iso_group: 'root' + iso_mode: '0600' + validate_kickstart: false + temporary_mount_path: '/mnt' + temporary_work_dir_path: '/home/steffen/workdir' + temporary_work_dir_path_owner: 'root' + temporary_work_dir_path_group: 'root' + temporary_work_dir_path_mode: '0755' + temporary_work_dir_source_files_path: 'src' + temporary_work_dir_source_files_path_owner: 'root' + temporary_work_dir_source_files_path_group: 'root' + temporary_work_dir_source_files_path_mode: '0755' + dest_dir_path: '/home/steffen/workdir' + custom_iso_owner: 'root' + custom_iso_group: 'root' + custom_iso_mode: '0755' + force_recreate_custom_iso: true + implant_md5: true + enable_fips: true + api_token: !vault | + $ANSIBLE_VAULT;1.1;AES256 +``` + License ------- diff --git a/defaults/main.yml b/defaults/main.yml index 0e8e389..956b7f4 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -54,6 +54,9 @@ _def_pxelinux_cfg_path: 'isolinux/isolinux.cfg' # relative path within the temporary_work_dir_source_files_path to the grub.conf file _def_grub_cfg_path: 'isolinux/grub.conf' +# relative path within temporary_work_dir_source_files_path to the UEFI grub.conf file +_def_grub_cfg_path_uefi: 'EFI/BOOT/grub.cfg' + # whether to clean up the downloaded ISO _def_cleanup_iso: false @@ -84,5 +87,22 @@ _def_implant_md5: true # package name that provides the implantisomd5 command _def_implantisomd5_package_name: 'isomd5sum' +# relative path within the temporary_work_dir_source_files_path to the UEFI image file +_def_uefi_image_path: 'images/efiboot.img' + # whether to quiet asserts _def_quiet_assert: true + +# whether to enable FIPS mode for the ISO +_def_enable_fips: false + +# post sections to insert into the kickstart file +_def_post_sections: + - name: 'User creation' + template: 'post__users.j2' + + - name: 'FIPS' + template: 'post__fips.j2' + + - name: 'Autorelabel' + template: 'post__autorelabel.j2' diff --git a/tasks/assert.yml b/tasks/assert.yml index 573b6c1..9161b42 100644 --- a/tasks/assert.yml +++ b/tasks/assert.yml @@ -7,7 +7,6 @@ - lookup('ansible.builtin.vars', item) is defined - lookup('ansible.builtin.vars', item) | bool | string == lookup('ansible.builtin.vars', item) | string - lookup('ansible.builtin.vars', item) is boolean - - lookup('ansible.builtin.vars', item) | type_debug == 'bool' success_msg: "Variable '{{ item }}' defined properly - value: '{{ lookup('ansible.builtin.vars', item) }}'" fail_msg: "Variable '{{ item }}' failed to validate" quiet: '{{ _quiet_assert }}' @@ -18,6 +17,7 @@ - '_cleanup_work_dir' - '_force_recreate_custom_iso' - '_implant_md5' + - '_enable_fips' loop_control: label: 'variable: {{ item }}' @@ -114,3 +114,153 @@ fail_msg: "Variable '_api_token' failed to validate" quiet: '{{ _quiet_assert }}' no_log: true + +- name: 'Ensure optional variables, are defined properly, if set (list)' + ansible.builtin.assert: + that: + - lookup('ansible.builtin.vars', __var) is defined + - lookup('ansible.builtin.vars', __var) | list | string == lookup('ansible.builtin.vars', __var) | string + - lookup('ansible.builtin.vars', __var) is sequence + success_msg: "Variable '{{ __var }}' defined properly" + fail_msg: "Variable '{{ __var }}' failed to validate" + quiet: '{{ _quiet_assert }}' + no_log: true + when: > + lookup('ansible.builtin.vars', __var, default='') != '' and + lookup('ansible.builtin.vars', __var, default='') | length > 0 + loop: + - '_users' + - '_post_sections' + register: '__tmp_list_variables' + loop_control: + loop_var: '__var' + label: 'variable: {{ __var }}' + +- name: 'Ensure post_sections are defined properly' + ansible.builtin.assert: + that: + - _section.name is defined + - _section.name is string + - _section.name != None + - _section.name != '' + + - _section.template is defined + - _section.template is string + - _section.template != None + - _section.template != '' + + # load the template to see if it exists + - lookup('template', _section.template) | length > 0 + + loop: '{{ _post_sections }}' + loop_control: + loop_var: '_section' + when: > + _post_sections is defined + and _post_sections | length > 0 + +- name: 'Ensure _users is defined properly' + ansible.builtin.assert: + that: + # _user.name + - _user.name is defined + - _user.name is string + - _user.name != None + - _user.name != '' + + # _user.gecos + - (_user.gecos is defined) | ternary(_user.gecos | default(None) is string, true) + - (_user.gecos is defined) | ternary(_user.gecos | default(None) != None, true) + - (_user.gecos is defined) | ternary(_user.gecos | default(None) != '', true) + + # _user.shell + - (_user.shell is defined) | ternary(_user.shell | default(None) is string, true) + - (_user.shell is defined) | ternary(_user.shell | default(None) != None, true) + - (_user.shell is defined) | ternary(_user.shell | default(None) != '', true) + + # _user.home + - (_user.home is defined) | ternary(_user.home | default(None) is string, true) + - (_user.home is defined) | ternary(_user.home | default(None) != None, true) + - (_user.home is defined) | ternary(_user.home | default(None) != '', true) + + # _user.password + - (_user.password is defined) | ternary(_user.password | default(None) is string, true) + - (_user.password is defined) | ternary(_user.password | default(None) != None, true) + - (_user.password is defined) | ternary(_user.password | default(None) != '', true) + + # _user.gid + - (_user.gid is defined) | ternary(_user.gid | default(None) | int | string == _user.gid | default(None) | string, true) + - (_user.gid is defined) | ternary(_user.gid | default(None) is number, true) + - (_user.gid is defined) | ternary(_user.gid | default(None) is integer, true) + - (_user.gid is defined) | ternary(_user.gid | default(None) >= 1, true) + + # _user.uid + - (_user.uid is defined) | ternary(_user.uid | default(None) | int | string == _user.uid | default(None) | string, true) + - (_user.uid is defined) | ternary(_user.uid | default(None) is number, true) + - (_user.uid is defined) | ternary(_user.uid | default(None) is integer, true) + - (_user.uid is defined) | ternary(_user.uid | default(None) >= 1, true) + + # _user.create_user_group + - (_user.create_user_group is defined) | ternary(_user.create_user_group | default(None) | bool | string == _user.create_user_group | default(None) | string, true) + - (_user.create_user_group is defined) | ternary(_user.create_user_group | default(None) is boolean, true) + + # _user.lock + - (_user.lock is defined) | ternary(_user.lock | default(None) | bool | string == _user.lock | default(None) | string, true) + - (_user.lock is defined) | ternary(_user.lock | default(None) is boolean, true) + + # _user.privileged + - (_user.privileged is defined) | ternary(_user.privileged | default(None) | bool | string == _user.privileged | default(None) | string, true) + - (_user.privileged is defined) | ternary(_user.privileged | default(None) is boolean, true) + + # _user.groups + - (_user.groups is defined) | ternary(_user.groups | default([]) | list | string == _user.groups | default(None) | string, true) + - (_user.groups is defined) | ternary(_user.groups | default([]) is sequence, true) + + # _user.authorized_keys + - (_user.authorized_keys is defined) | ternary(_user.authorized_keys | default([]) | list | string == _user.authorized_keys | default(None) | string, true) + - (_user.authorized_keys is defined) | ternary(_user.authorized_keys | default([]) is sequence, true) + success_msg: 'Users are defined correctly' + fail_msg: 'One or more users failed to validated correctly' + no_log: true + loop: '{{ _users }}' + loop_control: + loop_var: '_user' + when: > + _users is defined + and _users | length > 0 + +- name: 'Ensure _users does not specify conflicting options' + ansible.builtin.assert: + that: + - _user.home is defined + - _user.home is string + - _user.home != '' + - _user.home != None + success_msg: 'No conflicting options specified' + fail_msg: 'A user needs a home directory when authorized keys are specified' + no_log: true + loop: '{{ _users }}' + loop_control: + loop_var: '_user' + when: > + _user.authorized_keys is defined + and _user.authorized_keys | length > 0 + +- name: 'Skip block if no variables defined beforehand' + when: > + __tmp_list_variables.results is defined and + __tmp_list_variables.results | map(attribute='skipped', default=[]) | select() | length > 0 + block: + + - name: 'Show variables that have been skipped to check, due to being undefined' + ansible.builtin.debug: + msg: 'Variable name: {{ __var }}' + loop: + - "{{ __tmp_list_variables['results'] | default([]) }}" + loop_control: + loop_var: '__var' + label: '{{ __var }}' + + - name: 'Ensure above variables are not important to you, as they are not going to be used!' + ansible.builtin.pause: + seconds: 5 diff --git a/tasks/build_user_statement.yml b/tasks/build_user_statement.yml new file mode 100644 index 0000000..f4c1ace --- /dev/null +++ b/tasks/build_user_statement.yml @@ -0,0 +1,74 @@ +--- +- name: 'Set fact: Start building the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ 'user --name=' ~ _user.name }}" + +- name: 'Set fact: Insert gecos into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --gecos=\"' ~ _user.gecos ~ '\"' }}" + when: > + _user.gecos is defined + and _user.gecos != '' + and _user.gecos != None + +- name: 'Set fact: Insert uid into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --uid=' ~ _user.uid }}" + when: > + _user.uid is defined + and _user.uid | string != '' + +- name: 'Set fact: Insert gid into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --gid=' ~ _user.gid }}" + when: > + _user.gid is defined + and _user.gid | string != '' + +- name: 'Set fact: Insert groups into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --groups=' ~ _user.groups | join(',') }}" + when: > + _user.groups is defined + and _user.groups | string != '' + +- name: 'Set fact: Insert homedir into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --homedir=' ~ _user.home }}" + when: > + _user.home is defined + and _user.home | string != '' + +- name: 'Set fact: Insert shell into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --shell=' ~ _user.shell }}" + when: > + _user.shell is defined + and _user.shell | string != '' + +- name: 'Set fact: Insert lock into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: "{{ _user_statement ~ ' --lock' }}" + when: > + _user.lock is defined + and _user.lock + +- name: 'Set fact: Insert password into the user statement for user {{ _user.name }}' + ansible.builtin.set_fact: + _user_statement: > + {{ + _user_statement ~ ' --iscrypted --password=' ~ + _user.password | string | ansible.builtin.password_hash(hashtype='sha512') + }} + no_log: true + when: > + _user.gid is defined + and _user.gid | string != '' + +- name: 'Insert user creation statement into the provided kickstart for user {{ _user.name }}' + ansible.builtin.lineinfile: + path: '{{ __work_dir_kickstart_path }}' + regex: '^user\s--name={{ _user.name }}.+$' + line: '{{ _user_statement }}' + no_log: true + become: true diff --git a/tasks/create_iso.yml b/tasks/create_iso.yml index 8627e80..56f9c64 100644 --- a/tasks/create_iso.yml +++ b/tasks/create_iso.yml @@ -1,7 +1,7 @@ --- - name: 'Ensure xorriso is present' ansible.builtin.package: - name: '{{ _xorriso_package_name }}' + name: '{{ _xorriso_package_name }},syslinux' state: 'present' become: true @@ -29,6 +29,9 @@ ansible.builtin.set_fact: __iso_label: '{{ __t_label.stdout }}' +# Note: Adding '-no-emul-boot' *twice* is necessary to avoid the following issue: +# https://unix.stackexchange.com/questions/491043/boot-grub-efi-img-invalid-image-size +# Command has been built according to https://access.redhat.com/solutions/60959 - name: 'Create the ISO with the included kickstart at: {{ __dest_iso_path }}' ansible.builtin.command: argv: @@ -37,27 +40,35 @@ - '{{ __dest_iso_path }}' - '-b' - '{{ _isolinux_bin_path }}' - - '-c' - - '{{ _boot_cat_path }}' - '-J' - '-R' - '-l' - - '-v' + - '-c' + - '{{ _boot_cat_path }}' - '-no-emul-boot' - '-boot-load-size' - '4' - '-boot-info-table' - '-eltorito-alt-boot' + - '-e' + - '{{ _uefi_image_path }}' + - '-no-emul-boot' - '-graft-points' - '-joliet-long' - '-V' - '{{ __iso_label }}' - - '-volid' - - '{{ __iso_label }}' - '{{ __src_files_path }}' creates: '{{ __dest_iso_path }}' become: true +- name: 'Ensure ISO is bootable via BIOS and UEFI' # noqa: no-changed-when + ansible.builtin.command: + argv: + - 'isohybrid' + - '--uefi' + - '{{ __dest_iso_path }}' + become: true + - name: 'Block: Handle implanting of MD5 into ISO' become: true when: > diff --git a/tasks/download_iso.yml b/tasks/download_iso.yml index eccc6d9..8a711ca 100644 --- a/tasks/download_iso.yml +++ b/tasks/download_iso.yml @@ -66,3 +66,4 @@ mode: '{{ _iso_mode }}' checksum: 'sha256:{{ _checksum }}' timeout: '{{ _download_timeout }}' + become: true diff --git a/tasks/fips.yml b/tasks/fips.yml new file mode 100644 index 0000000..4a100f1 --- /dev/null +++ b/tasks/fips.yml @@ -0,0 +1,29 @@ +--- +- name: 'Update kernel parameters to enable FIPS compliant cryptography (MD5 implanting requested)' + ansible.builtin.lineinfile: + path: '{{ _cfg_path }}' + regexp: '^(.+)(hd:LABEL[A-z0-9_=-]+)(\sinst\.ks=\2:/ks\.cfg)?(\srd.live.check\squiet)(\sfips=1)?$' + line: '\1\2 inst.ks=\2:/ks.cfg\4 fips=1' + backrefs: true + become: true + loop: + - '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' # BIOS + - '{{ __src_files_path }}/{{ _grub_cfg_path_uefi }}' # UEFI + loop_control: + loop_var: '_cfg_path' + when: > + _implant_md5 is defined + and _implant_md5 + +- name: 'BIOS/UEFI: Update kernel parameters to enable FIPS compliant cryptography' + ansible.builtin.lineinfile: + path: '{{ _cfg_path }}' + regexp: '^(.+)(hd:LABEL[A-z0-9_=-]+)(\sinst\.ks=\2:/ks\.cfg)?(\squiet)(\sfips=1)?$' + line: '\1\2 inst.ks=\2:/ks.cfg\4 fips=1' + backrefs: true + become: true + loop: + - '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' # BIOS + - '{{ __src_files_path }}/{{ _grub_cfg_path_uefi }}' # UEFI + loop_control: + loop_var: '_cfg_path' diff --git a/tasks/kickstart.yml b/tasks/kickstart.yml index ab0fae8..a8684ad 100644 --- a/tasks/kickstart.yml +++ b/tasks/kickstart.yml @@ -26,8 +26,48 @@ path: '{{ __work_dir_kickstart_path }}' regex: '^rootpw.*$' line: "rootpw --iscrypted {{ _kickstart_root_password | string | ansible.builtin.password_hash(hashtype='sha512') }}" + no_log: true become: true +- name: 'Insert group creations into the provided kickstart' + ansible.builtin.lineinfile: + path: '{{ __work_dir_kickstart_path }}' + regex: '^group\s--name={{ _group.name }}.*$' + line: >- + {{ + 'group --name=' ~ _group.name ~ + ' --gid=' ~ _group.gid if _group.gid is defined else '' + }} + become: true + no_log: true + when: > + _group.create_user_group is defined + and _group.create_user_group + loop: '{{ _users }}' + loop_control: + loop_var: '_group' + +- name: 'Include tasks to insert user statements into the provided kickstart' + ansible.builtin.include_tasks: + file: 'build_user_statement.yml' + vars: + _user: '{{ _usr }}' + no_log: true + loop: '{{ _users }}' + loop_control: + loop_var: '_usr' + +- name: 'Insert post sections' + ansible.builtin.blockinfile: + path: '{{ __work_dir_kickstart_path }}' + block: "{{ lookup('template', _post.template) }}" + marker_begin: "{{ 'BEGIN: ' ~ _post.name }}" + marker_end: "{{ 'END: ' ~ _post.name }}" + become: true + loop: '{{ _post_sections }}' + loop_control: + loop_var: '_post' + - name: 'Handle validation of kickstart' when: > _validate_kickstart is defined @@ -68,46 +108,70 @@ or not _implant_md5 block: - - name: 'Update kernel parameters to include the kickstart file (MD5 implanting not requested)' - ansible.builtin.lineinfile: - path: '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' - regexp: '^(.+)(hd:LABEL[A-z0-9_=-]+)(\sinst\.ks=\2:/ks\.cfg)?(\squiet)$' - line: '\1\2 inst.ks=\2:/ks.cfg\4' - backrefs: true - - - name: 'Remove default menu entry to test the media' + - name: 'BIOS: Remove default menu entry to test the media' ansible.builtin.replace: path: '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' after: '\s+Test this' regexp: '^\s+menu\sdefault\n' replace: '' - - name: 'Add default menu entry to install the media' + - name: 'BIOS: Add default menu entry to install the media' ansible.builtin.lineinfile: path: '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' insertafter: '\s+menu\slabel\s\^Install' line: ' menu default' - - name: 'Set the default to menu entry 0' + - name: 'BIOS/UEFI: Set the default to menu entry 0' ansible.builtin.lineinfile: - path: '{{ __src_files_path }}/{{ _grub_cfg_path }}' + path: '{{ _cfg_path }}' regexp: '^default=\d' line: 'default=0' + loop: + - '{{ __src_files_path }}/{{ _grub_cfg_path }}' # BIOS + - '{{ __src_files_path }}/{{ _grub_cfg_path_uefi }}' # UEFI + loop_control: + loop_var: '_cfg_path' - name: 'Update kernel parameters to include the kickstart file (MD5 implanting requested)' ansible.builtin.lineinfile: - path: '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' + path: '{{ _cfg_path }}' regexp: '^(.+)(hd:LABEL[A-z0-9_=-]+)(\sinst\.ks=\2:/ks\.cfg)?(\srd.live.check\squiet)$' line: '\1\2 inst.ks=\2:/ks.cfg\4' backrefs: true become: true + loop: + - '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' # BIOS + - '{{ __src_files_path }}/{{ _grub_cfg_path_uefi }}' # UEFI + loop_control: + loop_var: '_cfg_path' when: > _implant_md5 is defined and _implant_md5 -- name: 'Set the timeout for the selection within GRUB to {{ _grub_menu_selection_timeout }}' +- name: 'BIOS/UEFI: Update kernel parameters to include the kickstart file' + ansible.builtin.lineinfile: + path: '{{ _cfg_path }}' + regexp: '^(.+)(hd:LABEL[A-z0-9_=-]+)(\sinst\.ks=\2:/ks\.cfg)?(\squiet)$' + line: '\1\2 inst.ks=\2:/ks.cfg\4' + backrefs: true + become: true + loop: + - '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' # BIOS + - '{{ __src_files_path }}/{{ _grub_cfg_path_uefi }}' # UEFI + loop_control: + loop_var: '_cfg_path' + + +- name: 'BIOS: Set the timeout for the selection within GRUB to {{ _grub_menu_selection_timeout }}' ansible.builtin.lineinfile: path: '{{ __src_files_path }}/{{ _pxelinux_cfg_path }}' regexp: '^timeout\s\d+' line: 'timeout {{ _grub_menu_selection_timeout | int * 10 }}' # we need the time in milliseconds become: true + +- name: 'UEFI: Set the timeout for the selection within GRUB to {{ _grub_menu_selection_timeout }}' + ansible.builtin.lineinfile: + path: '{{ __src_files_path }}/{{ _grub_cfg_path_uefi }}' + regexp: '^set\stimeout=\d+' + line: 'set timeout={{ _grub_menu_selection_timeout }}' + become: true diff --git a/tasks/main.yml b/tasks/main.yml index 99f751f..4fad268 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -11,6 +11,21 @@ ansible.builtin.include_tasks: file: 'download_iso.yml' +- name: 'Include tasks to mount the downloaded ISO and extract its contents' + ansible.builtin.include_tasks: + file: 'extract_files.yml' + when: > + ( + _kickstart_path is defined + and _kickstart_path != '' + and _kickstart_path != None + ) + or + ( + _enable_fips is defined + and _enable_fips + ) + - name: 'Handle including tasks when kickstart file is given' when: > _kickstart_path is defined @@ -18,13 +33,30 @@ and _kickstart_path != None block: - - name: 'Include tasks to mount the downloaded ISO and extract its contents' + - name: 'Include tasks to process the provided kickstart file and add it to the ISO' ansible.builtin.include_tasks: - file: 'extract_files.yml' + file: 'kickstart.yml' - - name: 'Include tasks to update process the provided kickstart file' +- name: 'Handle including tasks when FIPS is required and/or kickstart file is given' + when: > + ( + _kickstart_path is defined + and _kickstart_path != '' + and _kickstart_path != None + ) + or + ( + _enable_fips is defined + and _enable_fips + ) + block: + + - name: 'Include tasks to enable FIPS mode' ansible.builtin.include_tasks: - file: 'kickstart.yml' + file: 'fips.yml' + when: > + _enable_fips is defined + and _enable_fips - name: 'Include tasks to create the ISO with the kickstart included' ansible.builtin.include_tasks: diff --git a/templates/post__autorelabel.j2 b/templates/post__autorelabel.j2 new file mode 100644 index 0000000..26ecce7 --- /dev/null +++ b/templates/post__autorelabel.j2 @@ -0,0 +1,7 @@ +%post --interpreter=/bin/bash --log=/tmp/post_autorelabel.log --erroronfail +logger "Telling the system to relabel SELinux contexts upon restart" +touch "/.autorelabel" + +# tell the system to write all data to disk (if there is any unwritten data) +sync +%end diff --git a/templates/post__fips.j2 b/templates/post__fips.j2 new file mode 100644 index 0000000..4ae4589 --- /dev/null +++ b/templates/post__fips.j2 @@ -0,0 +1,7 @@ +%post --interpreter=/bin/bash --log=/tmp/post_fips.log --erroronfail +logger "Starting anaconda postinstall to enable FIPS" + +{% if _enable_fips is defined and _enable_fips %} +fips-mode-setup --enable +{% endif %}{# if _enable_fips is defined and _enable_fips #} +%end diff --git a/templates/post__users.j2 b/templates/post__users.j2 new file mode 100644 index 0000000..6f14cde --- /dev/null +++ b/templates/post__users.j2 @@ -0,0 +1,34 @@ +%post --interpreter=/bin/bash --log=/tmp/post_authorized_keys.log --erroronfail +logger "Starting anaconda postinstall to add authorized keys to users" + +{% if _users is defined and _users | length > 0 %} +{% for _user in _users %} +logger "Creating user {{ _user.name }}" +{% if _user.home is defined and _user.authorized_keys is defined and _user.authorized_keys | length > 0 %} + +# Create SSH directory with appropriate permissions +mkdir -p {{ _user.home }}/.ssh +chmod 0700 {{ _user.home }}/.ssh +chown {{ _user.name}}: {{ _user.home }}/.ssh + +# Add authorized keys +{% for _key in _user.authorized_keys %} +echo "{{ _key }}" >> {{ _user.home }}/.ssh/authorized_keys +{% endfor %} + +# Ensure correct permissions and owner on the authorized_keys file +chown {{ _user.name}}: {{ _user.home }}/.ssh/authorized_keys +chmod 0600 {{ _user.home }}/.ssh/authorized_keys + +# Restore SELinux context +restorecon -R {{ _user.home }}/.ssh +{% endif %}{# if _user.home is defined #} +{% if _user.privileged is defined and _user.privileged %} +# Allow sudo commands without password +cat <<- EOF >> {{ _user.sudoers_file | default('/etc/sudoers.d/' ~ _user.name | replace('-', '_')) }} +{{ _user.name }} ALL=(ALL) NOPASSWD: ALL +EOF +{% endif %}{# if _user.privileged is defined and _user.privileged #} +{% endfor %}{# for _user in _users #} +{% endif %}{# if _users is defined and _users | length > 0 #} +%end diff --git a/vars/main.yml b/vars/main.yml index 8f7ca89..5455d86 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -89,6 +89,9 @@ _force_recreate_custom_iso: '{{ force_recreate_custom_iso | default(_def_force_r # relative path within the temporary_work_dir_source_files_path to the grub.conf file _grub_cfg_path: '{{ grub_cfg_path | default(_def_grub_cfg_path) }}' +# relative path within temporary_work_dir_source_files_path to the UEFI grub.conf file +_grub_cfg_path_uefi: '{{ grub_cfg_path_uefi | default(_def_grub_cfg_path_uefi) }}' + # defines how long to wait in the GRUB menu before using the default boot option _grub_menu_selection_timeout: '{{ grub_menu_selection_timeout | default(_def_grub_menu_selection_timeout) }}' @@ -104,5 +107,17 @@ _quiet_assert: '{{ quiet_assert | default(_def_quiet_assert) }}' # root password to set to in the provided kickstart _kickstart_root_password: '{{ kickstart_root_password | default(undef()) }}' +# relative path within the temporary_work_dir_source_files_path to the UEFI image file +_uefi_image_path: '{{ uefi_image_path | default(_def_uefi_image_path) }}' + # path to the kickstart file to put into the ISO _kickstart_path: '{{ kickstart_path | default(undef()) }}' + +# whether to enable FIPS mode for the ISO +_enable_fips: '{{ enable_fips | default(_def_enable_fips) }}' + +# users to create during kickstart installation +_users: '{{ users | default(undef()) }}' + +# post sections to insert into the kickstart file +_post_sections: '{{ post_sections | default(_def_post_sections) }}'