Author SHA1 Message Date
Misthios 99400fb03e Merge pull request 'fedora-intune: query Defender through systemd-run instead of a snapshot' (#8) from misthios/defender-query-via-systemd-run into main
Build containers / Compute changes (push) Successful in 21s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m25s
Build containers / Prune old releases and tags (push) Successful in 27s
Reviewed-on: #8
2026-10-20 14:16:28 +00:00
Wesley van TilburgandClaude Opus 5.5 ca115922bd fedora-intune: query Defender through systemd-run instead of a snapshot
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 14s
Build containers / Prune old releases and tags (pull_request) Skipped
The health snapshot's temp directory, chmod and mv in /run every 5
minutes showed up in Defender's timeline as "Data Staged" and
"Executable permissions were added". Drop the snapshot service and
timer. The compliance script now runs `mdatp health` through
systemd-run, so systemd starts mdatp and Defender answers it even when
the script runs inside himmelblaud-tasks. Without systemd-run access it
asks mdatp directly, as before. No image support needed, so the script
works on other distros as is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 14:12:59 +00:00
Misthios 082ae219b1 Merge pull request 'fedora-intune: take the first Defender health snapshot right after boot' (#7) from misthios/defender-snapshot-at-boot into main
Build containers / Compute changes (push) Successful in 20s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m28s
Build containers / Prune old releases and tags (push) Successful in 27s
Reviewed-on: #7
2026-10-20 14:02:43 +00:00
Wesley van TilburgandClaude Opus 5.5 49b360dffa fedora-intune: take the first Defender health snapshot right after boot
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 12s
Build containers / Prune old releases and tags (pull_request) Skipped
The first snapshot came 2 minutes after boot, so the compliance check at
the first sign-in found none, fell back to mdatp (which won't answer it)
and reported Defender as not running. Himmelblau then doesn't check again
for 8 hours. Start the timer 20 seconds after boot, and wait up to
2 minutes for the daemon to answer so that snapshot isn't a false
"not running". The timer runs it in the background, so boot isn't held up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 13:47:07 +00:00
Misthios 811025773e Merge pull request 'fedora-intune: give the Intune compliance check a Defender health snapshot' (#6) from misthios/defender-health-snapshot into main
Build containers / Compute changes (push) Successful in 19s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m31s
Build containers / Prune old releases and tags (push) Successful in 26s
Reviewed-on: #6
2026-10-20 13:25:02 +00:00
Wesley van TilburgandClaude Opus 5.5 111d3bea66 fedora-intune: give the Intune compliance check a Defender health snapshot
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 9s
Build containers / Prune old releases and tags (pull_request) Skipped
Himmelblau runs the custom compliance script inside himmelblaud-tasks,
and Defender refuses to answer `mdatp health` there: the daemon can't
read /proc/<himmelblaud_tasks>/cmdline while checking the caller
("Permission denied" in microsoft_defender_err.log). The script saw no
daemon, and every Defender rule failed although Defender was healthy.

Add mdatp-health-snapshot.timer, which saves the health fields to
/run/mdatp-health every 5 minutes as root, outside that service. The
compliance script reads the snapshot when it's under 20 minutes old and
otherwise asks mdatp directly, so it still works on other distros.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 13:23:17 +00:00
Misthios 4d780e900a Merge pull request 'Misthios/fedora intune defender os version' (#5) from misthios/fedora-intune-defender-os-version into main
Build containers / Compute changes (push) Successful in 22s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m35s
Build containers / Prune old releases and tags (push) Successful in 26s
Reviewed-on: #5
2026-10-20 12:49:33 +00:00
Wesley van TilburgandClaude Opus 5.5 e8cfa8d4cf fedora-intune: don't repeat the version in the reported OS version
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 13s
Build containers / Prune old releases and tags (pull_request) Skipped
libhimmelblau reports "PRETTY_NAME VERSION_ID" as the OS version, and
Fedora's PRETTY_NAME already contains the version, so Entra showed
"Fedora Linux 44.20261020.0 (Forty Four) 44.20261020.0". In the
os-release the Himmelblau daemons see, set PRETTY_NAME to NAME, giving
"Fedora Linux 44.20261020.0". Nothing else in Himmelblau reads
PRETTY_NAME.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:48:44 +00:00
Wesley van TilburgandClaude Opus 5.5 5504f4e7aa fedora-intune: install the Defender managed config when Defender starts
The managed config shipped in /etc never reached updated devices:
mdatp-firstboot creates /etc/opt/microsoft/mdatp on the device, so ostree's
/etc merge treats that directory as local and doesn't add new image files
under it. VM 123 had the file in /usr/etc but not in /etc, and Defender
kept its defaults.

Ship it in /usr/share/mdatp-managed/ and install it into
/etc/opt/microsoft/mdatp/managed/ from an ExecStartPre in mdatp.service.
This also applies changes from newer images and undoes local edits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:48:44 +00:00
Misthios 561036582c Merge pull request 'Misthios/defender onboarding validation' (#4) from misthios/defender-onboarding-validation into main
Build containers / Compute changes (push) Successful in 21s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m40s
Build containers / Prune old releases and tags (push) Successful in 28s
Reviewed-on: #4
2026-10-20 12:23:30 +00:00
Wesley van TilburgandClaude Opus 5.5 86039eeb9d fedora-intune: enforce Defender protection settings
Defender's defaults left behavior monitoring off and every setting
unmanaged. Ship /etc/opt/microsoft/mdatp/managed/mdatp_managed.json:
real-time protection, behavior monitoring, PUA blocking, cloud-delivered
protection at the high block level, automatic definition updates and
sample submission, and scheduled scans (daily quick at 12:00, weekly full
on Wednesday 12:30, low priority). Users can't change managed settings.
The build fails if the file isn't valid JSON.

Network protection (Insiders channels only) and tamper protection
(audit-only preview on Linux) are left out.

The compliance script and rules now also check behavior monitoring and
cloud-delivered protection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:23:30 +00:00
Wesley van TilburgandClaude Opus 5.5 3afef0d821 fedora-intune: validate the onboarding JSON in the Defender script
A paste into Intune that lost the first "{" line produced an unreadable
mdatp_onboard.json, and Defender reported "No license found". Check that
the pasted JSON parses and has onboardingInfo before writing it, and log
what to fix otherwise.

The placeholder also appeared twice, so a find-and-replace of it broke
the script; match it by prefix so it appears only once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:23:30 +00:00
Misthios 07434cedab Merge pull request 'fedora-intune: grant sudo to the Linux admins Entra group' (#3) from misthios/fedora-intune-sudo-groups into main
Build containers / fedora-cosmic (44) (push) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (push) Canceled after 0s
Build containers / fedora-remote (44) (push) Canceled after 0s
Build containers / fedora-intune (44) (push) Canceled after 0s
Build containers / Compute changes (push) Canceled after 12s
Build containers / Prune old releases and tags (push) Successful in 26s
Reviewed-on: #3
2026-10-20 12:22:57 +00:00
Wesley van TilburgandClaude Opus 5.5 0e76505715 fedora-intune: grant sudo to the Linux admins Entra group
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 12s
Build containers / Prune old releases and tags (pull_request) Skipped
local_sudo_group was set to wheel but sudo_groups was empty, so no Entra
user ever got sudo. Members of the Entra group
c3b1cfcc-7834-47eb-948b-a22204276453 now get wheel at sign-in, and lose
it again after leaving the group.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:30:20 +00:00
Misthios f48a0ffaf6 Merge pull request 'Misthios/fedora intune os version defender' (#2) from misthios/fedora-intune-os-version-defender into main
Build containers / Compute changes (push) Successful in 20s
Build containers / fedora-intune (44) (push) Canceled after 0s
Build containers / fedora-cosmic (44) (push) Canceled after 33s
Build containers / fedora-cosmic (rawhide) (push) Canceled after 33s
Build containers / fedora-remote (44) (push) Canceled after 34s
Build containers / Prune old releases and tags (push) Successful in 24s
Reviewed-on: #2
2026-10-20 09:22:41 +00:00
Wesley van TilburgandClaude Opus 5.5 1dbe7f6895 ci: only rebuild changed images
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 9s
Build containers / Prune old releases and tags (pull_request) Skipped
The changes job ran on the bare runner, which has no git (the build jobs
install it before checkout). actions/checkout then fell back to a tarball
without history, `git diff` failed, `|| true` turned that into an empty
change list, and an empty list means "rebuild everything" - on every push
and PR.

Run the job in the buildroot container with git and nodejs, warn when the
diff fails instead of hiding it, treat a diff with no changes as "build
nothing", and let a manual run pick one image. The weekly scheduled run
still rebuilds all images for package updates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:21:08 +00:00
Wesley van TilburgandClaude Opus 5.5 2b78113124 fedora-intune: install Defender from Microsoft's Fedora 44 repo
The RHEL 10 repo was there for the Intune app, which only ships for RHEL;
that app is gone and the repo now only provides mdatp. Use Microsoft's
Fedora 44 repo instead: Fedora is a supported Defender distribution
(servers and the Linux desktop preview), and it carries a newer build
(101.26091.0003 vs 101.26081.0010 in the RHEL 10 repo). Same signing key
and signed metadata.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:19:16 +00:00
Wesley van TilburgandClaude Opus 5.5 6afa1d21e6 fedora-intune: cron for Intune config scripts, Defender remediation script
Himmelblau installs Intune Linux custom configuration scripts as
/etc/cron.d jobs, and the image had no cron, so a script policy failed to
apply. Add cronie and enable crond.

Add a custom configuration script that repairs Microsoft Defender on its
schedule: finishes first-boot setup, restarts a stopped daemon, onboards with
the tenant's mdatp_onboard.json (pasted into the script in Intune), and
re-enables real-time protection and updates definitions when needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:15:28 +00:00
Wesley van TilburgandClaude Opus 5.5 75c3b502d7 fedora-intune: report full image version to Intune, Defender compliance script
Himmelblau reports VERSION_ID from /etc/os-release as the device OS version,
so Intune showed "44". Give himmelblaud and himmelblaud-tasks their own
os-release (bind-mounted in their mount namespace only) with VERSION_ID set to
OSTREE_VERSION, e.g. 44.20261019.0. The rest of the system keeps the stock
VERSION_ID that toolbox, flatpak and dnf expect.

Add an Intune custom compliance discovery script and rules for Microsoft
Defender for Endpoint (installed, running, onboarded, licensed, healthy,
real-time protection, definitions up to date). These are uploaded to Intune,
not shipped in the image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:13:03 +00:00
Misthios 350b381200 Merge pull request 'fedora-intune: minimal GNOME workstation with Intune and Defender' (#1) from misthios/fedora-intune into main
Build containers / Compute changes (push) Successful in 2s
Build containers / fedora-remote (44) (push) Successful in 10m59s
Build containers / fedora-cosmic (rawhide) (push) Successful in 14m3s
Build containers / fedora-cosmic (44) (push) Successful in 16m7s
Build containers / fedora-intune (44) (push) Successful in 19m15s
Build containers / Prune old releases and tags (push) Successful in 28s
Reviewed-on: #1
2026-10-19 11:58:28 +00:00
MisthiosandClaude Opus 5.5 ad58569968 fedora-intune: Entra SSO in Microsoft Edge
Build containers / Compute changes (pull_request) Successful in 2s
Build containers / fedora-remote (44) (pull_request) Successful in 11m15s
Build containers / fedora-cosmic (rawhide) (pull_request) Successful in 13m15s
Build containers / fedora-cosmic (44) (pull_request) Successful in 15m8s
Build containers / fedora-intune (44) (pull_request) Successful in 11m50s
Build containers / Prune old releases and tags (pull_request) Skipped
himmelblau-sso registers its native messaging host and force-installs
the Linux Entra SSO extension only for Chrome, Chromium and Firefox.
Edge reads /etc/opt/edge, so ship the same two files there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 11:19:46 +00:00
MisthiosandClaude Opus 5.5 8e6fedc05e fedora-intune: use Himmelblau instead of the Microsoft Intune app
Build containers / Compute changes (pull_request) Successful in 8s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (44) (pull_request) Canceled after 7m11s
Build containers / Prune old releases and tags (pull_request) Skipped
Go with Entra ID sign-in through Himmelblau for fedora-intune and drop
the Microsoft Intune app (intune-portal, identity broker) variant. With
a single image, the workstation, Edge and Defender manifests move back
from images/shared into images/fedora-intune, and CI builds just that
image again. The Microsoft repo is now limited to mdatp and Edge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 11:07:54 +00:00
MisthiosandClaude Opus 5.5 4f71d350fa fedora-intune: fix boot issues, add Himmelblau variant
Build containers / Compute changes (pull_request) Successful in 2s
Build containers / fedora-remote (44) (pull_request) Successful in 11m9s
Build containers / fedora-cosmic (rawhide) (pull_request) Successful in 12m8s
Build containers / fedora-cosmic (44) (pull_request) Successful in 14m20s
Build containers / fedora-intune (44) (pull_request) Canceled after 10m35s
Build containers / fedora-intune-himmelblau (44) (pull_request) Canceled after 9m41s
Build containers / Prune old releases and tags (pull_request) Skipped
Fixes found by building and inspecting the image:

- Select the Intune authselect profile with-altfiles. Without it system
  users in /usr/lib/passwd (gdm, pipewire, mdatp) do not resolve and the
  login screen never starts. The build now fails if gdm does not resolve.
- Ship /opt as a plain directory (opt-usrlocal: root). Defender installs
  to /opt/microsoft/mdatp and keeps state in /var/opt/microsoft/mdatp;
  with the default /opt -> /var/opt symlink those were the same
  read-only directory. rpm-ostree still relocates /opt content to
  /usr/lib/opt, so finalize.d/60-opt-links links it back (only for images
  where /opt is a directory; fedora-cosmic/remote are unaffected).
- Run Defender's %post on first boot (mdatp-firstboot.service): in a
  compose /var is read-only and the machine GUID/state must not be
  shared. mdatp.service waits for it.

New image fedora-intune-himmelblau: the same workstation with Entra ID
sign-in through Himmelblau (Community Nightly packages), which joins the
device at the first sign-in and enrolls it in Intune (apply_policy)
instead of the Microsoft Intune app. Workstation, Edge and Defender
manifests move to images/shared so both images use them; build.sh copies
images/shared/*.yaml and CI builds both images in one matrix job.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 11:06:50 +00:00
MisthiosandClaude Opus 5.5 6f27bfb4dc fedora-intune: drop no-op preset for the D-Bus activated device broker
Build containers / Compute changes (pull_request) Successful in 1s
Build containers / fedora-remote (44) (pull_request) Successful in 10m37s
Build containers / fedora-cosmic (rawhide) (pull_request) Successful in 12m58s
Build containers / fedora-cosmic (44) (pull_request) Successful in 13m59s
Build containers / fedora-intune (44) (pull_request) Failing after 8m20s
Build containers / Prune old releases and tags (pull_request) Skipped
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 09:55:20 +00:00
MisthiosandClaude Opus 5.5 591ccd342e ci: skip registry login for pull requests
Build containers / Compute changes (pull_request) Successful in 2s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 1m25s
Build containers / fedora-cosmic (44) (pull_request) Canceled after 1m30s
Build containers / fedora-remote (44) (pull_request) Canceled after 1m30s
Build containers / Prune old releases and tags (pull_request) Skipped
PR builds never push, and Gitea gives them no secrets, so the login with
an empty token failed every PR build before it started.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 09:51:54 +00:00
MisthiosandClaude Opus 5.5 809bdc3bcb fedora-intune: minimal GNOME workstation with Intune and Defender
Build containers / Compute changes (pull_request) Successful in 1s
Build containers / fedora-cosmic (rawhide) (pull_request) Failing after 21s
Build containers / fedora-cosmic (44) (pull_request) Failing after 28s
Build containers / fedora-remote (44) (pull_request) Failing after 28s
Build containers / fedora-intune (44) (pull_request) Failing after 25s
Build containers / Prune old releases and tags (pull_request) Skipped
New image for testing Microsoft Intune enrollment and Linux compliance
policies: a minimal GNOME workstation (gnome-initial-setup, Ptyxis,
Files) with the Intune app, the Microsoft identity broker, Edge and
Defender for Endpoint (mdatp) from packages.microsoft.com (RHEL 10
builds, limited to those packages with includepkgs).

No Kanidm: the Intune app installs its own authselect profile with
pam_intune for the password compliance checks. Its %post cannot select
the profile or load the SELinux module during a compose, so both are
redone in postprocess.

build.sh now copies every *.yaml of an image, so manifests can be split
into more files than manifest.yaml and custom.yaml.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 09:50:01 +00:00
31 changed files with 893 additions and 17 deletions

No files matched your search

+84 -12
View File
@@ -2,6 +2,12 @@ name: Build containers
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
image:
description: Image to build
type: choice
options: [all, fedora-cosmic, fedora-remote, fedora-intune]
default: all
pull_request: pull_request:
branches: ["main"] branches: ["main"]
push: push:
@@ -15,10 +21,17 @@ jobs:
changes: changes:
name: Compute changes name: Compute changes
runs-on: job-v2 runs-on: job-v2
# The runner has no git: without it actions/checkout falls back to a
# tarball with no history, `git diff` fails and every image is rebuilt.
container:
image: "quay.io/fedora-ostree-desktops/buildroot:44"
outputs: outputs:
cosmic: ${{ steps.changes.outputs.cosmic }} cosmic: ${{ steps.changes.outputs.cosmic }}
remote: ${{ steps.changes.outputs.remote }} remote: ${{ steps.changes.outputs.remote }}
intune: ${{ steps.changes.outputs.intune }}
steps: steps:
- name: Install tools
run: dnf install -y nodejs git
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -26,20 +39,57 @@ jobs:
- name: Detect changed images - name: Detect changed images
id: changes id: changes
shell: bash shell: bash
env:
EVENT: ${{ github.event_name }}
BEFORE: ${{ github.event.before }}
SHA: ${{ github.sha }}
BASE_REF: ${{ github.base_ref }}
DISPATCH_IMAGE: ${{ inputs.image }}
run: | run: |
set -euo pipefail set -euo pipefail
if [[ "${{ github.event_name }}" == "push" \ # Diff the event's commits; any failure rebuilds everything, but
&& -n "${{ github.event.before }}" \ # say so instead of hiding it.
&& "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then diff_or_all() {
changed="$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" || true)" if ! changed="$(git diff --name-only "$@")"; then
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "::warning::git diff $* failed; rebuilding every image"
changed="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" || true)" changed=""
else elif [[ -z "${changed}" ]]; then
changed="" # Nothing changed (e.g. an empty merge): build nothing rather
fi # than everything.
changed="(none)"
fi
}
case "${EVENT}" in
push)
if [[ -n "${BEFORE}" && "${BEFORE}" != 0000000000000000000000000000000000000000 ]]; then
diff_or_all "${BEFORE}" "${SHA}"
else
echo "::warning::push without a previous commit; rebuilding every image"
changed=""
fi
;;
pull_request)
diff_or_all "origin/${BASE_REF}...HEAD"
;;
workflow_dispatch)
# Pretend the chosen image's directory changed; "all" (or no
# input) rebuilds everything.
if [[ -n "${DISPATCH_IMAGE}" && "${DISPATCH_IMAGE}" != all ]]; then
changed="images/${DISPATCH_IMAGE}/"
else
changed=""
fi
;;
*)
# schedule: the weekly rebuild picks up package updates for all.
changed=""
;;
esac
printf 'Changed paths:\n%s\n' "${changed:-(all)}"
eval "$(printf '%s\n' "${changed}" | ./ci-changes.sh)" eval "$(printf '%s\n' "${changed}" | ./ci-changes.sh)"
echo "cosmic=${cosmic}" >> "$GITHUB_OUTPUT" echo "cosmic=${cosmic}" >> "$GITHUB_OUTPUT"
echo "remote=${remote}" >> "$GITHUB_OUTPUT" echo "remote=${remote}" >> "$GITHUB_OUTPUT"
echo "intune=${intune}" >> "$GITHUB_OUTPUT"
build-cosmic: build-cosmic:
name: ${{ matrix.image }} (${{ matrix.distro }}) name: ${{ matrix.image }} (${{ matrix.distro }})
@@ -102,6 +152,8 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Log in to registry - name: Log in to registry
# Only the push step needs it, and PR builds get no secrets.
if: github.event_name != 'pull_request'
env: env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
@@ -170,10 +222,30 @@ jobs:
steps: *build_steps 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: prune:
name: Prune old releases and tags name: Prune old releases and tags
runs-on: job-v2 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. # Run even if some builds were skipped or failed.
if: ${{ always() && github.event_name != 'pull_request' }} if: ${{ always() && github.event_name != 'pull_request' }}
container: container:
@@ -191,7 +263,7 @@ jobs:
env: env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: | run: |
for image in fedora-cosmic fedora-remote; do for image in fedora-cosmic fedora-remote fedora-intune; do
./prune-releases.sh "${image}" ./prune-releases.sh "${image}"
done done
- name: Prune old registry tags - name: Prune old registry tags
@@ -203,6 +275,6 @@ jobs:
authfile=/tmp/prune-auth.json authfile=/tmp/prune-auth.json
echo "${REGISTRY_TOKEN}" | skopeo login -u "${REGISTRY_USERNAME}" \ echo "${REGISTRY_TOKEN}" | skopeo login -u "${REGISTRY_USERNAME}" \
--password-stdin --authfile "${authfile}" git.plabble.org --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 AUTHFILE="${authfile}" ./prune-registry.sh "${image}" Misthios 44 rawhide
done done
+1 -2
View File
@@ -191,8 +191,7 @@ case "${BUILD_MODE}" in
# Overlay our manifests/support files into the upstream tree so that # Overlay our manifests/support files into the upstream tree so that
# relative includes and *.repo discovery keep working. # relative includes and *.repo discovery keep working.
cp "${IMAGE_DIR}/manifest.yaml" "${BUILD_DIR}/upstream/" cp "${IMAGE_DIR}"/*.yaml "${BUILD_DIR}/upstream/"
cp "${IMAGE_DIR}/custom.yaml" "${BUILD_DIR}/upstream/"
if compgen -G "${IMAGE_DIR}/repos/*.repo" >/dev/null; then if compgen -G "${IMAGE_DIR}/repos/*.repo" >/dev/null; then
cp "${IMAGE_DIR}"/repos/*.repo "${BUILD_DIR}/upstream/" cp "${IMAGE_DIR}"/repos/*.repo "${BUILD_DIR}/upstream/"
fi fi
+8 -3
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Given changed paths on stdin, print `cosmic=<bool>` and `remote=<bool>` so the # Given changed paths on stdin, print `cosmic=<bool>`, `remote=<bool>` and
# workflow can skip images that didn't change. Shared files or build tooling # `intune=<bool>` so the workflow can skip images that didn't change. Shared
# changes rebuild everything; empty input (schedule/manual) rebuilds everything. # files or build tooling changes rebuild everything; empty input
# (schedule/manual) rebuilds everything.
set -euo pipefail set -euo pipefail
changed="$(cat || true)" changed="$(cat || true)"
@@ -13,12 +14,16 @@ grep -qE '^images/shared/' <<< "${changed}" && all=1
cosmic=false cosmic=false
remote=false remote=false
intune=false
if [[ "${all}" == 1 ]]; then if [[ "${all}" == 1 ]]; then
cosmic=true cosmic=true
remote=true remote=true
intune=true
fi fi
grep -qE '^images/fedora-cosmic/' <<< "${changed}" && cosmic=true grep -qE '^images/fedora-cosmic/' <<< "${changed}" && cosmic=true
grep -qE '^images/fedora-remote/' <<< "${changed}" && remote=true grep -qE '^images/fedora-remote/' <<< "${changed}" && remote=true
grep -qE '^images/fedora-intune/' <<< "${changed}" && intune=true
echo "cosmic=${cosmic}" echo "cosmic=${cosmic}"
echo "remote=${remote}" echo "remote=${remote}"
echo "intune=${intune}"
+10
View File
@@ -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; Microsoft's Defender repo (fedora/44) has no rawhide build.
declare -A UPSTREAM_REFS=(
[44]=f44
)
+55
View File
@@ -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
+60
View File
@@ -0,0 +1,60 @@
# Microsoft Defender for Endpoint on Linux (mdatp), Fedora 44 build, from the
# microsoft-fedora44 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-fedora44
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"]
# Enforced Defender settings (shown as "[managed]" in `mdatp health`; local
# changes are refused): real-time protection, behavior monitoring, PUA
# blocking, cloud protection at the "high" block level, automatic updates
# and sample submission, a daily quick scan at 12:00 and a weekly full scan
# on Wednesday 12:30 (low priority, up to 2 h later at random). Settings
# reference: https://learn.microsoft.com/defender-endpoint/linux-preferences
# Not set here: network protection (Insiders channels only) and tamper
# protection (audit-only preview on Linux).
- ["mdatp_managed.json", "/usr/share/mdatp-managed/mdatp_managed.json"]
# Installed into /etc/opt/microsoft/mdatp/managed/ whenever Defender starts;
# see the drop-in.
- ["mdatp-managed-config.conf", "/usr/lib/systemd/system/mdatp.service.d/20-managed-config.conf"]
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}"
# Fail the build on a typo rather than ship a config Defender can't read.
python3 -m json.tool /usr/share/mdatp-managed/mdatp_managed.json > /dev/null
rpm -q --qf '%{POSTIN}' mdatp > /usr/libexec/mdatp-postinst
chmod 0755 /usr/libexec/mdatp-firstboot /usr/libexec/mdatp-postinst
@@ -0,0 +1,9 @@
# Report the full image version (VERSION_ID=44.20261019.0) to Entra ID and
# Intune instead of "44"; see /usr/libexec/himmelblau-os-release. Only this
# service's mount namespace sees the replacement os-release.
[Unit]
Requires=himmelblau-os-release.service
After=himmelblau-os-release.service
[Service]
BindReadOnlyPaths=/run/himmelblau-os-release:/etc/os-release
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
# Himmelblau daemons; they pull in their sockets with Requires=.
enable himmelblaud.service
enable himmelblaud-tasks.service
enable himmelblau-hsm-pin-init.service
# Runs the Intune custom configuration scripts Himmelblau puts in /etc/cron.d.
enable crond.service
@@ -0,0 +1,3 @@
enable gdm.service
enable qemu-guest-agent.service
enable sshd.service
@@ -0,0 +1,5 @@
{
"ExtensionInstallForcelist": [
"jlnfnnolkbjieggibinobhkjdfbpcohn;https://clients2.google.com/service/update2/crx"
]
}
@@ -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/"
]
}
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Write the os-release that the Himmelblau daemons see (10-os-release.conf):
# the image's /usr/lib/os-release with VERSION_ID set to the full image version
# (OSTREE_VERSION, e.g. 44.20261019.0), so Intune shows the build and not just
# "44". Himmelblau reports VERSION_ID as the device's OS version and has no
# setting for it. The rest of the system keeps the stock VERSION_ID, which
# toolbox, flatpak and dnf expect to be the plain release.
#
# PRETTY_NAME becomes just NAME: libhimmelblau reports "PRETTY_NAME VERSION_ID"
# as the OS version string, and Fedora's PRETTY_NAME already contains the
# version ("Fedora Linux 44.20261020.0 (Forty Four) 44.20261020.0"). Nothing
# else in Himmelblau reads PRETTY_NAME.
set -euo pipefail
SRC=/usr/lib/os-release
DEST=/run/himmelblau-os-release
version="$(. "${SRC}" && echo "${OSTREE_VERSION:-${IMAGE_VERSION:-}}")"
name="$(. "${SRC}" && echo "${NAME:-Linux}")"
tmp="$(mktemp "${DEST}.XXXXXX")"
if [[ -n "${version}" ]]; then
sed -e "s/^VERSION_ID=.*/VERSION_ID=${version}/" \
-e "s/^PRETTY_NAME=.*/PRETTY_NAME=\"${name}\"/" "${SRC}" > "${tmp}"
else
# No image version (not an ostree/bootc deployment): pass it through as-is.
cat "${SRC}" > "${tmp}"
fi
chmod 0644 "${tmp}"
mv -f "${tmp}" "${DEST}"
@@ -0,0 +1,10 @@
[Unit]
Description=Generate the os-release reported by Himmelblau to Intune
DefaultDependencies=no
After=local-fs.target
Before=himmelblaud.service himmelblaud-tasks.service
[Service]
Type=oneshot
ExecStart=/usr/libexec/himmelblau-os-release
RemainAfterExit=yes
@@ -0,0 +1,20 @@
# 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
# Linux admins: members get wheel (sudo) at sign-in, and lose it again within
# local_groups_reconcile_interval after leaving the group.
sudo_groups = c3b1cfcc-7834-47eb-948b-a22204276453
# Home directories named after the account (e.g. /home/jdoe), with a UPN alias.
home_attr = CN
home_alias = SPN
use_etc_skel = true
@@ -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
@@ -0,0 +1,3 @@
# Don't start Defender before mdatp-firstboot.service has created its state.
[Unit]
ConditionPathExists=/var/lib/mdatp-firstboot.done
@@ -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
@@ -0,0 +1,7 @@
# Install the image's enforced Defender settings each time Defender starts.
# The image can't ship them in /etc directly: mdatp-firstboot creates
# /etc/opt/microsoft/mdatp on the device, so on updates that directory counts
# as local and ostree's /etc merge never adds new files under it. Copying on
# every start also picks up changes from newer images and undoes local edits.
[Service]
ExecStartPre=+/usr/bin/install -D -Z -m 0644 /usr/share/mdatp-managed/mdatp_managed.json /etc/opt/microsoft/mdatp/managed/mdatp_managed.json
@@ -0,0 +1,29 @@
{
"antivirusEngine": {
"enforcementLevel": "real_time",
"behaviorMonitoring": "enabled",
"scanAfterDefinitionUpdate": true,
"scanArchives": true,
"disallowedThreatActions": ["allow", "restore"],
"threatTypeSettingsMergePolicy": "admin_only",
"threatTypeSettings": [
{ "key": "potentially_unwanted_application", "value": "block" },
{ "key": "archive_bomb", "value": "audit" }
],
"scheduledScan": "enabled"
},
"scheduledScan": {
"dailyConfiguration": { "timeOfDay": 720 },
"weeklyConfiguration": { "dayOfWeek": 4, "scanType": "full", "timeOfDay": 750 },
"lowPriorityScheduledScan": true,
"checkForDefinitionsUpdate": true,
"randomizeScanStartTime": 2
},
"cloudService": {
"enabled": true,
"diagnosticLevel": "required",
"automaticSampleSubmissionConsent": "safe",
"automaticDefinitionUpdateEnabled": true,
"cloudBlockLevel": "high"
}
}
+77
View File
@@ -0,0 +1,77 @@
# 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
# Intune custom configuration scripts are installed as /etc/cron.d jobs.
- cronie
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"]
# Report the full image version (44.20261019.0) as the OS version in Intune.
- ["himmelblau-os-release", "/usr/libexec/himmelblau-os-release"]
- ["himmelblau-os-release.service", "/usr/lib/systemd/system/himmelblau-os-release.service"]
- ["10-himmelblau-os-release.conf", "/usr/lib/systemd/system/himmelblaud.service.d/10-os-release.conf"]
- ["10-himmelblau-os-release.conf", "/usr/lib/systemd/system/himmelblaud-tasks.service.d/10-os-release.conf"]
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
chmod 0755 /usr/libexec/himmelblau-os-release
test -d /etc/cron.d
# 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
@@ -0,0 +1,130 @@
{
"Rules": [
{
"SettingName": "DefenderInstalled",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not installed.",
"Description": "Microsoft Defender for Endpoint (mdatp) must be installed on this device."
}
]
},
{
"SettingName": "DefenderRunning",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not running.",
"Description": "The Microsoft Defender service (mdatp.service) is not responding. Restart the device or contact IT."
}
]
},
{
"SettingName": "DefenderOnboarded",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not onboarded.",
"Description": "This device is not onboarded to your organization's Microsoft Defender tenant. Contact IT."
}
]
},
{
"SettingName": "DefenderLicensed",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not licensed.",
"Description": "Microsoft Defender has no valid license on this device. Contact IT."
}
]
},
{
"SettingName": "DefenderHealthy",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender reports a problem.",
"Description": "Run 'mdatp health' to see the reported issues, or contact IT."
}
]
},
{
"SettingName": "DefenderRealTimeProtection",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Real-time protection is off.",
"Description": "Microsoft Defender real-time protection must be enabled."
}
]
},
{
"SettingName": "DefenderBehaviorMonitoring",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Behavior monitoring is off.",
"Description": "Microsoft Defender behavior monitoring must be enabled. Update the device to the latest image or contact IT."
}
]
},
{
"SettingName": "DefenderCloudProtection",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Cloud-delivered protection is off.",
"Description": "Microsoft Defender cloud-delivered protection must be enabled. Update the device to the latest image or contact IT."
}
]
},
{
"SettingName": "DefenderDefinitionsUpToDate",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender definitions are out of date.",
"Description": "Connect the device to the network so Microsoft Defender can update its security intelligence."
}
]
}
]
}
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# Intune custom compliance discovery script for Linux: is Microsoft Defender
# for Endpoint (mdatp) installed, onboarded, licensed and protecting?
#
# Upload in Intune > Endpoint security > Device compliance > Scripts (Linux),
# and pair it with defender-compliance.json in a Linux compliance policy
# (Custom compliance). Himmelblau runs it as root inside
# himmelblaud-tasks.service and sends the JSON on stdout to Intune, which
# evaluates it against the rules. Every setting is always emitted, so a missing
# or stopped Defender reads as non-compliant instead of as a script error.
MDATP=/usr/bin/mdatp
# Defender won't answer `mdatp health` started from inside himmelblaud-tasks
# (it can't read that process while checking the caller), so ask through
# systemd, which runs mdatp as its own child. Elsewhere, ask mdatp directly.
health="$(systemd-run --quiet --wait --pipe --collect "${MDATP}" health 2>/dev/null)"
grep -q '^healthy' <<< "${health}" || health="$("${MDATP}" health 2>/dev/null)"
# Print one field of the `mdatp health` output ("name : value") without
# quotes or a trailing " [managed]".
field() {
grep -m1 "^$1 *:" <<< "${health}" \
| sed -e 's/^[^:]*: *//' -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//'
}
bool() {
[[ "$1" == true ]] && echo true || echo false
}
installed=false
running=false
healthy=false
licensed=false
onboarded=false
rtp=false
behavior_monitoring=false
cloud_protection=false
definitions_ok=false
definitions_age=-1
version=""
if [[ -x "${MDATP}" ]]; then
installed=true
if [[ "$(field healthy)" =~ ^(true|false)$ ]]; then
# The daemon answered.
running=true
healthy="$(bool "$(field healthy)")"
licensed="$(bool "$(field licensed)")"
rtp="$(bool "$(field real_time_protection_enabled)")"
[[ "$(field behavior_monitoring)" == enabled ]] && behavior_monitoring=true
cloud_protection="$(bool "$(field cloud_enabled)")"
[[ -n "$(field org_id)" ]] && onboarded=true
[[ "$(field definitions_status)" == up_to_date ]] && definitions_ok=true
age="$(field definitions_updated_minutes_ago)"
[[ "${age}" =~ ^[0-9]+$ ]] && definitions_age=$(( age / 1440 ))
version="$(field app_version)"
version="${version//[^0-9A-Za-z._-]/}"
fi
fi
cat <<EOF
{
"DefenderInstalled": ${installed},
"DefenderRunning": ${running},
"DefenderHealthy": ${healthy},
"DefenderLicensed": ${licensed},
"DefenderOnboarded": ${onboarded},
"DefenderRealTimeProtection": ${rtp},
"DefenderBehaviorMonitoring": ${behavior_monitoring},
"DefenderCloudProtection": ${cloud_protection},
"DefenderDefinitionsUpToDate": ${definitions_ok},
"DefenderDefinitionsAgeDays": ${definitions_age},
"DefenderVersion": "${version}"
}
EOF
+110
View File
@@ -0,0 +1,110 @@
#!/bin/bash
# Intune custom configuration script for Linux: bring Microsoft Defender for
# Endpoint (mdatp) back to the state defender-compliance.sh checks for.
#
# Intune has no "run only when non-compliant" trigger for Linux, so this runs
# on the policy's schedule and only acts on what is wrong; on a healthy device
# it only reads `mdatp health`. Himmelblau installs it as an /etc/cron.d job.
#
# Intune > Devices > Configuration > Create > Linux > Settings catalog >
# Custom Configuration:
# Execution context: root
# Execution frequency: every 1 hour (or 15 minutes while rolling out)
# Execution retries: 1
# Script: this file, with ONBOARDING_JSON filled in
#
# Exit status is non-zero when Defender is still not healthy afterwards.
# Contents of mdatp_onboard.json from the Defender portal: Settings > Endpoints
# > Onboarding > Linux Server > "Local Script (Python)". The downloaded
# MicrosoftDefenderATPOnboardingLinuxServer.py writes this file to
# /etc/opt/microsoft/mdatp/ on the machine it runs on; paste that file's
# contents here. Leave the placeholder to skip onboarding.
ONBOARDING_JSON=$(cat <<'JSON'
REPLACE_WITH_MDATP_ONBOARD_JSON
JSON
)
MDATP=/usr/bin/mdatp
ONBOARD_FILE=/etc/opt/microsoft/mdatp/mdatp_onboard.json
log() {
logger -t defender-remediation -- "$*"
echo "$*"
}
# Print one `mdatp health` field without quotes, a trailing " [managed]" or
# the "ATTENTION: ..." lines mdatp prints while unlicensed.
field() {
"${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \
| sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//'
}
# Wait up to 2 minutes for the daemon to answer.
wait_for_daemon() {
local i
for i in $(seq 1 24); do
[[ "$(field healthy)" =~ ^(true|false)$ ]] && return 0
sleep 5
done
return 1
}
if [[ ! -x "${MDATP}" ]]; then
log "mdatp is not installed; it ships in the image, so update or rebase the device."
exit 1
fi
# The image finishes the Defender install on first boot (mdatp-firstboot).
if [[ ! -f /var/lib/mdatp-firstboot.done ]]; then
log "Defender first-boot setup has not completed; starting it."
systemctl start mdatp-firstboot.service
fi
if ! systemctl is-active --quiet mdatp.service; then
log "mdatp.service is not running; restarting it."
systemctl restart mdatp.service
fi
if ! wait_for_daemon; then
log "The Defender daemon does not respond."
exit 1
fi
if [[ -z "$(field org_id)" ]]; then
# Prefix match, so the placeholder appears only once in this file and a
# find-and-replace of it can't hit this line.
if [[ "${ONBOARDING_JSON}" == REPLACE_WITH_* ]]; then
log "Defender is not onboarded and this script has no onboarding JSON."
elif ! printf '%s\n' "${ONBOARDING_JSON}" \
| python3 -c 'import json, sys; assert "onboardingInfo" in json.load(sys.stdin)' 2>/dev/null; then
# A paste that lost the first "{" or last "}" line would otherwise leave
# Defender with an unreadable onboarding file ("No license found").
log "The onboarding JSON in this script is not valid; paste mdatp_onboard.json again, including its first '{' and last '}' lines."
exit 1
else
log "Defender is not onboarded; writing ${ONBOARD_FILE}."
install -d -m 0755 "$(dirname "${ONBOARD_FILE}")"
printf '%s\n' "${ONBOARDING_JSON}" > "${ONBOARD_FILE}.tmp"
chown root:mdatp "${ONBOARD_FILE}.tmp"
chmod 0600 "${ONBOARD_FILE}.tmp"
mv -f "${ONBOARD_FILE}.tmp" "${ONBOARD_FILE}"
systemctl restart mdatp.service
wait_for_daemon || { log "The Defender daemon does not respond after onboarding."; exit 1; }
fi
fi
# Settings Defender's own policy manages show " [managed]" and can't be
# changed locally; this only fixes a locally disabled real-time protection.
if [[ "$(field real_time_protection_enabled)" != true ]]; then
log "Real-time protection is off; enabling it."
"${MDATP}" config real-time-protection --value enabled
fi
if [[ "$(field definitions_status)" != up_to_date ]]; then
log "Definitions are not up to date; updating them."
"${MDATP}" definitions update
fi
healthy="$(field healthy)"
log "healthy=${healthy} licensed=$(field licensed) org_id=$(field org_id) rtp=$(field real_time_protection_enabled) definitions=$(field definitions_status)"
[[ "${healthy}" == true ]]
+39
View File
@@ -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-fedora44
- microsoft-edge
include:
- base-atomic.yaml
- custom.yaml
# Entra ID sign-in and Intune.
- himmelblau.yaml
- microsoft-edge.yaml
- defender.yaml
+15
View File
@@ -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"]
@@ -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
+23
View File
@@ -0,0 +1,23 @@
# packages.microsoft.com, limited with includepkgs to the packages the image
# uses, so nothing else from the Microsoft repo can replace a Fedora package.
#
# Fedora 44 build of Defender (mdatp): Fedora is a supported Defender
# distribution, servers and desktops. Pinned to 44 rather than $releasever so a
# new Fedora release doesn't fail the build before Microsoft publishes its repo.
[microsoft-fedora44]
name=Microsoft Production (Fedora 44)
baseurl=https://packages.microsoft.com/fedora/44/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
+20
View File
@@ -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