Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .sanity-ansible-ignore-2.23.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
plugins/modules/sr_fingerprint.py validate-modules:missing-gplv3-license
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Type: `int`
Use this variable to protect boot parameters with a password.

**WARNING**: Changing the bootloader password is not idempotent.
Use `bootloader_password_hash` for idempotent password configuration.
These two inputs cannot be used together.

The bootloader username is always `root`.

Expand All @@ -124,6 +126,33 @@ Default: `null`

Type: `string`

### bootloader_password_hash

Use this variable to set a precomputed GRUB PBKDF2 SHA512 password hash
for the bootloader user `root`. Generate the hash with
`grub2-mkpasswd-pbkdf2` and store it in Ansible Vault.
Supply only the resulting `grub.pbkdf2.sha512...` hash, without the command's
explanatory text or a trailing newline. The hash must have a positive iteration
count, a nonempty hexadecimal salt consisting of whole bytes, and a 64-byte
hexadecimal digest.

Setting the same hash repeatedly is idempotent. If unset or `null`, no hash
is written. An empty string is invalid; use `bootloader_remove_password: true`
with this variable unset or `null` to remove a password.

Do not combine this variable with a non-null `bootloader_password` or with
`bootloader_remove_password: true`.

For example, where `vault_bootloader_password_hash` contains the generated hash:

```yaml
bootloader_password_hash: "{{ vault_bootloader_password_hash }}"
```

Default: `null`

Type: `string`

### bootloader_remove_password

Set this variable to `true` to remove the bootloader password.
Expand Down
1 change: 1 addition & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ bootloader_settings: []
bootloader_timeout: null

bootloader_password: null
bootloader_password_hash: null
bootloader_remove_password: false

bootloader_reboot_ok: false
Expand Down
10 changes: 10 additions & 0 deletions meta/argument_specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ argument_specs:
username is always `root`. This value should come
from an Ansible vault.

bootloader_password_hash:
type: raw
default: null
description: >
Precomputed GRUB PBKDF2 SHA512 password hash for the root
boot loader user. When null or unset, no hash is written.
Setting the same hash repeatedly is idempotent. Store the hash
in Ansible Vault. Cannot be combined with bootloader_password
or bootloader_remove_password=true.

bootloader_remove_password:
type: bool
default: false
Expand Down
1 change: 1 addition & 0 deletions pytest_extra_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

# ansible and dependencies for all supported platforms
ansible-core ; python_version > "2.6"
mock ; python_version < "3.0"
26 changes: 26 additions & 0 deletions tasks/assert_role_vars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,32 @@
bootloader_password must be null or a string,
got {{ bootloader_password | type_debug }}

- name: Assert bootloader_password_hash is null or a GRUB PBKDF2 SHA512 hash
ansible.builtin.assert:
that:
- >-
(bootloader_password_hash is none)
or (bootloader_password_hash is string
and bootloader_password_hash is
match('^grub[.]pbkdf2[.]sha512[.][1-9][0-9]*[.]'
~ '([0-9A-Fa-f]{2})+[.][0-9A-Fa-f]{128}\Z'))
fail_msg: >-
bootloader_password_hash must be null or a GRUB PBKDF2 SHA512 hash
with a positive iteration count, hexadecimal salt, and 64-byte digest
no_log: "{{ bootloader_secure_logging }}"

- name: Assert bootloader_password_hash does not conflict with password settings
ansible.builtin.assert:
that:
- >-
bootloader_password_hash is none
or (bootloader_password is none
and not bootloader_remove_password | bool)
fail_msg: >-
bootloader_password_hash cannot be combined with bootloader_password
or bootloader_remove_password=true
no_log: "{{ bootloader_secure_logging }}"

- name: Assert kernel in bootloader_settings is defined and valid type
ansible.builtin.assert:
that:
Expand Down
13 changes: 11 additions & 2 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,16 @@
- name: Use a general grub conf path if UEFI path has a stub config
when: __bootloader_grub_conf_stat.stat.exists | bool
block:
- name: Verify if there is a stab config in {{ __bootloader_grub_conf }}
- name: Verify if there is a stub config in {{ __bootloader_grub_conf }}
shell: grep "configfile" {{ __bootloader_grub_conf }} || true
changed_when: false
register: __bootloader_grep_configfile
ignore_errors: true
check_mode: false

- name: Use a general grub and user conf path if UEFI path has a stub config
when:
- __bootloader_grep_configfile.stdout | length > 0
- __bootloader_grep_configfile.stdout | d("") | length > 0
- __bootloader_grub_conf == __bootloader_uefi_conf_dir ~ 'grub.cfg'
set_fact:
__bootloader_grub_conf: /boot/grub2/grub.cfg
Expand Down Expand Up @@ -162,6 +163,14 @@
changed_when: true
no_log: "{{ bootloader_secure_logging }}"

