diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 68d93d0..140a38f 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -18,6 +18,7 @@ jobs: outputs: cosmic: ${{ steps.changes.outputs.cosmic }} remote: ${{ steps.changes.outputs.remote }} + intune: ${{ steps.changes.outputs.intune }} steps: - name: Checkout uses: actions/checkout@v4 @@ -40,6 +41,7 @@ jobs: eval "$(printf '%s\n' "${changed}" | ./ci-changes.sh)" echo "cosmic=${cosmic}" >> "$GITHUB_OUTPUT" echo "remote=${remote}" >> "$GITHUB_OUTPUT" + echo "intune=${intune}" >> "$GITHUB_OUTPUT" build-cosmic: name: ${{ matrix.image }} (${{ matrix.distro }}) @@ -102,6 +104,8 @@ jobs: fetch-depth: 0 - name: Log in to registry + # Only the push step needs it, and PR builds get no secrets. + if: github.event_name != 'pull_request' env: REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} @@ -170,10 +174,30 @@ jobs: steps: *build_steps + build-intune: + name: fedora-intune (44) + needs: changes + if: needs.changes.outputs.intune == 'true' + runs-on: job-v2 + + 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 + DISTRO: "44" + ARCH: x86_64 + REGISTRY: git.plabble.org/misthios + GITEA_URL: https://git.plabble.org + GITEA_REPO: Misthios/bootc-images + + steps: *build_steps + prune: name: Prune old releases and tags runs-on: job-v2 - needs: [changes, build-cosmic, build-remote] + needs: [changes, build-cosmic, build-remote, build-intune] # Run even if some builds were skipped or failed. if: ${{ always() && github.event_name != 'pull_request' }} container: @@ -191,7 +215,7 @@ jobs: env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | - for image in fedora-cosmic fedora-remote; do + for image in fedora-cosmic fedora-remote fedora-intune; do ./prune-releases.sh "${image}" done - name: Prune old registry tags @@ -203,6 +227,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; do + for image in fedora-cosmic fedora-remote fedora-intune; do AUTHFILE="${authfile}" ./prune-registry.sh "${image}" Misthios 44 rawhide done diff --git a/build.sh b/build.sh index 6dddb87..c29c9bf 100755 --- a/build.sh +++ b/build.sh @@ -191,8 +191,7 @@ case "${BUILD_MODE}" in # Overlay our manifests/support files into the upstream tree so that # relative includes and *.repo discovery keep working. - cp "${IMAGE_DIR}/manifest.yaml" "${BUILD_DIR}/upstream/" - cp "${IMAGE_DIR}/custom.yaml" "${BUILD_DIR}/upstream/" + cp "${IMAGE_DIR}"/*.yaml "${BUILD_DIR}/upstream/" if compgen -G "${IMAGE_DIR}/repos/*.repo" >/dev/null; then cp "${IMAGE_DIR}"/repos/*.repo "${BUILD_DIR}/upstream/" fi diff --git a/ci-changes.sh b/ci-changes.sh index d2cfd35..3e05a0d 100755 --- a/ci-changes.sh +++ b/ci-changes.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Given changed paths on stdin, print `cosmic=` and `remote=` so the -# workflow can skip images that didn't change. Shared files or build tooling -# changes rebuild everything; empty input (schedule/manual) rebuilds everything. +# Given changed paths on stdin, print `cosmic=`, `remote=` and +# `intune=` so the workflow can skip images that didn't change. Shared +# files or build tooling changes rebuild everything; empty input +# (schedule/manual) rebuilds everything. set -euo pipefail changed="$(cat || true)" @@ -13,12 +14,16 @@ grep -qE '^images/shared/' <<< "${changed}" && all=1 cosmic=false remote=false +intune=false if [[ "${all}" == 1 ]]; then cosmic=true remote=true + intune=true 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 echo "cosmic=${cosmic}" echo "remote=${remote}" +echo "intune=${intune}" diff --git a/images/fedora-intune/build.conf b/images/fedora-intune/build.conf new file mode 100644 index 0000000..967992b --- /dev/null +++ b/images/fedora-intune/build.conf @@ -0,0 +1,10 @@ +# Build configuration for images/fedora-intune. Sourced by build.sh. + +BUILD_MODE=upstream + +UPSTREAM_REPO="https://gitlab.com/fedora/ostree/ci-test.git" + +# Stable only; Defender comes from Microsoft's RHEL 10 repo, not rawhide. +declare -A UPSTREAM_REFS=( + [44]=f44 +) diff --git a/images/fedora-intune/custom.yaml b/images/fedora-intune/custom.yaml new file mode 100644 index 0000000..4faefce --- /dev/null +++ b/images/fedora-intune/custom.yaml @@ -0,0 +1,55 @@ +# Minimal GNOME workstation. + +packages: + # Fedora integration normally provided by the upstream fedora.yaml (which we + # opt out of to avoid the Firefox RPM). + - fedora-release + - fedora-release-ostree-desktop + - fedora-flathub-remote + + # GNOME desktop: gdm pulls gnome-session, gnome-settings-daemon, + # accountsservice, dconf and gnome-keyring-pam. gnome-initial-setup creates + # the first local user on first boot. + - gdm + - gnome-shell + - gnome-control-center + - gnome-initial-setup + - nautilus + - ptyxis + - pipewire + - pipewire-pulseaudio + - wireplumber + - xdg-desktop-portal-gnome + - xdg-user-dirs-gtk + - flatpak + + # Session infrastructure base-atomic lacks with recommends off; see + # fedora-remote/custom.yaml. + - dbus-daemon + - systemd-pam + - mesa-dri-drivers + - mesa-libEGL + + # The upstream initramfs config adds dracut's tpm2-tss module, which needs the + # tpm2 binary. fedora-remote gets it through Kanidm; list it explicitly here. + - tpm2-tools + + # Proxmox guest integration (agent, console clipboard and resizing). + - qemu-guest-agent + - spice-vdagent + +# Microsoft Edge is the browser (Conditional Access needs it). +exclude-packages: + - firefox + +add-files: + - ["50-workstation.preset", "/usr/lib/systemd/system-preset/50-workstation.preset"] + # Key-only SSH for local accounts. + - ["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. + - | + #!/usr/bin/env bash + set -xeuo pipefail + ln -sf /dev/null /etc/systemd/system/uresourced.service diff --git a/images/fedora-intune/defender.yaml b/images/fedora-intune/defender.yaml new file mode 100644 index 0000000..801d529 --- /dev/null +++ b/images/fedora-intune/defender.yaml @@ -0,0 +1,46 @@ +# Microsoft Defender for Endpoint on Linux (mdatp), RHEL 10 build, from the +# microsoft-rhel10 repo (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-workstation-sshd.conf b/images/fedora-intune/files/10-workstation-sshd.conf new file mode 100644 index 0000000..9f429e1 --- /dev/null +++ b/images/fedora-intune/files/10-workstation-sshd.conf @@ -0,0 +1,6 @@ +# Key-only SSH for local accounts (add keys to ~/.ssh/authorized_keys). +PubkeyAuthentication yes +PermitRootLogin no +PasswordAuthentication no +PermitEmptyPasswords no +KbdInteractiveAuthentication no diff --git a/images/fedora-intune/files/50-defender.preset b/images/fedora-intune/files/50-defender.preset new file mode 100644 index 0000000..dec91a5 --- /dev/null +++ b/images/fedora-intune/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-himmelblau-user.preset b/images/fedora-intune/files/50-himmelblau-user.preset new file mode 100644 index 0000000..54b6ed8 --- /dev/null +++ b/images/fedora-intune/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/files/50-himmelblau.preset b/images/fedora-intune/files/50-himmelblau.preset new file mode 100644 index 0000000..9822af2 --- /dev/null +++ b/images/fedora-intune/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/files/50-workstation.preset b/images/fedora-intune/files/50-workstation.preset new file mode 100644 index 0000000..5501062 --- /dev/null +++ b/images/fedora-intune/files/50-workstation.preset @@ -0,0 +1,3 @@ +enable gdm.service +enable qemu-guest-agent.service +enable sshd.service diff --git a/images/fedora-intune/files/edge-himmelblau-policy.json b/images/fedora-intune/files/edge-himmelblau-policy.json new file mode 100644 index 0000000..0dbdbcf --- /dev/null +++ b/images/fedora-intune/files/edge-himmelblau-policy.json @@ -0,0 +1,5 @@ +{ + "ExtensionInstallForcelist": [ + "jlnfnnolkbjieggibinobhkjdfbpcohn;https://clients2.google.com/service/update2/crx" + ] +} diff --git a/images/fedora-intune/files/edge-linux_entra_sso.json b/images/fedora-intune/files/edge-linux_entra_sso.json new file mode 100644 index 0000000..50a0e32 --- /dev/null +++ b/images/fedora-intune/files/edge-linux_entra_sso.json @@ -0,0 +1,9 @@ +{ + "name": "linux_entra_sso", + "description": "Entra ID SSO via Microsoft Identity Broker", + "path": "/usr/bin/linux-entra-sso", + "type": "stdio", + "allowed_origins": [ + "chrome-extension://jlnfnnolkbjieggibinobhkjdfbpcohn/" + ] +} diff --git a/images/fedora-intune/files/himmelblau.conf b/images/fedora-intune/files/himmelblau.conf new file mode 100644 index 0000000..b973b44 --- /dev/null +++ b/images/fedora-intune/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/files/mdatp-firstboot b/images/fedora-intune/files/mdatp-firstboot new file mode 100644 index 0000000..2659d11 --- /dev/null +++ b/images/fedora-intune/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/fedora-intune/files/mdatp-firstboot.conf b/images/fedora-intune/files/mdatp-firstboot.conf new file mode 100644 index 0000000..32b84b9 --- /dev/null +++ b/images/fedora-intune/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/fedora-intune/files/mdatp-firstboot.service b/images/fedora-intune/files/mdatp-firstboot.service new file mode 100644 index 0000000..44b2d97 --- /dev/null +++ b/images/fedora-intune/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/fedora-intune/himmelblau.yaml b/images/fedora-intune/himmelblau.yaml new file mode 100644 index 0000000..b8b8e96 --- /dev/null +++ b/images/fedora-intune/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/manifest.yaml b/images/fedora-intune/manifest.yaml new file mode 100644 index 0000000..84107b0 --- /dev/null +++ b/images/fedora-intune/manifest.yaml @@ -0,0 +1,39 @@ +# 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. + +metadata: + summary: Fedora minimal GNOME workstation with Entra ID sign-in and Intune + +variables: + # Opt out of the upstream fedora.yaml so we don't pull the Firefox RPM. + distro: "fedora-intune" + variant: "intune" + +# Keep it small: no weak/optional dependencies. +recommends: false + +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. The vendor +# directories are linked into /opt by finalize.d/60-opt-links. +opt-usrlocal: root + +repos: + - himmelblau + - microsoft-rhel10 + - microsoft-edge + +include: + - base-atomic.yaml + - custom.yaml + # Entra ID sign-in and Intune. + - himmelblau.yaml + - microsoft-edge.yaml + - defender.yaml diff --git a/images/fedora-intune/microsoft-edge.yaml b/images/fedora-intune/microsoft-edge.yaml new file mode 100644 index 0000000..1b6530d --- /dev/null +++ b/images/fedora-intune/microsoft-edge.yaml @@ -0,0 +1,15 @@ +# Microsoft Edge, from the microsoft-edge repo (repos/microsoft.repo). +# Conditional Access to Microsoft 365 web apps on Linux requires Edge. +# +# Entra SSO in Edge: himmelblau-sso only registers its native messaging host +# and force-installs the Linux Entra SSO extension for Chrome, Chromium and +# Firefox. Edge reads /etc/opt/edge, so add the same two files there. + +repo-packages: + - repo: microsoft-edge + packages: + - microsoft-edge-stable + +add-files: + - ["edge-linux_entra_sso.json", "/etc/opt/edge/native-messaging-hosts/linux_entra_sso.json"] + - ["edge-himmelblau-policy.json", "/etc/opt/edge/policies/managed/himmelblau.json"] diff --git a/images/fedora-intune/repos/himmelblau.repo b/images/fedora-intune/repos/himmelblau.repo new file mode 100644 index 0000000..81bbf0b --- /dev/null +++ b/images/fedora-intune/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/repos/microsoft.repo b/images/fedora-intune/repos/microsoft.repo new file mode 100644 index 0000000..3c6438f --- /dev/null +++ b/images/fedora-intune/repos/microsoft.repo @@ -0,0 +1,19 @@ +# packages.microsoft.com, limited with includepkgs to the packages the image +# uses, 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/ +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=https://packages.microsoft.com/keys/microsoft-2025.asc +includepkgs=mdatp + +[microsoft-edge] +name=Microsoft Edge +baseurl=https://packages.microsoft.com/yumrepos/edge/ +enabled=1 +gpgcheck=1 +gpgkey=https://packages.microsoft.com/keys/microsoft.asc +includepkgs=microsoft-edge-stable 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