diff --git a/.ostree/README.md b/.ostree/README.md new file mode 100644 index 0000000..f5e6931 --- /dev/null +++ b/.ostree/README.md @@ -0,0 +1,3 @@ +*NOTE*: The `*.txt` files are used by `get_ostree_data.sh` to create the lists +of packages, and to find other system roles used by this role. DO NOT use them +directly. diff --git a/.ostree/get_ostree_data.sh b/.ostree/get_ostree_data.sh new file mode 100755 index 0000000..cec08b0 --- /dev/null +++ b/.ostree/get_ostree_data.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +set -euo pipefail + +ostree_dir="${OSTREE_DIR:-"$(dirname "$(realpath "$0")")"}" + +if [ -z "${4:-}" ] || [ "${1:-}" = help ] || [ "${1:-}" = -h ]; then + cat <&2 echo ERROR - could not find role "$role" - please use ANSIBLE_COLLECTIONS_PATH + exit 2 +} + +get_packages() { + local ostree_dir pkgtype pkgfile rolefile + ostree_dir="$1" + for pkgtype in "${pkgtypes[@]}"; do + for suff in "" "-$distro" "-${distro}-${major_ver}" "-${distro}-${ver}"; do + pkgfile="$ostree_dir/packages-${pkgtype}${suff}.txt" + if [ -f "$pkgfile" ]; then + cat "$pkgfile" + fi + done + rolefile="$ostree_dir/roles-${pkgtype}.txt" + if [ -f "$rolefile" ]; then + local roles role rolepath + roles="$(cat "$rolefile")" + for role in $roles; do + rolepath="$(get_rolepath "$ostree_dir" "$role")" + if [ -z "$rolepath" ]; then + 1>&2 echo ERROR - could not find role "$role" - please use ANSIBLE_COLLECTIONS_PATH + exit 2 + fi + get_packages "$rolepath" + done + fi + done | sort -u +} + +format_packages_json() { + local comma pkgs pkg + comma="" + pkgs="[" + while read -r pkg; do + pkgs="${pkgs}${comma}\"${pkg}\"" + comma=, + done + pkgs="${pkgs}]" + echo "$pkgs" +} + +format_packages_raw() { + cat +} + +format_packages_yaml() { + while read -r pkg; do + echo "- $pkg" + done +} + +format_packages_toml() { + while read -r pkg; do + echo "[[packages]]" + echo "name = \"$pkg\"" + echo "version = \"*\"" + done +} + +distro="${distro_ver%%-*}" +ver="${distro_ver##*-}" +if [[ "$ver" =~ ^([0-9]*) ]]; then + major_ver="${BASH_REMATCH[1]}" +else + echo ERROR: cannot parse major version number from version "$ver" + exit 1 +fi + +"get_$category" "$ostree_dir" | "format_${category}_$format" diff --git a/.ostree/packages-runtime.txt b/.ostree/packages-runtime.txt new file mode 100644 index 0000000..55ab8d1 --- /dev/null +++ b/.ostree/packages-runtime.txt @@ -0,0 +1,2 @@ +fapolicyd +fapolicyd-selinux diff --git a/.ostree/packages-testing.txt b/.ostree/packages-testing.txt new file mode 100644 index 0000000..573257f --- /dev/null +++ b/.ostree/packages-testing.txt @@ -0,0 +1,2 @@ +gcc +glibc-devel diff --git a/.sanity-ansible-ignore-2.12.txt b/.sanity-ansible-ignore-2.12.txt new file mode 100644 index 0000000..680d766 --- /dev/null +++ b/.sanity-ansible-ignore-2.12.txt @@ -0,0 +1 @@ +roles/fapolicyd/.ostree/get_ostree_data.sh shebang!skip diff --git a/.sanity-ansible-ignore-2.13.txt b/.sanity-ansible-ignore-2.13.txt new file mode 100644 index 0000000..680d766 --- /dev/null +++ b/.sanity-ansible-ignore-2.13.txt @@ -0,0 +1 @@ +roles/fapolicyd/.ostree/get_ostree_data.sh shebang!skip diff --git a/.sanity-ansible-ignore-2.14.txt b/.sanity-ansible-ignore-2.14.txt new file mode 100644 index 0000000..680d766 --- /dev/null +++ b/.sanity-ansible-ignore-2.14.txt @@ -0,0 +1 @@ +roles/fapolicyd/.ostree/get_ostree_data.sh shebang!skip diff --git a/.sanity-ansible-ignore-2.15.txt b/.sanity-ansible-ignore-2.15.txt new file mode 100644 index 0000000..680d766 --- /dev/null +++ b/.sanity-ansible-ignore-2.15.txt @@ -0,0 +1 @@ +roles/fapolicyd/.ostree/get_ostree_data.sh shebang!skip diff --git a/.sanity-ansible-ignore-2.9.txt b/.sanity-ansible-ignore-2.9.txt new file mode 100644 index 0000000..680d766 --- /dev/null +++ b/.sanity-ansible-ignore-2.9.txt @@ -0,0 +1 @@ +roles/fapolicyd/.ostree/get_ostree_data.sh shebang!skip diff --git a/README-ostree.md b/README-ostree.md new file mode 100644 index 0000000..a9f0185 --- /dev/null +++ b/README-ostree.md @@ -0,0 +1,66 @@ +# rpm-ostree + +The role supports running on [rpm-ostree](https://coreos.github.io/rpm-ostree/) +systems. The primary issue is that the `/usr` filesystem is read-only, and the +role cannot install packages. Instead, it will just verify that the necessary +packages and any other `/usr` files are pre-installed. The role will change the +package manager to one that is compatible with `rpm-ostree` systems. + +## Building + +To build an ostree image for a particular operating system distribution and +version, use the script `.ostree/get_ostree_data.sh` to get the list of +packages. If the role uses other system roles, then the script will include the +packages for the other roles in the list it outputs. The list of packages will +be sorted in alphanumeric order. + +Usage: + +```bash +.ostree/get_ostree_data.sh packages runtime DISTRO-VERSION FORMAT +``` + +`DISTRO-VERSION` is in the format that Ansible uses for `ansible_distribution` +and `ansible_distribution_version` - for example, `Fedora-38`, `CentOS-8`, +`RedHat-9.4` + +`FORMAT` is one of `toml`, `json`, `yaml`, `raw` + +* `toml` - each package in a TOML `[[packages]]` element + +```toml +[[packages]] +name = "package-a" +version = "*" +[[packages]] +name = "package-b" +version = "*" +... +``` + +* `yaml` - a YAML list of packages + +```yaml +- package-a +- package-b +... +``` + +* `json` - a JSON list of packages + +```json +["package-a","package-b",...] +``` + +* `raw` - a plain text list of packages, one per line + +```bash +package-a +package-b +... +``` + +What format you choose depends on which image builder you are using. For +example, if you are using something based on +[osbuild-composer](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/composing_installing_and_managing_rhel_for_edge_images/index#creating-an-image-builder-blueprint-for-a-rhel-for-edge-image-using-the-command-line-interface_composing-a-rhel-for-edge-image-using-image-builder-command-line), +you will probably want to use the `toml` output format. diff --git a/README.md b/README.md index 00ef27e..9414d81 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Fapolicyd -Fapolicyd System Role +Fapolicyd System Role ## Requirements @@ -8,7 +8,13 @@ This role is only supported on RHEL8.1+/CentOS8.1+ and Fedora distributions. Con ### Collection requirements -None. +If you want to manage `rpm-ostree` systems with this role, you will need to +install additional collections. Please run the following command line to +install the collection. + +```bash +ansible-galaxy collection install -vv -r meta/collection-requirements.yml +``` ## Role Variables @@ -53,7 +59,9 @@ Default `[]` - it can take list of files that will be marked as trusted. - fapolicyd ``` +## rpm-ostree +See README-ostree.md ## License diff --git a/defaults/main.yml b/defaults/main.yml index b2f18c6..0d1fe12 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -16,7 +16,7 @@ fapolicyd_setup_trust: "{{ '' if ansible_facts.distribution_version is # can be "none", "size", "sha256", "ima" # in case of ima, kernel's IMA has to be setup correctly fapolicyd_setup_integrity: "{{ '' if ansible_facts.distribution_version is - version('8.3', '<=') else 'none' }}" + version('8.3', '<=') else 'none' }}" # set permissive mode fapolicyd_setup_permissive: false @@ -25,4 +25,4 @@ fapolicyd_setup_permissive: false # list of trusted files fapolicyd_add_trusted_file: "{{ '' if ansible_facts.distribution_version is - version('8.2', '<=') else [] }}" + version('8.2', '<=') else [] }}" diff --git a/handlers/main.yml b/handlers/main.yml deleted file mode 100644 index 89b1ac0..0000000 --- a/handlers/main.yml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: MIT ---- -- name: Handler for fapolicyd to restart services - service: - name: "{{ item }}" - state: restarted - loop: "{{ __fapolicyd_services }}" diff --git a/meta/collection-requirements.yml b/meta/collection-requirements.yml new file mode 100644 index 0000000..0110bbe --- /dev/null +++ b/meta/collection-requirements.yml @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +--- +collections: + - ansible.posix diff --git a/tasks/main.yml b/tasks/main.yml index b3abe61..1432222 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,18 +1,13 @@ --- -- name: Gather needed facts - setup: - gather_subset: - - '!all' - - '!min' - - distribution_major_version - when: ansible_facts.distribution_major_version is not defined +- name: Set platform/version specific variables + include_tasks: set_vars.yml - name: System check fail: msg: - - Only Red Hat Enterprise Linux >= 8.1 and Fedora is supported - - System: "{{ ansible_facts.os_family }}" - - Version: "{{ ansible_facts.distribution_version }}" + - Only Enterprise Linux >= 8.1 and Fedora are supported + - System - {{ ansible_facts.os_family }} + - Version - {{ ansible_facts.distribution_version }} when: (ansible_facts.os_family != "RedHat") or (ansible_facts.distribution_version is version("8.1", "<")) @@ -52,20 +47,22 @@ - name: Check failed conditions fail: msg: Multiple failed conditions - when: __failed_check_trust is failed or __failed_check_integrity is failed or + when: __failed_check_trust is failed or __failed_check_integrity is failed or __failed_check_trusted_file is failed - name: Install fapolicyd packages package: - name: - - "{{ __fapolicyd_packages }}" + name: "{{ __fapolicyd_packages }}" state: present + use: "{{ (__fapolicyd_is_ostree | d(false)) | + ternary('ansible.posix.rhel_rpm_ostree', omit) }}" - name: Install fapolicyd-selinux packages package: - name: - - "{{ __fapolicyd_selinux_packages }}" + name: "{{ __fapolicyd_selinux_packages }}" state: present + use: "{{ (__fapolicyd_is_ostree | d(false)) | + ternary('ansible.posix.rhel_rpm_ostree', omit) }}" when: ansible_facts.distribution_version is version("8.3", ">=") - name: Copy fapolicyd configuration file @@ -74,13 +71,16 @@ dest: "{{ __fapolicyd_dir }}/{{ __fapolicyd_conf }}" owner: root group: fapolicyd - mode: '0644' + mode: "0644" + register: __fapolicy_conf - name: Run fapolicyd configuration check command: fapolicyd-cli --check-config check_mode: false changed_when: false - when: ansible_facts.distribution_version is version("8.6", ">=") + when: + - ansible_facts.distribution_version is version("8.6", ">=") + - __fapolicy_conf is changed - name: Trustdb cleanup command: fapolicyd-cli --file delete / @@ -102,14 +102,16 @@ name: "{{ __fapolicyd_services }}" state: restarted enabled: true + when: fapolicyd_setup_enable_service | bool ignore_errors: true register: __fapolicyd_restart - name: Check fapolicyd logs - command: journalctl -n5 -u "{{ __fapolicyd_services }}" + command: journalctl -n5 -u {{ __fapolicyd_services | quote }} register: __fapolicyd_results changed_when: false when: __fapolicyd_restart is failed + failed_when: __fapolicyd_restart is failed - name: Making sure fapolicyd does not run if it was set so service: @@ -117,11 +119,3 @@ state: stopped enabled: false when: not fapolicyd_setup_enable_service - -- name: Print fapolicyd logs - debug: - msg: "{{ __fapolicyd_results.stdout_lines }}" - failed_when: true - when: - - __fapolicyd_restart is failed - - __fapolicyd_results.stdout_lines is defined diff --git a/tasks/set_vars.yml b/tasks/set_vars.yml new file mode 100644 index 0000000..a57891f --- /dev/null +++ b/tasks/set_vars.yml @@ -0,0 +1,33 @@ +--- +- name: Ensure ansible_facts used by role + setup: + gather_subset: "{{ __fapolicyd_required_facts_subsets }}" + when: __fapolicyd_required_facts | + difference(ansible_facts.keys() | list) | length > 0 + +- name: Determine if system is ostree and set flag + when: not __fapolicyd_is_ostree is defined + block: + - name: Check if system is ostree + stat: + path: /run/ostree-booted + register: __ostree_booted_stat + + - name: Set flag to indicate system is ostree + set_fact: + __fapolicyd_is_ostree: "{{ __ostree_booted_stat.stat.exists }}" + +- name: Set platform/version specific variables + include_vars: "{{ __vars_file }}" + loop: + - "{{ ansible_facts['os_family'] }}.yml" + - "{{ ansible_facts['distribution'] }}.yml" + - >- + {{ ansible_facts['distribution'] ~ '_' ~ + ansible_facts['distribution_major_version'] }}.yml + - >- + {{ ansible_facts['distribution'] ~ '_' ~ + ansible_facts['distribution_version'] }}.yml + vars: + __vars_file: "{{ role_path }}/vars/{{ item }}" + when: __vars_file is file diff --git a/tests/roles/linux-system-roles.fapolicyd/handlers b/tests/roles/linux-system-roles.fapolicyd/handlers deleted file mode 120000 index 3b11237..0000000 --- a/tests/roles/linux-system-roles.fapolicyd/handlers +++ /dev/null @@ -1 +0,0 @@ -../../../handlers \ No newline at end of file diff --git a/tests/tests_default.yml b/tests/tests_default.yml index 10e2606..fcbcdc7 100644 --- a/tests/tests_default.yml +++ b/tests/tests_default.yml @@ -10,4 +10,4 @@ include_tasks: tasks/check_header.yml vars: __file: "{{ __fapolicyd_dir }}/{{ __fapolicyd_conf }}" - __fingerprint: system_role:template + __fingerprint: "system_role:fapolicyd" diff --git a/tests/tests_trusted_execution.yml b/tests/tests_trusted_execution.yml index affa47f..072f953 100644 --- a/tests/tests_trusted_execution.yml +++ b/tests/tests_trusted_execution.yml @@ -1,129 +1,166 @@ --- - name: Basic test for fapolicyd hosts: all - vars: - __directories: - - path: /var/tmp/executable_binaries - mode: '0755' - - path: "{{ __bootloader_binaries_dir }}/source" - mode: '0755' - __bootloader_binaries_dir: /var/tmp/executable_binaries - __bootloader_source_dir: "{{ __bootloader_binaries_dir }}/source" - __bootloader_source_file: "{{ __bootloader_source_dir }}/main.c" - __bootloader_exe1: "{{ __bootloader_binaries_dir }}/exe1" - __bootloader_exe2: "{{ __bootloader_binaries_dir }}/exe2" - __bootloader_user: testuser - tasks: - - name: Create directories for executable binaries and source files - file: - path: "{{ item.path }}" - state: directory - mode: "{{ item.mode }}" - loop: "{{ __directories }}" - - - name: Create C source code (binary1) - copy: - content: | - int main(void) { - return 0; - } - dest: "{{ __bootloader_source_file }}" - mode: '0755' - - - name: Install GCC and glibc-devel - package: - name: - - gcc - - glibc-devel - state: present - - - name: Compile C programs (exe1) - command: gcc -o {{ __bootloader_exe1 }} {{ __bootloader_source_file }} - register: compile_result - changed_when: "compile_result.rc != 0" - - - name: Compile C programs (exe2) - command: gcc -g -o {{ __bootloader_exe2 }} {{ __bootloader_source_file }} - register: compile_result - changed_when: "compile_result.rc != 0" - - - name: Create a new user - user: - name: "{{ __bootloader_user }}" - state: present - shell: /bin/bash - - - name: Run the role - include_role: - name: linux-system-roles.fapolicyd + - name: Run tests vars: - fapolicyd_setup_enable_service: true - fapolicyd_setup_integrity: sha256 - fapolicyd_setup_trust: rpmdb,file - fapolicyd_add_trusted_file: - - /etc/passwd - - /etc/fapolicyd/fapolicyd.conf - - /etc/krb5.conf - - "{{ __bootloader_exe1 }}" - - - name: Run trusted binary exe1 - command: su - {{ __bootloader_user }} -c {{ __bootloader_exe1 }} - register: run_exe - changed_when: "run_exe.rc != 0" - - - name: Replace binary exe1 with exe2 - shell: cat {{ __bootloader_exe2 }} > {{ __bootloader_exe1 }} - register: cat_exe - changed_when: "cat_exe.rc != 0" - - - name: Run untrusted binary exe2 - command: su - {{ __bootloader_user }} -c {{ __bootloader_exe2 }} - register: run_exe - changed_when: false - failed_when: "run_exe.rc != 126" - - - name: Check now untrusted exe1 after replacement - command: su - {{ __bootloader_user }} -c {{ __bootloader_exe1 }} - register: run_exe - changed_when: false - failed_when: "run_exe.rc != 126" - - - name: Delete binary exe1 from trustdb - command: fapolicyd-cli -f delete {{ __bootloader_exe1 }} - register: delete_from_db - changed_when: "delete_from_db.rc != 0" - - - name: Update trustdb - command: fapolicyd-cli --update - register: update_db - changed_when: "update_db.rc != 0" - - - name: Run untrusted exe1 after removing from trustdb - command: su - {{ __bootloader_user }} -c {{ __bootloader_exe1 }} - register: run_exe - changed_when: false - failed_when: "run_exe.rc != 126" - - - name: Add binary exe1 to trustdb - command: fapolicyd-cli -f add {{ __bootloader_exe1 }} - register: add_to_db - changed_when: "add_to_db.rc != 0" - - - name: Update trustdb - command: fapolicyd-cli --update - register: update_db - changed_when: "update_db.rc != 0" - - - name: Run trusted exe1 - command: su - {{ __bootloader_user }} -c {{ __bootloader_exe1 }} - register: run_exe - changed_when: "run_exe.rc != 0" - - - name: Clean up binaries - file: - path: "{{ item }}" - state: absent - with_items: - - "{{ __bootloader_exe1 }}" - - "{{ __bootloader_exe2 }}" + __test_fpd_binaries_dir: >- + {{ __fapolicyd_tmpdir.path }}/executable_binaries + __test_fpd_source_dir: "{{ __test_fpd_binaries_dir }}/source" + __test_fpd_source_file: "{{ __test_fpd_source_dir }}/main.c" + __test_fpd_exe1: "{{ __test_fpd_binaries_dir }}/exe1" + __test_fpd_exe2: "{{ __test_fpd_binaries_dir }}/exe2" + __test_fpd_user: fapolicyd_test1_user + block: + - name: Create temp test directory + tempfile: + path: /var/tmp + prefix: fapolicyd_ + state: directory + register: __fapolicyd_tmpdir + + - name: Create directories for executable binaries and source files + file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ __fapolicyd_tmpdir.path }}" + - "{{ __test_fpd_binaries_dir }}" + - "{{ __test_fpd_source_dir }}" + + - name: Create C source code (binary1) + copy: + content: | + int main(void) { + return 0; + } + dest: "{{ __test_fpd_source_file }}" + mode: "0644" + + - name: Determine if system is ostree and set flag + when: not __fapolicyd_is_ostree is defined + block: + - name: Check if system is ostree + stat: + path: /run/ostree-booted + register: __ostree_booted_stat + + - name: Set flag to indicate system is ostree + set_fact: + __fapolicyd_is_ostree: "{{ __ostree_booted_stat.stat.exists }}" + + - name: Install GCC and glibc-devel + package: + name: + - gcc + - glibc-devel + state: present + use: "{{ (__fapolicyd_is_ostree | d(false)) | + ternary('ansible.posix.rhel_rpm_ostree', omit) }}" + + - name: Compile C programs (exe1) + command: >- + gcc -o {{ __test_fpd_exe1 | quote }} + {{ __test_fpd_source_file | quote }} + changed_when: true + + - name: Compile C programs (exe2) + command: >- + gcc -g -o {{ __test_fpd_exe2 | quote }} + {{ __test_fpd_source_file | quote }} + changed_when: true + + - name: Create a new user + user: + name: "{{ __test_fpd_user }}" + state: present + shell: /bin/bash + + - name: Run the role + include_role: + name: linux-system-roles.fapolicyd + vars: + fapolicyd_setup_enable_service: true + fapolicyd_setup_integrity: sha256 + fapolicyd_setup_trust: rpmdb,file + fapolicyd_add_trusted_file: + - /etc/passwd + - /etc/fapolicyd/fapolicyd.conf + - /etc/krb5.conf + - "{{ __test_fpd_exe1 }}" + + - name: Run trusted binary exe1 + command: >- + su - {{ __test_fpd_user | quote }} -c + {{ __test_fpd_exe1 | quote }} + changed_when: false + + - name: Replace binary exe1 with exe2 + copy: + src: "{{ __test_fpd_exe2 }}" + dest: "{{ __test_fpd_exe1 }}" + remote_src: true + mode: "0755" + + - name: Run untrusted binary exe2 + command: >- + su - {{ __test_fpd_user | quote }} -c + {{ __test_fpd_exe2 | quote }} + register: run_exe + changed_when: false + failed_when: run_exe.rc != 126 + + - name: Check now untrusted exe1 after replacement + command: >- + su - {{ __test_fpd_user | quote }} -c + {{ __test_fpd_exe1 | quote }} + register: run_exe + changed_when: false + failed_when: run_exe.rc != 126 + + - name: Delete binary exe1 from trustdb + command: fapolicyd-cli -f delete {{ __test_fpd_exe1 | quote }} + changed_when: true + + - name: Update trustdb + command: fapolicyd-cli --update + changed_when: true + + - name: Run untrusted exe1 after removing from trustdb + command: >- + su - {{ __test_fpd_user | quote }} -c + {{ __test_fpd_exe1 | quote }} + register: run_exe + changed_when: false + failed_when: run_exe.rc != 126 + + - name: Add binary exe1 to trustdb + command: fapolicyd-cli -f add {{ __test_fpd_exe1 | quote }} + changed_when: true + + - name: Update trustdb + command: fapolicyd-cli --update + changed_when: true + + - name: Run trusted exe1 + command: >- + su - {{ __test_fpd_user | quote }} -c + {{ __test_fpd_exe1 | quote }} + changed_when: true + always: + - name: Clean up temp directory + file: + path: "{{ __fapolicyd_tmpdir.path }}" + state: absent + + - name: Remove test user + user: + name: "{{ __test_fpd_user }}" + state: absent + + - name: Shutdown fapolicyd + service: + name: fapolicyd + state: stopped + enabled: false diff --git a/vars/main.yml b/vars/main.yml index 16bf6b6..275c581 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -14,8 +14,14 @@ __fapolicyd_packages: fapolicyd __fapolicyd_selinux_packages: fapolicyd-selinux # ansible_facts required by the role -__template_required_facts: +__fapolicyd_required_facts: - distribution - distribution_major_version - distribution_version - os_family + +# the subsets of ansible_facts that need to be gathered in case any of the +# facts in required_facts is missing; see the documentation of +# the 'gather_subset' parameter of the 'setup' module +__fapolicyd_required_facts_subsets: "{{ ['!all', '!min'] + + __fapolicyd_required_facts }}"