You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2026/10/20 09:14:12 UTC

(airflow) 01/20: Base CI and PROD images on hardened Python images

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch hardened-python-base-images
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 1c816dfd331ac32534e122f7268fb145710d97ad
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sat Oct 10 09:45:45 2026 +0100

    Base CI and PROD images on hardened Python images
    
    Squashed from 19 commits (backup branch: backup/hardened-python-base-images-pre-rebase) before rebasing onto main.
    
    Generated-by: Claude Opus 5
    Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
---
 .github/workflows/mirror-hardened-base-images.yml  |  73 +++++
 Dockerfile                                         | 308 +++++++++-----------
 Dockerfile.ci                                      | 312 ++++++++++-----------
 airflow-core/newsfragments/73038.significant.rst   |  22 ++
 dev/breeze/doc/09_release_management_tasks.rst     |  28 ++
 dev/breeze/doc/ci/02_images.md                     |  18 +-
 .../doc/images/output_release-management.svg       |  38 +--
 .../doc/images/output_release-management.txt       |   2 +-
 ...utput_release-management_mirror-base-images.svg | 116 ++++++++
 ...utput_release-management_mirror-base-images.txt |   1 +
 .../output_setup_check-all-params-in-groups.svg    |  20 +-
 .../output_setup_check-all-params-in-groups.txt    |   2 +-
 .../output_setup_regenerate-command-images.svg     |   2 +-
 .../output_setup_regenerate-command-images.txt     |   2 +-
 .../commands/release_management_commands.py        |  82 +++++-
 .../commands/release_management_commands_config.py |   9 +
 dev/breeze/src/airflow_breeze/global_constants.py  |  21 +-
 .../airflow_breeze/params/common_build_params.py   |   3 +-
 dev/breeze/tests/test_mirror_base_images.py        |  87 ++++++
 docker-stack-docs/build-arg-ref.rst                |  14 +-
 docker-stack-docs/build.rst                        |  64 ++++-
 docker-stack-docs/changelog.rst                    |  35 +++
 .../customizing/add-build-essential-custom.sh      |   2 +-
 .../docker-examples/customizing/custom-sources.sh  |   2 +-
 .../customizing/pypi-dev-runtime-deps.sh           |   2 +-
 .../customizing/pypi-extras-and-deps.sh            |   2 +-
 .../customizing/pypi-selected-version.sh           |   2 +-
 .../restricted/restricted_environments.sh          |   2 +-
 docs/spelling_wordlist.txt                         |   1 +
 scripts/ci/prek/upgrade_important_versions.py      |  70 ++---
 scripts/docker/common.sh                           |  16 +-
 scripts/docker/install_mssql.sh                    |   2 +-
 scripts/docker/install_mysql.sh                    |   2 +-
 scripts/docker/install_os_dependencies.sh          | 254 +++++++----------
 scripts/docker/install_postgres.sh                 |   2 +-
 scripts/in_container/run_ci_tests.sh               |  11 +-
 .../ci/prek/test_upgrade_important_versions.py     |  38 +++
 37 files changed, 1066 insertions(+), 601 deletions(-)

diff --git a/.github/workflows/mirror-hardened-base-images.yml b/.github/workflows/mirror-hardened-base-images.yml
new file mode 100644
index 00000000000..8cd43953671
--- /dev/null
+++ b/.github/workflows/mirror-hardened-base-images.yml
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+name: Mirror hardened base images
+# Copies the Docker Hardened Image (https://dhi.io) tags that Airflow images are built on into a
+# public ghcr.io repository owned by the project.
+#
+# Pulling from dhi.io requires a Docker Hub login. Requiring one to build an Airflow image would
+# mean every contributor and every CI job needs Docker Hub credentials, so the tags are mirrored
+# instead and `BASE_IMAGE` defaults to the mirror. The images are Apache-2.0 licensed, so
+# redistributing them is allowed.
+#
+# `docker buildx imagetools create` copies the multi-platform manifest between registries without
+# pulling the layers into the runner, so a refresh of all Python versions is cheap.
+on:  # yamllint disable-line rule:truthy
+  workflow_dispatch:
+  schedule:
+    # Docker rebuilds the hardened images as CVEs are fixed, keeping the same tags. Re-running
+    # weekly is what picks those rebuilds up.
+    - cron: '27 4 * * 1'
+permissions:
+  contents: read
+concurrency:
+  group: mirror-hardened-base-images
+  cancel-in-progress: false
+jobs:
+  mirror:
+    name: "Mirror hardened base images"
+    runs-on: ["ubuntu-22.04"]
+    # Never runs on forks - they have neither the credentials nor the package to push to.
+    if: github.repository == 'apache/airflow'
+    permissions:
+      contents: read
+      packages: write
+    steps:
+      - name: "Cleanup repo"
+        shell: bash
+        run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
+        with:
+          persist-credentials: false
+      - name: "Install Breeze"
+        uses: ./.github/actions/breeze
+      # dhi.io accepts any Docker Hub account - the free tier covers the images we mirror - so this
+      # reuses the credentials the DockerHub release workflows already use.
+      - name: "Login to dhi.io"
+        env:
+          DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
+          DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
+        run: echo "${DOCKERHUB_TOKEN}" | docker login dhi.io -u "${DOCKERHUB_USER}" --password-stdin
+      - name: "Login to ghcr.io"
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          ACTOR: ${{ github.actor }}
+        run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${ACTOR}" --password-stdin
+      - name: "Mirror base images"
+        run: breeze release-management mirror-base-images
diff --git a/Dockerfile b/Dockerfile
index 8d31ff9c9d0..81a679ade9c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -50,23 +50,17 @@ ARG AIRFLOW_USER_HOME_DIR=/home/airflow
 # latest released version here
 ARG AIRFLOW_VERSION="3.3.2"
 
-ARG BASE_IMAGE="debian:bookworm-slim"
-ARG AIRFLOW_PYTHON_VERSION="3.13.16"
-
-# PYTHON_LTO: Controls whether Python is built with Link-Time Optimization (LTO).
-#
-# Link-Time Optimization uses MD5 checksums during the compilation process to verify
-# object files and intermediate representations. In FIPS-compliant environments, MD5
-# is blocked as it's not an approved cryptographic algorithm (see FIPS 140-2/140-3).
-# This can cause Python builds with LTO to fail when FIPS mode is enabled.
+# BASE_IMAGE: the Docker Hardened Image that Python comes from. Both the build stage and the final
+# image are based on it, so Python is never compiled during the build.
 #
-# When building FIPS-compliant images, set this to "false" to disable LTO:
-#   docker build --build-arg PYTHON_LTO="false" ...
+# The default is Airflow's public mirror of the upstream "dhi.io/python" image, so building needs no
+# registry credentials. The upstream image itself requires a "docker login dhi.io".
 #
-# Default: "true" (LTO enabled for better performance)
-#
-# Related: https://github.com/apache/airflow/issues/58337
-ARG PYTHON_LTO="true"
+# The FIPS-validated variants require a paid Docker subscription and cannot be mirrored or made the
+# default, but you can build a FIPS-compliant image by pointing the build at one:
+#   docker build . --build-arg BASE_IMAGE="dhi.io/python:3.13.16-debian12-fips-dev"
+ARG BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev"
+ARG AIRFLOW_PYTHON_VERSION="3.13.16"
 
 # You can swap comments between those two args to test pip from the main version
 # When you attempt to test if the version of `pip` from specified branch works for our builds
@@ -119,15 +113,13 @@ if [[ "$#" != 1 ]]; then
     exit 1
 fi
 
-AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.11.16}
-PYTHON_LTO=${PYTHON_LTO:-true}
 GOLANG_MAJOR_MINOR_VERSION=${GOLANG_MAJOR_MINOR_VERSION:-1.24.4}
 TEMURIN_VERSION=${TEMURIN_VERSION:-11}
 NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
 PNPM_VERSION=${PNPM_VERSION:-10.28.1}
 RUSTUP_DEFAULT_TOOLCHAIN=${RUSTUP_DEFAULT_TOOLCHAIN:-stable}
 RUSTUP_VERSION=${RUSTUP_VERSION:-1.29.0}
-COSIGN_VERSION=${COSIGN_VERSION:-3.0.5}
+PYTHON_HOME=${PYTHON_HOME:-/opt/python}
 
 if [[ "${1}" == "runtime" ]]; then
     INSTALLATION_TYPE="RUNTIME"
@@ -154,38 +146,25 @@ freetds-dev \
 git \
 graphviz \
 graphviz-dev \
+gzip \
 krb5-user \
-lcov \
 ldap-utils \
-libbluetooth-dev \
-libbz2-dev \
 libc6-dev \
-libdb-dev \
 libev-dev \
 libev4 \
 libffi-dev \
-libgdbm-compat-dev \
-libgdbm-dev \
 libgeos-dev \
 libkrb5-dev \
 libldap2-dev \
 libleveldb-dev \
 libleveldb1d \
-liblzma-dev \
-libncurses5-dev \
-libreadline6-dev \
 libsasl2-2 \
 libsasl2-dev \
 libsasl2-modules \
-libsqlite3-dev \
 libssl-dev \
 libxmlsec1 \
 libxmlsec1-dev \
-libzstd-dev \
 locales \
-lsb-release \
-lzma \
-lzma-dev \
 openssh-client \
 openssl \
 pkg-config \
@@ -194,10 +173,8 @@ sasl2-bin \
 sqlite3 \
 sudo \
 tdsodbc \
-tk-dev \
 unixodbc \
 unixodbc-dev \
-uuid-dev \
 wget \
 xz-utils \
 zlib1g-dev \
@@ -224,6 +201,9 @@ netcat-openbsd\
     echo
     echo "APPLIED INSTALLATION CONFIGURATION FOR DEBIAN VERSION: ${debian_version}"
     echo
+    # libxmlsec1-openssl was added because libxmlsec1 ships no crypto engine of its own - the engines
+    # are separate packages - so the "xmlsec" module (pulled in by python3-saml) imported with
+    # "libxmlsec1-openssl.so.1: cannot open shared object file" without it.
     if [[ "${RUNTIME_APT_DEPS=}" == "" ]]; then
         RUNTIME_APT_DEPS="\
 ${debian_version_apt_deps} \
@@ -242,8 +222,8 @@ libgeos-dev \
 libsasl2-2 \
 libsasl2-modules \
 libxmlsec1 \
+libxmlsec1-openssl \
 locales \
-lsb-release \
 openssh-client \
 rsync \
 sasl2-bin \
@@ -272,10 +252,42 @@ function install_docker_cli() {
     apt-get install -y --no-install-recommends docker-ce-cli
 }
 