- name: Install precomputed boot loader password hash
ansible.builtin.copy:
content: "GRUB2_PASSWORD={{ bootloader_password_hash }}"
dest: "{{ __bootloader_user_conf }}"
mode: "{{ __bootloader_conf_mode }}"
when: bootloader_password_hash is not none
no_log: "{{ bootloader_secure_logging }}"

- name: Remove boot loader password configuration
file:
path: "{{ __bootloader_user_conf }}"
Expand Down
32 changes: 32 additions & 0 deletions tests/tasks/check_invalid_password_hash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-License-Identifier: MIT
---
- name: Reset password hash validation result
ansible.builtin.set_fact:
__bootloader_test_hash_rejected: false

- name: Try invalid password hash settings
block:
- name: Validate invalid password hash settings
ansible.builtin.include_tasks: run_role_with_clear_facts.yml
vars:
__sr_tasks_from: assert_role_vars.yml
bootloader_password_hash: "{{ __bootloader_test_invalid_hash.hash }}"
bootloader_password: >-
{{ __bootloader_test_invalid_hash.password | default(none) }}
bootloader_remove_password: >-
{{ __bootloader_test_invalid_hash.remove | default(false) }}
# Test-only values; expose the assertion message to verify the failure.
bootloader_secure_logging: false
rescue:
- name: Verify failure comes from password hash validation
ansible.builtin.assert:
that:
- "'bootloader_password_hash' in ansible_failed_result.msg"

- name: Record expected validation failure
ansible.builtin.set_fact:
__bootloader_test_hash_rejected: true

- name: Assert invalid password hash settings were rejected
ansible.builtin.assert:
that: __bootloader_test_hash_rejected
23 changes: 23 additions & 0 deletions tests/tests_invalid_input.yml
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,28 @@
assert_role_vars should reject bootloader_password
when given a boolean value

- name: Reject invalid or conflicting password hashes
ansible.builtin.include_tasks: tasks/check_invalid_password_hash.yml
loop:
- hash: ""
- hash: 123
- hash: true
- hash: []
- hash: {}
- hash: not-a-hash
- hash: "{{ 'grub.pbkdf2.sha512.0.AA.' ~ ('AB' * 64) }}"
- hash: "{{ 'grub.pbkdf2.sha512.10000.A.' ~ ('AB' * 64) }}"
- hash: "{{ 'grub.pbkdf2.sha512.10000.GG.' ~ ('AB' * 64) }}"
- hash: "{{ 'grub.pbkdf2.sha512.10000.AA.' ~ ('AB' * 63) }}"
- hash: "{{ 'grub.pbkdf2.sha512.10000.AA.' ~ ('AB' * 64) }}\n"
- hash: "{{ 'grub.pbkdf2.sha512.10000.AA.' ~ ('AB' * 64) }}"
password: test-pass
- hash: "{{ 'grub.pbkdf2.sha512.10000.AA.' ~ ('AB' * 64) }}"
remove: true
loop_control:
loop_var: __bootloader_test_invalid_hash
label: Password hash validation case

# --- Test: kernel as integer ---
- name: Assert rejects kernel as integer
block:
Expand Down Expand Up @@ -441,6 +463,7 @@
always:
- name: Clear test facts
ansible.builtin.set_fact:
__bootloader_test_hash_rejected:
__invalid_input_settings_type_failed:
__invalid_input_missing_kernel_failed:
__invalid_input_default_subopt_type_failed:
Expand Down
106 changes: 106 additions & 0 deletions tests/tests_password_hash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# SPDX-License-Identifier: MIT
# This code was generated with ChatGPT using model Astra Light 6.
---
- name: Test precomputed bootloader password hashes
hosts: all
tasks:
- name: Skip on s390x architecture
ansible.builtin.include_tasks: tasks/skip_on_s390x.yml

- name: Test hash configuration and clean up afterwards
block:
- name: Configure a precomputed password hash
ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml
vars:
bootloader_password_hash: "{{ __bootloader_test_hash }}"
__sr_public: true

- name: Read the configured password
ansible.builtin.slurp:
src: "{{ __bootloader_user_conf }}"
register: __bootloader_test_content

- name: Verify the exact hash was installed
ansible.builtin.assert:
that:
- >-
__bootloader_test_content.content | b64decode ==
'GRUB2_PASSWORD=' ~ __bootloader_test_hash

- name: Record password file state
ansible.builtin.stat:
path: "{{ __bootloader_user_conf }}"
register: __bootloader_test_before

- name: Configure the same hash again
ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml
vars:
bootloader_password_hash: "{{ __bootloader_test_hash }}"

