Skip to content

Commit e3b017d

Browse files
committed
Add support for generating signed sysexts
Use systemd-repart for creating signed DDI sysexts. Signing is performed via Azure Keyvault PKCS11 token, which is installed in the Docker image. We build both, signed discoverable disk image and raw unsigned image.
1 parent 692f082 commit e3b017d

File tree

7 files changed

+328
-30
lines changed

7 files changed

+328
-30
lines changed

.github/workflows/release.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,19 @@ jobs:
5656
continue-on-error: true
5757

5858
runs-on: ubuntu-24.04
59+
container:
60+
image: ghcr.io/flatcar/ubuntu-keyvault-pkcs11-token:main
61+
options: >-
62+
--privileged
63+
-v ${{ github.workspace }}:${{ github.workspace }}
64+
-v /tmp:/tmp
65+
-w ${{ github.workspace }}
66+
volumes:
67+
- /var/run/docker.sock:/var/run/docker.sock
5968
permissions:
6069
# allow the action to create a release
6170
contents: write
71+
id-token: write
6272
steps:
6373
- name: Set up qemu / binmft misc for cross-platform builds
6474
uses: docker/setup-qemu-action@v3
@@ -80,12 +90,21 @@ jobs:
8090
xz-utils \
8191
erofs-utils
8292
93+
- name: Azure Login
94+
uses: azure/login@v1
95+
with:
96+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
97+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
98+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
99+
83100
- name: build
84101
id: build
85102
env:
86103
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
104+
KEYVAULT_NAME: ${{ secrets.KEYVAULT_NAME }}
105+
KEYVAULT_CERT_NAME: ${{ secrets.KEYVAULT_CERT_NAME }}
87106
run: |
88-
pushd bakery
107+
cd bakery
89108
./release.sh ${{ matrix.release }}
90109
91110
- name: create a new release

lib/generate.sh

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,43 @@ function _create_sysupdate() {
102102
function _version_ge() {
103103
test "$(printf '%s\n' "$@" | sort -rV | head -n 1)" == "$1";
104104
}
105+
106+
function _generate_signed_sysext() {
107+
BASEDIR="$1"
108+
OUTPUT_IMAGE="$2"
109+
FS_FORMAT="$3"
110+
111+
# Create temporary working directory
112+
WORKDIR=$(mktemp -d)
113+
trap 'rm -rf "$WORKDIR"' RETURN
114+
115+
cp -r "${libroot}/sysext.repart.d/" "$WORKDIR"
116+
sed -Ei "s/^Format=erofs/Format=${FS_FORMAT}/" "$WORKDIR/sysext.repart.d/10-root.conf"
117+
118+
PKCS_TOKEN_NAME="pkcs11:token=${KEYVAULT_CERT_NAME}"
119+
PKCS11_ENV=(
120+
AZURE_KEYVAULT_URL="https://${KEYVAULT_NAME}.vault.azure.net/"
121+
PKCS11_MODULE_PATH="${PKCS11_MODULE_PATH:-/usr/local/lib/pkcs11/azure-keyvault-pkcs11.so}"
122+
AZURE_KEYVAULT_PKCS11_DEBUG=1
123+
)
124+
125+
CERT_CONTENT=$(az keyvault certificate download \
126+
--vault-name "$KEYVAULT_NAME" \
127+
--name "${KEYVAULT_CERT_NAME}" \
128+
--file /dev/stdout)
129+
130+
env "${PKCS11_ENV[@]}" systemd-repart \
131+
--empty=create \
132+
--size=auto \
133+
--private-key-source=engine:pkcs11 \
134+
--private-key="$PKCS_TOKEN_NAME" \
135+
--certificate=<(echo "$CERT_CONTENT") \
136+
--definitions="${WORKDIR}/sysext.repart.d" \
137+
--copy-source="$BASEDIR" \
138+
"$OUTPUT_IMAGE"
139+
140+
echo "Signed sysext created: $OUTPUT_IMAGE"
141+
}
105142
# --
106143

107144
function _generate_sysext() {
@@ -110,33 +147,58 @@ function _generate_sysext() {
110147
local format="$3"
111148
local ext_fs_size="$4"
112149
local fname="$5"
150+
local signed_ddi="$6"
151+
152+
if [[ ${signed_ddi} == true ]] ; then
153+
extname="${extname}-signed-ddi"
154+
fi
113155

114156
announce "Creating extension image '${fname}' and generating SHA256SUM"
115-
case "$format" in
116-
btrfs)
117-
# Note: We didn't chown to root:root, meaning that the file ownership is left as is
118-
mkfs.btrfs --mixed -m single -d single --shrink --compress zstd --rootdir "${basedir}" "${fname}"
119-
;;
120-
ext2|ext4)
121-
truncate -s "${ext_fs_size}" "${fname}"
122-
mkfs."${format}" -E root_owner=0:0 -d "${basedir}" "${fname}"
123-
resize2fs -M "${fname}"
124-
;;
125-
squashfs)
126-
VERSION=$({ mksquashfs -version || true ; } | head -n1 | cut -d " " -f 3)
127-
ARG=(-all-root -noappend)
128-
if _version_ge "${VERSION}" "4.6.1"; then
129-
ARG+=('-xattrs-exclude' '^btrfs.')
130-
fi
131-
mksquashfs "${basedir}" "${fname}" "${ARG[@]}"
132-
;;
133-
erofs)
134-
# Compression recommendations from https://erofs.docs.kernel.org/en/latest/mkfs.html
135-
# and forcing a zero UUID for reproducibility (maybe we could also hash the name and version)
136-
mkfs.erofs -zlz4hc,12 -C65536 -Efragments,ztailpacking -Uclear --all-root "${fname}" "${basedir}"
137-
;;
138157

