diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 140a38f..1949247 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -175,17 +175,22 @@ jobs: steps: *build_steps build-intune: - name: fedora-intune (44) + name: ${{ matrix.image }} (44) needs: changes if: needs.changes.outputs.intune == 'true' runs-on: job-v2 + strategy: + fail-fast: false + matrix: + image: [fedora-intune, fedora-intune-himmelblau] + container: image: "quay.io/fedora-ostree-desktops/buildroot:44" options: "--security-opt=label=disable --privileged --user 0:0 --device=/dev/fuse --volume /:/run/host:rw" env: - IMAGE: fedora-intune + IMAGE: ${{ matrix.image }} DISTRO: "44" ARCH: x86_64 REGISTRY: git.plabble.org/misthios @@ -215,7 +220,7 @@ jobs: env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | - for image in fedora-cosmic fedora-remote fedora-intune; do + for image in fedora-cosmic fedora-remote fedora-intune fedora-intune-himmelblau; do ./prune-releases.sh "${image}" done - name: Prune old registry tags @@ -227,6 +232,6 @@ jobs: authfile=/tmp/prune-auth.json echo "${REGISTRY_TOKEN}" | skopeo login -u "${REGISTRY_USERNAME}" \ --password-stdin --authfile "${authfile}" git.plabble.org - for image in fedora-cosmic fedora-remote fedora-intune; do + for image in fedora-cosmic fedora-remote fedora-intune fedora-intune-himmelblau; do AUTHFILE="${authfile}" ./prune-registry.sh "${image}" Misthios 44 rawhide done diff --git a/build.sh b/build.sh index c29c9bf..95027c6 100755 --- a/build.sh +++ b/build.sh @@ -201,7 +201,10 @@ case "${BUILD_MODE}" in if [[ -d "${IMAGE_DIR}/files" ]]; then cp -a "${IMAGE_DIR}/files/." "${BUILD_DIR}/upstream/" fi - # Shared overlay (files/repos used by multiple images). + # Shared overlay (manifests/files/repos used by multiple images). + if compgen -G "${REPO_ROOT}/images/shared/*.yaml" >/dev/null; then + cp "${REPO_ROOT}"/images/shared/*.yaml "${BUILD_DIR}/upstream/" + fi if [[ -d "${REPO_ROOT}/images/shared/files" ]]; then cp -a "${REPO_ROOT}/images/shared/files/." "${BUILD_DIR}/upstream/" fi diff --git a/ci-changes.sh b/ci-changes.sh index 3e05a0d..b5f603b 100755 --- a/ci-changes.sh +++ b/ci-changes.sh @@ -22,7 +22,8 @@ if [[ "${all}" == 1 ]]; then fi grep -qE '^images/fedora-cosmic/' <<< "${changed}" && cosmic=true grep -qE '^images/fedora-remote/' <<< "${changed}" && remote=true -grep -qE '^images/fedora-intune/' <<< "${changed}" && intune=true +# Both fedora-intune and fedora-intune-himmelblau. +grep -qE '^images/fedora-intune' <<< "${changed}" && intune=true echo "cosmic=${cosmic}" echo "remote=${remote}" diff --git a/images/fedora-intune-himmelblau/build.conf b/images/fedora-intune-himmelblau/build.conf new file mode 100644 index 0000000..2029669 --- /dev/null +++ b/images/fedora-intune-himmelblau/build.conf @@ -0,0 +1,10 @@ +# Build configuration for images/fedora-intune-himmelblau. Sourced by build.sh. + +BUILD_MODE=upstream + +UPSTREAM_REPO="https://gitlab.com/fedora/ostree/ci-test.git" + +# Stable only, like fedora-intune. +declare -A UPSTREAM_REFS=( + [44]=f44 +) diff --git a/images/fedora-intune-himmelblau/files/50-himmelblau-user.preset b/images/fedora-intune-himmelblau/files/50-himmelblau-user.preset new file mode 100644 index 0000000..54b6ed8 --- /dev/null +++ b/images/fedora-intune-himmelblau/files/50-himmelblau-user.preset @@ -0,0 +1,3 @@ +# Intune compliance check at login and on a timer, for every user. +enable himmelblau-compliance-check.service +enable himmelblau-compliance-check.timer diff --git a/images/fedora-intune-himmelblau/files/50-himmelblau.preset b/images/fedora-intune-himmelblau/files/50-himmelblau.preset new file mode 100644 index 0000000..9822af2 --- /dev/null +++ b/images/fedora-intune-himmelblau/files/50-himmelblau.preset @@ -0,0 +1,4 @@ +# Himmelblau daemons; they pull in their sockets with Requires=. +enable himmelblaud.service +enable himmelblaud-tasks.service +enable himmelblau-hsm-pin-init.service diff --git a/images/fedora-intune-himmelblau/files/himmelblau.conf b/images/fedora-intune-himmelblau/files/himmelblau.conf new file mode 100644 index 0000000..b973b44 --- /dev/null +++ b/images/fedora-intune-himmelblau/files/himmelblau.conf @@ -0,0 +1,17 @@ +# Himmelblau configuration. See himmelblau.conf(5) and +# https://himmelblau-idm.org/docs/reference/himmelblau-conf/ +# +# No tenant settings are needed: `domain` defaults to the domain of the first +# user who signs in, and that user joins the device to Entra ID. Restrict who +# may sign in with pam_allow_groups (group object IDs or UPNs). + +[global] +# Enroll the device in Intune and apply/report compliance policies. +apply_policy = true +# Fedora's admin group; members of sudo_groups (Entra group object IDs) are +# added to it. +local_sudo_group = wheel +# Home directories named after the account (e.g. /home/jdoe), with a UPN alias. +home_attr = CN +home_alias = SPN +use_etc_skel = true diff --git a/images/fedora-intune-himmelblau/himmelblau.yaml b/images/fedora-intune-himmelblau/himmelblau.yaml new file mode 100644 index 0000000..b8b8e96 --- /dev/null +++ b/images/fedora-intune-himmelblau/himmelblau.yaml @@ -0,0 +1,68 @@ +# Himmelblau: Entra ID sign-in (PAM/NSS), Entra join, Hello PIN, Intune +# enrollment and compliance, and browser SSO. + +repo-packages: + - repo: himmelblau + packages: + - himmelblau + - pam-himmelblau + - nss-himmelblau + - himmelblau-selinux + # Shows a QR code on the login screen for the MFA/device-code prompts. + - himmelblau-qr-greeter + # Entra SSO in Edge (native messaging host) via the Himmelblau broker. + - himmelblau-broker + - himmelblau-sso + +packages: + - authselect + - nss-altfiles + # himmelblau-selinux compiles its policy module at install time. + - make + - selinux-policy-devel + +add-files: + - ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"] + - ["50-himmelblau.preset", "/usr/lib/systemd/system-preset/50-himmelblau.preset"] + - ["50-himmelblau-user.preset", "/usr/lib/systemd/user-preset/50-himmelblau-user.preset"] + +postprocess: + # The package's %post selects its authselect vendor profile but drops + # with-altfiles; without it system users (gdm, pipewire, mdatp, ...) in + # /usr/lib/passwd do not resolve and the login screen never starts. + - | + #!/usr/bin/env bash + set -xeuo pipefail + authselect select himmelblau with-altfiles with-pam-gnome-keyring --force --nobackup + for user in gdm mdatp; do + getent passwd "${user}" >/dev/null + done + grep -q pam_himmelblau.so /etc/pam.d/system-auth + + # himmelblau-selinux builds and loads its module in %posttrans, which does + # nothing in a compose; do it here. + - | + #!/usr/bin/env bash + set -xeuo pipefail + tmp="$(mktemp -d)" + cp /usr/share/selinux/packages/himmelblaud/himmelblaud.{te,if,fc} "${tmp}/" + make -C "${tmp}" -f /usr/share/selinux/devel/Makefile himmelblaud.pp + semodule -n -i "${tmp}/himmelblaud.pp" + rm -rf "${tmp}" + + # Show the QR greeter on the GDM login screen, not only in user sessions. + - | + #!/usr/bin/env bash + set -xeuo pipefail + install -d /etc/dconf/db/gdm.d + cat > /etc/dconf/db/gdm.d/01-himmelblau-qr-greeter <<'CONF' + [org/gnome/shell] + enabled-extensions=['qr-greeter@himmelblau-idm.org'] + CONF + dconf update + + # Compliance check (user timer) and broker for every user. + - | + #!/usr/bin/env bash + set -xeuo pipefail + systemctl --user --global preset-all diff --git a/images/fedora-intune-himmelblau/manifest.yaml b/images/fedora-intune-himmelblau/manifest.yaml new file mode 100644 index 0000000..dca6595 --- /dev/null +++ b/images/fedora-intune-himmelblau/manifest.yaml @@ -0,0 +1,37 @@ +# Minimal GNOME workstation where users sign in with their Entra ID account +# (Himmelblau), with Microsoft Edge and Microsoft Defender for Endpoint. +# Himmelblau joins the device to Entra ID at the first sign-in and, with +# apply_policy, enrolls it in Intune and reports compliance itself, so the +# Microsoft Intune app (fedora-intune) is not included. + +metadata: + summary: Fedora minimal GNOME workstation with Entra ID sign-in (Himmelblau) + +variables: + # Opt out of the upstream fedora.yaml so we don't pull the Firefox RPM. + distro: "fedora-intune-himmelblau" + variant: "intune-himmelblau" + +# Keep it small: no weak/optional dependencies. +recommends: false + +default_target: graphical.target + +ref: fedora-intune-himmelblau/${releasever_ref}/${basearch}/intune-himmelblau + +# /opt as a plain directory; see fedora-intune/manifest.yaml. +opt-usrlocal: root + +repos: + - himmelblau + - microsoft-rhel10 + - microsoft-edge + +include: + - base-atomic.yaml + # Shared with fedora-intune (images/shared). + - workstation.yaml + - microsoft-edge.yaml + - defender.yaml + # Entra ID sign-in and Intune. + - himmelblau.yaml diff --git a/images/fedora-intune-himmelblau/repos/himmelblau.repo b/images/fedora-intune-himmelblau/repos/himmelblau.repo new file mode 100644 index 0000000..81bbf0b --- /dev/null +++ b/images/fedora-intune-himmelblau/repos/himmelblau.repo @@ -0,0 +1,9 @@ +# Himmelblau Community Nightly packages (free development builds). The +# maintained Stable repositories need a subscription entitlement token: +# https://himmelblau-idm.org/stable-repositories/ +[himmelblau] +name=Himmelblau Community Nightly (Fedora 44) +baseurl=https://packages.himmelblau-idm.org/nightly/latest/rpm/fedora44 +enabled=1 +gpgcheck=1 +gpgkey=https://packages.himmelblau-idm.org/himmelblau.asc diff --git a/images/fedora-intune/defender.yaml b/images/fedora-intune/defender.yaml deleted file mode 100644 index 95ddb8d..0000000 --- a/images/fedora-intune/defender.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Microsoft Defender for Endpoint on Linux (mdatp), RHEL 10 build. -# -# mdatp's %post does the real install: it writes mdatp.service and the -# mde_netfilter_v2 units, loads its SELinux module and creates a machine GUID -# and state under /var/opt/microsoft/mdatp. In a compose the daemon cannot -# start and /var is not shipped, so the units and SELinux module are handled -# here and the per-machine state is left to the first start on the device. -# -# Onboarding is per tenant and not part of the image: run the onboarding script -# from the Defender portal on the device (see the Intune test doc). - -repo-packages: - - repo: microsoft-rhel10 - packages: - - mdatp - -packages: - # Used by the Defender TVM/inventory scripts and the onboarding script. - - python3 - -add-files: - - ["50-defender.preset", "/usr/lib/systemd/system-preset/50-defender.preset"] - -postprocess: - - | - #!/usr/bin/env bash - set -xeuo pipefail - conf=/opt/microsoft/mdatp/conf - # Units the %post copies into place; install them if it did not get there. - for unit in mdatp.service mde_netfilter_v2.service mde_netfilter_v2.socket; do - if [[ ! -f "/usr/lib/systemd/system/${unit}" ]]; then - install -m 0644 "${conf}/${unit}" "/usr/lib/systemd/system/${unit}" - fi - done - semodule -n -i "${conf}/selinux_policies/out/mdatp_service.pp" - # Never ship a machine GUID or onboarding state baked in at build time. - ls -laR /etc/opt/microsoft /var/opt/microsoft 2>/dev/null || true - rm -rf /var/opt/microsoft/mdatp - rm -f /etc/opt/microsoft/mdatp/mdatp_onboard.json diff --git a/images/fedora-intune/files/50-defender.preset b/images/fedora-intune/files/50-defender.preset deleted file mode 100644 index fb0cbb5..0000000 --- a/images/fedora-intune/files/50-defender.preset +++ /dev/null @@ -1,3 +0,0 @@ -# Defender daemon and its network filter (socket-activated). -enable mdatp.service -enable mde_netfilter_v2.socket diff --git a/images/fedora-intune/intune.yaml b/images/fedora-intune/intune.yaml index 3bb1d14..4b28509 100644 --- a/images/fedora-intune/intune.yaml +++ b/images/fedora-intune/intune.yaml @@ -1,23 +1,14 @@ -# Microsoft Intune app for Linux (intune-portal), the Microsoft identity broker -# and Microsoft Edge (needed for Conditional Access to Microsoft 365 web apps). +# Microsoft Intune app for Linux (intune-portal) and the Microsoft identity +# broker, from the microsoft-rhel10 repo (shared/repos/microsoft.repo). # # Fedora is not a supported Intune distribution. The RHEL 10 builds are used -# because they link against the same library generation as Fedora 44. The -# repos are limited to these packages with includepkgs (repos/microsoft.repo), -# so nothing else from the RHEL repo can replace a Fedora package. - -repos: - - microsoft-rhel10 - - microsoft-edge +# because they link against the same library generation as Fedora 44. repo-packages: - repo: microsoft-rhel10 packages: - intune-portal - microsoft-identity-broker - - repo: microsoft-edge - packages: - - microsoft-edge-stable packages: # intune-portal scriptlet dependencies (authselect profile + SELinux module) @@ -26,6 +17,7 @@ packages: - checkpolicy - policycoreutils - sssd-client + - nss-altfiles # Secret storage for the identity broker tokens. - gnome-keyring - libsecret @@ -57,7 +49,10 @@ postprocess: -e '/^session[[:space:]]\+required[[:space:]]\+pam_unix.so/a session optional pam_intune.so' \ "${profile}/${f}" done - authselect select custom/intune-profile --force --nobackup + # with-altfiles: system users (gdm, pipewire, mdatp, ...) live in + # /usr/lib/passwd on rpm-ostree systems, not in /etc/passwd as on RHEL. + authselect select custom/intune-profile with-altfiles --force --nobackup + getent passwd gdm >/dev/null - | #!/usr/bin/env bash diff --git a/images/fedora-intune/manifest.yaml b/images/fedora-intune/manifest.yaml index 1cfc6fd..acecd60 100644 --- a/images/fedora-intune/manifest.yaml +++ b/images/fedora-intune/manifest.yaml @@ -18,8 +18,21 @@ default_target: graphical.target ref: fedora-intune/${releasever_ref}/${basearch}/intune +# Ship /opt as a plain read-only directory instead of a symlink to /var/opt. +# Defender installs to /opt/microsoft/mdatp and keeps its state in +# /var/opt/microsoft/mdatp; with the default symlink those are the same, +# read-only directory. Needs composefs, which upstream enables. +opt-usrlocal: root + +repos: + - microsoft-rhel10 + - microsoft-edge + include: - base-atomic.yaml - - custom.yaml - - intune.yaml + # Shared with fedora-intune-himmelblau (images/shared). + - workstation.yaml + - microsoft-edge.yaml - defender.yaml + # Microsoft Intune app. + - intune.yaml diff --git a/images/shared/defender.yaml b/images/shared/defender.yaml new file mode 100644 index 0000000..f1a4232 --- /dev/null +++ b/images/shared/defender.yaml @@ -0,0 +1,46 @@ +# Microsoft Defender for Endpoint on Linux (mdatp), RHEL 10 build, from the +# microsoft-rhel10 repo (shared/repos/microsoft.repo). +# +# mdatp's %post does the real install: units, SELinux module, and per-machine +# state under /var/opt/microsoft/mdatp (machine GUID, definitions database, +# logs). In a compose /var is read-only, and that state must not be shared +# between machines anyway, so the %post is saved into the image and run once on +# first boot by mdatp-firstboot.service. mdatp.service waits for it. +# +# Onboarding is per tenant and not part of the image: run the onboarding script +# from the Defender portal on the device. + +repo-packages: + - repo: microsoft-rhel10 + packages: + - mdatp + +packages: + # The %post is a bash script; Defender's inventory scripts and the + # onboarding script are Python. + - bash + - python3 + +add-files: + - ["mdatp-firstboot", "/usr/libexec/mdatp-firstboot"] + - ["mdatp-firstboot.service", "/usr/lib/systemd/system/mdatp-firstboot.service"] + - ["mdatp-firstboot.conf", "/usr/lib/systemd/system/mdatp.service.d/10-firstboot.conf"] + - ["50-defender.preset", "/usr/lib/systemd/system-preset/50-defender.preset"] + +postprocess: + - | + #!/usr/bin/env bash + set -xeuo pipefail + # The %post wrote the units during the compose; fail the build if a newer + # mdatp stops doing that, rather than shipping a Defender that never starts. + for unit in mdatp.service mde_netfilter_v2.service mde_netfilter_v2.socket; do + test -f "/usr/lib/systemd/system/${unit}" + done + # rpm-ostree keeps /opt content in /usr/lib/opt until the commit. + for root in /usr/lib/opt /opt; do + pp="${root}/microsoft/mdatp/conf/selinux_policies/out/mdatp_service.pp" + [[ -f "${pp}" ]] && break + done + semodule -n -i "${pp}" + rpm -q --qf '%{POSTIN}' mdatp > /usr/libexec/mdatp-postinst + chmod 0755 /usr/libexec/mdatp-firstboot /usr/libexec/mdatp-postinst diff --git a/images/fedora-intune/files/10-intune-sshd.conf b/images/shared/files/10-workstation-sshd.conf similarity index 100% rename from images/fedora-intune/files/10-intune-sshd.conf rename to images/shared/files/10-workstation-sshd.conf diff --git a/images/shared/files/50-defender.preset b/images/shared/files/50-defender.preset new file mode 100644 index 0000000..dec91a5 --- /dev/null +++ b/images/shared/files/50-defender.preset @@ -0,0 +1,4 @@ +# Defender: first-boot setup, the daemon and its network filter. +enable mdatp-firstboot.service +enable mdatp.service +enable mde_netfilter_v2.socket diff --git a/images/fedora-intune/files/50-workstation.preset b/images/shared/files/50-workstation.preset similarity index 100% rename from images/fedora-intune/files/50-workstation.preset rename to images/shared/files/50-workstation.preset diff --git a/images/shared/files/finalize.d/60-opt-links b/images/shared/files/finalize.d/60-opt-links new file mode 100755 index 0000000..0eed2b7 --- /dev/null +++ b/images/shared/files/finalize.d/60-opt-links @@ -0,0 +1,20 @@ +#!/bin/bash +# Runs in the target root during `rpm-ostree compose image` (edition 2024 +# finalize.d hook). rpm-ostree relocates RPM content under /opt to +# /usr/lib/opt. With `opt-usrlocal: root` /opt is a plain directory in the +# image and nothing links it back, so link each vendor directory. Images on the +# default (/opt -> /var/opt, linked by tmpfiles.d) are left alone. +set -euo pipefail + +root="${PWD}" + +[[ -d "${root}/usr/lib/opt" ]] || exit 0 +if [[ -L "${root}/opt" || ! -d "${root}/opt" ]]; then + exit 0 +fi + +for dir in "${root}"/usr/lib/opt/*; do + name="$(basename "${dir}")" + ln -sfn "../usr/lib/opt/${name}" "${root}/opt/${name}" + echo "Linked /opt/${name} -> /usr/lib/opt/${name}" +done diff --git a/images/shared/files/mdatp-firstboot b/images/shared/files/mdatp-firstboot new file mode 100644 index 0000000..2659d11 --- /dev/null +++ b/images/shared/files/mdatp-firstboot @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Finish the Defender (mdatp) install on first boot by running the package's +# own %post, saved at build time: it creates the machine GUID, state, logs and +# definitions database under /var and then enables and starts mdatp.service. +set -euo pipefail + +MARKER=/var/lib/mdatp-firstboot.done + +[[ -f "${MARKER}" ]] && exit 0 + +# mdatp.service only starts once the marker exists (10-firstboot.conf), and the +# %post starts it, so create the marker first and drop it again on failure. +touch "${MARKER}" +if ! bash /usr/libexec/mdatp-postinst 1; then + rm -f "${MARKER}" + exit 1 +fi diff --git a/images/shared/files/mdatp-firstboot.conf b/images/shared/files/mdatp-firstboot.conf new file mode 100644 index 0000000..32b84b9 --- /dev/null +++ b/images/shared/files/mdatp-firstboot.conf @@ -0,0 +1,3 @@ +# Don't start Defender before mdatp-firstboot.service has created its state. +[Unit] +ConditionPathExists=/var/lib/mdatp-firstboot.done diff --git a/images/shared/files/mdatp-firstboot.service b/images/shared/files/mdatp-firstboot.service new file mode 100644 index 0000000..44b2d97 --- /dev/null +++ b/images/shared/files/mdatp-firstboot.service @@ -0,0 +1,15 @@ +[Unit] +Description=Finish the Microsoft Defender for Endpoint install on first boot +Documentation=https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-endpoint-linux +After=network-online.target +Wants=network-online.target +ConditionPathExists=!/var/lib/mdatp-firstboot.done + +[Service] +Type=oneshot +ExecStart=/usr/libexec/mdatp-firstboot +RemainAfterExit=yes +TimeoutStartSec=15min + +[Install] +WantedBy=multi-user.target diff --git a/images/shared/microsoft-edge.yaml b/images/shared/microsoft-edge.yaml new file mode 100644 index 0000000..fefb37f --- /dev/null +++ b/images/shared/microsoft-edge.yaml @@ -0,0 +1,7 @@ +# Microsoft Edge, from the microsoft-edge repo (shared/repos/microsoft.repo). +# Conditional Access to Microsoft 365 web apps on Linux requires Edge. + +repo-packages: + - repo: microsoft-edge + packages: + - microsoft-edge-stable diff --git a/images/fedora-intune/repos/microsoft.repo b/images/shared/repos/microsoft.repo similarity index 75% rename from images/fedora-intune/repos/microsoft.repo rename to images/shared/repos/microsoft.repo index 9247f7d..7ae2a91 100644 --- a/images/fedora-intune/repos/microsoft.repo +++ b/images/shared/repos/microsoft.repo @@ -1,3 +1,6 @@ +# packages.microsoft.com, limited with includepkgs to the packages the images +# use, so nothing else from the RHEL repo can replace a Fedora package. + [microsoft-rhel10] name=Microsoft Production (RHEL 10) baseurl=https://packages.microsoft.com/rhel/10/prod/ diff --git a/images/fedora-intune/custom.yaml b/images/shared/workstation.yaml similarity index 86% rename from images/fedora-intune/custom.yaml rename to images/shared/workstation.yaml index cc99cea..3d66f76 100644 --- a/images/fedora-intune/custom.yaml +++ b/images/shared/workstation.yaml @@ -1,4 +1,5 @@ -# Minimal GNOME workstation. +# Minimal GNOME workstation, shared by the fedora-intune images. Include it +# from a manifest that sets `opt-usrlocal: root` (finalize.d/60-opt-links). packages: # Fedora integration normally provided by the upstream fedora.yaml (which we @@ -45,7 +46,7 @@ exclude-packages: add-files: - ["50-workstation.preset", "/usr/lib/systemd/system-preset/50-workstation.preset"] # Key-only SSH for local accounts. - - ["10-intune-sshd.conf", "/etc/ssh/sshd_config.d/10-intune-sshd.conf"] + - ["10-workstation-sshd.conf", "/etc/ssh/sshd_config.d/10-workstation-sshd.conf"] postprocess: # uresourced breaks the GDM greeter's user manager; see fedora-remote.