- name: Predict changing the hash in check mode
ansible.builtin.include_tasks:
file: tasks/run_role_with_clear_facts.yml
apply:
check_mode: true
diff: true
vars:
bootloader_password_hash: "{{ __bootloader_test_new_hash }}"

- name: Verify password file is unchanged
ansible.builtin.stat:
path: "{{ __bootloader_user_conf }}"
register: __bootloader_test_after

- name: Assert idempotency and check mode preserved the file
ansible.builtin.assert:
that:
- >-
__bootloader_test_before.stat.checksum ==
__bootloader_test_after.stat.checksum
- >-
__bootloader_test_before.stat.mtime ==
__bootloader_test_after.stat.mtime
- __bootloader_test_after.stat.mode == __bootloader_conf_mode

- name: Change the password hash
ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml
vars:
bootloader_password_hash: "{{ __bootloader_test_new_hash }}"

- name: Leave the password unmanaged
ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml
vars:
bootloader_password_hash: null

- name: Read the changed password
ansible.builtin.slurp:
src: "{{ __bootloader_user_conf }}"
register: __bootloader_test_content

- name: Verify the changed hash was preserved
ansible.builtin.assert:
that:
- >-
__bootloader_test_content.content | b64decode ==
'GRUB2_PASSWORD=' ~ __bootloader_test_new_hash

- name: Remove the password
ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml
vars:
bootloader_remove_password: true

- name: Check password file was removed
ansible.builtin.stat:
path: "{{ __bootloader_user_conf }}"
register: __bootloader_test_removed

- name: Assert password file was removed
ansible.builtin.assert:
that: not __bootloader_test_removed.stat.exists
always:
- name: Remove test password
ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml
vars:
bootloader_password_hash: null
bootloader_remove_password: true
tags: tests::cleanup
43 changes: 43 additions & 0 deletions tests/vars/vault-variables.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-License-Identifier: MIT
---
# Test-only hashes; the test vault password is in tests/vault_pwd.
__bootloader_test_hash: !vault |
$ANSIBLE_VAULT;1.1;AES256
36393032626465646465663830343861656263366136386433633836643362653034343133323535
3330663362346336346461386138346564353235306635660a363363613535643535306132613964
31653465613164663234343536623737326561653732623237313334356162646133333263643561
3630346265373535320a666562343564626463623262313732323337333431626465326138336462
31393231313438376132393264366634643564636632643561333230373365306465623264633938
32376538316663613765623762373062616134616631383866383730353634396366663038386235
37326530373762626162376234353635333235313639663234353164616134623562646535323035
35393662653966383265633363363461393030653839613763386333663865633634376562363332
38373463336162393066306465326262383830663933393637356265383239356433613861326637
39353633346566303564333139636663386663336432623063353034633738306236363630653335
64643435316437303938353438383566373032646264373132616530653133373438333565653530
61633031306565373065326161393135393633666462366637346232376564313733316137373435
37616233633836346662336537356436393132616432623131366230363334626665386132663739
35326564386238633737383832383865393737386662616565373036323865613234633234373330
62353836383538633762646539353563333636646237633761343139323130366233663633663461
36313839663264353062653534653636306362313031316434383866373063336539386532336638
61623935653964353833666635313765346439386233396463366330643239663735666563313365
6639316136343337346135326331653939323462386230313238
__bootloader_test_new_hash: !vault |
$ANSIBLE_VAULT;1.1;AES256
38343664623539623364396233306631313533396162333234616561396165636332663064346337
3239333537636431393431653436336537636363646433620a356166336431303835343861356139
65313264636238633566323339653538323862313364643835336664356263616262326138373831
3232623063633833320a626436386131363934383639373362356435343633656536346230643233
62313633306238343030666535323765656237333663383030663339333066386261363763393966
36663666313633363365353965343065653939323539336564623234333338333931323332323862
63363238393662373833323039323836303433663032333765393361646432383734366334613164
65303937393363343031653437353263333861383134323537346563346237633166303530386336
66316463313838376534303035616436313064353263393234383731656263323637396234643563
36653039393233396631666535373837373564326236353436303831383738353237353635313430
61643738386231666361616337306630373434393836373736383965316235383861323464303961
39623837633130666465636634633033323766373933663731653433353830663133616435663362
32316534356331333761663730316233653632353738353238366538373861393737326536633731
63613866646465633536346133326335613365636265393336383037326638363931303264383934
34353932613732373539613630636164636438343835326433366466303937626363613562633464
37373965623737336239353262393932326434393433376638653936623936623031623437306564
61336531353535666563623037306239323832613464663236363339303732336238653464646463
3337356239396133643961306266666130613837656335353366
1 change: 1 addition & 0 deletions tests/vault_pwd
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b910c3847ab3a6d7aa62e6995167c323a68c5001b01722c6caa472e6964106ae
Loading