diff --git a/examples/update-secure-boot-certificate/update-secure-boot-certificate-inventory-EXAMPLE.yml b/examples/update-secure-boot-certificate/update-secure-boot-certificate-inventory-EXAMPLE.yml new file mode 100644 index 000000000..2acc005cf --- /dev/null +++ b/examples/update-secure-boot-certificate/update-secure-boot-certificate-inventory-EXAMPLE.yml @@ -0,0 +1,59 @@ +--- +# +# Copyright (c) 2025 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# This is an example inventory file for use with the +# usr/share/ansible/stx-ansible/playbooks/update_secure_boot_certificate.yml +# playbook. +# +# To run the playbook, define an overrides file (as shown here) +# with the required variable settings and pass it as a parameter +# on the ansible command-line. +# +# Example ansible command: +# ansible-playbook /usr/share/ansible/stx-ansible/playbooks/update_secure_boot_certificate.yml \ +# -i @my-inventory-file.yml \ +# --extra-vars "target_list=localhost,subcloud1" + +# Use target_list to specify individual subclouds, or a comma-separated +# list of subclouds such as 'subcloud1,subcloud2'. To target all online +# subclouds at once, use 'target_list=all_online_subclouds'. +# +# To target the system controller or standalone systems, use 'target_list=localhost'. +# +all: + vars: + # The contents to the secure boot certificate to be installed and the KEK + # to allow writing in UEFI db. + secure_boot_cert: + key_exchange_key: + + children: + # This will be applied to all online subclouds. + # Use the example below in hosts to override specific settings for a subcloud, such as passwords. + target_group: + vars: + # SSH password to connect to all subclouds + ansible_ssh_user: sysadmin + ansible_ssh_pass: + # Sudo password + ansible_become_pass: +# Add a child group, as shown below, if you need individual +# overrides for specific subcloud hosts. +# Use the hosts section to add the list of hosts. +# Use the vars section to override target_group variables, +# such as the ssh password. +# Note that you can also override multiple hosts at once or +# have multiple child groups if necessary. +# Example: +# children: +# different_password_group: +# vars: +# ansible_ssh_user: sysadmin +# ansible_ssh_pass: +# ansible_become_pass: +# hosts: +# subcloud1: +# subcloud2: diff --git a/playbookconfig/src/playbooks/host_vars/update-secure-boot-certificate/default.yml b/playbookconfig/src/playbooks/host_vars/update-secure-boot-certificate/default.yml new file mode 100644 index 000000000..7e34dbc10 --- /dev/null +++ b/playbookconfig/src/playbooks/host_vars/update-secure-boot-certificate/default.yml @@ -0,0 +1,2 @@ +--- +ansible_ssh_common_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" diff --git a/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/check-certificates.yml b/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/check-certificates.yml new file mode 100644 index 000000000..b44fbfee9 --- /dev/null +++ b/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/check-certificates.yml @@ -0,0 +1,102 @@ +--- +# +# Copyright (c) 2025 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +- name: Get certificate expiration date + shell: echo -n '{{ secure_boot_cert }}' | base64 -d | openssl x509 -noout -enddate + register: cert_expiration + changed_when: false + +- name: Extract expiration date string + set_fact: + cert_expiration_date: "{{ cert_expiration.stdout | regex_replace('^notAfter=', '') }}" + +- name: Fail if the certificate has expired + fail: + msg: "The certificate has expired on {{ cert_expiration_date }}." + when: now() >= (cert_expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')) + +- debug: + msg: "Certificate is valid until {{ cert_expiration_date }}." + +- name: Compare provided certificate with current ones + block: + - name: Make temporary directory + tempfile: + state: directory + register: tempdir + + - name: Extract DB certificates from UEFI + command: mokutil --export --db + args: + chdir: "{{ tempdir.path }}" + + - name: Get exported DB files + find: + paths: "{{ tempdir.path }}" + patterns: "DB*.der" + register: db_files + + - name: Get installed DB certificates + command: openssl x509 -inform der -in {{ item.path }} -outform pem + loop: "{{ db_files.files }}" + register: db_certs + changed_when: false + no_log: true + + - name: Check if provided certificate is already in DB + set_fact: + cert_already_installed: >- + {{ (secure_boot_cert | b64decode | trim) in + (db_certs.results | map(attribute='stdout') | map('trim') | list) }} + always: + - name: Delete temporary directory + file: + path: "{{ tempdir.path }}" + state: absent + +- name: Compare provided KEK with current ones + block: + - name: Make temporary directory + tempfile: + state: directory + register: tempdir + + - name: Extract all from UEFI + shell: | + mokutil --export --db + mokutil --export --kek + args: + chdir: "{{ tempdir.path }}" + + - name: Get exported KEK files + find: + paths: "{{ tempdir.path }}" + patterns: "KEK*.der" + register: kek_files + + - name: Get public key from provided KEK + shell: echo -n '{{ key_exchange_key }}' | base64 -d | openssl pkey -pubout -outform PEM + register: kek_public_key + changed_when: false + + - name: Get installed KEK certificates + command: openssl x509 -inform der -in {{ item.path }} -pubkey -noout + loop: "{{ kek_files.files }}" + register: kek_certs + changed_when: false + + - name: Check if provided KEK is valid + fail: + msg: "The provided KEK is not valid." + when: not kek_public_key.stdout in (kek_certs.results | map(attribute='stdout') | list) + + always: + - name: Delete temporary directory + file: + path: "{{ tempdir.path }}" + state: absent + + when: not cert_already_installed diff --git a/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/check-secure-boot-enabled.yml b/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/check-secure-boot-enabled.yml new file mode 100644 index 000000000..6eaec15ea --- /dev/null +++ b/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/check-secure-boot-enabled.yml @@ -0,0 +1,15 @@ +--- +# +# Copyright (c) 2025 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +- name: Get Secure Boot state + command: mokutil --sb-state + register: mokutil + changed_when: false + failed_when: false + +- name: Stop execution on this host if Secure Boot is disabled + meta: end_host + when: "'enabled' not in mokutil.stdout" diff --git a/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/main.yml b/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/main.yml new file mode 100644 index 000000000..b5ebbd8fd --- /dev/null +++ b/playbookconfig/src/playbooks/roles/update-secure-boot-certificate/tasks/main.yml @@ -0,0 +1,83 @@ +--- +# +# Copyright (c) 2025 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# These tasks add a certificate to UEFI secure boot db. +# +- name: Check if Secure Boot is enabled + include_tasks: check-secure-boot-enabled.yml + +- name: Fail if secure_boot_cert is not defined + fail: + msg: >- + Please provide secure_boot_cert containing base64-encoded file + when: secure_boot_cert is undefined + +- name: Fail if key_exchange_key is not defined + fail: + msg: >- + Please provide key_exchange_key containing base64-encoded file + when: key_exchange_key is undefined + +# Precheck to ensure provided certificate is not expired. +- name: Check if provided certificate is valid + include_tasks: check-certificates.yml + +- debug: + msg: Provided certificate is already installed + when: cert_already_installed + +- name: Install new UEFI Secure Boot certificate + become: yes + block: + - name: Find db file(s) + find: + paths: /sys/firmware/efi/efivars + patterns: db-* + register: db_files + + - name: Make db writable + file: + path: "{{ item.path }}" + attributes: -i + loop: "{{ db_files.files }}" + + - name: Make temporary directory + tempfile: + state: directory + register: tempdir + + - name: Save secure boot certificate to a file + copy: + content: "{{ secure_boot_cert | b64decode }}" + dest: "{{ tempdir.path }}/secure_boot_cert.pem" + + - name: Save key exchange key to a file + copy: + content: "{{ key_exchange_key | b64decode }}" + dest: "{{ tempdir.path }}/KEK.key" + + - name: Install secure boot certificate + command: >- + efi-updatevar -a -c {{ tempdir.path }}/secure_boot_cert.pem -k {{ tempdir.path }}/KEK.key db + register: install_cert + + - debug: + msg: Secure Boot certificate has been successfully installed. + when: install_cert.rc == 0 + + always: + - name: Restore db file(s) attributes + file: + path: "{{ item.path }}" + attributes: +i + loop: "{{ db_files.files }}" + + - name: Delete temporary directory + file: + path: "{{ tempdir.path }}" + state: absent + + when: not cert_already_installed diff --git a/playbookconfig/src/playbooks/update_secure_boot_certificate.yml b/playbookconfig/src/playbooks/update_secure_boot_certificate.yml new file mode 100644 index 000000000..6f8c96016 --- /dev/null +++ b/playbookconfig/src/playbooks/update_secure_boot_certificate.yml @@ -0,0 +1,129 @@ +--- +# +# Copyright (c) 2025 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# This playbook is intended to be run on system controller or +# standalone systems. +# +# It provides the capability to update the secure boot certificate. +# +# To run the playbook, the user would define an overrides file that +# provides the required variable settings, passing this on the ansible +# command-line as a parameter. +# (see update-secure-boot-certificate-inventory-EXAMPLE.yml) +# +# Example command: +# ansible-playbook /usr/share/ansible/stx-ansible/playbooks/update_secure_boot_certificate.yml \ +# -i @update-secure-boot-certificate-inventory.yml \ +# --extra-vars "target_list=subcloud1" +# +# Use target_list to target individual subclouds, or a comma-separated +# list of subclouds such as 'subcloud1,subcloud2'. To target all online +# subclouds at once use target_list=all_online_subclouds. +# +# To target only the system controller or standalone systems use target_list=localhost. +# + +- name: Prepare target list and groups + hosts: localhost + gather_facts: no + tasks: + - name: Fail if target_list is not defined + fail: + msg: >- + Please provide the target list in extra-vars. + Example --extra-vars "target_list=subcloud1,subcloud2" + or "target_list=all_online_subclouds" to target all online subclouds + in 'dcmanager subcloud list' + when: target_list is undefined + + - name: Get all online subclouds in the system + block: + - name: Get online subclouds from dcmanager + shell: | + source /etc/platform/openrc + dcmanager subcloud list -c name -c availability | awk ' $4 == "online" { print $2 }' + register: subclouds + changed_when: false + + - name: Add host to target_group + add_host: + name: "{{ item }}" + groups: target_group + with_items: "{{ subclouds.stdout_lines }}" + when: "'all_online_subclouds' in target_list" + + - name: Get additional hosts from extra-vars + add_host: + name: "{{ item }}" + groups: target_group + with_items: "{{ target_list.split(',') }}" + when: "item not in ('localhost', 'all_online_subclouds')" + +- name: Run playbook in subclouds + hosts: target_group + tasks: + - block: + - name: Run playbook + command: > + ansible-playbook + /usr/share/ansible/stx-ansible/playbooks/update_secure_boot_certificate.yml + -e "ansible_ssh_user={{ ansible_ssh_user }} ansible_ssh_pass={{ ansible_ssh_pass }} + ansible_become_pass={{ ansible_become_pass }} target_list=localhost + secure_boot_cert={{ secure_boot_cert }} key_exchange_key={{ key_exchange_key }}" + --tags hosts_only -v + register: playbook_run + no_log: true + + always: + - name: Print the output of the playbook + debug: + msg: >- + {{ playbook_run.get('stdout', + 'Failed to run certificate recovery on other nodes.') }} + failed_when: playbook_run.rc != 0 + +# Discover all hosts and add them to `target_group`. +# - If running in central, add subclouds to `skip_group` since they were handled +# in the previous play. +# - If running in a subcloud, start from this play due to the `hosts_only` tag. +- name: Get available hosts + hosts: localhost + gather_facts: no + tags: hosts_only + tasks: + - name: Skip subclouds + add_host: + name: "{{ item }}" + groups: skip_group + with_items: "{{ groups.get('target_group', []) }}" + + - name: Get available hosts + shell: | + source /etc/platform/openrc + system host-list --column hostname --column mgmt_ip --column availability --format yaml + register: stx_hosts + changed_when: false + + - name: Add discovered hosts to inventory + add_host: + name: "{{ item.hostname }}" + groups: target_group + with_items: "{{ stx_hosts.stdout | from_yaml | json_query('[?availability!=`offline`]') }}" + +# Run the role only on discovered hosts in the current region: +# - Skips localhost, since controller-0 and controller-1 are in `target_group` +# - In central, skips discovered subclouds +# - In subclouds, runs only on local discovered hosts +- name: Update secure boot certificate on target group + hosts: target_group,!skip_group + gather_facts: no + tags: hosts_only + vars_files: + - host_vars/update-secure-boot-certificate/default.yml + strategy: free + roles: + - common/check-connectivity + - update-secure-boot-certificate