Skip to content

Commit c768bd9

Browse files
committed
sealing: Composefs sealed boot examples with e2e tests
Two examples of composefs-backed bootc hosts with Secure Boot: - sealing/: Minimal sealed UKI boot. Containerfile builds a CentOS Stream 10 host with signed systemd-boot, flattened rootfs, and a signed UKI embedding the composefs digest. One build secret (db.key), cert committed to repo. - sealing-gen2-ipe/: Full sealed host with app-signing (cfsctl, composefs signatures, sealed-httpd service) and IPE kernel patches for overlay verity validation. The e2e workflow tests both: builds each host image with ephemeral Secure Boot keys, boots a bcvk VM with --composefs-backend, and verifies verity=require on the root mount. Assisted-by: OpenCode (Claude Opus 4)
0 parents  commit c768bd9

27 files changed

Lines changed: 2322 additions & 0 deletions

.github/workflows/e2e-sealing.yml

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
name: E2E sealed composefs boot
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
# Job 1: UKI-only sealed boot (sealing/)
14+
# Builds a minimal composefs host and verifies it boots with verity=require.
15+
sealed-uki:
16+
name: Sealed UKI boot (sealing/)
17+
runs-on: ubuntu-24.04
18+
steps:
19+
- uses: actions/checkout@v5
20+
21+
- name: Setup (podman, libvirt, bcvk, virt-firmware)
22+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
23+
with:
24+
libvirt: 'true'
25+
26+
- name: Generate ephemeral Secure Boot keys
27+
run: |
28+
set -euo pipefail
29+
mkdir -p sealing/target/keys sealing/keys/
30+
31+
for name in PK KEK db; do
32+
openssl req -new -x509 -newkey rsa:2048 -nodes \
33+
-keyout "sealing/target/keys/sb-${name}.key" \
34+
-out "sealing/target/keys/sb-${name}.crt" \
35+
-days 1 -subj "/CN=composefs-ci-${name}/"
36+
ln -sf "sb-${name}.key" "sealing/target/keys/${name}.key"
37+
ln -sf "sb-${name}.crt" "sealing/target/keys/${name}.crt"
38+
done
39+
40+
cp sealing/target/keys/sb-db.crt sealing/keys/db.crt
41+
42+
- name: Build sealed host image
43+
working-directory: sealing
44+
run: |
45+
podman build -f Containerfile.host \
46+
--secret id=secureboot_key,src=target/keys/sb-db.key \
47+
-t localhost/sealed-host:latest .
48+
49+
- name: Boot and verify composefs root
50+
working-directory: sealing
51+
run: |
52+
set -euo pipefail
53+
VM_NAME="sealed-uki"
54+
55+
echo "==> Booting sealed host VM..."
56+
bcvk libvirt run --detach --ssh-wait --name "${VM_NAME}" \
57+
--filesystem=ext4 \
58+
--secure-boot-keys target/keys \
59+
localhost/sealed-host:latest
60+
61+
echo "==> Waiting for multi-user.target..."
62+
bcvk libvirt ssh "${VM_NAME}" -- \
63+
timeout 180 bash -c \
64+
'systemctl is-active multi-user.target || journalctl -b --no-pager -o cat UNIT=multi-user.target --follow | grep -q -m1 "Reached target"'
65+
66+
echo "==> Running verification checks..."
67+
bcvk libvirt ssh "${VM_NAME}" -- bash -c '
68+
set -euo pipefail
69+
70+
echo "--- kernel ---"
71+
uname -r
72+
73+
echo "--- root mount ---"
74+
mount_line=$(mount | grep " / " || true)
75+
echo " ${mount_line}"
76+
if echo "${mount_line}" | grep -q "verity=require"; then
77+
echo " OK: composefs root with verity=require"
78+
else
79+
echo " FAIL: verity=require not found on root mount"
80+
mount
81+
exit 1
82+
fi
83+
84+
echo "--- cmdline ---"
85+
cat /proc/cmdline
86+
if grep -q "composefs=" /proc/cmdline; then
87+
echo " OK: composefs= present in cmdline"
88+
else
89+
echo " FAIL: composefs= not in cmdline"
90+
exit 1
91+
fi
92+
93+
echo ""
94+
echo "=== COMPOSEFS SEALED UKI BOOT VERIFIED ==="
95+
'
96+
97+
- name: Cleanup VM
98+
if: always()
99+
working-directory: sealing
100+
run: bcvk libvirt rm --stop --force sealed-uki 2>/dev/null || true
101+
102+
- name: Dump VM journal on failure
103+
if: failure()
104+
working-directory: sealing
105+
run: |
106+
bcvk libvirt ssh sealed-uki -- journalctl -b --no-pager 2>/dev/null || true
107+
bcvk libvirt ssh sealed-uki -- mount 2>/dev/null || true
108+
109+
# Job 2: Full sealed host with app-signing (sealing-gen2-ipe/)
110+
# Builds the gen2 host (includes cfsctl, app-signing cert, sealed-httpd
111+
# service) and verifies the composefs boot. The sealed-httpd service
112+
# won't have an image to pull, but the host boot itself must succeed.
113+
sealed-gen2:
114+
name: Sealed gen2 host boot (sealing-gen2-ipe/)
115+
runs-on: ubuntu-24.04
116+
steps:
117+
- uses: actions/checkout@v5
118+
119+
- name: Setup (podman, libvirt, bcvk, virt-firmware)
120+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
121+
with:
122+
libvirt: 'true'
123+
124+
- name: Generate ephemeral keys (Secure Boot + composefs signing)
125+
run: |
126+
set -euo pipefail
127+
mkdir -p /tmp/keys
128+
129+
# Secure Boot keys
130+
for name in PK KEK db; do
131+
openssl req -new -x509 -newkey rsa:2048 -nodes \
132+
-keyout "/tmp/keys/sb-${name}.key" \
133+
-out "/tmp/keys/sb-${name}.crt" \
134+
-days 1 -subj "/CN=composefs-ci-${name}/"
135+
ln -sf "sb-${name}.key" "/tmp/keys/${name}.key"
136+
ln -sf "sb-${name}.crt" "/tmp/keys/${name}.crt"
137+
done
138+
139+
# App-signing key (gen2 needs this for the image build)
140+
openssl req -x509 -newkey rsa:4096 -nodes \
141+
-keyout /tmp/keys/composefs-signing.key \
142+
-out /tmp/keys/composefs-signing.pem \
143+
-days 1 -subj '/CN=composefs-ci-app-signing/'
144+
145+
cp /tmp/keys/composefs-signing.pem sealing-gen2-ipe/app-signing-cert.pem
146+
147+
- name: Build gen2 sealed host image
148+
working-directory: sealing-gen2-ipe
149+
run: |
150+
podman build -f Containerfile.host \
151+
--secret id=secureboot_key,src=/tmp/keys/sb-db.key \
152+
--secret id=secureboot_cert,src=/tmp/keys/sb-db.crt \
153+
-t localhost/sealed-host-gen2:latest .
154+
155+
- name: Boot and verify composefs root
156+
run: |
157+
set -euo pipefail
158+
VM_NAME="sealed-gen2"
159+
160+
echo "==> Booting gen2 sealed host VM..."
161+
bcvk libvirt run --detach --ssh-wait --name "${VM_NAME}" \
162+
--filesystem=ext4 \
163+
--secure-boot-keys /tmp/keys \
164+
localhost/sealed-host-gen2:latest
165+
166+
echo "==> Waiting for multi-user.target..."
167+
bcvk libvirt ssh "${VM_NAME}" -- \
168+
timeout 180 bash -c \
169+
'systemctl is-active multi-user.target || journalctl -b --no-pager -o cat UNIT=multi-user.target --follow | grep -q -m1 "Reached target"'
170+
171+
echo "==> Running verification checks..."
172+
bcvk libvirt ssh "${VM_NAME}" -- bash -c '
173+
set -euo pipefail
174+
175+
echo "--- kernel ---"
176+
uname -r
177+
178+
echo "--- root mount ---"
179+
mount_line=$(mount | grep " / " || true)
180+
echo " ${mount_line}"
181+
if echo "${mount_line}" | grep -q "verity=require"; then
182+
echo " OK: composefs root with verity=require"
183+
else
184+
echo " FAIL: verity=require not found on root mount"
185+
mount
186+
exit 1
187+
fi
188+
189+
echo "--- cmdline ---"
190+
cat /proc/cmdline
191+
if grep -q "composefs=" /proc/cmdline; then
192+
echo " OK: composefs= present in cmdline"
193+
else
194+
echo " FAIL: composefs= not in cmdline"
195+
exit 1
196+
fi
197+
198+
echo "--- cfsctl ---"
199+
cfsctl --version
200+
201+
echo "--- composefs-load-appkeys.service ---"
202+
if systemctl is-active --quiet composefs-load-appkeys.service; then
203+
echo " OK: keyring service active"
204+
else
205+
echo " INFO: keyring service not active (expected: cert was ephemeral)"
206+
systemctl status composefs-load-appkeys.service --no-pager 2>/dev/null || true
207+
fi
208+
209+
echo "--- sealed-httpd.service ---"
210+
echo " INFO: sealed-httpd expected to fail (no image pushed to GHCR)"
211+
systemctl status sealed-httpd.service --no-pager 2>/dev/null || true
212+
213+
echo ""
214+
echo "=== COMPOSEFS GEN2 SEALED BOOT VERIFIED ==="
215+
'
216+
217+
- name: Cleanup VM
218+
if: always()
219+
run: bcvk libvirt rm --stop --force sealed-gen2 2>/dev/null || true
220+
221+
- name: Dump VM journal on failure
222+
if: failure()
223+
run: |
224+
bcvk libvirt ssh sealed-gen2 -- journalctl -b --no-pager 2>/dev/null || true
225+
bcvk libvirt ssh sealed-gen2 -- mount 2>/dev/null || true

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Composefs sealed boot examples
2+
3+
Two examples of composefs-backed bootc hosts with Secure Boot:
4+
5+
| Directory | What |
6+
|---|---|
7+
| [`sealing/`](sealing/) | Sealed UKI boot — minimal composefs host with signed UKI |
8+
| [`sealing-gen2-ipe/`](sealing-gen2-ipe/) | + sealed app containers, fs-verity signatures, IPE kernel patches |
9+
10+
`sealing/` is the foundational layer. `sealing-gen2-ipe/` adds app-level
11+
integrity (signed containers verified by composefs + IPE policies).
12+
13+
See each directory's README for details.

sealing-gen2-ipe/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target/
2+
app-signing-cert.pem

sealing-gen2-ipe/Containerfile.app

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Containerfile.app — Minimal httpd application container
2+
#
3+
# This is a plain OCI container image, not a bootc image.
4+
# Sealing happens AFTER build via cfsctl (see Justfile seal-app target).
5+
6+
FROM quay.io/centos/centos:stream10
7+
8+
RUN dnf install -y httpd && dnf clean all
9+
10+
RUN cat > /var/www/html/index.html <<'HTML'
11+
<!DOCTYPE html>
12+
<html>
13+
<head><title>Sealed composefs demo</title></head>
14+
<body>
15+
<h1>Hello from a sealed container</h1>
16+
<p>This container's filesystem is verified by composefs fs-verity signatures.</p>
17+
</body>
18+
</html>
19+
HTML
20+
21+
EXPOSE 80
22+
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]

0 commit comments

Comments
 (0)