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
204 changes: 204 additions & 0 deletions acl/build_rpm_image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
# --download-rpms [no-op] Kept for pipeline compatibility
# --group=GROUP Image group: developer|production|prod (default: production)
# --help Show this help message
# --ipe-mode=MODE IPE runtime mode: disabled|audit|enforcing (default: disabled)
# 'enforcing' is reserved and rejected at build time.
# --ipe-signing-mode=MODE IPE signing mode: ephemeral|esrp (default: ephemeral)
# --img-name=NAME Base image name prefix (default: acl_production)
# Final image will be NAME_image.bin, VM image will be NAME_qemu_uefi_image.img
# --keep-vm Keep VM running after scripts complete (write state to .vm-state.env)
Expand Down Expand Up @@ -116,6 +119,7 @@ fi
# shellcheck source=../build_library/retry_with_backoff.sh
source "${SCRIPT_DIR}/build_library/retry_with_backoff.sh"
source "${SCRIPT_DIR}/build_library/standalone_sysext_util.sh"
source "${SCRIPT_DIR}/build_library/rpm/ipe_artifact.sh"
cd "${SCRIPT_DIR}"

# Default configuration
Expand Down Expand Up @@ -171,6 +175,19 @@ export IMAGE_VERSION_ID="${IMAGE_VERSION_ID:-}"
export IMAGE_BUILD_ID="${IMAGE_BUILD_ID:-}"
# Extra kernel cmdline args baked into a UKI debug addon (e.g., for boot profiling)
export EXTRA_KERNEL_CMDLINE="${EXTRA_KERNEL_CMDLINE:-}"
# Build-time IPE mode and signing mode. Runtime activation is selected only
# through the Azure IMDS acl-node-security-profile tag.
IPE_MODE_OVERRIDE_SET=false
if [[ -v ACL_IPE_MODE ]]; then
IPE_MODE_OVERRIDE_SET=true
fi
ACL_IPE_MODE="${ACL_IPE_MODE:-disabled}"

IPE_SIGNING_MODE_OVERRIDE_SET=false
if [[ -v ACL_IPE_SIGNING_MODE ]]; then
IPE_SIGNING_MODE_OVERRIDE_SET=true
fi
ACL_IPE_SIGNING_MODE="${ACL_IPE_SIGNING_MODE:-ephemeral}"

# Pipeline build identifier — used for deterministic gallery image versions in CI.
BUILD_ID="${BUILD_ID:-}"
Expand Down Expand Up @@ -231,6 +248,141 @@ show_help() {
exit 0
}

validate_ipe_boot_path() {
local vm_type="${1:-}"
local -a az_vm_args=()

[[ "${ACL_IPE_CAPABLE}" == "false" ]] && return 0
if [[ "${BOOTLOADER_MODE}" != "uki" ]]; then
error "IPE requires BOOTLOADER_MODE=uki"
return 1
fi
if [[ "${vm_type}" == "qemu" ]]; then
error "IPE runtime mode testing requires the Azure IMDS UKI path"
return 1
fi
Comment thread
aadhar-agarwal marked this conversation as resolved.
if [[ "${vm_type}" == "azure" ]]; then
ipe_split_argument_string az_vm_args "${AZ_VM_ARGS:-}"
if ! ipe_reject_azure_image_overrides "${az_vm_args[@]}"; then
error "--az-vm-args cannot override the image for IPE-capable Azure images"
return 1
fi
fi
return 0
}

validate_ipe_mode_value() {
case "${ACL_IPE_MODE}" in
disabled|audit) ;;
enforcing)
error "IPE mode 'enforcing' is reserved for future use and is not supported by this build (use 'audit' or 'disabled')"
return 1
;;
*)
error "Invalid IPE mode: ${ACL_IPE_MODE} (expected disabled, audit, or enforcing)"
return 1
;;
esac
}

configure_ipe_mode() {
validate_ipe_mode_value || return 1
case "${ACL_IPE_MODE}" in
disabled) ACL_IPE_CAPABLE=false ;;
audit) ACL_IPE_CAPABLE=true ;;
esac
case "${ACL_IPE_SIGNING_MODE}" in
ephemeral|esrp) ;;
*)
error "Invalid IPE signing mode: ${ACL_IPE_SIGNING_MODE} (expected ephemeral or esrp)"
return 1
;;
esac
export ACL_IPE_MODE ACL_IPE_SIGNING_MODE ACL_IPE_CAPABLE
}

