1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
|
#+title: Mounting
#+date: 2026-03-17 Mon
#+author: W. Kosior
#+email: wkosior@agh.edu.pl
#+HTML_HEAD: <link rel="stylesheet" href="../org-dark-styles.css">
In these exercises we are going to configure an "ephemeral" system that keeps
all filesystem changes in RAM and forgets them when rebooted. It's a suitable
setup for places where a kind of "guest mode" is needed and other solutions are
unfavored.
We are going to run the configured system inside a QEMU virtual machine and
automate the configuration with Ansible.
To be graded, send
1. the required screenshots,
2. your =playbook.yaml=, and
3. yout initramfs script
to @@html:<a href="email:wkosior@agh.edu.pl">wkosior@agh.edu.pl</a>@@ along with
your names. You can work in teams of up to three people.
* Useful Resources
[[https://docs.ansible.com/][Ansible documentation]]
[[https://manpages.debian.org/testing/initramfs-tools-core/initramfs-tools.7.en.html][man initramfs-tools]]
[[https://www.yaml.info/learn/index.html][a YAML tutorial and reference]]
* Our Setup
We shall be using [[https://pluton.kt.agh.edu.pl/~wokosior/bso-vms/debian-13-nocloud-amd64.qcow2][the Debian VM image]] from [[../01-secure-booting.org][the first topic]]. Its credentials are
=root=/=security=. It can be run with the following QEMU command.
#+begin_src shell-script
qemu-system-x86_64 \
-m 1G \
-enable-kvm \
-hda debian-13-nocloud-amd64.qcow2 \
-net nic -net user,hostfwd=tcp::30022-:22 \
-nographic # Replace with `-vga virtio' for a GUI.
#+end_src
If possible, choose one one your team's computers for running Ansible (PC "A")
and one for running QEMU (PC "Q").
Ansible is a tool that uses SSH connections to depoloy configuration to remote
machines. To be able to use it, generate an SSH keypair on "A" if you haven't
already. Then, enable key-based logins by copying the public part to the guest.
#+begin_src shell-script
IP_OF_Q=10.20.30.40 # Fill in.
ssh-keygen # If you do not yet posses a keypair under `~/.ssh/'.
ssh-copy-id -p 30022 root@"$IP_OF_Q"
ssh -p 30022 root@"$IP_OF_Q" 'echo "*hacker voice*: I'\''m in!"' # Verify.
#+end_src
Ansible shall benefit from a configuration file describing the host(s) it is
going to connect to. Place the following inside =inventory.yaml= on "A".
#+begin_src yaml
ungrouped:
hosts:
ephemeral:
ansible_user: root
ansible_host: 10.20.30.40 # Fill in with the IP address of "Q".
ansible_port: 30022
#+end_src
Make sure Ansible is installed (e.g., through APT).
Verify with Ansible's =ping= module that it is able to connect to the host that
we've just named =ephemenral=.
#+begin_src shell-script
ansible -i inventory.yaml -m ansible.builtin.ping ephemeral
#+end_src
Ansible operates on tasks that can be performed on one or more of our hosts.
The tasks are written in playbooks using YAML format. Place the following in
=playbook.yaml= on "A". This configuration consists of just one "Hello, World!"
task. Perform the task using a command provided below.
#+begin_src yaml
- name: Ansible playbook stub
hosts: ephemeral
tasks:
- name: Debug
ansible.builtin.debug:
msg: Hello, World!
#+end_src
#+begin_src shell-script
ansible-playbook -i inventory.yaml playbook.yaml
#+end_src
*Prepare screenshot 1: output of a successful =ansible-playbook= command with
the "Hello, World!" task.*
Note that you can speed up Ansible tasks by disabling fact gathering, for
example with the following environment variable.
#+begin_src shell-script
export ANSIBLE_GATHERING=explicit
#+end_src
You might also want to explicitly specify the Python interpreter on the target
to avoid a warning from Ansible. You can add the following to your inventory
config file.
#+begin_src yaml
ansible_python_interpreter: /usr/bin/python3
#+end_src
* Hacking The Initramfs
As described in =man initramf-tools=, the initramfs image in Debian can be
customized by adding
- names of kernel modules and files to be included,
- *hooks* that execute during the generation process, and
- *scripts* that execute from within the initramfs.
Let's add a simple script that will spawn a shell for us to play with the
initramfs during boot. Place the following in a file named
=spawn-initramfs-shell= on "A".
#+begin_src shell-script
#!/bin/sh
case $1 in
prereqs)
exit 0
;;
esac
/bin/sh -i # play around interactively
#+end_src
The =case= statement is there, because initramfs scripts are actually executed
twice.
- Once on the system, during generation, with argument =prereqs=, to allow
ordering the scripts in case there are some dependencies between them.
- Once during boot, without the =prereqs= argument.
Distribution-shipped initramfs hooks and scripts reside under =/usr/share/=.
User-supplied scripts should be placed under =/etc/initramfs-tools/scripts/=.
Depending on when you want your script to be executed (before or after the new
root filesystem is mounted, etc.), you should place it in a different
subdirectory of =scripts/=. We'd like =spawn-initramfs-shell= to be executed
after the root filesystem is already mounted, so we'll place it under
=scripts/local-bottom=. Add the following =copy= task to your playbook.
#+begin_src yaml
- name: Install `spawn-initramfs-shell'
ansible.builtin.copy:
src: spawn-initramfs-shell
dest: /etc/initramfs-tools/scripts/local-bottom/
mode: '744'
#+end_src
Then, use Ansible module =command= to allow updating target's =initrd.img= with
the playbook. Add the following task after the last one.
#+begin_src yaml
- name: Update Initramfs
ansible.builtin.command:
cmd: update-initramfs -c -k all
#+end_src
Run the playbook again. Once the new initramfs is generated, use the =reboot=
module of Ansible.
#+begin_src shell-script
ansible -i inventory.yaml -m ansible.builtin.reboot ephemeral
#+end_src
The guest shold reboot to a minimal shell available in the initramfs. From "Q",
try poking around in the initramfs.
#+begin_src shell-script
# Look around.
ls /
ls /bin/
mount
echo $rootmnt
ls /root/
lsmod # Command likely absent.
chroot $rootmnt /bin/sh # ;)
#+end_src
*Prepare screenshot 2: output of the commands above when executed in the
initramfs shell.*
When you exit the shell, your initramfs script shall terminate and the system
shall continue booting normally.
#+begin_src shell-script
exit
#+end_src
* Installing BusyBox
BusyBox is a popular a single-executable implementation of the most popular unix
commands. Debian shall automatically include BusyBox (instead of the utilities
you've just been using) in an intramfs image. This will give us more convenient
tooling for use in an initramfs shell. Use the following playbook task (place
it somewhere before =Update Initramfs=).
#+begin_src yaml
- name: Install BusyBox
ansible.builtin.apt:
name: busybox
#+end_src
Our goal is to overlay a RAM-based filesystem on top of the primary root
filesystem. This requires the =overlay= kernel module to be present in the
initramfs. Let's include it by appending the kernel module to
=/etc/initramfs-tools/modules=. Use the following playbook task (also place it
somewhere before =Update Initramfs=).
#+begin_src yaml
- name: Add overlayfs to initramfs
ansible.builtin.shell:
cmd: printf 'overlay\n' >> /etc/initramfs-tools/modules
#+end_src
Notice that we used Ansible module =shell= rather than =command=. The latter
would not allow us to use shell redirection in the command.
After rebooting, in the (now BusyBox-based) initramfs shell, mount a RAM-based
filesystem.
#+begin_src shell-script
mkdir /ovlay
mount -t tmpfs -o size=100% my-root-overlay-tmpfs /ovlay
#+end_src
The =size=100%= option tells the kernel not to limit the amount of data stored
in this filesystem in any way.
Now, use the =mount -o move= to re-attach an already mounted root filesystem
under =/ovlay/lower=.
#+begin_src shell-script
mkdir /ovlay/upper /ovlay/lower /ovlay/work
mount -o move "${rootmnt}" /ovlay/lower
#+end_src
Now, mount the overlayfs. Use =/ovlay/upper= for the actual added & modified
files. Use =/ovlay/work= for additional files used by the driver. Have the
overlayed root filesystem appear right where it was previously mounted without
overlay.
#+begin_src shell-script
mount -t overlay \
-o lowerdir=/ovlay/lower,upperdir=/ovlay/upper,workdir=/ovlay/work \
my-root-overlayfs "${rootmnt}"
#+end_src
To see how overlayfs works, try adding a file to the overlay and see that it
does not get created in the lower directory.
#+begin_src shell-script
touch "${rootmnt}"/my-file
ls "${rootmnt}"
ls /ovlay/lower
#+end_src
*Prepare screenshot 3: output of the commands above with =my-file= present in
the overlay and absent under =/ovlay/lower=.*
After exitig the shell and booting, check that the current root filesystem is
indeed the overlayfs.
#+begin_src shell-script
mount | grep ' on / '
#+end_src
You can even play around and to some ephemeral damage :)
#+begin_src shell-script
rm -rf /usr/bin/
#+end_src
* Optimizing the Playbook
=Update Initramfs= takes a lot of time. And it is only needed if there are
actual changes to our initramfs configuration. Optimize the playbook. First,
add the following before the =Add overlayfs to initramfs= task.
#+begin_src yaml
- name: Check if initramfs has overlayfs
ansible.builtin.command:
cmd: grep -qE ^overlay /etc/initramfs-tools/modules
register: overlayfs_in_initramfs
ignore_errors: true
#+end_src
Add the following to the =Add overlayfs to initramfs= to have it execute only
when needed.
#+begin_src yaml
when: overlayfs_in_initramfs is failed
register: enlist_overlayfs_module
#+end_src
add the following to the =Install BusyBox= task, to allow checking for changes.
#+begin_src yaml
register: install_busybox
#+end_src
add the following to the =Install `spawn-initramfs-shell'= task to allow
checking for changes.
#+begin_src yaml
register: install_initramfs_shell_spawner
#+end_src
Add the following to the =Update Initramfs= task to have it execute only when
needed. Note that this YAML construct makes lines after "=>=" treated as a
single-line string value under the key =when=.
#+begin_src yaml
when: >
install_busybox.changed or
enlist_overlayfs_module.changed or
install_initramfs_shell_spawner.changed
#+end_src
Execute the playbook again. It should not be trying to execute the lengthy
=Update Initramfs= task now, unless we change something this task relies on.
*Prepare screenshot 4: output of the =ansible-playbook= command with =Update
Initramfs= task skipped.*
* Initramfs Script for Root Overlay
We are now going to put the overlay mounting code in a script. It shall run
from initramfs and make the system ephemeral provided that an =overlay-root=
kernel command line argument is passed.
Reboot the VM if you're still booted with the overlay. Create a new initramfs
script with a name of your choice. Inside it, place the following.
#+begin_src shell-script
#!/bin/sh
case $1 in
prereqs)
exit 0
;;
esac
# Check if `overlay-root' kernel command-line arg was passed.
DO_OVERLAY=
for ARG in $(cat /proc/cmdline); do
if [ overlay-root = "$ARG" ]; then
DO_OVERLAY=something
fi
done
# Only proceed if `overlay-root' was present.
if [ -z "$DO_OVERLAY" ]; then
exit 0
fi
#+end_src
In this script, after the conditional, add the commands previously used to mount
the overlay on top of the new root filesystem.
Add a playbook task analogous to the =Install `spawn-initramfs-shell'=. The
task should install your new script. Remember to update the =when= key of the
=Update Initramfs= task so that it gets run if just the new script contents get
updated at some point.
Now, remove
- the =spawn-initramfs-shell= script (in the VM!),
- the task that installs it, and
- the related part of the =when= condition of =Update Initramfs= task.
Run the playbook.
Note: at this point, if you
- fail to remove =spawn-initramfs-shell= in the VM,
- fail to update the initramfs after removal, or
- do so when booted to our ephemeral system,
you shall still get the initramfs shell upon each reboot. Either properly
re-generate the initramfs without =spawn-initramfs-shell= or bear this
inconvenience 😉.
Reboot *with a QEMU GUI* (unless you feel brave and eager to use Emacs key
bindings instead of arrow keys). In GRUB menu, press =e= to edit the boot menu
entry. Add =overlay-root= somewhere in the Linux command line. Then, boot the
edited entry.
Verify that the currently running system has an RAM-based overlay on the root
filesystem.
#+begin_src shell-script
cat /proc/cmdline
mount | grep ' on / '
#+end_src
*Prepare screenshot 5: output of the commands above indicating that our script
has properly mounted the overlay.*
* Permanently Adding a Kernel Command Line Argument
Reboot (so that the system is once again modifiable).
Add the following tasks. They shall update the bootloader config to
automatically pass =overlay-root= to the kernel when booting.
#+begin_src yaml
- name: Check if GRUB command line includes `overlay-root'
ansible.builtin.shell:
cmd: |
grep -qE 'GRUB_CMDLINE_LINUX_DEFAULT=".*overlay-root' \
/etc/default/grub
register: overlay_in_grub_cmdline
ignore_errors: true
- name: Add `overlay-root' to GRUB command line
ansible.builtin.shell:
cmd: |
sed -i 's/\(GRUB_CMDLINE_LINUX_DEFAULT="\).*"/\1 overlay-root"/' \
/etc/default/grub
when: overlay_in_grub_cmdline is failed
- name: Update GRUB & its configuration
ansible.builtin.command:
cmd: update-grub
when: overlay_in_grub_cmdline is failed
#+end_src
Run the playbook and then look for =overlay= in = /etc/default/grub= and in
=/boot/grub/grub.cfg= to see the effect.
#+begin_src shell-script
cat /etc/default/grub
less /boot/grub/grub.cfg
#+end_src
Reboot (in text mode, without GUI). Changes to the system are now by default
not persisted.
* Kexec
/For this task, make sure you are booted in text mode and see guest's serial
output in a terminal on "Q"./
We might want to sometimes reboot the machine in non-ephemeral mode, for example
to perform updates or change some configuration. The kexec functionality of the
Linux kernel shall allow us to achieve this easily. Install =kexec-tools=
package using Ansible module =apt= (put this at the very beginning of your
playbook).
#+begin_src yaml
- name: Install kexec
ansible.builtin.apt:
name: kexec-tools
#+end_src
After running the playbook, see how we can easily obtain the (current or target)
kernel command line and the the filename of our kernel image.
#+begin_src shell-script
cat /proc/cmdline
cat /proc/cmdline | sed 's/overlay-root//'
ls /boot/vmlinuz-$(uname -r)
#+end_src
Use =kexec= command to prepare the system for booting again, with the same
kernel and initramfs, but with the =overlay-root= parameter removed.
#+begin_src shell-script
kexec --load \
--append="$(cat /proc/cmdline | sed 's/overlay-root//')" \
--initrd=/boot/initrd.img-$(uname -r) \
/boot/vmlinuz-$(uname -r)
#+end_src
A reboot can be triggered by either =kexec --exec= or =reboot= command. The
latter is preferred in our case as it makes sure that system's all devices are
shut down before the reboot.
#+begin_src shell-script
reboot # Automatically picks up and uses a kexec-loaded kernel.
#+end_src
*Prepare screenshot 6: Part of VM's serial output with phrases =Rebooting with
kexec= and =kexec_core: Starting new kernel= visible.*
After rebooting, verify that there is no overlayfs in use.
#+begin_src shell-script
cat /proc/cmdline # After reboot.
mount | grep ' on / '
#+end_src
* Integrating Ansible Playbook with Kexec
We can now make the playbook automatically reboot the system into non-ephemeral
mode before deploying any configuration to it.
At the beginning of the playbook (after the =Install kexec= task and before all
the others) create a new task using =ansible.builtin.shell=. Name it =Load
kexec kernel without `overlay-root' arg=. Have the task execute the kexec
command you have just used.
Add another task named =Reboot to non-ephemeral= immediately after. Use
=ansible.builtin.reboot= (without any other keys).
Change task name =Install BusyBox= to =Install packages=, and change its
property =name: busybox= to =name: ['busybox', 'kexec-tools']=. This is needed
to have Ansible install =kexec-tools= to the persistent system as well in case
it is not yet there.
At the very end of the playbook, add another two tasks that load a kernel with
kexec and reboot. This time, *append* the =overlay-root= kernel argument
instead of removing it.
Run the playbook.
*Prepare screenshot 7: The output of successfully executed playbook using kexec.*
* Adding a User and Preventing Login in Non-Ephemeral Mode
An ephemeral system like ours would typically have an account for guests. Add
such account using this playbook task (place it in an appropriate place in the
task list).
#+begin_src yaml
- name: Add `student' user
ansible.builtin.user:
name: student
password: '$y$j9T$CEEtmktyUYCFMlaeO0PIJ0$BoKJOYSU1eJrigIHcTyTIs1tN3NUYHDFbXBSe4PS4jC'
#+end_src
Run the playbook and see the effect. Try logging in as the new user (the
account's credentials are =student=/=student=).
Now, we shall prevent this user from logging in when the system is booted
without overlay. We do not want the guest to be able to make any persistent
changes (be it changes to files in user's home directory or default shell change
achieved through =chsh=).
Update the playbook to use [[https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/user_module.html][an appropriate parameter of ansible.builtin.user]] to
make the password locked by default.
Then, update your initramfs script to automatically unlock the password when
booting in our ephemeral mode. To achieve this, in the script you can either
- bind-mount the overlayed =${rootmnt}= to =/etc= in initramfs and have
initramfs' BusyBox' =passwd= command operate on it, or
- temporaily chroot into the overlayed =${rootmnt}= and use system's usual
=passwd= executable for the task.
Whichever method you choose, here are usage messages of the relevant commands in
BusyBox.
#+begin_example
~ # mount --help
BusyBox v1.37.0 (Debian 1:1.37.0-6+b5) multi-call binary.
Usage: mount [OPTIONS] [-o OPT] DEVICE NODE
Mount a filesystem. Filesystem autodetection requires /proc.
-a Mount all filesystems in fstab
-f Dry run
-i Don't run mount helper
-r Read-only mount
-t FSTYPE[,...] Filesystem type(s)
-T FILE Read FILE instead of /etc/fstab
-O OPT Mount only filesystems with option OPT (-a only)
-o OPT:
loop Ignored (loop devices are autodetected)
[a]sync Writes are [a]synchronous
[no]atime Disable/enable updates to inode access times
[no]diratime Disable/enable atime updates to directories
[no]relatime Disable/enable atime updates relative to modification time
[no]dev (Dis)allow use of special device files
[no]exec (Dis)allow use of executable files
[no]suid (Dis)allow set-user-id-root programs
[r]shared Convert [recursively] to a shared subtree
[r]slave Convert [recursively] to a slave subtree
[r]private Convert [recursively] to a private subtree
[un]bindable Make mount point [un]able to be bind mounted
[r]bind Bind a file or directory [recursively] to another location
move Relocate an existing mount point
remount Remount a mounted filesystem, changing flags
ro Same as -r
There are filesystem-specific -o flags.
#+end_example
#+begin_example
~ # chroot --help
BusyBox v1.37.0 (Debian 1:1.37.0-6+b5) multi-call binary.
Usage: chroot NEWROOT [PROG ARGS]
Run PROG with root directory set to NEWROOT
#+end_example
*Prepare screenshot 8: The contents of =/etc/shadow= with =student='s password
locked.*
Do not forget to include your initramfs script in the email!
* Extras (for Bonus Points!)
Optimize the playbook to only perform the final reboot if the current kernel has
not been booted with the =overlay-root= argument.
During boot we see some error messages related to systemd trying to perform some
operations on the root filesystem (which has its *original* entry still present
in =/etc/fstab=). Find a way to avoid these errors. You can try modifying the
fstab from your initramfs script or changing the configuration of systemd
services.
|