Ansible Playbook to add cert to UEFI Secure Boot

This commit adds an Ansible Playbook that installs a new certificate to
UEFI secure boot trusted certificates DB in all available hosts and,
optionally, in all subclouds.

Test Plan:
PASS: Build playbookconfig package and image.
PASS: Run playbook in an AIO-SX and check that it added the certificate.
PASS: Run playbook in an AIO-DX and check that it added the certificate
      to both controllers.
PASS: Run playbook in a DC and check it added the cert to subcloud
      controllers.
PASS: Run playbook in a DC with a subcloud containing a worker node and
      check that it added the certificate to all hosts, including
      subcloud worker node.
PASS: Run playbook in a DC containing a host with secure boot disabled
      and check that it skips that host without failing.
PASS: Run playbook with an expired certificate as input and see it fail.

Story: 2011352
Task: 51687

Change-Id: Ie72fb67059addbe3f0fa341c81d0143c035e3e3d
Signed-off-by: Rodrigo Tavares <Rodrigo.DosSantosTavares@windriver.com>
This commit is contained in:
Rodrigo Tavares 2025-02-12 17:01:23 -03:00
parent 3c6fa9262c
commit 511746493d
6 changed files with 390 additions and 0 deletions

View File

@ -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: <base64_cert>
key_exchange_key: <base64_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: <sysadmin-pwd>
# Sudo password
ansible_become_pass: <sysadmin-pwd>
# 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: <sysadmin-pwd>
# ansible_become_pass: <sysadmin-pwd>
# hosts:
# subcloud1:
# subcloud2:

View File

@ -0,0 +1,2 @@
---
ansible_ssh_common_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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