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:
parent
3c6fa9262c
commit
511746493d
@ -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:
|
@ -0,0 +1,2 @@
|
||||
---
|
||||
ansible_ssh_common_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
@ -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
|
@ -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"
|
@ -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
|
129
playbookconfig/src/playbooks/update_secure_boot_certificate.yml
Normal file
129
playbookconfig/src/playbooks/update_secure_boot_certificate.yml
Normal 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
|
Loading…
x
Reference in New Issue
Block a user