Author SHA1 Message Date
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 884 additions and 14 deletions

No files matched your search

+81 -9
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=""
elif [[ -z "${changed}" ]]; then
# Nothing changed (e.g. an empty merge): build nothing rather
# 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 else
changed="" changed=""
fi 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."
}
]
}
]
}
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# Intune custom compliance discovery script for Linux: is Microsoft Defender
# for Endpoint (mdatp) installed, onboarded, licensed and protecting?
#
# Upload in Intune > Devices > 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
# Print one `mdatp health` field without quotes or a trailing " [managed]".
# Unlicensed or unonboarded, mdatp prints "ATTENTION: ..." lines first.
field() {
"${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \
| sed -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