+function keep_image_conffiles() {
+    # The hardened base images ship a modified /etc/debian_version, so any apt run that upgrades
+    # base-files stops at dpkg's interactive conffile prompt and fails a non-interactive build. Making
+    # dpkg keep the image's copy by default covers every apt run - this build's and those of images
+    # extending it - so no install command has to remember the flags.
+    mkdir -p /etc/dpkg/dpkg.cfg.d
+    printf '%s\n' force-confdef force-confold > /etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles
+}
+
+function restore_debian_base_files() {
+    # The hardened base images ship a minimal /etc, but Debian maintainer scripts assume the files
+    # a stock Debian has: sasl2-bin chowns its run directory to the "sasl" group from base-passwd,
+    # and tmux registers its shell with add-shell, which reads /etc/shells. The base-passwd package
+    # only ships the reference copies of the account files - update-passwd is what merges them
+    # into /etc.
+    # libpam-runtime generates the /etc/pam.d/common-* files that the PAM configs already in the
+    # image "@include" - without them "adduser --gecos" aborts with a PAM error from chfn.
+    # Debian Policy lets a maintainer script rely on any essential package without declaring it, and
+    # libgcrypt20 takes that up: its postinst runs a helper with a "#!/bin/dash" shebang. The hardened
+    # images ship /bin/sh as a symlink to bash and no dash at all, so configuring the package aborts
+    # with exit 127 and takes the whole apt transaction with it. Installing it first restores the
+    # assumption, and hands /bin/sh back to dash the way a stock Debian has it.
+    apt-get install -y --no-install-recommends dash
+    apt-get install -y --no-install-recommends base-passwd libpam-runtime
+    update-passwd
+    if [[ ! -e /etc/shells ]]; then
+        printf '%s\n' "# /etc/shells: valid login shells" /bin/sh /bin/bash > /etc/shells
+    fi
+}
+
 function install_debian_dev_dependencies() {
+    keep_image_conffiles
     apt-get update
     apt-get install -yqq --no-install-recommends apt-utils >/dev/null 2>&1
-    apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
+    restore_debian_base_files
+    apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
     # shellcheck disable=SC2086
     export ${ADDITIONAL_DEV_APT_ENV?}
     if [[ ${DEV_APT_COMMAND} != "" ]]; then
@@ -305,14 +317,25 @@ function install_additional_dev_dependencies() {
 }
 
 function link_python() {
+    # Airflow images have always exposed Python under /usr/python - documentation, volume mounts and
+    # user customizations refer to that path - while the hardened base images ship it in /opt/python,
+    # so keep the historical location working as a symlink.
+    if [[ ! -e /usr/python ]]; then
+        ln -sv "${PYTHON_HOME}" /usr/python
+    fi
+    # The hardened base images have no /usr/local tree at all
+    mkdir -p /usr/local/bin /usr/local/lib
     # link python binaries to /usr/local/bin and /usr/python/bin with and without 3 suffix
     # Links in /usr/local/bin are needed for tools that expect python to be there
     # Links in /usr/python/bin are needed for tools that are detecting home of python installation including
     # lib/site-packages. The /usr/python/bin should be first in PATH in order to help with the last part.
     for dst in pip3 python3 python3-config; do
         src="$(echo "${dst}" | tr -d 3)"
+        if [[ ! -e "/usr/python/bin/${dst}" ]]; then
+            continue
+        fi
         echo "Linking ${dst} in /usr/local/bin and /usr/python/bin"
-        ln -sv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
+        ln -sfv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
         for dir in /usr/local/bin /usr/python/bin; do
             if [[ ! -e "${dir}/${src}" ]]; then
                 echo "Creating ${src} - > ${dst} link in ${dir}"
@@ -332,62 +355,47 @@ function link_python() {
     ldconfig
 }
 
-function install_debian_runtime_dependencies() {
-    apt-get update
-    apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
-    apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
-    # shellcheck disable=SC2086
-    export ${ADDITIONAL_RUNTIME_APT_ENV?}
-    if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
-        bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
-    fi
-    if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
-        bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
-    fi
-    apt-get update
-    # shellcheck disable=SC2086
-    apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
-    apt-get autoremove -yqq --purge
-    apt-get clean
-    link_python
-    rm -rf /var/lib/apt/lists/* /var/log/*
+function restore_thread_stack_size() {
+    # The hardened base images build Python with -DTHREAD_STACK_SIZE=0x100000, so every thread Python
+    # starts gets a 1 MiB stack instead of glibc's default (the 8 MiB "ulimit -s" the previous images
+    # used). On Python 3.12 and 3.13 the C recursion guard is a fixed depth count sized for the larger
+    # stack, so deeply nested input - e.g. a JSON body parsed in a web server worker thread - overflows
+    # the stack and kills the process instead of raising RecursionError. A .pth file runs at every
+    # interpreter start-up without taking the sitecustomize module name users may already rely on.
+    local site_packages
+    site_packages="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
+    echo "Restoring the 8 MiB default thread stack size in ${site_packages}"
+    echo "import threading; threading.stack_size(8 * 1024 * 1024)" > "${site_packages}/airflow-thread-stack-size.pth"
 }
 
-function install_cosign() {
-    local arch
-    arch="$(dpkg --print-architecture)"
-    declare -A cosign_sha256s=(
-        # https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign_checksums.txt
-        [amd64]="db15cc99e6e4837daabab023742aaddc3841ce57f193d11b7c3e06c8003642b2"
-        [arm64]="d098f3168ae4b3aa70b4ca78947329b953272b487727d1722cb3cb098a1a20ab"
-    )
-    local cosign_sha256="${cosign_sha256s[${arch}]}"
-    if [[ -z "${cosign_sha256}" ]]; then
-        echo "Unsupported architecture for cosign: ${arch}"
-        exit 1
-    fi
-    curl -fsSL --retry 3 --retry-delay 5 \
-        "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-${arch}" \
-        -o /tmp/cosign
-    echo "${cosign_sha256}  /tmp/cosign" | sha256sum --check
-    chmod +x /tmp/cosign
+function compile_python_stdlib() {
+    # The hardened base images ship the standard library with no .pyc files at all. Python then misses
+    # the bytecode cache on every stdlib import, and in a read-only or non-writable directory it cannot
+    # create one - each miss leaves a negative dentry in the kernel, which grows without bound in a
+    # long-running container and can exhaust memory on the host. Compiling the standard library here
+    # restores what the previously compiled-in-image Python shipped.
+    # See https://github.com/apache/airflow/pull/58944 and https://lwn.net/Articles/814535/
+    local stdlib
+    stdlib="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["stdlib"])')"
+    echo "Compiling Python standard library in ${stdlib}"
+    # compileall exits non-zero when any file fails to compile, and the standard library ships files
+    # that are meant not to compile (deliberately broken syntax used by the test suite).
+    /usr/python/bin/python -m compileall -q -j "$(nproc)" -o 0 -o 1 -o 2 "${stdlib}" || true
 }
 
-function install_python() {
-    # If system python (3.11 in bookworm) is installed (via automatic installation of some dependencies for example), we need
-    # to fail and make sure that it is not there, because there can be strange interactions if we install
-    # newer version and system libraries are installed, because
-    # when you create a virtualenv part of the shared libraries of Python can be taken from the system
-    # Installation leading to weird errors when you want to install some modules - for example when you install ssl:
+function check_no_system_python() {
+    # Python comes from the hardened base image (in /opt/python) and must stay the only Python in the
+    # image. A system Python pulled in as a dependency of an apt package shares its shared libraries
+    # with ours and leads to errors such as:
     # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
     if dpkg -l | grep '^ii' | grep '^ii  libpython' >/dev/null; then
         echo
         echo "ERROR! System python is installed by one of the previous steps"
         echo
-        echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython3.11 is installed:"
+        echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython is installed:"
         echo
         apt-get install -yqq aptitude >/dev/null
-        aptitude why libpython3.11
+        aptitude why "$(dpkg -l | grep '^ii  libpython' | head -1 | awk '{print $2}')"
         echo
         exit 1
     else
@@ -395,86 +403,32 @@ function install_python() {
         echo "GOOD! System python is not installed - OK"
         echo
     fi
-    wget --tries=3 --waitretry=5 -O python.tar.xz "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz"
-    local major_minor_version
-    major_minor_version="${AIRFLOW_PYTHON_VERSION%.*}"
-    echo "Verifying Python ${AIRFLOW_PYTHON_VERSION} (${major_minor_version})"
-    # Sigstore verification (PEP 761)
-    declare -A sigstore_identities=(
-        # https://peps.python.org/pep-0664/#release-manager-and-crew
-        [3.11]="pablogsal@python.org"
-        # https://peps.python.org/pep-0693/#release-manager-and-crew
-        [3.12]="thomas@python.org"
-        # https://peps.python.org/pep-0719/#release-manager-and-crew
-        [3.13]="thomas@python.org"
-        # https://peps.python.org/pep-0745/#release-manager-and-crew
-        [3.14]="hugo@python.org"
-        # https://peps.python.org/pep-0790/#release-manager-and-crew
-        [3.15]="hugo@python.org"
-    )
-    declare -A sigstore_issuers=(
-        [3.11]="https://accounts.google.com"
-        [3.12]="https://accounts.google.com"
-        [3.13]="https://accounts.google.com"
-        [3.14]="https://github.com/login/oauth"
-        [3.15]="https://github.com/login/oauth"
-    )
-    wget --tries=3 --waitretry=5 -O python.tar.xz.sigstore \
-        "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz.sigstore"
-    install_cosign
-    local identity="${sigstore_identities[${major_minor_version}]}"
-    local issuer="${sigstore_issuers[${major_minor_version}]}"
-    /tmp/cosign verify-blob \
-        --bundle python.tar.xz.sigstore \
-        --certificate-identity "${identity}" \
-        --certificate-oidc-issuer "${issuer}" \
-        python.tar.xz
-    rm -f python.tar.xz.sigstore /tmp/cosign
-    mkdir -p /usr/src/python
-    tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz
-    rm python.tar.xz
-    cd /usr/src/python
-    arch="$(dpkg --print-architecture)"; arch="${arch##*-}"
-    gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"
-    EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"
-    EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer";
-    LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"
-    LDFLAGS="${LDFLAGS:--Wl},--strip-all"
-    # Link-Time Optimization (LTO) uses MD5 checksums for object file verification during
-    # compilation. In FIPS mode, MD5 is blocked as a non-approved algorithm, causing builds
-    # to fail. The PYTHON_LTO variable allows disabling LTO for FIPS-compliant builds.
-    # See: https://github.com/apache/airflow/issues/58337
-    local lto_option=""
-    if [[ "${PYTHON_LTO:-true}" == "true" ]]; then
-        lto_option="--with-lto"
+}
+
+function install_debian_runtime_dependencies() {
+    keep_image_conffiles
+    apt-get update
+    apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
+    restore_debian_base_files
+    apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
+    # shellcheck disable=SC2086
+    export ${ADDITIONAL_RUNTIME_APT_ENV?}
+    if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
+        bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
     fi
-    local build_log
-    build_log=$(mktemp)
-    echo "Building Python ${AIRFLOW_PYTHON_VERSION} from source..."
-    if ! (
-        ./configure --enable-optimizations --prefix=/usr/python/ --with-ensurepip --build="$gnuArch" \
-            --enable-loadable-sqlite-extensions --enable-option-checking=fatal \
-                --enable-shared ${lto_option} && \
-        make -s -j "$(nproc)" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
-            "LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" python && \
-        make -s -j "$(nproc)" install
-    ) > "${build_log}" 2>&1; then
-        echo
-        echo "ERROR! Python build failed. Build output:"
-        echo
-        cat "${build_log}"
-        rm -f "${build_log}"
-        exit 1
+    if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
+        bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
     fi
-    rm -f "${build_log}"
-    cd /
-    rm -rf /usr/src/python
-    find /usr/python -depth \
-      \( \
-        \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
-        -o \( -type f -a \( -name 'libpython*.a' \) \) \
-    \) -exec rm -rf '{}' +
+    apt-get update
+    # shellcheck disable=SC2086
+    apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
+    apt-get autoremove -yqq --purge
+    apt-get clean
+    check_no_system_python
     link_python
+    restore_thread_stack_size
+    compile_python_stdlib
+    rm -rf /var/lib/apt/lists/* /var/log/*
 }
 
 function install_golang() {
@@ -573,7 +527,10 @@ if [[ "${INSTALLATION_TYPE}" == "RUNTIME" ]]; then
 else
     get_dev_apt_deps
     install_debian_dev_dependencies
-    install_python
+    check_no_system_python
+    link_python
+    restore_thread_stack_size
+    compile_python_stdlib
     install_additional_dev_dependencies
     install_rustup
     if [[ "${INSTALLATION_TYPE}" == "CI" ]]; then
@@ -677,7 +634,7 @@ install_mariadb_client() {
     echo "${COLOR_YELLOW}MariaDB client protocol-compatible with MySQL client.${COLOR_RESET}"
     echo
 
-    echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(lsb_release -cs) main" > \
+    echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(common::debian_codename) main" > \
         /etc/apt/sources.list.d/mariadb.list
     # Make sure that dependencies from MariaDB repo are preferred over Debian dependencies
     printf "Package: *\nPin: release o=MariaDB\nPin-Priority: 999\n" > /etc/apt/preferences.d/mariadb
@@ -721,7 +678,7 @@ function install_mssql_client() {
     echo "${COLOR_BLUE}Installing mssql client${COLOR_RESET}"
     echo
 
-    echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(lsb_release -rs)/prod $(lsb_release -cs) main" > \
+    echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(common::debian_release)/prod $(common::debian_codename) main" > \
         /etc/apt/sources.list.d/mssql-release.list &&
     mkdir -p /opt/microsoft/msodbcsql18 &&
     touch /opt/microsoft/msodbcsql18/ACCEPT_EULA &&
@@ -765,7 +722,7 @@ install_postgres_client() {
 
     common::import_trusted_gpg "7FCC7D46ACCC4CF8" "postgres"
 
-    echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > \
+    echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(common::debian_codename)-pgdg main" > \
         /etc/apt/sources.list.d/pgdg.list
     apt-get update
     apt-get install --no-install-recommends -y "${packages[@]}"
@@ -912,12 +869,12 @@ function common::get_constraints_location() {
 
 function common::show_packaging_tool_version_and_location() {
    echo "PATH=${PATH}"
-   echo "Installed pip: $(pip --version): $(which pip)"
+   echo "Installed pip: $(pip --version): $(command -v pip)"
    if [[ ${PACKAGING_TOOL} == "pip" ]]; then
        echo "${COLOR_BLUE}Using 'pip' to install Airflow${COLOR_RESET}"
    else
        echo "${COLOR_BLUE}Using 'uv' to install Airflow${COLOR_RESET}"
-       echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(which uv 2>/dev/null)"
+       echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(command -v uv 2>/dev/null)"
    fi
 }
 
@@ -929,7 +886,7 @@ function common::install_packaging_tools() {
         echo
     else
         echo
-        echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(which python)${COLOR_RESET}"
+        echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(command -v python)${COLOR_RESET}"
         echo
     fi
     if [[ ${AIRFLOW_PIP_VERSION=} == "" ]]; then
@@ -993,6 +950,16 @@ function common::install_packaging_tools() {
     fi
 }
 
+function common::debian_codename() {
+    # shellcheck disable=SC1091
+    (. /etc/os-release; printf '%s\n' "${VERSION_CODENAME}")
+}
+
+function common::debian_release() {
+    # shellcheck disable=SC1091
+    (. /etc/os-release; printf '%s\n' "${VERSION_ID}")
+}
+
 function common::import_trusted_gpg() {
     common::get_colors
 
@@ -1895,15 +1862,12 @@ ENV DEV_APT_DEPS=${DEV_APT_DEPS} \
     ADDITIONAL_DEV_APT_ENV=${ADDITIONAL_DEV_APT_ENV} \
     AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION}
 
-ARG PYTHON_LTO
-
 ENV RUSTUP_HOME="/usr/local/rustup"
 ENV CARGO_HOME="/home/airflow/.cargo"
 ENV PATH="${CARGO_HOME}/bin:${PATH}"
 
 COPY --from=scripts install_os_dependencies.sh /scripts/docker/
-COPY scripts/docker/keys/ /scripts/docker/keys/
-RUN PYTHON_LTO=${PYTHON_LTO} bash /scripts/docker/install_os_dependencies.sh dev
+RUN bash /scripts/docker/install_os_dependencies.sh dev
 
 # In case system python is installed, setting LD_LIBRARY_PATH prevents any case the system python
 # libraries will be accidentally used before the library installed from sources (which is newer and
@@ -1925,6 +1889,7 @@ COPY --from=scripts common.sh /scripts/docker/
 # Only copy mysql/mssql installation scripts for now - so that changing the other
 # scripts which are needed much later will not invalidate the docker layer here
 COPY --from=scripts install_mysql.sh install_mssql.sh install_postgres.sh /scripts/docker/
+COPY scripts/docker/keys/ /scripts/docker/keys/
 
 RUN bash /scripts/docker/install_mysql.sh dev && \
     bash /scripts/docker/install_mssql.sh dev && \
@@ -2155,9 +2120,6 @@ ENV RUNTIME_APT_DEPS=${RUNTIME_APT_DEPS} \
     GUNICORN_CMD_ARGS="--worker-tmp-dir /dev/shm" \
     AIRFLOW_INSTALLATION_METHOD=${AIRFLOW_INSTALLATION_METHOD}
 
-ARG PYTHON_LTO
-
-COPY --from=airflow-build-image "/usr/python/" "/usr/python/"
 COPY --from=scripts install_os_dependencies.sh /scripts/docker/
 RUN bash /scripts/docker/install_os_dependencies.sh runtime
 
diff --git a/Dockerfile.ci b/Dockerfile.ci
index 754c3650c94..45ec75b4df6 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -18,21 +18,16 @@
 #
 # WARNING: THIS DOCKERFILE IS NOT INTENDED FOR PRODUCTION USE OR DEPLOYMENT.
 #
-# PYTHON_LTO: Controls whether Python is built with Link-Time Optimization (LTO).
+# BASE_IMAGE: the Docker Hardened Image that Python comes from. Both the build stage and the final
+# image are based on it, so Python is never compiled during the build.
 #
-# Link-Time Optimization uses MD5 checksums during the compilation process to verify
-# object files and intermediate representations. In FIPS-compliant environments, MD5
-# is blocked as it's not an approved cryptographic algorithm (see FIPS 140-2/140-3).
-# This can cause Python builds with LTO to fail when FIPS mode is enabled.
+# The default is Airflow's public mirror of the upstream "dhi.io/python" image, so building needs no
+# registry credentials. The upstream image itself requires a "docker login dhi.io".
 #
-# When building FIPS-compliant images, set this to "false" to disable LTO:
-#   docker build --build-arg PYTHON_LTO="false" ...
-#
-# Default: "true" (LTO enabled for better performance)
-#
-# Related: https://github.com/apache/airflow/issues/58337
-ARG PYTHON_LTO="true"
-ARG BASE_IMAGE="debian:bookworm-slim"
+# The FIPS-validated variants require a paid Docker subscription and cannot be mirrored or made the
+# default, but you can build a FIPS-compliant image by pointing the build at one:
+#   docker build . --build-arg BASE_IMAGE="dhi.io/python:3.13.16-debian12-fips-dev"
+ARG BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev"
 
 ##############################################################################################
 # This is the script image where we keep all inlined bash scripts needed in other segments
@@ -59,15 +54,13 @@ if [[ "$#" != 1 ]]; then
     exit 1
 fi
 
-AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.11.16}
-PYTHON_LTO=${PYTHON_LTO:-true}
 GOLANG_MAJOR_MINOR_VERSION=${GOLANG_MAJOR_MINOR_VERSION:-1.24.4}
 TEMURIN_VERSION=${TEMURIN_VERSION:-11}
 NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
 PNPM_VERSION=${PNPM_VERSION:-10.28.1}
 RUSTUP_DEFAULT_TOOLCHAIN=${RUSTUP_DEFAULT_TOOLCHAIN:-stable}
 RUSTUP_VERSION=${RUSTUP_VERSION:-1.29.0}
-COSIGN_VERSION=${COSIGN_VERSION:-3.0.5}
+PYTHON_HOME=${PYTHON_HOME:-/opt/python}
 
 if [[ "${1}" == "runtime" ]]; then
     INSTALLATION_TYPE="RUNTIME"
@@ -94,38 +87,25 @@ freetds-dev \
 git \
 graphviz \
 graphviz-dev \
+gzip \
 krb5-user \
-lcov \
 ldap-utils \
-libbluetooth-dev \
-libbz2-dev \
 libc6-dev \
-libdb-dev \
 libev-dev \
 libev4 \
 libffi-dev \
-libgdbm-compat-dev \
-libgdbm-dev \
 libgeos-dev \
 libkrb5-dev \
 libldap2-dev \
 libleveldb-dev \
 libleveldb1d \
-liblzma-dev \
-libncurses5-dev \
-libreadline6-dev \
 libsasl2-2 \
 libsasl2-dev \
 libsasl2-modules \
-libsqlite3-dev \
 libssl-dev \
 libxmlsec1 \
 libxmlsec1-dev \
-libzstd-dev \
 locales \
-lsb-release \
-lzma \
-lzma-dev \
 openssh-client \
 openssl \
 pkg-config \
@@ -134,10 +114,8 @@ sasl2-bin \
 sqlite3 \
 sudo \
 tdsodbc \
-tk-dev \
 unixodbc \
 unixodbc-dev \
-uuid-dev \
 wget \
 xz-utils \
 zlib1g-dev \
@@ -164,6 +142,9 @@ netcat-openbsd\
     echo
     echo "APPLIED INSTALLATION CONFIGURATION FOR DEBIAN VERSION: ${debian_version}"
     echo
+    # libxmlsec1-openssl was added because libxmlsec1 ships no crypto engine of its own - the engines
+    # are separate packages - so the "xmlsec" module (pulled in by python3-saml) imported with
+    # "libxmlsec1-openssl.so.1: cannot open shared object file" without it.
     if [[ "${RUNTIME_APT_DEPS=}" == "" ]]; then
         RUNTIME_APT_DEPS="\
 ${debian_version_apt_deps} \
@@ -182,8 +163,8 @@ libgeos-dev \
 libsasl2-2 \
 libsasl2-modules \
 libxmlsec1 \
+libxmlsec1-openssl \
 locales \
-lsb-release \
 openssh-client \
 rsync \
 sasl2-bin \
@@ -212,10 +193,42 @@ function install_docker_cli() {
     apt-get install -y --no-install-recommends docker-ce-cli
 }
 
+function keep_image_conffiles() {
+    # The hardened base images ship a modified /etc/debian_version, so any apt run that upgrades
+    # base-files stops at dpkg's interactive conffile prompt and fails a non-interactive build. Making
+    # dpkg keep the image's copy by default covers every apt run - this build's and those of images
+    # extending it - so no install command has to remember the flags.
+    mkdir -p /etc/dpkg/dpkg.cfg.d
+    printf '%s\n' force-confdef force-confold > /etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles
+}
+
+function restore_debian_base_files() {
+    # The hardened base images ship a minimal /etc, but Debian maintainer scripts assume the files
+    # a stock Debian has: sasl2-bin chowns its run directory to the "sasl" group from base-passwd,
+    # and tmux registers its shell with add-shell, which reads /etc/shells. The base-passwd package
+    # only ships the reference copies of the account files - update-passwd is what merges them
+    # into /etc.
+    # libpam-runtime generates the /etc/pam.d/common-* files that the PAM configs already in the
+    # image "@include" - without them "adduser --gecos" aborts with a PAM error from chfn.
+    # Debian Policy lets a maintainer script rely on any essential package without declaring it, and
+    # libgcrypt20 takes that up: its postinst runs a helper with a "#!/bin/dash" shebang. The hardened
+    # images ship /bin/sh as a symlink to bash and no dash at all, so configuring the package aborts
+    # with exit 127 and takes the whole apt transaction with it. Installing it first restores the
+    # assumption, and hands /bin/sh back to dash the way a stock Debian has it.
+    apt-get install -y --no-install-recommends dash
+    apt-get install -y --no-install-recommends base-passwd libpam-runtime
+    update-passwd
+    if [[ ! -e /etc/shells ]]; then
+        printf '%s\n' "# /etc/shells: valid login shells" /bin/sh /bin/bash > /etc/shells
+    fi
+}
+
 function install_debian_dev_dependencies() {
+    keep_image_conffiles
     apt-get update
     apt-get install -yqq --no-install-recommends apt-utils >/dev/null 2>&1
-    apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
+    restore_debian_base_files
+    apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
     # shellcheck disable=SC2086
     export ${ADDITIONAL_DEV_APT_ENV?}
     if [[ ${DEV_APT_COMMAND} != "" ]]; then
@@ -245,14 +258,25 @@ function install_additional_dev_dependencies() {
 }
 
 function link_python() {
+    # Airflow images have always exposed Python under /usr/python - documentation, volume mounts and
+    # user customizations refer to that path - while the hardened base images ship it in /opt/python,
+    # so keep the historical location working as a symlink.
+    if [[ ! -e /usr/python ]]; then
+        ln -sv "${PYTHON_HOME}" /usr/python
+    fi
+    # The hardened base images have no /usr/local tree at all
+    mkdir -p /usr/local/bin /usr/local/lib
     # link python binaries to /usr/local/bin and /usr/python/bin with and without 3 suffix
     # Links in /usr/local/bin are needed for tools that expect python to be there
     # Links in /usr/python/bin are needed for tools that are detecting home of python installation including
     # lib/site-packages. The /usr/python/bin should be first in PATH in order to help with the last part.
     for dst in pip3 python3 python3-config; do
         src="$(echo "${dst}" | tr -d 3)"
+        if [[ ! -e "/usr/python/bin/${dst}" ]]; then
+            continue
+        fi
         echo "Linking ${dst} in /usr/local/bin and /usr/python/bin"
-        ln -sv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
+        ln -sfv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
         for dir in /usr/local/bin /usr/python/bin; do
             if [[ ! -e "${dir}/${src}" ]]; then
                 echo "Creating ${src} - > ${dst} link in ${dir}"
@@ -272,62 +296,47 @@ function link_python() {
     ldconfig
 }
 
-function install_debian_runtime_dependencies() {
-    apt-get update
-    apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
-    apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
-    # shellcheck disable=SC2086
-    export ${ADDITIONAL_RUNTIME_APT_ENV?}
-    if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
-        bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
-    fi
-    if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
-        bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
-    fi
-    apt-get update
-    # shellcheck disable=SC2086
-    apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
-    apt-get autoremove -yqq --purge
-    apt-get clean
-    link_python
-    rm -rf /var/lib/apt/lists/* /var/log/*
+function restore_thread_stack_size() {
+    # The hardened base images build Python with -DTHREAD_STACK_SIZE=0x100000, so every thread Python
+    # starts gets a 1 MiB stack instead of glibc's default (the 8 MiB "ulimit -s" the previous images
+    # used). On Python 3.12 and 3.13 the C recursion guard is a fixed depth count sized for the larger
+    # stack, so deeply nested input - e.g. a JSON body parsed in a web server worker thread - overflows
+    # the stack and kills the process instead of raising RecursionError. A .pth file runs at every
+    # interpreter start-up without taking the sitecustomize module name users may already rely on.
+    local site_packages
+    site_packages="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
+    echo "Restoring the 8 MiB default thread stack size in ${site_packages}"
+    echo "import threading; threading.stack_size(8 * 1024 * 1024)" > "${site_packages}/airflow-thread-stack-size.pth"
 }
 
-function install_cosign() {
-    local arch
-    arch="$(dpkg --print-architecture)"
-    declare -A cosign_sha256s=(
-        # https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign_checksums.txt
-        [amd64]="db15cc99e6e4837daabab023742aaddc3841ce57f193d11b7c3e06c8003642b2"
-        [arm64]="d098f3168ae4b3aa70b4ca78947329b953272b487727d1722cb3cb098a1a20ab"
-    )
-    local cosign_sha256="${cosign_sha256s[${arch}]}"
-    if [[ -z "${cosign_sha256}" ]]; then
-        echo "Unsupported architecture for cosign: ${arch}"
-        exit 1
-    fi
-    curl -fsSL --retry 3 --retry-delay 5 \
-        "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-${arch}" \
-        -o /tmp/cosign
-    echo "${cosign_sha256}  /tmp/cosign" | sha256sum --check
-    chmod +x /tmp/cosign
+function compile_python_stdlib() {
+    # The hardened base images ship the standard library with no .pyc files at all. Python then misses
+    # the bytecode cache on every stdlib import, and in a read-only or non-writable directory it cannot
+    # create one - each miss leaves a negative dentry in the kernel, which grows without bound in a
+    # long-running container and can exhaust memory on the host. Compiling the standard library here
+    # restores what the previously compiled-in-image Python shipped.
+    # See https://github.com/apache/airflow/pull/58944 and https://lwn.net/Articles/814535/
+    local stdlib
+    stdlib="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["stdlib"])')"
+    echo "Compiling Python standard library in ${stdlib}"
+    # compileall exits non-zero when any file fails to compile, and the standard library ships files
+    # that are meant not to compile (deliberately broken syntax used by the test suite).
+    /usr/python/bin/python -m compileall -q -j "$(nproc)" -o 0 -o 1 -o 2 "${stdlib}" || true
 }
 
-function install_python() {
-    # If system python (3.11 in bookworm) is installed (via automatic installation of some dependencies for example), we need
-    # to fail and make sure that it is not there, because there can be strange interactions if we install
-    # newer version and system libraries are installed, because
-    # when you create a virtualenv part of the shared libraries of Python can be taken from the system
-    # Installation leading to weird errors when you want to install some modules - for example when you install ssl:
+function check_no_system_python() {
+    # Python comes from the hardened base image (in /opt/python) and must stay the only Python in the
+    # image. A system Python pulled in as a dependency of an apt package shares its shared libraries
+    # with ours and leads to errors such as:
     # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
     if dpkg -l | grep '^ii' | grep '^ii  libpython' >/dev/null; then
         echo
         echo "ERROR! System python is installed by one of the previous steps"
         echo
-        echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython3.11 is installed:"
+        echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython is installed:"
         echo
         apt-get install -yqq aptitude >/dev/null
-        aptitude why libpython3.11
+        aptitude why "$(dpkg -l | grep '^ii  libpython' | head -1 | awk '{print $2}')"
         echo
         exit 1
     else
@@ -335,86 +344,32 @@ function install_python() {
         echo "GOOD! System python is not installed - OK"
         echo
     fi
-    wget --tries=3 --waitretry=5 -O python.tar.xz "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz"
-    local major_minor_version
-    major_minor_version="${AIRFLOW_PYTHON_VERSION%.*}"
-    echo "Verifying Python ${AIRFLOW_PYTHON_VERSION} (${major_minor_version})"
-    # Sigstore verification (PEP 761)
-    declare -A sigstore_identities=(
-        # https://peps.python.org/pep-0664/#release-manager-and-crew
-        [3.11]="pablogsal@python.org"
-        # https://peps.python.org/pep-0693/#release-manager-and-crew
-        [3.12]="thomas@python.org"
-        # https://peps.python.org/pep-0719/#release-manager-and-crew
-        [3.13]="thomas@python.org"
-        # https://peps.python.org/pep-0745/#release-manager-and-crew
-        [3.14]="hugo@python.org"
-        # https://peps.python.org/pep-0790/#release-manager-and-crew
-        [3.15]="hugo@python.org"
-    )
-    declare -A sigstore_issuers=(
-        [3.11]="https://accounts.google.com"
-        [3.12]="https://accounts.google.com"
-        [3.13]="https://accounts.google.com"
-        [3.14]="https://github.com/login/oauth"
-        [3.15]="https://github.com/login/oauth"
-    )
-    wget --tries=3 --waitretry=5 -O python.tar.xz.sigstore \
-        "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz.sigstore"
-    install_cosign
-    local identity="${sigstore_identities[${major_minor_version}]}"
-    local issuer="${sigstore_issuers[${major_minor_version}]}"
-    /tmp/cosign verify-blob \
-        --bundle python.tar.xz.sigstore \
-        --certificate-identity "${identity}" \
-        --certificate-oidc-issuer "${issuer}" \
-        python.tar.xz
-    rm -f python.tar.xz.sigstore /tmp/cosign
-    mkdir -p /usr/src/python
-    tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz
-    rm python.tar.xz
-    cd /usr/src/python
-    arch="$(dpkg --print-architecture)"; arch="${arch##*-}"
-    gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"
-    EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"
-    EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer";
-    LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"
-    LDFLAGS="${LDFLAGS:--Wl},--strip-all"
-    # Link-Time Optimization (LTO) uses MD5 checksums for object file verification during
-    # compilation. In FIPS mode, MD5 is blocked as a non-approved algorithm, causing builds
-    # to fail. The PYTHON_LTO variable allows disabling LTO for FIPS-compliant builds.
-    # See: https://github.com/apache/airflow/issues/58337
-    local lto_option=""
-    if [[ "${PYTHON_LTO:-true}" == "true" ]]; then
-        lto_option="--with-lto"
-    fi
-    local build_log
-    build_log=$(mktemp)
-    echo "Building Python ${AIRFLOW_PYTHON_VERSION} from source..."
-    if ! (
-        ./configure --enable-optimizations --prefix=/usr/python/ --with-ensurepip --build="$gnuArch" \
-            --enable-loadable-sqlite-extensions --enable-option-checking=fatal \
-                --enable-shared ${lto_option} && \
-        make -s -j "$(nproc)" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
-            "LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" python && \
-        make -s -j "$(nproc)" install
-    ) > "${build_log}" 2>&1; then
-        echo
-        echo "ERROR! Python build failed. Build output:"
-        echo
-        cat "${build_log}"
-        rm -f "${build_log}"
-        exit 1
+}
+
+function install_debian_runtime_dependencies() {
+    keep_image_conffiles
+    apt-get update
+    apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
+    restore_debian_base_files
+    apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
+    # shellcheck disable=SC2086
+    export ${ADDITIONAL_RUNTIME_APT_ENV?}
+    if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
+        bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
     fi
-    rm -f "${build_log}"
-    cd /
-    rm -rf /usr/src/python
-    find /usr/python -depth \
-      \( \
-        \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
-        -o \( -type f -a \( -name 'libpython*.a' \) \) \
-    \) -exec rm -rf '{}' +
+    if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
+        bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+    fi
+    apt-get update
+    # shellcheck disable=SC2086
+    apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
+    apt-get autoremove -yqq --purge
+    apt-get clean
+    check_no_system_python
     link_python
+    restore_thread_stack_size
+    compile_python_stdlib
+    rm -rf /var/lib/apt/lists/* /var/log/*
 }
 
 function install_golang() {
@@ -513,7 +468,10 @@ if [[ "${INSTALLATION_TYPE}" == "RUNTIME" ]]; then
 else
     get_dev_apt_deps
     install_debian_dev_dependencies
-    install_python
+    check_no_system_python
+    link_python
+    restore_thread_stack_size
+    compile_python_stdlib
     install_additional_dev_dependencies
     install_rustup
     if [[ "${INSTALLATION_TYPE}" == "CI" ]]; then
@@ -617,7 +575,7 @@ install_mariadb_client() {
     echo "${COLOR_YELLOW}MariaDB client protocol-compatible with MySQL client.${COLOR_RESET}"
     echo
 
-    echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(lsb_release -cs) main" > \
+    echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(common::debian_codename) main" > \
         /etc/apt/sources.list.d/mariadb.list
     # Make sure that dependencies from MariaDB repo are preferred over Debian dependencies
     printf "Package: *\nPin: release o=MariaDB\nPin-Priority: 999\n" > /etc/apt/preferences.d/mariadb
@@ -661,7 +619,7 @@ function install_mssql_client() {
     echo "${COLOR_BLUE}Installing mssql client${COLOR_RESET}"
     echo
 
-    echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(lsb_release -rs)/prod $(lsb_release -cs) main" > \
+    echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(common::debian_release)/prod $(common::debian_codename) main" > \
         /etc/apt/sources.list.d/mssql-release.list &&
     mkdir -p /opt/microsoft/msodbcsql18 &&
     touch /opt/microsoft/msodbcsql18/ACCEPT_EULA &&
@@ -705,7 +663,7 @@ install_postgres_client() {
 
     common::import_trusted_gpg "7FCC7D46ACCC4CF8" "postgres"
 
-    echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > \
+    echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(common::debian_codename)-pgdg main" > \
         /etc/apt/sources.list.d/pgdg.list
     apt-get update
     apt-get install --no-install-recommends -y "${packages[@]}"
@@ -852,12 +810,12 @@ function common::get_constraints_location() {
 
 function common::show_packaging_tool_version_and_location() {
    echo "PATH=${PATH}"
-   echo "Installed pip: $(pip --version): $(which pip)"
+   echo "Installed pip: $(pip --version): $(command -v pip)"
    if [[ ${PACKAGING_TOOL} == "pip" ]]; then
        echo "${COLOR_BLUE}Using 'pip' to install Airflow${COLOR_RESET}"
    else
        echo "${COLOR_BLUE}Using 'uv' to install Airflow${COLOR_RESET}"
-       echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(which uv 2>/dev/null)"
+       echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(command -v uv 2>/dev/null)"
    fi
 }
 
@@ -869,7 +827,7 @@ function common::install_packaging_tools() {
         echo
     else
         echo
-        echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(which python)${COLOR_RESET}"
+        echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(command -v python)${COLOR_RESET}"
         echo
     fi
     if [[ ${AIRFLOW_PIP_VERSION=} == "" ]]; then
@@ -933,6 +891,16 @@ function common::install_packaging_tools() {
     fi
 }
 
+function common::debian_codename() {
+    # shellcheck disable=SC1091
+    (. /etc/os-release; printf '%s\n' "${VERSION_CODENAME}")
+}
+
+function common::debian_release() {
+    # shellcheck disable=SC1091
+    (. /etc/os-release; printf '%s\n' "${VERSION_ID}")
+}
+
 function common::import_trusted_gpg() {
     common::get_colors
 
@@ -1745,9 +1713,13 @@ RUN echo "Base image version: ${BASE_IMAGE}"
 ARG DEV_APT_COMMAND=""
 ARG ADDITIONAL_DEV_APT_COMMAND=""
 ARG ADDITIONAL_DEV_ENV_VARS=""
+# libenchant-2-dev is needed on top of libenchant-2-2: pyenchant locates the library via
+# ctypes.util.find_library(), which without ldconfig in the hardened base falls back to
+# linking with gcc and so needs the unversioned libenchant-2.so symlink the -dev package ships.
 ARG ADDITIONAL_DEV_APT_DEPS="bash-completion dumb-init git gdb graphviz krb5-user \
-    less libenchant-2-2 libgcc-11-dev libgeos-dev libpq-dev net-tools netcat-openbsd \
-    openssh-server postgresql-client software-properties-common rsync tmux unzip vim xxd"
+    less libenchant-2-2 libenchant-2-dev libgcc-11-dev libgeos-dev libpq-dev net-tools \
+    netcat-openbsd openssh-server postgresql-client software-properties-common rsync tmux \
+    unzip vim xxd"
 
 ARG ADDITIONAL_DEV_APT_ENV=""
 
@@ -1767,12 +1739,9 @@ ENV RUSTUP_HOME="/usr/local/rustup"
 ENV CARGO_HOME="/usr/local/cargo"
 ENV PATH="${CARGO_HOME}/bin:${PATH}"
 
-ARG PYTHON_LTO
-
 COPY --from=scripts install_os_dependencies.sh /scripts/docker/
-COPY scripts/docker/keys/ /scripts/docker/keys/
 
-RUN PYTHON_LTO=${PYTHON_LTO} bash /scripts/docker/install_os_dependencies.sh ci
+RUN bash /scripts/docker/install_os_dependencies.sh ci
 
 # In case system python is installed, setting LD_LIBRARY_PATH prevents any case the system python
 # libraries will be accidentally used before the library installed from sources (which is newer and
@@ -1784,6 +1753,7 @@ COPY --from=scripts common.sh /scripts/docker/
 # Only copy mysql/mssql installation scripts for now - so that changing the other
 # scripts which are needed much later will not invalidate the docker layer here.
 COPY --from=scripts install_mysql.sh install_mssql.sh install_postgres.sh /scripts/docker/
+COPY scripts/docker/keys/ /scripts/docker/keys/
 
 ARG HOME=/root
 ARG AIRFLOW_IMAGE_TYPE="ci"
diff --git a/airflow-core/newsfragments/73038.significant.rst b/airflow-core/newsfragments/73038.significant.rst
new file mode 100644
index 00000000000..b9625700087
--- /dev/null
+++ b/airflow-core/newsfragments/73038.significant.rst
@@ -0,0 +1,22 @@
+Airflow's CI and production images are now based on `Docker Hardened Images <https://dhi.io>`_ for Python
+
+Python is no longer compiled from source during the image build - it comes from the hardened base image
+instead, which removes the source download, its signature verification and the compilation itself from the
+build (the OS dependency layer of the production image went from roughly 210s to 105s in a local
+cache-disabled build). Python still lives in ``/usr/python``, which is now a symlink to the ``/opt/python``
+location the base image uses, so paths inside the image are unchanged.
+
+The hardened images ship the standard library without any ``.pyc`` files, so the image build compiles it.
+Without that, every standard library import would miss the bytecode cache and be unable to populate it -
+the standard library is owned by root and the image runs as ``airflow`` - and each miss leaks a negative
+dentry, which is the memory growth that was fixed for the previous images in #58944.
+
+The ``BASE_IMAGE`` build argument now defaults to ``ghcr.io/apache/airflow/base/python:<version>-debian12-dev``,
+Airflow's public mirror of the upstream ``dhi.io/python`` image - building an image needs no registry
+credentials. Anyone overriding ``BASE_IMAGE`` with a plain ``debian:bookworm-slim`` image has to switch to
+an image that already provides Python.
+
+The ``PYTHON_LTO`` build argument is gone. It existed to disable Link-Time Optimization when compiling
+Python in FIPS mode, and there is no Python compilation left to configure. To build a FIPS-compliant image,
+point ``BASE_IMAGE`` at a FIPS variant of the hardened image instead, for example
+``dhi.io/python:3.13.16-debian12-fips-dev``.
diff --git a/dev/breeze/doc/09_release_management_tasks.rst b/dev/breeze/doc/09_release_management_tasks.rst
index 25caf147ac8..4d22d9a61ca 100644
--- a/dev/breeze/doc/09_release_management_tasks.rst
+++ b/dev/breeze/doc/09_release_management_tasks.rst
@@ -1255,6 +1255,34 @@ Example usage:
      breeze release-management constraints-version-check --python 3.11 --airflow-constraints-mode constraints-source-providers --explain-why
 
 
+Mirror the hardened Python base images
+""""""""""""""""""""""""""""""""""""""
+
+The CI and PROD images are built on a Docker Hardened Image for Python. Pulling that image from
+its origin requires a Docker Hub login, so the tags Airflow builds on are mirrored to Airflow's
+own public registry and ``BASE_IMAGE`` points there - building the images then needs no registry
+credentials at all. A scheduled workflow keeps the mirror current; this command is what it runs,
+and what you run by hand when a new Python patch release needs mirroring straight away.
+
+Both the pinned tag and the floating major/minor tag are mirrored, so documentation and ad-hoc
+builds can keep referring to the floating one.
+
+These are all available flags of ``release-management mirror-base-images`` command:
+
+.. image:: ./images/output_release-management_mirror-base-images.svg
+  :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_release-management_mirror-base-images.svg
+  :width: 100%
+  :alt: Breeze mirror base images
+
+Example usage:
+
+.. code-block:: bash
+
+     breeze release-management mirror-base-images --python 3.10
+
+Without ``--python`` every currently supported Python version is mirrored.
+
+
 -----
 
 Next step: Follow the `UI Tasks <10_ui_tasks.rst>`_ to learn more about UI tasks.
diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md
index 9fc62bd5e02..6c8050c064b 100644
--- a/dev/breeze/doc/ci/02_images.md
+++ b/dev/breeze/doc/ci/02_images.md
@@ -379,7 +379,7 @@ plugin installed.
 ``` bash
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull \
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg ADDITIONAL_AIRFLOW_EXTRAS="jdbc" \
   --build-arg ADDITIONAL_PYTHON_DEPS="pandas" \
   --build-arg ADDITIONAL_DEV_APT_DEPS="gcc g++" \
@@ -404,7 +404,7 @@ comment](https://github.com/apache/airflow/issues/8605#issuecomment-690065621):
 ``` bash
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull \
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg AIRFLOW_INSTALLATION_METHOD="apache-airflow" \
   --build-arg ADDITIONAL_AIRFLOW_EXTRAS="slack" \
   --build-arg ADDITIONAL_PYTHON_DEPS="apache-airflow-providers-odbc \
@@ -429,7 +429,7 @@ can be used for CI images:
 
 | Build argument                    | Default value               | Description                                                                                                       |
 |-----------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------|
-| `PYTHON_BASE_IMAGE`               | `python:3.11-slim-bookworm` | Base Python image                                                                                                 |
+| `BASE_IMAGE`                      | `ghcr.io/apache/airflow/base/python:<AIRFLOW_PYTHON_VERSION>-debian12-dev` | Mirror of the hardened base image Python comes from                                           |
 | `PYTHON_MAJOR_MINOR_VERSION`      | `3.11`                      | major/minor version of Python (should match base image)                                                           |
 | `DEPENDENCIES_EPOCH_NUMBER`       | `2`                         | increasing this number will reinstall all apt dependencies                                                        |
 | `ADDITIONAL_PIP_INSTALL_FLAGS`    |                             | additional `pip` flags passed to the installation commands (except when reinstalling `pip` itself)                |
@@ -465,7 +465,7 @@ This builds the CI image in version 3.11 with default extras ("all").
 ``` bash
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
    --pull \
-   --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" --tag my-image:0.0.1
+   --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" --tag my-image:0.0.1
 ```
 
 This builds the CI image in version 3.11 with "gcp" extra only.
@@ -473,7 +473,7 @@ This builds the CI image in version 3.11 with "gcp" extra only.
 ``` bash
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull \
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg AIRFLOW_EXTRAS=gcp --tag my-image:0.0.1
 ```
 
@@ -482,7 +482,7 @@ This builds the CI image in version 3.11 with "apache-beam" extra added.
 ``` bash
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull \
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg ADDITIONAL_AIRFLOW_EXTRAS="apache-beam" --tag my-image:0.0.1
 ```
 
@@ -492,7 +492,7 @@ added.
 ``` bash
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull \
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg ADDITIONAL_PYTHON_DEPS="mssql" --tag my-image:0.0.1
 ```
 
@@ -502,7 +502,7 @@ apt dev dependencies added.
 ```
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg ADDITIONAL_DEV_APT_DEPS="gcc g++" --tag my-image:0.0.1
 ```
 
@@ -512,7 +512,7 @@ This builds the CI image in version 3.11 with "jdbc" extra and
 ```
 DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
   --pull \
-  --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+  --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian12-dev" \
   --build-arg AIRFLOW_EXTRAS=jdbc \
   --tag my-image:0.0.1
 ```
diff --git a/dev/breeze/doc/images/output_release-management.svg b/dev/breeze/doc/images/output_release-management.svg
index ed3cab0d811..ab02bee7fd7 100644
--- a/dev/breeze/doc/images/output_release-management.svg
+++ b/dev/breeze/doc/images/output_release-management.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1709.1999999999998" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 1733.6" xmlns="http://www.w3.org/2000/svg">
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -42,7 +42,7 @@
 
     <defs>
     <clipPath id="breeze-release-management-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="1658.1999999999998" />
+      <rect x="0" y="0" width="1463.0" height="1682.6" />
     </clipPath>
     <clipPath id="breeze-release-management-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -245,9 +245,12 @@
 <clipPath id="breeze-release-management-line-66">
     <rect x="0" y="1611.9" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-release-management-line-67">
+    <rect x="0" y="1636.3" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1707.2" rx="8"/><text class="breeze-release-management-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;release-management</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1731.6" rx="8"/><text class="breeze-release-management-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;release-management</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -311,20 +314,21 @@
 </text><text class="breeze-release-management-r5" x="0" y="1264.4" textLength="12.2" clip-path="url(#breeze-release-management-line-51)">│</text><text class="breeze-release-management-r4" x="24.4" y="1264.4" textLength="451.4" clip-path="url(#breeze-release-management-line-51)">prepare-mypy-distributions&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="500.2" y="1264.4" textLength="939.4" clip-path="url(#breeze-release- [...]
 </text><text class="breeze-release-management-r5" x="0" y="1288.8" textLength="1464" clip-path="url(#breeze-release-management-line-52)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1288.8" textLength="12.2" clip-path="url(#breeze-release-management-line-52)">
 </text><text class="breeze-release-management-r5" x="0" y="1313.2" textLength="24.4" clip-path="url(#breeze-release-management-line-53)">╭─</text><text class="breeze-release-management-r5" x="24.4" y="1313.2" textLength="292.8" clip-path="url(#breeze-release-management-line-53)">&#160;Other&#160;release&#160;commands&#160;</text><text class="breeze-release-management-r5" x="317.2" y="1313.2" textLength="1122.4" clip-path="url(#breeze-release-management-line-53)">───────────────────────── [...]
-</text><text class="breeze-release-management-r5" x="0" y="1337.6" textLength="12.2" clip-path="url(#breeze-release-management-line-54)">│</text><text class="breeze-release-management-r4" x="24.4" y="1337.6" textLength="292.8" clip-path="url(#breeze-release-management-line-54)">add-back-references&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1337.6" textLength="1098" clip-path="url(#breeze-release-management-line-54)">Command&#160;to&#160;ad [...]
-</text><text class="breeze-release-management-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-release-management-line-55)">│</text><text class="breeze-release-management-r4" x="24.4" y="1362" textLength="292.8" clip-path="url(#breeze-release-management-line-55)">prepare-python-client&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1362" textLength="1098" clip-path="url(#breeze-release-management-line-55)">Prepares&#160;python&#160;client&#160;p [...]
-</text><text class="breeze-release-management-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-release-management-line-56)">│</text><text class="breeze-release-management-r4" x="24.4" y="1386.4" textLength="292.8" clip-path="url(#breeze-release-management-line-56)">publish-docs&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1386.4" textLength="1098" clip-path="url(#breeze-release-managemen [...]
-</text><text class="breeze-release-management-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-release-management-line-57)">│</text><text class="breeze-release-management-r4" x="24.4" y="1410.8" textLength="292.8" clip-path="url(#breeze-release-management-line-57)">generate-constraints&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1410.8" textLength="1098" clip-path="url(#breeze-release-management-line-57)">Generates&#160;pinned&#160;c [...]
-</text><text class="breeze-release-management-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-release-management-line-58)">│</text><text class="breeze-release-management-r4" x="24.4" y="1435.2" textLength="292.8" clip-path="url(#breeze-release-management-line-58)">update-constraints&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1435.2" textLength="1098" clip-path="url(#breeze-release-management-line-58)">Update&#160;releas [...]
-</text><text class="breeze-release-management-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-release-management-line-59)">│</text><text class="breeze-release-management-r4" x="24.4" y="1459.6" textLength="292.8" clip-path="url(#breeze-release-management-line-59)">publish-docs-to-s3&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1459.6" textLength="1098" clip-path="url(#breeze-release-management-line-59)">Publishes&#160;doc [...]
-</text><text class="breeze-release-management-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-release-management-line-60)">│</text><text class="breeze-release-management-r4" x="24.4" y="1484" textLength="292.8" clip-path="url(#breeze-release-management-line-60)">publish-schemas-to-s3&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1484" textLength="1098" clip-path="url(#breeze-release-management-line-60)">Publishes&#160;generated&#160;JSON&#160 [...]
-</text><text class="breeze-release-management-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-release-management-line-61)">│</text><text class="breeze-release-management-r4" x="24.4" y="1508.4" textLength="292.8" clip-path="url(#breeze-release-management-line-61)">verify-rc-by-pmc&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1508.4" textLength="1098" clip-path="url(#breeze-release-management-line-61)">[EXPERIM [...]
-</text><text class="breeze-release-management-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-release-management-line-62)">│</text><text class="breeze-release-management-r4" x="24.4" y="1532.8" textLength="292.8" clip-path="url(#breeze-release-management-line-62)">check-release-files&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="341.6" y="1532.8" textLength="1098" clip-path="url(#breeze-release-management-line-62)">Verify&#160;that&#160;a [...]
-</text><text class="breeze-release-management-r5" x="0" y="1557.2" textLength="1464" clip-path="url(#breeze-release-management-line-63)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1557.2" textLength="12.2" clip-path="url(#breeze-release-management-line-63)">
-</text><text class="breeze-release-management-r5" x="0" y="1581.6" textLength="24.4" clip-path="url(#breeze-release-management-line-64)">╭─</text><text class="breeze-release-management-r5" x="24.4" y="1581.6" textLength="122" clip-path="url(#breeze-release-management-line-64)">&#160;Commands&#160;</text><text class="breeze-release-management-r5" x="146.4" y="1581.6" textLength="1293.2" clip-path="url(#breeze-release-management-line-64)">─────────────────────────────────────────────────── [...]
-</text><text class="breeze-release-management-r5" x="0" y="1606" textLength="12.2" clip-path="url(#breeze-release-management-line-65)">│</text><text class="breeze-release-management-r4" x="24.4" y="1606" textLength="305" clip-path="url(#breeze-release-management-line-65)">constraints-version-check</text><text class="breeze-release-management-r1" x="353.8" y="1606" textLength="1085.8" clip-path="url(#breeze-release-management-line-65)">Check&#160;constraints&#160;against&#160;released&#16 [...]
-</text><text class="breeze-release-management-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-release-management-line-66)">│</text><text class="breeze-release-management-r4" x="24.4" y="1630.4" textLength="305" clip-path="url(#breeze-release-management-line-66)">merge-prod-images&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="353.8" y="1630.4" textLength="1085.8" clip-path="url(#breeze-release-management-line-66)">Merge&# [...]
-</text><text class="breeze-release-management-r5" x="0" y="1654.8" textLength="1464" clip-path="url(#breeze-release-management-line-67)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1654.8" textLength="12.2" clip-path="url(#breeze-release-management-line-67)">
+</text><text class="breeze-release-management-r5" x="0" y="1337.6" textLength="12.2" clip-path="url(#breeze-release-management-line-54)">│</text><text class="breeze-release-management-r4" x="24.4" y="1337.6" textLength="280.6" clip-path="url(#breeze-release-management-line-54)">add-back-references&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1337.6" textLength="1110.2" clip-path="url(#breeze-release-management-line-54)">Command&#160;to&#160;add&#1 [...]
+</text><text class="breeze-release-management-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-release-management-line-55)">│</text><text class="breeze-release-management-r4" x="24.4" y="1362" textLength="280.6" clip-path="url(#breeze-release-management-line-55)">prepare-python-client&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1362" textLength="1110.2" clip-path="url(#breeze-release-management-line-55)">Prepares&#160;python&#160;client&#160;packa [...]
+</text><text class="breeze-release-management-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-release-management-line-56)">│</text><text class="breeze-release-management-r4" x="24.4" y="1386.4" textLength="280.6" clip-path="url(#breeze-release-management-line-56)">publish-docs&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1386.4" textLength="1110.2" clip-path="url(#breeze-release-management-li [...]
+</text><text class="breeze-release-management-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-release-management-line-57)">│</text><text class="breeze-release-management-r4" x="24.4" y="1410.8" textLength="280.6" clip-path="url(#breeze-release-management-line-57)">generate-constraints&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1410.8" textLength="1110.2" clip-path="url(#breeze-release-management-line-57)">Generates&#160;pinned&#160;const [...]
+</text><text class="breeze-release-management-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-release-management-line-58)">│</text><text class="breeze-release-management-r4" x="24.4" y="1435.2" textLength="280.6" clip-path="url(#breeze-release-management-line-58)">update-constraints&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1435.2" textLength="1110.2" clip-path="url(#breeze-release-management-line-58)">Update&#160;released&# [...]
+</text><text class="breeze-release-management-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-release-management-line-59)">│</text><text class="breeze-release-management-r4" x="24.4" y="1459.6" textLength="280.6" clip-path="url(#breeze-release-management-line-59)">publish-docs-to-s3&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1459.6" textLength="1110.2" clip-path="url(#breeze-release-management-line-59)">Publishes&#160;docs&#1 [...]
+</text><text class="breeze-release-management-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-release-management-line-60)">│</text><text class="breeze-release-management-r4" x="24.4" y="1484" textLength="280.6" clip-path="url(#breeze-release-management-line-60)">publish-schemas-to-s3&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1484" textLength="1110.2" clip-path="url(#breeze-release-management-line-60)">Publishes&#160;generated&#160;JSON&#160;sch [...]
+</text><text class="breeze-release-management-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-release-management-line-61)">│</text><text class="breeze-release-management-r4" x="24.4" y="1508.4" textLength="280.6" clip-path="url(#breeze-release-management-line-61)">verify-rc-by-pmc&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1508.4" textLength="1110.2" clip-path="url(#breeze-release-management-line-61)">[EXPERIMENTA [...]
+</text><text class="breeze-release-management-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-release-management-line-62)">│</text><text class="breeze-release-management-r4" x="24.4" y="1532.8" textLength="280.6" clip-path="url(#breeze-release-management-line-62)">check-release-files&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1532.8" textLength="1110.2" clip-path="url(#breeze-release-management-line-62)">Verify&#160;that&#160;all&# [...]
+</text><text class="breeze-release-management-r5" x="0" y="1557.2" textLength="12.2" clip-path="url(#breeze-release-management-line-63)">│</text><text class="breeze-release-management-r4" x="24.4" y="1557.2" textLength="280.6" clip-path="url(#breeze-release-management-line-63)">mirror-base-images&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="329.4" y="1557.2" textLength="1110.2" clip-path="url(#breeze-release-management-line-63)">Mirror&#160;the&#160;h [...]
+</text><text class="breeze-release-management-r5" x="0" y="1581.6" textLength="1464" clip-path="url(#breeze-release-management-line-64)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1581.6" textLength="12.2" clip-path="url(#breeze-release-management-line-64)">
+</text><text class="breeze-release-management-r5" x="0" y="1606" textLength="24.4" clip-path="url(#breeze-release-management-line-65)">╭─</text><text class="breeze-release-management-r5" x="24.4" y="1606" textLength="122" clip-path="url(#breeze-release-management-line-65)">&#160;Commands&#160;</text><text class="breeze-release-management-r5" x="146.4" y="1606" textLength="1293.2" clip-path="url(#breeze-release-management-line-65)">───────────────────────────────────────────────────────── [...]
+</text><text class="breeze-release-management-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-release-management-line-66)">│</text><text class="breeze-release-management-r4" x="24.4" y="1630.4" textLength="305" clip-path="url(#breeze-release-management-line-66)">constraints-version-check</text><text class="breeze-release-management-r1" x="353.8" y="1630.4" textLength="1085.8" clip-path="url(#breeze-release-management-line-66)">Check&#160;constraints&#160;against&#160;releas [...]
+</text><text class="breeze-release-management-r5" x="0" y="1654.8" textLength="12.2" clip-path="url(#breeze-release-management-line-67)">│</text><text class="breeze-release-management-r4" x="24.4" y="1654.8" textLength="305" clip-path="url(#breeze-release-management-line-67)">merge-prod-images&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-release-management-r1" x="353.8" y="1654.8" textLength="1085.8" clip-path="url(#breeze-release-management-line-67)">Merge&# [...]
+</text><text class="breeze-release-management-r5" x="0" y="1679.2" textLength="1464" clip-path="url(#breeze-release-management-line-68)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1679.2" textLength="12.2" clip-path="url(#breeze-release-management-line-68)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_release-management.txt b/dev/breeze/doc/images/output_release-management.txt
index 1a2955706e0..98674687d9e 100644
--- a/dev/breeze/doc/images/output_release-management.txt
+++ b/dev/breeze/doc/images/output_release-management.txt
@@ -1 +1 @@
-186f43f8d9e9e522daa2e16d537a46d3
+0b7200fc4be8b419c11762aff2314daf
diff --git a/dev/breeze/doc/images/output_release-management_mirror-base-images.svg b/dev/breeze/doc/images/output_release-management_mirror-base-images.svg
new file mode 100644
index 00000000000..b098e589c44
--- /dev/null
+++ b/dev/breeze/doc/images/output_release-management_mirror-base-images.svg
@@ -0,0 +1,116 @@
+<svg class="rich-terminal" viewBox="0 0 1482 391.59999999999997" xmlns="http://www.w3.org/2000/svg">
+    <!-- Generated with Rich https://www.textualize.io -->
+    <style>
+
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Regular"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+        font-style: normal;
+        font-weight: 400;
+    }
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Bold"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+        font-style: bold;
+        font-weight: 700;
+    }
+
+    .breeze-release-management-mirror-base-images-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .breeze-release-management-mirror-base-images-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .breeze-release-management-mirror-base-images-r1 { fill: #c5c8c6 }
+.breeze-release-management-mirror-base-images-r2 { fill: #d0b344 }
+.breeze-release-management-mirror-base-images-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-release-management-mirror-base-images-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-release-management-mirror-base-images-r5 { fill: #868887 }
+.breeze-release-management-mirror-base-images-r6 { fill: #98a84b;font-weight: bold }
+.breeze-release-management-mirror-base-images-r7 { fill: #8d7b39 }
+    </style>
+
+    <defs>
+    <clipPath id="breeze-release-management-mirror-base-images-clip-terminal">
+      <rect x="0" y="0" width="1463.0" height="340.59999999999997" />
+    </clipPath>
+    <clipPath id="breeze-release-management-mirror-base-images-line-0">
+    <rect x="0" y="1.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-1">
+    <rect x="0" y="25.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-2">
+    <rect x="0" y="50.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-3">
+    <rect x="0" y="74.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-4">
+    <rect x="0" y="99.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-5">
+    <rect x="0" y="123.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-6">
+    <rect x="0" y="147.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-7">
+    <rect x="0" y="172.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-8">
+    <rect x="0" y="196.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-9">
+    <rect x="0" y="221.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-10">
+    <rect x="0" y="245.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-11">
+    <rect x="0" y="269.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-12">
+    <rect x="0" y="294.3" width="1464" height="24.65"/>
+            </clipPath>
+    </defs>
+
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="389.6" rx="8"/><text class="breeze-release-management-mirror-base-images-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;release-management&#160;mirror-base-images</text>
+            <g transform="translate(26,22)">
+            <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+            <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+            <circle cx="44" cy="0" r="7" fill="#28c840"/>
+            </g>
+        
+    <g transform="translate(9, 41)" clip-path="url(#breeze-release-management-mirror-base-images-clip-terminal)">
+    
+    <g class="breeze-release-management-mirror-base-images-matrix">
+    <text class="breeze-release-management-mirror-base-images-r1" x="1464" y="20" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-0)">
+</text><text class="breeze-release-management-mirror-base-images-r2" x="12.2" y="44.4" textLength="73.2" clip-path="url(#breeze-release-management-mirror-base-images-line-1)">Usage:</text><text class="breeze-release-management-mirror-base-images-r3" x="97.6" y="44.4" textLength="536.8" clip-path="url(#breeze-release-management-mirror-base-images-line-1)">breeze&#160;release-management&#160;mirror-base-images</text><text class="breeze-release-management-mirror-base-images-r1" x="646.6" y= [...]
+</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-2)">
+</text><text class="breeze-release-management-mirror-base-images-r1" x="12.2" y="93.2" textLength="1049.2" clip-path="url(#breeze-release-management-mirror-base-images-line-3)">Mirror&#160;the&#160;hardened&#160;Python&#160;base&#160;images&#160;Airflow&#160;builds&#160;on&#160;to&#160;Airflow&#x27;s&#160;public&#160;registry.</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-image [...]
+</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-4)">
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="142" textLength="24.4" clip-path="url(#breeze-release-management-mirror-base-images-line-5)">╭─</text><text class="breeze-release-management-mirror-base-images-r5" x="24.4" y="142" textLength="170.8" clip-path="url(#breeze-release-management-mirror-base-images-line-5)">&#160;Mirror&#160;flags&#160;</text><text class="breeze-release-management-mirror-base-images-r5" x="195.2" y="142" textLength="1244.4" clip-pat [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="166.4" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-6)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="166.4" textLength="97.6" clip-path="url(#breeze-release-management-mirror-base-images-line-6)">--python</text><text class="breeze-release-management-mirror-base-images-r6" x="146.4" y="166.4" textLength="24.4" clip-path="url(#breeze-rele [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-7)">│</text><text class="breeze-release-management-mirror-base-images-r1" x="195.2" y="190.8" textLength="134.2" clip-path="url(#breeze-release-management-mirror-base-images-line-7)">are&#160;used).&#160;</text><text class="breeze-release-management-mirror-base-images-r7" x="329.4" y="190.8" textLength="414.8" clip-path=" [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="215.2" textLength="1464" clip-path="url(#breeze-release-management-mirror-base-images-line-8)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-8)">
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="239.6" textLength="24.4" clip-path="url(#breeze-release-management-mirror-base-images-line-9)">╭─</text><text class="breeze-release-management-mirror-base-images-r5" x="24.4" y="239.6" textLength="195.2" clip-path="url(#breeze-release-management-mirror-base-images-line-9)">&#160;Common&#160;options&#160;</text><text class="breeze-release-management-mirror-base-images-r5" x="219.6" y="239.6" textLength="1220" cl [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="264" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-10)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="264" textLength="109.8" clip-path="url(#breeze-release-management-mirror-base-images-line-10)">--verbose</text><text class="breeze-release-management-mirror-base-images-r6" x="158.6" y="264" textLength="24.4" clip-path="url(#breeze-releas [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-11)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="288.4" textLength="109.8" clip-path="url(#breeze-release-management-mirror-base-images-line-11)">--dry-run</text><text class="breeze-release-management-mirror-base-images-r6" x="158.6" y="288.4" textLength="24.4" clip-path="url(#breeze- [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-12)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="312.8" textLength="109.8" clip-path="url(#breeze-release-management-mirror-base-images-line-12)">--help&#160;&#160;&#160;</text><text class="breeze-release-management-mirror-base-images-r6" x="158.6" y="312.8" textLength="24.4" clip-pat [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="337.2" textLength="1464" clip-path="url(#breeze-release-management-mirror-base-images-line-13)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-13)">
+</text>
+    </g>
+    </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_release-management_mirror-base-images.txt b/dev/breeze/doc/images/output_release-management_mirror-base-images.txt
new file mode 100644
index 00000000000..b4f7d77cbbe
--- /dev/null
+++ b/dev/breeze/doc/images/output_release-management_mirror-base-images.txt
@@ -0,0 +1 @@
+2a6076a8d0ea030bcb60f39117402feb
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
index ed6be314547..77c21159e89 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
@@ -231,16 +231,16 @@
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="532.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="532.4" textLength="671" clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">release-management:generate-issue-content-helm-chart&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="532.4" textLength [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="556.8" textLength="1256.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">release-management:generate-issue-content-providers&#160;|&#160;release-management:generate-providers-metadata&#160;|&#160;</text><text class="breeze-setup-c [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="581.2" textLength="1110.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">release-management:install-provider-distributions&#160;|&#160;release-management:merge-prod-images&#160;|&#160;</text><text class="breeze-setup-check-all-par [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:prepare-airflow-ctl-distributions&#160;|&#160;release-management:prepare-airflow-distributions&#160;|</text><text class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630" textLength="1171.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-helm-chart-package&#160;|&#160;release-management:prepare-helm-chart-tarball&#160;|&#160;</text><text class="breeze-setup-check-all-pa [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4" textLength="1220" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:prepare-mypy-distributions&#160;|&#160;release-management:prepare-provider-distributions&#160;|&#160;</text><text class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8" textLength="1159" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:prepare-provider-documentation&#160;|&#160;release-management:prepare-python-client&#160;|&#160;</text><text class="breeze-setup-check-all-p [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2" textLength="1085.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">release-management:prepare-tarball&#160;|&#160;release-management:prepare-task-sdk-distributions&#160;|&#160;</text><text class="breeze-setup-check-all-param [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6" textLength="902.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">release-management:publish-docs&#160;|&#160;release-management:publish-docs-to-s3&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5"  [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752" textLength="1024.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">release-management:publish-schemas-to-s3&#160;|&#160;release-management:release-prod-images&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-grou [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4" textLength="951.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">release-management:start-beta-process&#160;|&#160;release-management:start-rc-process&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8" textLength="854" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">release-management:start-release&#160;|&#160;release-management:tag-providers&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="145 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2" textLength="1110.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">release-management:update-constraints&#160;|&#160;release-management:update-providers-next-version&#160;|&#160;</text><text class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6" textLength="1159" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:mirror-base-images&#160;|&#160;release-management:prepare-airflow-ctl-distributions&#160;|&#160;</text><text class="breeze-setup-check-all-p [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630" textLength="1207.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-airflow-distributions&#160;|&#160;release-management:prepare-helm-chart-package&#160;|&#160;</text><text class="breeze-setup-check-all [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4" textLength="1171.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:prepare-helm-chart-tarball&#160;|&#160;release-management:prepare-mypy-distributions&#160;|&#160;</text><text class="breeze-setup-check-al [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8" textLength="1268.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:prepare-provider-distributions&#160;|&#160;release-management:prepare-provider-documentation&#160;|&#160;</text><text class="breeze-setup- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2" textLength="976" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">release-management:prepare-python-client&#160;|&#160;release-management:prepare-tarball&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6" textLength="1049.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">release-management:prepare-task-sdk-distributions&#160;|&#160;release-management:publish-docs&#160;|&#160;</text><text class="breeze-setup-check-all-params-i [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752" textLength="1012.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">release-management:publish-docs-to-s3&#160;|&#160;release-management:publish-schemas-to-s3&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-group [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4" textLength="988.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">release-management:release-prod-images&#160;|&#160;release-management:start-beta-process&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-grou [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">release-management:start-rc-process&#160;|&#160;release-management:start-release&#160;|&#160;release-management:tag-providers</text><text class="breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2" textLength="1134.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">|&#160;release-management:update-constraints&#160;|&#160;release-management:update-providers-next-version&#160;|&#160;</text><text class="breeze-setup-check- [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="849.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="849.6" textLength="1244.4" clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">release-management:verify-provider-distributions&#160;|&#160;release-management:verify-rc-by-pmc&#160;|&#160;run&#160;|&#160;sbom&#160;|&#160;</text><text cl [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="874" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">sbom:build-all-airflow-images&#160;|&#160;sbom:export-dependency-information&#160;|&#160;sbom:generate-providers-requirements</text><text class="breeze-setup-check [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="898.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4" textLength="1183.4" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">|&#160;sbom:update-sbom-information&#160;|&#160;setup&#160;|&#160;setup:autocomplete&#160;|&#160;setup:check-all-params-in-groups&#160;|&#160;</text><text cl [...]
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
index 9d053954e72..872eb21c410 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
@@ -1 +1 @@
-cd743de2ac920faaec5a33b506ede093
+a706f8a030b5da92b951da442d1b85f2
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
index a1ef0efe0d8..781ae597fd0 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -247,7 +247,7 @@
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-22)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="556.8" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-22)">release-management:generate-issue-content-core&#160;|&#160;release-management:generate-issue-content-helm-chart&#160;</text><text class="breeze-setup-regenerate- [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-23)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="581.2" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-23)">|&#160;release-management:generate-issue-content-providers&#160;|&#160;release-management:generate-providers-metadata</text><text class="breeze-setup-regenerate- [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-24)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="605.6" textLength="1134.6" clip-path="url(#breeze-setup-regenerate-command-images-line-24)">|&#160;release-management:install-provider-distributions&#160;|&#160;release-management:merge-prod-images&#160;|&#160;</text><text class="breeze-setup-regenerate [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="630" textLength="671" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">release-management:prepare-airflow-ctl-distributions&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="630" textLength="12.2" cli [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="630" textLength="1159" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">release-management:mirror-base-images&#160;|&#160;release-management:prepare-airflow-ctl-distributions&#160;|&#160;</text><text class="breeze-setup-regenerate-command- [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-26)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="654.4" textLength="1207.8" clip-path="url(#breeze-setup-regenerate-command-images-line-26)">release-management:prepare-airflow-distributions&#160;|&#160;release-management:prepare-helm-chart-package&#160;|&#160;</text><text class="breeze-setup-regenerat [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-27)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="678.8" textLength="1171.2" clip-path="url(#breeze-setup-regenerate-command-images-line-27)">release-management:prepare-helm-chart-tarball&#160;|&#160;release-management:prepare-mypy-distributions&#160;|&#160;</text><text class="breeze-setup-regenerate-c [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-28)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="703.2" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-28)">release-management:prepare-provider-distributions&#160;|&#160;release-management:prepare-provider-documentation&#160;</text><text class="breeze-setup-regenerate- [...]
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
index c925b44aa64..3ad23387733 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
@@ -1 +1 @@
-b34bcd0ca3348d1279d48f96682a25e5
+2d92f8a4ea15a11eba4c63d0d0d7c912
diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
index 472555b1685..3c9ad2d23bb 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
@@ -99,12 +99,16 @@ from airflow_breeze.global_constants import (
     DEFAULT_PYTHON_MAJOR_MINOR_VERSION,
     DEFAULT_PYTHON_MAJOR_MINOR_VERSION_FOR_IMAGES,
     DESTINATION_LOCATIONS,
+    HARDENED_PYTHON_IMAGE_MIRROR,
+    HARDENED_PYTHON_IMAGE_SOURCE,
     MULTI_PLATFORM,
     SCHEMA_DESTINATION_LOCATIONS,
     UV_VERSION,
     get_airflow_mypy_version,
     get_airflow_version,
     get_airflowctl_version,
+    get_hardened_python_base_image,
+    get_hardened_python_image_tag,
     get_task_sdk_version,
     get_ts_sdk_version,
 )
@@ -2474,7 +2478,7 @@ def release_prod_images(
     for python in python_versions:
         build_args = {
             "AIRFLOW_CONSTRAINTS": "constraints-no-providers",
-            "BASE_IMAGE": "debian:bookworm-slim",
+            "BASE_IMAGE": get_hardened_python_base_image(python),
             "AIRFLOW_PYTHON_VERSION": ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python, python),
             "AIRFLOW_VERSION": airflow_version,
             "INCLUDE_PRE_RELEASE": "true" if include_pre_release else "false",
@@ -5178,3 +5182,79 @@ def check_release_files(
     else:
         console.print("\n[success]All expected files are present![/]")
         sys.exit(0)
+
+
+HARDENED_PYTHON_CATALOG_URL = (
+    "https://raw.githubusercontent.com/docker-hardened-images/catalog/main/image/python/debian-12/"
+    "{python}-dev.yaml"
+)
+HARDENED_PYTHON_PATCHLEVEL_TAG = re.compile(r"^\s*-\s*(\d+\.\d+\.\d+)-debian12-dev\s*$", re.MULTILINE)
+
+
+def get_latest_hardened_python_patchlevel(python: str) -> str | None:
+    """
+    Return the newest patchlevel Docker publishes for a Python major.minor, or None if unknown.
+
+    Read from the image definition in Docker's public catalog. Mirroring it ahead of the pinned
+    patchlevel is what lets the "upgrade important versions" check bump the pin without the bump
+    pointing at a tag the mirror does not have yet.
+    """
+    import requests
+
+    try:
+        response = requests.get(HARDENED_PYTHON_CATALOG_URL.format(python=python), timeout=30)
+        response.raise_for_status()
+    except requests.RequestException as e:
+        console_print(f"[warning]Could not read the hardened Python {python} definition: {e}[/]")
+        return None
+    versions = HARDENED_PYTHON_PATCHLEVEL_TAG.findall(response.text)
+    if not versions:
+        return None
+    return max(versions, key=lambda version: tuple(int(part) for part in version.split(".")))
+
+
+@release_management_group.command(
+    name="mirror-base-images",
+    help="Mirror the hardened Python base images Airflow builds on to Airflow's public registry.",
+)
+@option_python_no_default
+@option_verbose
+@option_dry_run
+def mirror_base_images(python: str | None):
+    python_versions = CURRENT_PYTHON_MAJOR_MINOR_VERSIONS if python is None else [python]
+    failed: list[str] = []
+    for python_version in python_versions:
+        tag = get_hardened_python_image_tag(python_version)
+        source = f"{HARDENED_PYTHON_IMAGE_SOURCE}:{tag}"
+        # The floating major/minor tag is what documentation and ad-hoc builds refer to, so that they
+        # do not have to be edited on every Python patch release.
+        floating_tag = tag.replace(
+            ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python_version, python_version), python_version, 1
+        )
+        targets = [f"{HARDENED_PYTHON_IMAGE_MIRROR}:{tag}", f"{HARDENED_PYTHON_IMAGE_MIRROR}:{floating_tag}"]
+        copies = [(source, targets)]
+        pinned = ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python_version, python_version)
+        latest = get_latest_hardened_python_patchlevel(python_version)
+        if latest and latest != pinned:
+            latest_tag = tag.replace(pinned, latest, 1)
+            copies.append(
+                (
+                    f"{HARDENED_PYTHON_IMAGE_SOURCE}:{latest_tag}",
+                    [f"{HARDENED_PYTHON_IMAGE_MIRROR}:{latest_tag}"],
+                )
+            )
+        for copy_source, copy_targets in copies:
+            console_print(f"[info]Mirroring {copy_source} -> {', '.join(copy_targets)}[/]")
+            # imagetools copies the multi-platform manifest registry-to-registry, so the layers never
+            # travel through the machine running this.
+            tag_flags = [flag for target in copy_targets for flag in ("--tag", target)]
+            result = run_command(
+                ["docker", "buildx", "imagetools", "create", *tag_flags, copy_source],
+                check=False,
+            )
+            if result.returncode != 0:
+                failed.append(copy_source)
+    if failed:
+        console_print(f"[error]Failed to mirror: {', '.join(failed)}[/]")
+        sys.exit(1)
+    console_print("[success]All base images mirrored[/]")
diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
index 3cf90f73dc3..42a9761c2af 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
@@ -90,6 +90,7 @@ RELEASE_OTHER_COMMANDS: dict[str, str | list[str]] = {
         "publish-schemas-to-s3",
         "verify-rc-by-pmc",
         "check-release-files",
+        "mirror-base-images",
     ],
 }
 
@@ -104,6 +105,14 @@ RELEASE_MANAGEMENT_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
             ],
         }
     ],
+    "breeze release-management mirror-base-images": [
+        {
+            "name": "Mirror flags",
+            "options": [
+                "--python",
+            ],
+        }
+    ],
     "breeze release-management prepare-tarball": [
         {
             "name": "Tarball flags",
diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py
index 18850b8a0f7..e981943726a 100644
--- a/dev/breeze/src/airflow_breeze/global_constants.py
+++ b/dev/breeze/src/airflow_breeze/global_constants.py
@@ -903,13 +903,32 @@ PROVIDERS_COMPATIBILITY_TESTS_MATRIX: list[dict[str, str | list[str]]] = [
 ]
 
 ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION: dict[str, str] = {
-    "3.10": "3.10.21",
+    "3.10": "3.10.22",
     "3.11": "3.11.17",
     "3.12": "3.12.15",
     "3.13": "3.13.16",
     "3.14": "3.14.8",
 }
 
+# Airflow images are based on Docker Hardened Images (https://dhi.io). The "-dev" variant carries
+# apt, a shell and runs as root; the tags encode the Debian release rather than its codename.
+# Pulling from dhi.io requires a Docker Hub login, so the tags Airflow builds against are mirrored
+# to a public ghcr.io repository by the "Mirror hardened base images" workflow - building Airflow
+# images needs no registry credentials at all, which is what the default below points at.
+HARDENED_PYTHON_IMAGE_SOURCE = "dhi.io/python"
+HARDENED_PYTHON_IMAGE_MIRROR = "ghcr.io/apache/airflow/base/python"
+DEBIAN_VERSION_TO_HARDENED_IMAGE_DISTRO = {"bookworm": "debian12"}
+
+
+def get_hardened_python_image_tag(python: str, debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]) -> str:
+    distro = DEBIAN_VERSION_TO_HARDENED_IMAGE_DISTRO[debian_version]
+    return f"{ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python, python)}-{distro}-dev"
+
+
+def get_hardened_python_base_image(python: str, debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]) -> str:
+    return f"{HARDENED_PYTHON_IMAGE_MIRROR}:{get_hardened_python_image_tag(python, debian_version)}"
+
+
 # Number of slices for low dep tests
 NUMBER_OF_LOW_DEP_SLICES = 5
 
diff --git a/dev/breeze/src/airflow_breeze/params/common_build_params.py b/dev/breeze/src/airflow_breeze/params/common_build_params.py
index 6ab34d7e80f..15bb737c7ba 100644
--- a/dev/breeze/src/airflow_breeze/params/common_build_params.py
+++ b/dev/breeze/src/airflow_breeze/params/common_build_params.py
@@ -29,6 +29,7 @@ from airflow_breeze.global_constants import (
     APACHE_AIRFLOW_GITHUB_REPOSITORY,
     DOCKER_DEFAULT_PLATFORM,
     get_airflow_version,
+    get_hardened_python_base_image,
 )
 from airflow_breeze.utils.console import console_print
 from airflow_breeze.utils.platforms import get_normalized_platform
@@ -124,7 +125,7 @@ class CommonBuildParams:
         """Construct Python Base Image"""
         if self.python_image is not None:
             return self.python_image
-        return f"debian:{self.debian_version}-slim"
+        return get_hardened_python_base_image(self.python, self.debian_version)
 
     @property
     def airflow_image_repository(self):
diff --git a/dev/breeze/tests/test_mirror_base_images.py b/dev/breeze/tests/test_mirror_base_images.py
new file mode 100644
index 00000000000..5e56aa454d4
--- /dev/null
+++ b/dev/breeze/tests/test_mirror_base_images.py
@@ -0,0 +1,87 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from subprocess import CompletedProcess
+from unittest import mock
+
+import pytest
+import requests
+from click.testing import CliRunner
+
+from airflow_breeze.commands.release_management_commands import (
+    get_latest_hardened_python_patchlevel,
+    mirror_base_images,
+)
+from airflow_breeze.global_constants import ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION
+
+
+def _catalog_response(text: str) -> mock.MagicMock:
+    response = mock.MagicMock(spec=requests.Response)
+    response.text = text
+    return response
+
+
+@pytest.mark.parametrize(
+    ("definition", "expected"),
+    [
+        pytest.param(
+            "tags:\n  - 3.14-debian12-dev\n  - 3.14.8-debian12-dev\n", "3.14.8", id="patchlevel-tag"
+        ),
+        pytest.param(
+            "tags:\n  - 3.12.9-debian12-dev\n  - 3.12.15-debian12-dev\n", "3.12.15", id="numeric-max"
+        ),
+        pytest.param("tags:\n  - 3.14-debian12-dev\n", None, id="no-patchlevel-tag"),
+    ],
+)
+def test_latest_hardened_python_patchlevel_is_read_from_the_catalog(definition, expected):
+    with mock.patch("requests.get", autospec=True, return_value=_catalog_response(definition)) as get:
+        assert get_latest_hardened_python_patchlevel("3.14") == expected
+    assert get.call_args.args[0].endswith("/image/python/debian-12/3.14-dev.yaml")
+
+
+def test_latest_hardened_python_patchlevel_is_none_when_the_catalog_is_unreachable():
+    with mock.patch("requests.get", autospec=True, side_effect=requests.ConnectionError("down")):
+        assert get_latest_hardened_python_patchlevel("3.14") is None
+
+
+@pytest.mark.parametrize("newer_available", [True, False])
+def test_mirror_also_copies_a_newer_published_patchlevel(newer_available):
+    pinned = ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION["3.14"]
+    latest = "3.14.99" if newer_available else pinned
+    with (
+        mock.patch(
+            "airflow_breeze.commands.release_management_commands.get_latest_hardened_python_patchlevel",
+            autospec=True,
+            return_value=latest,
+        ),
+        mock.patch(
+            "airflow_breeze.commands.release_management_commands.run_command",
+            autospec=True,
+            return_value=CompletedProcess(args=[], returncode=0),
+        ) as run,
+    ):
+        result = CliRunner().invoke(mirror_base_images, ["--python", "3.14"], catch_exceptions=False)
+
+    assert result.exit_code == 0
+    sources = [call.args[0][-1] for call in run.call_args_list]
+    assert sources[0] == f"dhi.io/python:{pinned}-debian12-dev"
+    if newer_available:
+        assert sources[1:] == ["dhi.io/python:3.14.99-debian12-dev"]
+        assert "ghcr.io/apache/airflow/base/python:3.14.99-debian12-dev" in run.call_args_list[1].args[0]
+    else:
+        assert sources[1:] == []
diff --git a/docker-stack-docs/build-arg-ref.rst b/docker-stack-docs/build-arg-ref.rst
index 444c615cb50..4664f9f7211 100644
--- a/docker-stack-docs/build-arg-ref.rst
+++ b/docker-stack-docs/build-arg-ref.rst
@@ -32,7 +32,14 @@ Those are the most common arguments that you use when you want to build a custom
 +==========================================+===========================================+=============================================+
 | ``AIRFLOW_VERSION``                      | :subst-code:`|airflow-version|`           | Version of Airflow.                         |
 +------------------------------------------+-------------------------------------------+---------------------------------------------+
-| ``AIRFLOW_PYTHON_VERSION``               | ``3.13.16``                               | Version of Python.                          |
+| ``AIRFLOW_PYTHON_VERSION``               | ``3.13.16``                               | Python version recorded in the image        |
+|                                          |                                           | labels. The Python itself comes from        |
+|                                          |                                           | ``BASE_IMAGE``, so change both together.    |
++------------------------------------------+-------------------------------------------+---------------------------------------------+
+| ``BASE_IMAGE``                           | (see below the table)                     | Airflow's public mirror of the Docker       |
+|                                          |                                           | Hardened Image that Python comes from.      |
+|                                          |                                           | Point it at a ``dhi.io`` ``-fips-`` variant |
+|                                          |                                           | to build a FIPS-compliant image.            |
 +------------------------------------------+-------------------------------------------+---------------------------------------------+
 | ``AIRFLOW_EXTRAS``                       | (see below the table)                     | Default extras with which Airflow is        |
 |                                          |                                           | installed.                                  |
@@ -81,6 +88,11 @@ Those are the most common arguments that you use when you want to build a custom
     Before Airflow 2.2, the image also had ``AIRFLOW_GID`` parameter, but it did not provide any additional
     functionality - only added confusion - so it has been removed.
 
+The default ``BASE_IMAGE`` is Airflow's public mirror of the `Docker Hardened Image <https://dhi.io>`_ for
+Python, tagged with the same Python version as ``AIRFLOW_PYTHON_VERSION`` - for Airflow |airflow-version| that
+is ``ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev``. Pulling the mirror needs no credentials;
+pulling ``dhi.io`` directly requires a ``docker login dhi.io``.
+
 List of default extras in the production Dockerfile:
 
 .. BEGINNING OF EXTRAS LIST UPDATED BY PRE COMMIT
diff --git a/docker-stack-docs/build.rst b/docker-stack-docs/build.rst
index 15e7af68f4b..2d21117ea69 100644
--- a/docker-stack-docs/build.rst
+++ b/docker-stack-docs/build.rst
@@ -884,26 +884,72 @@ vetted by the security teams. It is also the most complex way of building the im
 expert of building and using Dockerfiles in order to use it and have to have specific needs of security if
 you want to follow that route.
 
+.. _image-hardened-base:
+
+Properties of the hardened base image
+.....................................
+
+Since Airflow 3.4.0 the images are built on the ``-dev`` variant of the
+`Docker Hardened Image <https://dhi.io>`_ for Python (``dhi.io/python``, mirrored to
+``ghcr.io/apache/airflow/base/python``) instead of compiling Python on ``debian:bookworm-slim``. Docker
+publishes the definitions these images are built from in the
+`docker-hardened-images/catalog <https://github.com/docker-hardened-images/catalog>`_ repository
+(``package/python`` for the Python build, ``image/python`` for the image). The differences from the
+previous images that can matter when you extend or run the image are:
+
+* **Python is patched.** Docker applies its own patches on top of the upstream CPython release
+  (``package/python/patch`` in the catalog). One of them makes ``pkgutil.get_data()`` reject resource
+  paths that are absolute or contain ``..`` (CVE-2026-3479) - upstream CPython only documents that
+  restriction and does not enforce it. Libraries that load bundled data through ``../`` paths fail with
+  ``ValueError: resource must be a relative path with no parent directory components``; for example
+  ``moto`` before 5.2.2.
+* **Threads get a 1 MiB stack.** The hardened Python is compiled with
+  ``-DTHREAD_STACK_SIZE=0x100000``, while the previous images used the ``glibc`` default (the 8 MiB
+  ``ulimit -s``). On Python 3.12 and 3.13 the interpreter's C recursion guard is a fixed depth that
+  assumes the larger stack, so deeply nested data - for example a JSON body parsed in a web server worker
+  thread - can crash the process instead of raising ``RecursionError``. The image restores the 8 MiB
+  default for threads started by Python with a ``airflow-thread-stack-size.pth`` file in the base
+  Python's ``site-packages``; delete that file if you need the smaller stacks.
+* **Python is built without profile-guided optimization.** The previous images were built with
+  ``--enable-optimizations`` and ``--with-lto``; the hardened Python uses only ``--with-lto``, so CPU-bound Python code
+  can run measurably slower.
+* **Python lives in** ``/opt/python``. ``/usr/python`` and the ``/usr/local/bin`` links point to it, so
+  the paths used before keep working. The standard library ships without ``.pyc`` files, so the build
+  compiles it into the image.
+* **The OS is minimal.** The base ships no compiler, ``curl``, ``wget``, ``git``, ``gzip``, ``which`` or
+  ``ldconfig``, no ``dash`` (``/bin/sh`` is ``bash``) and a stripped ``/etc`` - for example without
+  ``/etc/shells`` or the PAM ``common-*`` files. The build restores what Airflow and the Debian packages
+  it installs need; a custom image that relied on something else being present has to install it.
+* **Some OS files are modified.** ``/etc/os-release`` identifies the system as "Docker Hardened Images
+  (Debian)", and ``/etc/debian_version`` differs from the Debian package's copy, so an ``apt-get upgrade``
+  that touches ``base-files`` would stop at ``dpkg``'s interactive configuration-file prompt. The image
+  ships ``/etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles`` (``force-confdef`` and ``force-confold``), so
+  ``dpkg`` keeps the image's copy of a configuration file without asking - also in images that extend
+  it. A package upgrade therefore does not replace a configuration file that differs from the
+  package's version.
+* **The tags are rebuilt in place.** Docker republishes the same tags as CVEs are fixed, and Airflow
+  refreshes its mirror weekly, so rebuilding an image from the same ``BASE_IMAGE`` tag can pick up a newer
+  base. The weekly mirror also copies the newest Python patch release Docker publishes, and Airflow's
+  regular dependency upgrade moves the pinned patch level to it, so the images follow Docker's releases
+  rather than python.org's.
+
 .. _image-build-fips:
 
 Build images in FIPS-compliant environments
 ...........................................
 
-If you are building images in a FIPS-compliant environment, you might encounter issues with the default
-build process. For example, the default build process uses ``--with-lto`` (Link Time Optimization) when
-building Python, which might fail in FIPS mode because LTO uses MD5 checksums to verify object files
-during compilation, and MD5 is blocked in FIPS mode.
-
-In order to build the image in FIPS-compliant environment, you can use ``PYTHON_LTO`` build argument
-and set it to ``false``.
+Airflow images are based on the `Docker Hardened Images <https://dhi.io>`_ for Python, and Docker
+publishes FIPS-validated variants of those images. Those variants are only available with a paid
+Docker subscription, so they cannot be the default, but you can point the build at one with the
+``BASE_IMAGE`` build argument.
 
 .. code-block:: bash
 
-    docker build . --build-arg PYTHON_LTO="false" --tag my-image:my-tag
+    docker build . --build-arg BASE_IMAGE="dhi.io/python:3.13.16-debian12-fips-dev" --tag my-image:my-tag
 
 .. note::
 
-   While disabling LTO is necessary for FIPS compliance during the build process, it is not sufficient
+   While building on a FIPS-validated base image is necessary for FIPS compliance, it is not sufficient
    to make the image fully FIPS compliant. There might be other reasons for FIPS incompatibility
    (for example usage of non-FIPS compliant algorithms in the software installed in the image).
    You should verify the compliance of the image yourself.
diff --git a/docker-stack-docs/changelog.rst b/docker-stack-docs/changelog.rst
index 78f9c748475..a808fedcfb9 100644
--- a/docker-stack-docs/changelog.rst
+++ b/docker-stack-docs/changelog.rst
@@ -43,6 +43,41 @@ Airflow 3.4.0
   * The ``tdsodbc`` package was added to the image so that the FreeTDS ODBC driver
     (``libtdsodbc.so``) is available for connecting to Sybase/TDS databases via ODBC.
 
+  * The ``libxmlsec1-openssl`` package was added to the image. ``libxmlsec1`` contains no crypto
+    engine of its own, so ``import xmlsec`` (used by ``python3-saml``) failed with
+    ``libxmlsec1-openssl.so.1: cannot open shared object file``.
+
+In Airflow 3.4.0 the base image changed again - from ``debian:bookworm-slim`` with a Python compiled in
+the image, to the `Docker Hardened Image <https://dhi.io>`_ for Python, which already carries a Python
+built by Docker. This removes the Python compilation, and the source download and signature verification
+that went with it, from the build entirely - the OS dependency layer of the production image went from
+roughly 210s to 105s in a local cache-disabled build.
+
+* The ``BASE_IMAGE`` arg now defaults to ``ghcr.io/apache/airflow/base/python:<version>-debian12-dev``,
+  which is Airflow's public mirror of the upstream ``dhi.io/python`` image. Pulling from ``dhi.io``
+  directly requires a ``docker login dhi.io``; pulling the mirror requires nothing. If you pass your own
+  ``BASE_IMAGE``, it now has to be an image that already provides Python - a bare ``debian:bookworm-slim``
+  no longer works.
+* The ``PYTHON_LTO`` arg was removed. It only controlled Link-Time Optimization while compiling Python,
+  and there is no compilation left. To build a FIPS-compliant image, point ``BASE_IMAGE`` at a FIPS
+  variant of the hardened image, for example ``dhi.io/python:3.13.15-debian12-fips-dev`` (those variants
+  require a paid Docker subscription).
+* Python is installed in ``/opt/python/`` by the base image. ``/usr/python`` is a symlink to it, and the
+  ``/usr/local/bin`` symlinks are unchanged, so paths that worked before keep working.
+* The hardened images ship the standard library with no ``.pyc`` files, so the build compiles it into the
+  image. This keeps the behaviour introduced in Airflow 3.1.4 (see below): the standard library is owned
+  by root while the image runs as ``airflow``, so a missing bytecode cache could never be filled and every
+  import would leak a negative ``dentry``.
+* Python in the hardened images is not the plain upstream build: Docker patches it (``pkgutil.get_data()``
+  rejects ``..`` resource paths), builds it without profile-guided optimization, and compiles it with a
+  1 MiB default thread stack. The image restores the previous 8 MiB thread stack. See
+  :ref:`image-hardened-base` for these and the other differences from the previous images.
+
+As with any base image change, some ``apt`` packages that used to be present as a side effect are not
+there any more. The hardened images are deliberately minimal - they ship no compiler, no ``curl``,
+``wget``, ``git`` or ``gzip``, and a stripped ``/etc`` - so a custom image that relied on something being
+present may need to install it explicitly. See :doc:`Building the image <build>`.
+
 Airflow 3.1.4
 ~~~~~~~~~~~~~
 
diff --git a/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh b/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh
index 32882f2a3d6..1d4f1149050 100755
--- a/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh
+++ b/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh
@@ -31,7 +31,7 @@ export DOCKER_BUILDKIT=1
 
 docker build . \
     --pull \
-    --build-arg BASE_IMAGE="debian:bookworm-slim" \
+    --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev" \
     --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
     --build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
     --build-arg ADDITIONAL_PYTHON_DEPS="mpi4py==4.1.1" \
diff --git a/docker-stack-docs/docker-examples/customizing/custom-sources.sh b/docker-stack-docs/docker-examples/customizing/custom-sources.sh
index 83dc4aed7c7..19789d537ed 100755
--- a/docker-stack-docs/docker-examples/customizing/custom-sources.sh
+++ b/docker-stack-docs/docker-examples/customizing/custom-sources.sh
@@ -32,7 +32,7 @@ export DOCKER_BUILDKIT=1
 docker build . -f Dockerfile \
     --pull \
     --platform 'linux/amd64' \
-    --build-arg BASE_IMAGE="debian:bookworm-slim" \
+    --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev" \
     --build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
     --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
     --build-arg ADDITIONAL_AIRFLOW_EXTRAS="slack,odbc" \
diff --git a/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh b/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh
index 21682e1adbd..b9627732c1a 100755
--- a/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh
+++ b/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh
@@ -32,7 +32,7 @@ export DOCKER_BUILDKIT=1
 
 docker build . \
     --pull \
-    --build-arg BASE_IMAGE="debian:bookworm-slim" \
+    --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev" \
     --build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
     --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
     --build-arg ADDITIONAL_AIRFLOW_EXTRAS="jdbc" \
diff --git a/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh b/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh
index 7f3b53337b6..e3bfcb5c29d 100755
--- a/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh
+++ b/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh
@@ -31,7 +31,7 @@ export DOCKER_BUILDKIT=1
 
 docker build . \
     --pull \
-    --build-arg BASE_IMAGE="debian:bookworm-slim" \
+    --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev" \
     --build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
     --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
     --build-arg ADDITIONAL_AIRFLOW_EXTRAS="mssql,hdfs" \
diff --git a/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh b/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh
index 009fa3d58f4..cff604dbfc2 100755
--- a/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh
+++ b/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh
@@ -30,7 +30,7 @@ export AIRFLOW_VERSION=3.0.3
 export DOCKER_BUILDKIT=1
 
 docker build . \
-    --build-arg BASE_IMAGE="debian:bookworm-slim" \
+    --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev" \
     --build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
     --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
     --tag "my-pypi-selected-version:0.0.1"
diff --git a/docker-stack-docs/docker-examples/restricted/restricted_environments.sh b/docker-stack-docs/docker-examples/restricted/restricted_environments.sh
index 237f5ce4291..f2a0a53ee77 100755
--- a/docker-stack-docs/docker-examples/restricted/restricted_environments.sh
+++ b/docker-stack-docs/docker-examples/restricted/restricted_environments.sh
@@ -47,7 +47,7 @@ export DOCKER_BUILDKIT=1
 
 docker build . \
     --pull \
-    --build-arg BASE_IMAGE="debian:bookworm-slim" \
+    --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian12-dev" \
     --build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
     --build-arg AIRFLOW_INSTALLATION_METHOD="apache-airflow" \
     --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 029b8e21ba4..f6c47973296 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -344,6 +344,7 @@ cosmosdb
 cp
 cpu
 cpus
+CPython
 CRD
 crd
 CreateQueryOperator
diff --git a/scripts/ci/prek/upgrade_important_versions.py b/scripts/ci/prek/upgrade_important_versions.py
index 987d4f61449..c709f659628 100755
--- a/scripts/ci/prek/upgrade_important_versions.py
+++ b/scripts/ci/prek/upgrade_important_versions.py
@@ -401,53 +401,31 @@ def prune_obsolete_cooldown_overrides() -> bool:
     return True
 
 
-def get_all_python_versions() -> list[Version]:
-    """
-    Fetch all released Python versions by parsing the Python FTP directory listing.
-    This provides static information about all available Python releases.
-    """
-    if VERBOSE:
-        console.print("[bright_blue]Fetching all released Python versions from python.org FTP")
-    url = "https://www.python.org/ftp/python/"
-    headers = {"User-Agent": "Python requests"}
-    response = requests.get(url, headers=headers)
-    response.raise_for_status()
-
-    # Parse the HTML directory listing to extract version numbers
-    # The FTP directory listing has links like: <a href="3.12.1/">3.12.1/</a>
-    versions = []
-    # Match version patterns like "3.12.1/" in href attributes
-    version_pattern = re.compile(r'href="(\d+\.\d+\.\d+)/"')
-
-    for match in version_pattern.finditer(response.text):
-        version_str = match.group(1)
-        try:
-            # Parse as version to validate it's a proper version number
-            version_obj = Version(version_str)
-            # Only include Python 3.x versions
-            if version_obj.major == 3:
-                versions.append(version_obj)
-        except Exception:
-            # Skip invalid version strings
-            continue
-
-    return versions
+HARDENED_PYTHON_CATALOG_URL = (
+    "https://raw.githubusercontent.com/docker-hardened-images/catalog/main/image/python/debian-12/"
+    "{python_major_minor}-dev.yaml"
+)
+HARDENED_PYTHON_TAG_PATTERN = re.compile(r"^\s*-\s*(\d+\.\d+\.\d+)-debian12-dev\s*$", re.MULTILINE)
 
 
-def get_latest_python_version(python_major_minor: str, all_versions: list[Version]) -> str:
+def get_latest_python_version(python_major_minor: str) -> str:
     """
-    Fetch the latest released Python version for a given major.minor (e.g. '3.12') from FTP directory listing.
-    Uses static directory information rather than API calls.
+    Return the newest Python patchlevel published as a Docker Hardened Image for a major.minor.
+
+    The images are built on those hardened images, so the pinned patchlevel has to be one Docker has
+    published - a newer python.org release is of no use until Docker builds it. The image definitions
+    in the public catalog list the patchlevel tag each image is currently built from.
     """
-    # Only consider releases matching the major.minor.patch pattern
-    matching = [
-        version for version in all_versions if python_major_minor == f"{version.major}.{version.minor}"
-    ]
-    if not matching:
-        console.print(f"[bright_red]No released Python versions found for {python_major_minor}")
+    url = HARDENED_PYTHON_CATALOG_URL.format(python_major_minor=python_major_minor)
+    if VERBOSE:
+        console.print(f"[bright_blue]Fetching the hardened Python {python_major_minor} definition from {url}")
+    response = requests.get(url, headers={"User-Agent": "Python requests"})
+    response.raise_for_status()
+    versions = [Version(tag) for tag in HARDENED_PYTHON_TAG_PATTERN.findall(response.text)]
+    if not versions:
+        console.print(f"[bright_red]No hardened Python {python_major_minor} image tags found in {url}")
         sys.exit(1)
-    # Sort and return the latest version
-    latest_version = sorted(matching)[-1]
+    latest_version = max(versions)
     if VERBOSE:
         console.print(f"[bright_blue]Latest version for {python_major_minor}: {latest_version}")
     return str(latest_version)
@@ -677,6 +655,11 @@ GOLANG_PATTERNS: list[tuple[re.Pattern, Quoting]] = [
 
 AIRFLOW_IMAGE_PYTHON_PATTERNS: list[tuple[re.Pattern, Quoting]] = [
     (re.compile(r"(AIRFLOW_PYTHON_VERSION=)(\"[0-9.abrc]+\")"), Quoting.DOUBLE_QUOTED),
+    # Base image tags pin the same Python patchlevel as AIRFLOW_PYTHON_VERSION and have to move with it
+    (
+        re.compile(r"((?:dhi\.io|ghcr\.io/apache/airflow/base)/python:)([0-9]+\.[0-9]+\.[0-9.abrc]+)"),
+        Quoting.UNQUOTED,
+    ),
     (
         re.compile(r"(\| ``AIRFLOW_PYTHON_VERSION`` *\| )(``[0-9.abrc]+``)( *\|)"),
         Quoting.REVERSE_DOUBLE_QUOTED,
@@ -964,10 +947,9 @@ def fetch_python_versions() -> dict[str, str]:
     if not UPGRADE_PYTHON:
         return latest_python_versions
 
-    all_python_versions = get_all_python_versions()
     for python_major_minor_version in ALL_PYTHON_MAJOR_MINOR_VERSIONS:
         latest_python_versions[python_major_minor_version] = get_latest_python_version(
-            python_major_minor_version, all_python_versions
+            python_major_minor_version
         )
         if python_major_minor_version == DEFAULT_PROD_IMAGE_PYTHON_VERSION:
             console.print(
diff --git a/scripts/docker/common.sh b/scripts/docker/common.sh
index d8ebb2e261f..b18c87098c5 100644
--- a/scripts/docker/common.sh
+++ b/scripts/docker/common.sh
@@ -136,12 +136,12 @@ function common::get_constraints_location() {
 
 function common::show_packaging_tool_version_and_location() {
    echo "PATH=${PATH}"
-   echo "Installed pip: $(pip --version): $(which pip)"
+   echo "Installed pip: $(pip --version): $(command -v pip)"
    if [[ ${PACKAGING_TOOL} == "pip" ]]; then
        echo "${COLOR_BLUE}Using 'pip' to install Airflow${COLOR_RESET}"
    else
        echo "${COLOR_BLUE}Using 'uv' to install Airflow${COLOR_RESET}"
-       echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(which uv 2>/dev/null)"
+       echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(command -v uv 2>/dev/null)"
    fi
 }
 
@@ -153,7 +153,7 @@ function common::install_packaging_tools() {
         echo
     else
         echo
-        echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(which python)${COLOR_RESET}"
+        echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(command -v python)${COLOR_RESET}"
         echo
     fi
     if [[ ${AIRFLOW_PIP_VERSION=} == "" ]]; then
@@ -217,6 +217,16 @@ function common::install_packaging_tools() {
     fi
 }
 
+function common::debian_codename() {
+    # shellcheck disable=SC1091
+    (. /etc/os-release; printf '%s\n' "${VERSION_CODENAME}")
+}
+
+function common::debian_release() {
+    # shellcheck disable=SC1091
+    (. /etc/os-release; printf '%s\n' "${VERSION_ID}")
+}
+
 function common::import_trusted_gpg() {
     common::get_colors
 
diff --git a/scripts/docker/install_mssql.sh b/scripts/docker/install_mssql.sh
index f50f213d851..94abe6c7655 100644
--- a/scripts/docker/install_mssql.sh
+++ b/scripts/docker/install_mssql.sh
@@ -43,7 +43,7 @@ function install_mssql_client() {
     echo "${COLOR_BLUE}Installing mssql client${COLOR_RESET}"
     echo
 
-    echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(lsb_release -rs)/prod $(lsb_release -cs) main" > \
+    echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(common::debian_release)/prod $(common::debian_codename) main" > \
         /etc/apt/sources.list.d/mssql-release.list &&
     mkdir -p /opt/microsoft/msodbcsql18 &&
     touch /opt/microsoft/msodbcsql18/ACCEPT_EULA &&
diff --git a/scripts/docker/install_mysql.sh b/scripts/docker/install_mysql.sh
index 012ef9f14ac..68c5b768fe6 100644
--- a/scripts/docker/install_mysql.sh
+++ b/scripts/docker/install_mysql.sh
@@ -106,7 +106,7 @@ install_mariadb_client() {
     echo "${COLOR_YELLOW}MariaDB client protocol-compatible with MySQL client.${COLOR_RESET}"
     echo
 
-    echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(lsb_release -cs) main" > \
+    echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(common::debian_codename) main" > \
         /etc/apt/sources.list.d/mariadb.list
     # Make sure that dependencies from MariaDB repo are preferred over Debian dependencies
     printf "Package: *\nPin: release o=MariaDB\nPin-Priority: 999\n" > /etc/apt/preferences.d/mariadb
diff --git a/scripts/docker/install_os_dependencies.sh b/scripts/docker/install_os_dependencies.sh
index 7db52ba803c..8727e57a3d1 100644
--- a/scripts/docker/install_os_dependencies.sh
+++ b/scripts/docker/install_os_dependencies.sh
@@ -25,8 +25,6 @@ if [[ "$#" != 1 ]]; then
     exit 1
 fi
 
-AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.11.16}
-PYTHON_LTO=${PYTHON_LTO:-true}
 GOLANG_MAJOR_MINOR_VERSION=${GOLANG_MAJOR_MINOR_VERSION:-1.24.4}
 TEMURIN_VERSION=${TEMURIN_VERSION:-11}
 NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
@@ -35,7 +33,8 @@ NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
 PNPM_VERSION=${PNPM_VERSION:-10.28.1}
 RUSTUP_DEFAULT_TOOLCHAIN=${RUSTUP_DEFAULT_TOOLCHAIN:-stable}
 RUSTUP_VERSION=${RUSTUP_VERSION:-1.29.0}
-COSIGN_VERSION=${COSIGN_VERSION:-3.0.5}
+# The hardened Python base images ship Python under /opt/python.
+PYTHON_HOME=${PYTHON_HOME:-/opt/python}
 
 if [[ "${1}" == "runtime" ]]; then
     INSTALLATION_TYPE="RUNTIME"
@@ -62,38 +61,25 @@ freetds-dev \
 git \
 graphviz \
 graphviz-dev \
+gzip \
 krb5-user \
-lcov \
 ldap-utils \
-libbluetooth-dev \
-libbz2-dev \
 libc6-dev \
-libdb-dev \
 libev-dev \
 libev4 \
 libffi-dev \
-libgdbm-compat-dev \
-libgdbm-dev \
 libgeos-dev \
 libkrb5-dev \
 libldap2-dev \
 libleveldb-dev \
 libleveldb1d \
-liblzma-dev \
-libncurses5-dev \
-libreadline6-dev \
 libsasl2-2 \
 libsasl2-dev \
 libsasl2-modules \
-libsqlite3-dev \
 libssl-dev \
 libxmlsec1 \
 libxmlsec1-dev \
-libzstd-dev \
 locales \
-lsb-release \
-lzma \
-lzma-dev \
 openssh-client \
 openssl \
 pkg-config \
@@ -102,10 +88,8 @@ sasl2-bin \
 sqlite3 \
 sudo \
 tdsodbc \
-tk-dev \
 unixodbc \
 unixodbc-dev \
-uuid-dev \
 wget \
 xz-utils \
 zlib1g-dev \
@@ -132,6 +116,9 @@ netcat-openbsd\
     echo
     echo "APPLIED INSTALLATION CONFIGURATION FOR DEBIAN VERSION: ${debian_version}"
     echo
+    # libxmlsec1-openssl was added because libxmlsec1 ships no crypto engine of its own - the engines
+    # are separate packages - so the "xmlsec" module (pulled in by python3-saml) imported with
+    # "libxmlsec1-openssl.so.1: cannot open shared object file" without it.
     if [[ "${RUNTIME_APT_DEPS=}" == "" ]]; then
         RUNTIME_APT_DEPS="\
 ${debian_version_apt_deps} \
@@ -150,8 +137,8 @@ libgeos-dev \
 libsasl2-2 \
 libsasl2-modules \
 libxmlsec1 \
+libxmlsec1-openssl \
 locales \
-lsb-release \
 openssh-client \
 rsync \
 sasl2-bin \
@@ -180,10 +167,42 @@ function install_docker_cli() {
     apt-get install -y --no-install-recommends docker-ce-cli
 }
 
+function keep_image_conffiles() {
+    # The hardened base images ship a modified /etc/debian_version, so any apt run that upgrades
+    # base-files stops at dpkg's interactive conffile prompt and fails a non-interactive build. Making
+    # dpkg keep the image's copy by default covers every apt run - this build's and those of images
+    # extending it - so no install command has to remember the flags.
+    mkdir -p /etc/dpkg/dpkg.cfg.d
+    printf '%s\n' force-confdef force-confold > /etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles
+}
+
+function restore_debian_base_files() {
+    # The hardened base images ship a minimal /etc, but Debian maintainer scripts assume the files
+    # a stock Debian has: sasl2-bin chowns its run directory to the "sasl" group from base-passwd,
+    # and tmux registers its shell with add-shell, which reads /etc/shells. The base-passwd package
+    # only ships the reference copies of the account files - update-passwd is what merges them
+    # into /etc.
+    # libpam-runtime generates the /etc/pam.d/common-* files that the PAM configs already in the
+    # image "@include" - without them "adduser --gecos" aborts with a PAM error from chfn.
+    # Debian Policy lets a maintainer script rely on any essential package without declaring it, and
+    # libgcrypt20 takes that up: its postinst runs a helper with a "#!/bin/dash" shebang. The hardened
+    # images ship /bin/sh as a symlink to bash and no dash at all, so configuring the package aborts
+    # with exit 127 and takes the whole apt transaction with it. Installing it first restores the
+    # assumption, and hands /bin/sh back to dash the way a stock Debian has it.
+    apt-get install -y --no-install-recommends dash
+    apt-get install -y --no-install-recommends base-passwd libpam-runtime
+    update-passwd
+    if [[ ! -e /etc/shells ]]; then
+        printf '%s\n' "# /etc/shells: valid login shells" /bin/sh /bin/bash > /etc/shells
+    fi
+}
+
 function install_debian_dev_dependencies() {
+    keep_image_conffiles
     apt-get update
     apt-get install -yqq --no-install-recommends apt-utils >/dev/null 2>&1
-    apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
+    restore_debian_base_files
+    apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
     # shellcheck disable=SC2086
     export ${ADDITIONAL_DEV_APT_ENV?}
     if [[ ${DEV_APT_COMMAND} != "" ]]; then
@@ -213,14 +232,25 @@ function install_additional_dev_dependencies() {
 }
 
 function link_python() {
+    # Airflow images have always exposed Python under /usr/python - documentation, volume mounts and
+    # user customizations refer to that path - while the hardened base images ship it in /opt/python,
+    # so keep the historical location working as a symlink.
+    if [[ ! -e /usr/python ]]; then
+        ln -sv "${PYTHON_HOME}" /usr/python
+    fi
+    # The hardened base images have no /usr/local tree at all
+    mkdir -p /usr/local/bin /usr/local/lib
     # link python binaries to /usr/local/bin and /usr/python/bin with and without 3 suffix
     # Links in /usr/local/bin are needed for tools that expect python to be there
     # Links in /usr/python/bin are needed for tools that are detecting home of python installation including
     # lib/site-packages. The /usr/python/bin should be first in PATH in order to help with the last part.
     for dst in pip3 python3 python3-config; do
         src="$(echo "${dst}" | tr -d 3)"
+        if [[ ! -e "/usr/python/bin/${dst}" ]]; then
+            continue
+        fi
         echo "Linking ${dst} in /usr/local/bin and /usr/python/bin"
-        ln -sv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
+        ln -sfv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
         for dir in /usr/local/bin /usr/python/bin; do
             if [[ ! -e "${dir}/${src}" ]]; then
                 echo "Creating ${src} - > ${dst} link in ${dir}"
@@ -240,62 +270,47 @@ function link_python() {
     ldconfig
 }
 
-function install_debian_runtime_dependencies() {
-    apt-get update
-    apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
-    apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
-    # shellcheck disable=SC2086
-    export ${ADDITIONAL_RUNTIME_APT_ENV?}
-    if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
-        bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
-    fi
-    if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
-        bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
-    fi
-    apt-get update
-    # shellcheck disable=SC2086
-    apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
-    apt-get autoremove -yqq --purge
-    apt-get clean
-    link_python
-    rm -rf /var/lib/apt/lists/* /var/log/*
+function restore_thread_stack_size() {
+    # The hardened base images build Python with -DTHREAD_STACK_SIZE=0x100000, so every thread Python
+    # starts gets a 1 MiB stack instead of glibc's default (the 8 MiB "ulimit -s" the previous images
+    # used). On Python 3.12 and 3.13 the C recursion guard is a fixed depth count sized for the larger
+    # stack, so deeply nested input - e.g. a JSON body parsed in a web server worker thread - overflows
+    # the stack and kills the process instead of raising RecursionError. A .pth file runs at every
+    # interpreter start-up without taking the sitecustomize module name users may already rely on.
+    local site_packages
+    site_packages="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
+    echo "Restoring the 8 MiB default thread stack size in ${site_packages}"
+    echo "import threading; threading.stack_size(8 * 1024 * 1024)" > "${site_packages}/airflow-thread-stack-size.pth"
 }
 
-function install_cosign() {
-    local arch
-    arch="$(dpkg --print-architecture)"
-    declare -A cosign_sha256s=(
-        # https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign_checksums.txt
-        [amd64]="db15cc99e6e4837daabab023742aaddc3841ce57f193d11b7c3e06c8003642b2"
-        [arm64]="d098f3168ae4b3aa70b4ca78947329b953272b487727d1722cb3cb098a1a20ab"
-    )
-    local cosign_sha256="${cosign_sha256s[${arch}]}"
-    if [[ -z "${cosign_sha256}" ]]; then
-        echo "Unsupported architecture for cosign: ${arch}"
-        exit 1
-    fi
-    curl -fsSL --retry 3 --retry-delay 5 \
-        "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-${arch}" \
-        -o /tmp/cosign
-    echo "${cosign_sha256}  /tmp/cosign" | sha256sum --check
-    chmod +x /tmp/cosign
+function compile_python_stdlib() {
+    # The hardened base images ship the standard library with no .pyc files at all. Python then misses
+    # the bytecode cache on every stdlib import, and in a read-only or non-writable directory it cannot
+    # create one - each miss leaves a negative dentry in the kernel, which grows without bound in a
+    # long-running container and can exhaust memory on the host. Compiling the standard library here
+    # restores what the previously compiled-in-image Python shipped.
+    # See https://github.com/apache/airflow/pull/58944 and https://lwn.net/Articles/814535/
+    local stdlib
+    stdlib="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["stdlib"])')"
+    echo "Compiling Python standard library in ${stdlib}"
+    # compileall exits non-zero when any file fails to compile, and the standard library ships files
+    # that are meant not to compile (deliberately broken syntax used by the test suite).
+    /usr/python/bin/python -m compileall -q -j "$(nproc)" -o 0 -o 1 -o 2 "${stdlib}" || true
 }
 
-function install_python() {
-    # If system python (3.11 in bookworm) is installed (via automatic installation of some dependencies for example), we need
-    # to fail and make sure that it is not there, because there can be strange interactions if we install
-    # newer version and system libraries are installed, because
-    # when you create a virtualenv part of the shared libraries of Python can be taken from the system
-    # Installation leading to weird errors when you want to install some modules - for example when you install ssl:
+function check_no_system_python() {
+    # Python comes from the hardened base image (in /opt/python) and must stay the only Python in the
+    # image. A system Python pulled in as a dependency of an apt package shares its shared libraries
+    # with ours and leads to errors such as:
     # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
     if dpkg -l | grep '^ii' | grep '^ii  libpython' >/dev/null; then
         echo
         echo "ERROR! System python is installed by one of the previous steps"
         echo
-        echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython3.11 is installed:"
+        echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython is installed:"
         echo
         apt-get install -yqq aptitude >/dev/null
-        aptitude why libpython3.11
+        aptitude why "$(dpkg -l | grep '^ii  libpython' | head -1 | awk '{print $2}')"
         echo
         exit 1
     else
@@ -303,86 +318,32 @@ function install_python() {
         echo "GOOD! System python is not installed - OK"
         echo
     fi
-    wget --tries=3 --waitretry=5 -O python.tar.xz "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz"
-    local major_minor_version
-    major_minor_version="${AIRFLOW_PYTHON_VERSION%.*}"
-    echo "Verifying Python ${AIRFLOW_PYTHON_VERSION} (${major_minor_version})"
-    # Sigstore verification (PEP 761)
-    declare -A sigstore_identities=(
-        # https://peps.python.org/pep-0664/#release-manager-and-crew
-        [3.11]="pablogsal@python.org"
-        # https://peps.python.org/pep-0693/#release-manager-and-crew
-        [3.12]="thomas@python.org"
-        # https://peps.python.org/pep-0719/#release-manager-and-crew
-        [3.13]="thomas@python.org"
-        # https://peps.python.org/pep-0745/#release-manager-and-crew
-        [3.14]="hugo@python.org"
-        # https://peps.python.org/pep-0790/#release-manager-and-crew
-        [3.15]="hugo@python.org"
-    )
-    declare -A sigstore_issuers=(
-        [3.11]="https://accounts.google.com"
-        [3.12]="https://accounts.google.com"
-        [3.13]="https://accounts.google.com"
-        [3.14]="https://github.com/login/oauth"
-        [3.15]="https://github.com/login/oauth"
-    )
-    wget --tries=3 --waitretry=5 -O python.tar.xz.sigstore \
-        "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz.sigstore"
-    install_cosign
-    local identity="${sigstore_identities[${major_minor_version}]}"
-    local issuer="${sigstore_issuers[${major_minor_version}]}"
-    /tmp/cosign verify-blob \
-        --bundle python.tar.xz.sigstore \
-        --certificate-identity "${identity}" \
-        --certificate-oidc-issuer "${issuer}" \
-        python.tar.xz
-    rm -f python.tar.xz.sigstore /tmp/cosign
-    mkdir -p /usr/src/python
-    tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz
-    rm python.tar.xz
-    cd /usr/src/python
-    arch="$(dpkg --print-architecture)"; arch="${arch##*-}"
-    gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"
-    EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"
-    EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer";
-    LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"
-    LDFLAGS="${LDFLAGS:--Wl},--strip-all"
-    # Link-Time Optimization (LTO) uses MD5 checksums for object file verification during
-    # compilation. In FIPS mode, MD5 is blocked as a non-approved algorithm, causing builds
-    # to fail. The PYTHON_LTO variable allows disabling LTO for FIPS-compliant builds.
-    # See: https://github.com/apache/airflow/issues/58337
-    local lto_option=""
-    if [[ "${PYTHON_LTO:-true}" == "true" ]]; then
-        lto_option="--with-lto"
+}
+
+function install_debian_runtime_dependencies() {
+    keep_image_conffiles
+    apt-get update
+    apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
+    restore_debian_base_files
+    apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
+    # shellcheck disable=SC2086
+    export ${ADDITIONAL_RUNTIME_APT_ENV?}
+    if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
+        bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
     fi
-    local build_log
-    build_log=$(mktemp)
-    echo "Building Python ${AIRFLOW_PYTHON_VERSION} from source..."
-    if ! (
-        ./configure --enable-optimizations --prefix=/usr/python/ --with-ensurepip --build="$gnuArch" \
-            --enable-loadable-sqlite-extensions --enable-option-checking=fatal \
-                --enable-shared ${lto_option} && \
-        make -s -j "$(nproc)" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
-            "LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" python && \
-        make -s -j "$(nproc)" install
-    ) > "${build_log}" 2>&1; then
-        echo
-        echo "ERROR! Python build failed. Build output:"
-        echo
-        cat "${build_log}"
-        rm -f "${build_log}"
-        exit 1
+    if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
+        bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
     fi
-    rm -f "${build_log}"
-    cd /
-    rm -rf /usr/src/python
-    find /usr/python -depth \
-      \( \
-        \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
-        -o \( -type f -a \( -name 'libpython*.a' \) \) \
-    \) -exec rm -rf '{}' +
+    apt-get update
+    # shellcheck disable=SC2086
+    apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
+    apt-get autoremove -yqq --purge
+    apt-get clean
+    check_no_system_python
     link_python
+    restore_thread_stack_size
+    compile_python_stdlib
+    rm -rf /var/lib/apt/lists/* /var/log/*
 }
 
 function install_golang() {
@@ -481,7 +442,10 @@ if [[ "${INSTALLATION_TYPE}" == "RUNTIME" ]]; then
 else
     get_dev_apt_deps
     install_debian_dev_dependencies
-    install_python
+    check_no_system_python
+    link_python
+    restore_thread_stack_size
+    compile_python_stdlib
     install_additional_dev_dependencies
     install_rustup
     if [[ "${INSTALLATION_TYPE}" == "CI" ]]; then
diff --git a/scripts/docker/install_postgres.sh b/scripts/docker/install_postgres.sh
index 4af446a7a68..9a6c625bdee 100644
--- a/scripts/docker/install_postgres.sh
+++ b/scripts/docker/install_postgres.sh
@@ -43,7 +43,7 @@ install_postgres_client() {
 
     common::import_trusted_gpg "7FCC7D46ACCC4CF8" "postgres"
 
-    echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > \
+    echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(common::debian_codename)-pgdg main" > \
         /etc/apt/sources.list.d/pgdg.list
     apt-get update
     apt-get install --no-install-recommends -y "${packages[@]}"
diff --git a/scripts/in_container/run_ci_tests.sh b/scripts/in_container/run_ci_tests.sh
index 9bb1d149cf5..77f5e69abb7 100755
--- a/scripts/in_container/run_ci_tests.sh
+++ b/scripts/in_container/run_ci_tests.sh
@@ -23,17 +23,22 @@ echo "Starting the tests with those pytest arguments:" "${@}"
 echo
 set +e
 
+# A segfault kills the process before buffered output reaches the log, which leaves nothing
+# to diagnose from; both of these make the crash describe itself instead.
+export PYTHONFAULTHANDLER="1"
+export PYTHONUNBUFFERED="1"
+
 pytest "${@}"
 RES=$?
 
 if [[ ${RES} == "139" ]]; then
     echo "${COLOR_YELLOW}Sometimes Pytest fails at exiting with segfault, but all tests actually passed${COLOR_RESET}"
-    echo "${COLOR_YELLOW}We should ignore such case. Checking if junitxml file ${RESULT_LOG_FILE} is there with 0 errors and failures${COLOR_RESET}"
-    if [[ -f ${RESULT_LOG_FILE} ]]; then
+    echo "${COLOR_YELLOW}We should ignore such case. Checking if junitxml file ${RESULT_LOG_FILE:-<unset>} is there with 0 errors and failures${COLOR_RESET}"
+    if [[ -n ${RESULT_LOG_FILE:-} && -f ${RESULT_LOG_FILE} ]]; then
         python "${AIRFLOW_SOURCES}/scripts/in_container/check_junitxml_result.py" "${RESULT_LOG_FILE}"
         RES=$?
     else
-        echo "${COLOR_YELLOW}JunitXML file ${RESULT_LOG_FILE} does not exist. Proceeding with failure as we cannot check if there were no failures.${COLOR_RESET}"
+        echo "${COLOR_YELLOW}JunitXML file ${RESULT_LOG_FILE:-<unset>} does not exist. Proceeding with failure as we cannot check if there were no failures.${COLOR_RESET}"
     fi
 fi
 
diff --git a/scripts/tests/ci/prek/test_upgrade_important_versions.py b/scripts/tests/ci/prek/test_upgrade_important_versions.py
index 6eb9d28d663..97f9895717a 100644
--- a/scripts/tests/ci/prek/test_upgrade_important_versions.py
+++ b/scripts/tests/ci/prek/test_upgrade_important_versions.py
@@ -237,3 +237,41 @@ def test_flit_core_upgrade_updates_provider_pyproject_template(monkeypatch):
     )
 
     assert 'requires = ["flit_core==99.0.0"]' in new_content
+
+
+@pytest.mark.parametrize(
+    ("definition", "expected"),
+    [
+        pytest.param(
+            "tags:\n  - 3.14-debian12-dev\n  - 3.14.8-debian12-dev\nplatforms:\n",
+            "3.14.8",
+            id="floating-and-patchlevel-tags",
+        ),
+        pytest.param(
+            "tags:\n  - 3.12.9-debian12-dev\n  - 3.12.15-debian12-dev\n",
+            "3.12.15",
+            id="highest-patchlevel-wins-numerically",
+        ),
+    ],
+)
+def test_python_patchlevel_comes_from_the_hardened_image_catalog(monkeypatch, definition, expected):
+    from ci.prek import upgrade_important_versions as uiv
+
+    response = mock.MagicMock(spec=uiv.requests.Response)
+    response.text = definition
+    get = mock.create_autospec(uiv.requests.get, return_value=response)
+    monkeypatch.setattr(uiv.requests, "get", get)
+
+    assert uiv.get_latest_python_version("3.14") == expected
+    assert get.call_args.args[0].endswith("/image/python/debian-12/3.14-dev.yaml")
+
+
+def test_python_patchlevel_exits_when_the_catalog_lists_no_tag(monkeypatch):
+    from ci.prek import upgrade_important_versions as uiv
+
+    response = mock.MagicMock(spec=uiv.requests.Response)
+    response.text = "tags:\n  - 3.14-debian12-dev\n"
+    monkeypatch.setattr(uiv.requests, "get", mock.create_autospec(uiv.requests.get, return_value=response))
+
+    with pytest.raises(SystemExit):
+        uiv.get_latest_python_version("3.14")