load_artifact_ipe_signing_mode() {
local artifact_dir="$1"
local artifact_signing_mode
local expected_capability=""

if [[ "${IPE_MODE_OVERRIDE_SET}" == "true" ]]; then
[[ "${ACL_IPE_MODE}" == "audit" ]] && expected_capability=true ||
expected_capability=false
fi
artifact_signing_mode="$(
ipe_resolve_artifact_signing_mode "${artifact_dir}" "${expected_capability}"
)" || return 1

if [[ "${artifact_signing_mode}" == "disabled" ]]; then
ACL_IPE_MODE=disabled
configure_ipe_mode
return 0
fi

if [[ "${IPE_MODE_OVERRIDE_SET}" != "true" ]]; then
ACL_IPE_MODE=audit
fi

if [[ "${IPE_SIGNING_MODE_OVERRIDE_SET}" == "true" &&
"${ACL_IPE_SIGNING_MODE}" != "${artifact_signing_mode}" ]]; then
error "Requested IPE signing mode '${ACL_IPE_SIGNING_MODE}' does not match source image signing mode '${artifact_signing_mode}'"
return 1
fi
ACL_IPE_SIGNING_MODE="${artifact_signing_mode}"

configure_ipe_mode
}

operation_uses_vm_image() {
[[ "$BUILD_VM_IMAGE" == "true" ]] ||
[[ "$BUILD_TEST_IMAGE" == "true" ]] ||
[[ "$START_VM" == "true" ]] ||
[[ "$RUN_KOLA_TESTS" == "true" ]]
}

operation_uses_gallery_image() {
[[ -n "$ACG_IMAGE_VERSION_ID" ]] || [[ "$REUSE_IMAGE" == "true" ]]
}

operation_uses_local_image_artifact() {
if [[ "$REUSE_VM" == "true" ]] &&
[[ "$BUILD_VM_IMAGE" != "true" ]] &&
[[ "$BUILD_TEST_IMAGE" != "true" ]]; then
return 1
fi

[[ "$BUILD_IMAGE" != "true" ]] &&
! operation_uses_gallery_image &&
operation_uses_vm_image
Comment thread
aadhar-agarwal marked this conversation as resolved.
}

load_reused_vm_type() {
local state_file="${1:-${SCRIPT_DIR}/.vm-state.env}"
local key value state_vm_type=""

[[ "${REUSE_VM}" == "true" ]] || return 0
if [[ ! -r "${state_file}" ]]; then
error "--reuse-vm requires VM state at ${state_file}"
return 1
fi

while IFS='=' read -r key value; do
if [[ "${key}" == "VM_TYPE" ]]; then
state_vm_type="${value}"
break
fi
done < "${state_file}"

case "${state_vm_type}" in
azure|qemu) VM_TYPE="${state_vm_type}" ;;
*)
error "Invalid or missing VM_TYPE in ${state_file}"
return 1
;;
esac
}

# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
Expand All @@ -251,6 +403,26 @@ parse_args() {
GROUP="$2"
shift 2
;;
--ipe-mode=*)
ACL_IPE_MODE="${1#*=}"
IPE_MODE_OVERRIDE_SET=true
shift
;;
--ipe-mode)
ACL_IPE_MODE="$2"
IPE_MODE_OVERRIDE_SET=true
shift 2
;;
--ipe-signing-mode=*)
ACL_IPE_SIGNING_MODE="${1#*=}"
IPE_SIGNING_MODE_OVERRIDE_SET=true
shift
;;
--ipe-signing-mode)
ACL_IPE_SIGNING_MODE="$2"
IPE_SIGNING_MODE_OVERRIDE_SET=true
shift 2
;;
--img-name=*)
IMG_NAME="${1#*=}"
shift
Expand Down Expand Up @@ -601,6 +773,10 @@ parse_args() {
esac
done

# Fail unsupported IPE modes as early as possible, before any
# staging/build work (including artifact/gallery-image inspection below).
validate_ipe_mode_value || exit 1

# Validate --retry value
if [[ "$RETRY_ATTEMPTS" != "0" ]]; then
if ! [[ "$RETRY_ATTEMPTS" =~ ^[1-9][0-9]*$ ]]; then
Expand All @@ -613,6 +789,21 @@ parse_args() {
fi
fi

load_reused_vm_type || exit 1

if operation_uses_local_image_artifact; then
load_artifact_ipe_signing_mode \
"${SCRIPT_DIR}/__build__/images/images/${BOARD}/latest" ||
exit 1
fi
if operation_uses_gallery_image &&
[[ "${START_VM}" == "true" || "${RUN_KOLA_TESTS}" == "true" ]] &&
[[ "${IPE_MODE_OVERRIDE_SET}" == "false" ]]; then
warn "ACL_IPE_MODE is not set for gallery image validation; defaulting to disabled"
fi

configure_ipe_mode || exit 1

# Normalize group name
case "$GROUP" in
prod|production)
Expand All @@ -636,9 +827,17 @@ parse_args() {
;;
esac
fi
local ipe_vm_type=""
if operation_uses_vm_image; then
ipe_vm_type="${VM_TYPE}"
fi
validate_ipe_boot_path "${ipe_vm_type}" || exit 1

# Add platform-specific host-side tests when --run-tests is used.
if [[ "${RUN_TESTS:-false}" == "true" ]] && [[ "$VM_TYPE" == "azure" ]]; then
if [[ "${ACL_IPE_CAPABLE}" == "true" ]]; then
RUN_HOST_SCRIPTS+=("./acl/tests/ipe/run-ipe-mode-toggle-test.sh")
fi
RUN_HOST_SCRIPTS+=("./acl/tests/run-selinux-toggle-test.sh")
fi

Expand Down Expand Up @@ -870,6 +1069,7 @@ build_image() {
info " Output: ${OUTPUT_ROOT}"
info " Staging Dir: ${STAGING_DIR}"
info " Force Rebuild: ${FORCE_REBUILD}"
info " IPE Mode: ${ACL_IPE_MODE} (signing: ${ACL_IPE_SIGNING_MODE})"
echo

# Count RPMs
Expand Down Expand Up @@ -1054,6 +1254,9 @@ build_vm_image() {
# pulling a stale version.txt into local dev builds.
local version_args=()
local from_dir="${SCRIPT_DIR}/__build__/images/images/${BOARD}/latest"
load_artifact_ipe_signing_mode "${from_dir}" || exit 1
validate_ipe_boot_path "${vm_type}" || exit 1

if [[ "${NO_TTY:-false}" == "true" ]] && [[ -f "${from_dir}/version.txt" ]]; then
info "Installing artifact version.txt into manifest location (CI mode)"
cp "${from_dir}/version.txt" \
Expand Down Expand Up @@ -1261,6 +1464,7 @@ main() {

section "Azure Container Linux Image Builder"
info "Building ${BOARD} ${GROUP} image using Azure Linux RPMs"
info "IPE mode: ${ACL_IPE_MODE} (signing: ${ACL_IPE_SIGNING_MODE})"

check_prerequisites
print_summary
Expand Down
39 changes: 39 additions & 0 deletions acl/docs/BUILD_RPM_IMAGE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,27 @@ Build the Flatcar production image using RPM package sources.
./acl/build_rpm_image.sh --rebuild
```

**Optional IPE audit (UKI images)**

Use `ephemeral` for dev/test or `esrp` for later production signing by
`ACL-Publish-MCR-And-ACG`; both initially build a test-signed policy.

```bash
./acl/build_rpm_image.sh --rebuild --ipe-mode=audit --ipe-signing-mode=ephemeral
./acl/build_rpm_image.sh --rebuild --ipe-mode=audit --ipe-signing-mode=esrp
```

IPE defaults to disabled. On Azure, set the VM tag
`acl-node-security-profile=ipe=audit` to log would-deny events without blocking
execution. Policy-load failures are logged and leave IPE inactive without
blocking boot. `enforcing` is unsupported; QEMU runtime testing requires
`--ipe-mode=disabled`. See [architecture](architecture.md) for policy trust,
credential handling and dm-verity details.
The policy hash is reserved for the main UKI command line; IPE image
conversion rejects active UKI addons (including the optional
`EXTRA_KERNEL_CMDLINE` debug addon) that supply another
`acl.ipe.policy_sha256` token.

**Build output location:** `__build__/images/images/amd64-usr/latest/`

### Phase 4: Build VM Image (Optional)
Expand Down Expand Up @@ -322,6 +343,21 @@ By default, before starting a new Azure VM, all the pre-existing resource groups
./acl/build_rpm_image.sh --start-vm --vm-type=azure --no-cleanup
```

For test-signed IPE policies, including unpublished ESRP candidates, use the
Secure Boot smoke path with the matching `uki-signing-ca.pem`. Kernel-trusted
production policies can activate on Standard Gen2, but IPE does not select
that VM profile automatically. `--run-tests` still includes the Secure Boot check.

IPE-capable launches reject image overrides and `--reuse-image` before VM cleanup.
The latest gallery version has no verified IPE capability/signing contract, so
use a local IPE VHD or explicitly select a gallery version whose IPE
capability and signing provenance you have verified separately.
For local IPE VHDs, the launcher pins the resolved build directory before
preflight and uploads from that same directory even if `latest` moves. Keep
build-version directories unchanged during upload; pinning cannot prevent
in-place modification of a VHD.
Non-IPE `--reuse-image` behavior is unchanged.

You can also use the `--run-script` flag to run tests on the Azure VM, just like with the QEMU VM.

#### Access the VM
Expand Down Expand Up @@ -483,6 +519,9 @@ AZURE_TOKEN_CREDENTIALS=AzureCLICredential \
./run_azure_tests.sh amd64 2 cl.ignition.v1.once coreos.ignition.once
```

Kola boot success alone does not prove active IPE audit. Use the IPE smoke
test to verify policy activation and execution-correlated audit events.

**Arguments:**

- `amd64` or `arm64` — target architecture
Expand Down
4 changes: 4 additions & 0 deletions acl/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ The `/usr` partition (USR-A) is a read-only btrfs filesystem with zstd compressi
- The verity hash tree is stored in a dedicated hash partition (HASH-A, immediately following USR-A on disk).
- At boot, `systemd-veritysetup` activates the verity device using slot-specific parameters delivered via a per-slot systemd-stub addon: `systemd.verity_usr_data=PARTUUID=<usr-data-partition>`, `systemd.verity_usr_hash=PARTUUID=<usr-hash-partition>`, and `systemd.verity_usr_options=panic-on-corruption`.
- The main UKI cmdline stays slot-independent (`mount.usr=/dev/mapper/usr`); Trident switches slots by swapping which addon is active in `<uki>.efi.extra.d/`, so the same signed UKI boots either A or B. (The secondary GRUB boot path is out of scope for A/B update and is unchanged: it still uses the inline PARTUUID + hash-offset verity cmdline on the existing non-UKI partition layout.)
- The active slot addon carries `usrhash` without a detached root-hash signature or `root-hash-signature=` option. dm-verity still checks block integrity; `/usr` does not satisfy IPE's `dmverity_signature=TRUE` allow rule, so execution produces would-deny events when the policy is active in audit mode.
- IPE assets are selected through two independent build-time controls: `ACL_IPE_MODE=disabled|audit|enforcing` (default `disabled`; `enforcing` is reserved and rejected at build time) and `ACL_IPE_SIGNING_MODE=ephemeral|esrp` (default `ephemeral`), which only applies when `ACL_IPE_MODE=audit`. Azure launchers retain their existing defaults rather than selecting a VM security profile from IPE capability. Production policies trusted by the kernel can activate without Trusted Launch; test-signed policies still require their test trust.
- On IPE-capable (`audit`) images, the signed policy candidate CMS is staged at `${BUILD_DIR}/acl-ipe-policy/acl-ipe-policy.p7b.cred` and installed as a per-UKI `.extra.d` credential companion at `EFI/Linux/<uki>.efi.extra.d/acl-ipe-policy.p7b.cred`, alongside the active slot addon. The main UKI cmdline includes an `acl.ipe.policy_sha256=<hash>` token that binds the credential to the signed kernel command line independently of slot selection. At boot, systemd-stub makes the credential available at `/.extra/credentials/acl-ipe-policy.p7b.cred`. On Azure, the initramfs loader validates the credential SHA-256, loads the policy into the kernel, and activates it only when IMDS requests `ipe=audit` (runtime enforcement state `0`); `ipe=disabled`/`ipe=off` leave it loaded but inactive, and a manually requested `ipe=enforcing` is logged as an explicit unsupported diagnostic and safely falls back to inactive without failing boot. Loading is best effort: validation or loading failures are logged and leave IPE inactive without blocking boot. Runtime mode is never persisted as image/artifact metadata. Development and test builds use `ephemeral` signing mode. Production IPE validation uses `esrp` signing mode (the build still creates a build-local ephemeral candidate so pre-publish VHDs remain bootable; the `ACL-Publish-MCR-And-ACG` pipeline replaces it later), while normal production pipeline defaults remain `disabled` until activation is approved. The sole producer marker is the optional `${BUILD_DIR}/ipe-signing-mode` file (`ephemeral` or `esrp`); its absence means disabled, and `disabled` builds remove any staged policy data and stale ephemeral private-key/cert material.
- Before VM conversion re-signs an IPE UKI, it verifies the staged credential's signer, selects the UKI named by `loader.conf`, and requires that UKI's exact companion credential to match the staged bytes and its single `acl.ipe.policy_sha256` token. Inconsistent artifacts fail conversion rather than being discovered only at boot.
- Any corruption of `/usr` causes an immediate kernel panic, preventing the system from running a tampered image.

The A/B partition scheme (USR-A / USR-B) enables safe updates: the inactive slot is written, verified, and then atomically switched on reboot.
Expand Down
Loading
Loading