139-
esac
158+
VERSION=$({ mksquashfs -version || true ; } | head -n1 | cut -d " " -f 3)
159+
SQUASHFS_ARG=(-all-root -noappend)
160+
if _version_ge "${VERSION}" "4.6.1"; then
161+
SQUASHFS_ARG+=('-xattrs-exclude' "'^btrfs.'")
162+
fi
163+
164+
# Note: We didn't chown to root:root, meaning that the file ownership is left as is
165+
SYSTEMD_REPART_MKFS_OPTIONS_BTRFS="--mixed -m single -d single --shrink --compress zstd"
166+
SYSTEMD_REPART_MKFS_OPTIONS_EXT2="-E root_owner=0:0"
167+
SYSTEMD_REPART_MKFS_OPTIONS_EXT4="$SYSTEMD_REPART_MKFS_OPTIONS_EXT2"
168+
SYSTEMD_REPART_MKFS_OPTIONS_SQUASHFS="${SQUASHFS_ARG[*]}"
169+
# Compression recommendations from https://erofs.docs.kernel.org/en/latest/mkfs.html
170+
# and forcing a zero UUID for reproducibility (maybe we could also hash the name and version)
171+
SYSTEMD_REPART_MKFS_OPTIONS_EROFS="-zlz4hc,12 -C65536 -Efragments,ztailpacking -Uclear --all-root"
172+
173+
if [[ ${signed_ddi} == true ]] ; then
174+
(
175+
export SYSTEMD_REPART_MKFS_OPTIONS_BTRFS
176+
export SYSTEMD_REPART_MKFS_OPTIONS_EXT2
177+
export SYSTEMD_REPART_MKFS_OPTIONS_EXT4
178+
export SYSTEMD_REPART_MKFS_OPTIONS_SQUASHFS
179+
export SYSTEMD_REPART_MKFS_OPTIONS_EROFS
180+
_generate_signed_sysext "${basedir}" "${fname}" "$format"
181+
)
182+
else
183+
options_varname="SYSTEMD_REPART_MKFS_OPTIONS_${format^^}"
184+
case "$format" in
185+
btrfs)
186+
mkfs.btrfs ${!options_varname} --rootdir "${basedir}" "${fname}"
187+
;;
188+
ext2|ext4)
189+
truncate -s "${ext_fs_size}" "${fname}"
190+
mkfs."${format}" ${!options_varname} -d "${basedir}" "${fname}"
191+
resize2fs -M "${fname}"
192+
;;
193+
squashfs)
194+
mksquashfs "${basedir}" "${fname}" ${!options_varname}
195+
;;
196+
erofs)
197+
mkfs.erofs "${fname}" "${basedir}" ${!options_varname}
198+
;;
199+
esac
200+
fi
201+
140202
sha256sum "${fname}" > "SHA256SUMS.${extname}"
141203
announce "'${fname}' is now ready"
142204
}
@@ -196,6 +258,8 @@ function generate_sysext() {
196258
local basedir="$(get_positional_param "1" "${@}")"
197259
local arch="$(get_positional_param "2" "${@}")"
198260

261+
local signed_ddi="$(get_optional_param "signed-ddi" "false" "${@}")"
262+
199263
if [[ -z ${basedir} ]] || [[ ${basedir} == help ]] ; then
200264
_generate_sysext_help
201265
exit
@@ -208,17 +272,28 @@ function generate_sysext() {
208272

209273
local name="$(get_optional_param "name" "$(cd "$basedir"; basename "$(pwd)")" "${@}")"
210274
local fname="$(get_optional_param "output-file" "${name}" "${@}")"
211-
fname="${fname%%.raw}.raw"
275+
fname="${fname%%.raw}"
276+
if [[ ${signed_ddi} == true ]] ; then
277+
fname="${fname}-signed-ddi"
278+
fi
279+
fname="${fname}.raw"
212280
rm -f "${fname}"
213281

214282
export SOURCE_DATE_EPOCH
215283

216284
_create_metadata "$name" "$basedir" "$os" "$arch" "$reload_services"
217-
_generate_sysext "$name" "$basedir" "$format" "$ext_fs_size" "${fname}"
285+
_generate_sysext "$name" "$basedir" "$format" "$ext_fs_size" "${fname}" "$signed_ddi"
218286

219287
local sysupdate="$(get_optional_param "sysupdate" "false" "${@}")"
220288
if [[ ${sysupdate} == true ]] ; then
221-
_create_sysupdate "${name}"
289+
local config_name=""
290+
local match_pattern=""
291+
if [[ ${signed_ddi} == true ]] ; then
292+
config_name="${name}-signed-ddi.conf"
293+
match_pattern="${name}-@v-%a-signed-ddi.raw"
294+
fi
295+
296+
_create_sysupdate "${name}" "$match_pattern" "" "" "${config_name}"
222297
fi
223298
}
224299
# --

lib/setup_azure_keyvault.sh

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SUBSCRIPTION_ID="XXX"
5+
RG_NAME="XXX"
6+
LOCATION="westeurope"
7+
KV_NAME="sysext-bakery"
8+
CERT_NAME="sysext-bakery-signing"
9+
IDENTITY_NAME="sysext-bakery-github-actions"
10+
FED_CRED_NAME="gh-main"
11+
GITHUB_REPO="flatcar/sysext-bakery" # owner/repo
12+
GITHUB_BRANCH="main"
13+
14+
echo "Setting subscription..."
15+
az account set --subscription "$SUBSCRIPTION_ID"
16+
TENANT_ID="$(az account show --query tenantId -o tsv)"
17+
18+
echo "Creating resource group $RG_NAME in $LOCATION..."
19+
az group create -n "$RG_NAME" -l "$LOCATION" -o none
20+
21+
# ============
22+
# KEY VAULT
23+
# ============
24+
echo "Creating Key Vault $KV_NAME..."
25+
# Enable RBAC authorization and purge protection (soft-delete is on by default)
26+
#
27+
az keyvault show -n "$KV_NAME" -g "$RG_NAME" -o none 2>/dev/null || \
28+
az keyvault create \
29+
--name "$KV_NAME" \
30+
--resource-group "$RG_NAME" \
31+
--location "$LOCATION" \
32+
--sku standard \
33+
--enable-purge-protection true \
34+
--enable-rbac-authorization true \
35+
-o none
36+
KV_ID="$(az keyvault show -n "$KV_NAME" -g "$RG_NAME" --query id -o tsv)"
37+
38+
# 2) Get your Azure AD object id (the caller shown in the error)
39+
# This works when you're logged in as a user (not a service principal)
40+
USER_OID=$(az ad signed-in-user show --query id -o tsv)
41+
USER_NAME=$(az ad signed-in-user show --query userPrincipalName -o tsv)
42+
RG_ID=$(az group show -n "$RG_NAME" --query id -o tsv)
43+
44+
# 3) Grant yourself a data-plane role that includes certificate create
45+
# Pick ONE of the two; Administrator is broader than Certificates Officer.
46+
for role in "Key Vault Administrator" "Owner"; do
47+
echo "Granting $role role in RG $RG_NAME to $USER_NAME"
48+
az role assignment create \
49+
--assignee-object-id "$USER_OID" \
50+
--assignee-principal-type User \
51+
--role "$role" \
52+
--scope "$RG_ID" \
53+
-o none
54+
done
55+
56+
echo "Waiting 30 seconds for the permissions to apply. If the script fails, please wait and run it again."
57+
sleep 30
58+
59+
# ============
60+
# CERTIFICATE
61+
# ============
62+
echo "Creating self-signed certificate $CERT_NAME in $KV_NAME (default policy)..."
63+
DEFAULT_POLICY="$(az keyvault certificate get-default-policy)"
64+
az keyvault certificate show --vault-name "$KV_NAME" -n "$CERT_NAME" -o none 2>/dev/null || \
65+
az keyvault certificate create \
66+
--vault-name "$KV_NAME" \
67+
--name "$CERT_NAME" \
68+
--policy "$DEFAULT_POLICY" \
69+
-o none
70+
71+
# ===============================
72+
# USER-ASSIGNED MANAGED IDENTITY
73+
# ===============================
74+
echo "Creating user-assigned managed identity $IDENTITY_NAME..."
75+
az identity show --name "$IDENTITY_NAME" --resource-group "$RG_NAME" -o none 2>/dev/null || \
76+
az identity create \
77+
--name "$IDENTITY_NAME" \
78+
--resource-group "$RG_NAME" \
79+
--location "$LOCATION" \
80+
-o none
81+
82+
IDENTITY_JSON="$(az identity show -n "$IDENTITY_NAME" -g "$RG_NAME")"
83+
IDENTITY_CLIENT_ID="$(echo "$IDENTITY_JSON" | jq -r .clientId)"
84+
IDENTITY_PRINCIPAL_ID="$(echo "$IDENTITY_JSON" | jq -r .principalId)"
85+
# IDENTITY_ID="$(echo "$IDENTITY_JSON" | jq -r .id)"
86+
87+
echo "Identity clientId: $IDENTITY_CLIENT_ID"
88+
echo "Identity principalId: $IDENTITY_PRINCIPAL_ID"
89+
90+
# =====================================
91+
# FEDERATED CREDENTIAL FOR GITHUB OIDC
92+
# =====================================
93+
echo "Creating federated credential $FED_CRED_NAME on identity $IDENTITY_NAME for repo $GITHUB_REPO branch ${GITHUB_BRANCH}..."
94+
ISSUER="https://token.actions.githubusercontent.com"
95+
AUDIENCE="api://AzureADTokenExchange"
96+
SUBJECT="repo:${GITHUB_REPO}:ref:refs/heads/${GITHUB_BRANCH}"
97+
98+
az identity federated-credential show --name "$FED_CRED_NAME" --identity-name "$IDENTITY_NAME" --resource-group "$RG_NAME" -o none || \
99+
az identity federated-credential create \
100+
--name "$FED_CRED_NAME" \
101+
--identity-name "$IDENTITY_NAME" \
102+
--resource-group "$RG_NAME" \
103+
--issuer "$ISSUER" \
104+
--subject "$SUBJECT" \
105+
--audiences "$AUDIENCE" \
106+
-o none
107+
108+
# ==================================
109+
# ROLE ASSIGNMENTS (Key Vault RBAC)
110+
# ==================================
111+
112+
IDENTITY_NAME="sysext-bakery-github-actions"
113+
PRINCIPAL_ID=$(az identity show -n "$IDENTITY_NAME" -g "$RG_NAME" --query principalId -o tsv)
114+
for role in "Key Vault Crypto User" "Key Vault Secrets User" "Key Vault Reader"; do
115+
echo "Granting $role role in RG $RG_NAME to $IDENTITY_NAME"
116+
az role assignment create \
117+
--assignee-object-id "$PRINCIPAL_ID" \
118+
--assignee-principal-type ServicePrincipal \
119+
--role "$role" \
120+
--scope "$RG_ID" \
121+
-o none
122+
done
123+
124+
# az role assignment create \
125+
# --assignee-object-id "$PRINCIPAL_ID" \
126+
# --assignee-principal-type ServicePrincipal \
127+
# --role "Reader" \
128+
# --scope "$KV_ID"
129+
130+
echo "All done."
131+
echo "---------------------------------------------"
132+
echo "Key Vault: $KV_NAME"
133+
echo "Certificate: $CERT_NAME"
134+
echo "Managed Identity: $IDENTITY_NAME"
135+
echo "Identity clientId: $IDENTITY_CLIENT_ID"
136+
echo "Tenant ID: $TENANT_ID"
137+
echo "Subscription ID: $SUBSCRIPTION_ID"
138+
echo "---------------------------------------------"
139+
echo ""
140+
echo "Please set these GitHub secrets on repo ${GITHUB_REPO}:"
141+
echo ""
142+
echo "---------------------------------------------"
143+
echo "AZURE_CLIENT_ID : $IDENTITY_CLIENT_ID"
144+
echo "AZURE_SUBSCRIPTION_ID : $SUBSCRIPTION_ID"
145+
echo "AZURE_TENANT_ID : $TENANT_ID"
146+
echo "KEYVAULT_CERT_NAME : $CERT_NAME"
147+
echo "KEYVAULT_NAME : $KV_NAME"
148+
echo ""
149+
echo "You can do it like this:"
150+
echo ""
151+
echo "echo '$IDENTITY_CLIENT_ID' | gh secret set --repo '$GITHUB_REPO' AZURE_CLIENT_ID"
152+
echo "echo '$SUBSCRIPTION_ID' | gh secret set --repo '$GITHUB_REPO' AZURE_SUBSCRIPTION_ID"
153+
echo "echo '$TENANT_ID' | gh secret set --repo '$GITHUB_REPO' AZURE_TENANT_ID"
154+
echo "echo '$CERT_NAME' | gh secret set --repo '$GITHUB_REPO' KEYVAULT_CERT_NAME"
155+
echo "echo '$KV_NAME' | gh secret set --repo '$GITHUB_REPO' KEYVAULT_NAME"
156+
echo "---------------------------------------------"

lib/sysext.repart.d/10-root.conf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
#
3+
# This file is part of systemd.
4+
#
5+
# systemd is free software; you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as published by
7+
# the Free Software Foundation; either version 2.1 of the License, or
8+
# (at your option) any later version.
9+
10+
[Partition]
11+
Type=root
12+
Format=erofs
13+
CopyFiles=/usr/
14+
CopyFiles=/opt/
15+
Verity=data
16+
VerityMatchKey=root
17+
Minimize=best

0 commit comments

Comments
 (0)