diff --git a/.gitea/scripts/resolve-base-image.sh b/.gitea/scripts/resolve-base-image.sh new file mode 100755 index 0000000..1c5b074 --- /dev/null +++ b/.gitea/scripts/resolve-base-image.sh @@ -0,0 +1,35 @@ +#!/bin/bash +## resolve-base-image.sh IMAGE:TAG — CI only (not copied into images). +## Prints IMAGE@sha256: for a Docker Hub tag, looked up DIRECTLY at +## registry-1.docker.io over HTTPS (never via the pkg-cache mirror), so the +## build can say `FROM IMAGE@sha256:...` and the mirror can only serve +## content-addressed blobs that BuildKit verifies. Exits non-zero (prints +## nothing on stdout) if the lookup fails; the workflow then skips the mirror +## and pulls the tag directly from Docker Hub. HEAD requests do not count +## against Docker Hub pull rate limits. +## Canonical copy: cloud-apache-container/.gitea/scripts/resolve-base-image.sh +## (docs/pkg-cache.md there); other repos carry identical copies. +set -euo pipefail +ref="${1:?usage: resolve-base-image.sh IMAGE:TAG}" +name_re='^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)?$' +tag_re='^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' +name="${ref%:*}"; tag="${ref##*:}" +[ "$name" != "$ref" ] || { echo "resolve-base-image: '$ref' has no :tag" >&2; exit 2; } +[[ "$name" =~ $name_re ]] || { echo "resolve-base-image: bad image name '$name'" >&2; exit 2; } +[[ "$tag" =~ $tag_re ]] || { echo "resolve-base-image: bad tag '$tag'" >&2; exit 2; } +repo="$name"; [[ "$repo" == */* ]] || repo="library/$repo" # official images + +curl_opts=(-fsS --proto '=https' --retry 3 --retry-all-errors --max-time 20) +token=$(curl "${curl_opts[@]}" "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" | + python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])') +[ -n "$token" ] || { echo "resolve-base-image: no Docker Hub token for $repo" >&2; exit 1; } +hdrs=$(curl "${curl_opts[@]}" -I \ + -H "Authorization: Bearer ${token}" \ + -H 'Accept: application/vnd.oci.image.index.v1+json' \ + -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \ + -H 'Accept: application/vnd.oci.image.manifest.v1+json' \ + -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ + "https://registry-1.docker.io/v2/${repo}/manifests/${tag}") +digest=$(printf '%s\n' "$hdrs" | tr -d '\r' | awk 'tolower($1)=="docker-content-digest:" {print $2}' | tail -n1) +[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "resolve-base-image: no valid digest for $ref (got '${digest}')" >&2; exit 1; } +printf '%s@%s\n' "$name" "$digest" diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 5497dbd..ae19b83 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -12,23 +12,79 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Base image pinned by DIGEST, looked up directly at Docker Hub (never via + # the pkg-cache mirror), so the mirror can only serve content-addressed + # blobs that BuildKit verifies. If the lookup fails this job does NOT use + # the mirror and pulls the tag straight from Docker Hub. + - name: Resolve base image digest (Docker Hub, direct) + id: base + env: + BASE_TAG: almalinux/9-base:latest + run: | + set -uo pipefail + if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then + echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)" + echo "image=${ref}" >> "$GITHUB_OUTPUT" + echo "pinned=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used" + echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT" + echo "pinned=false" >> "$GITHUB_OUTPUT" + fi + + # Internal package cache (CT130 pkg-cache). Used ONLY if it answers over + # TLS with our pinned CA; otherwise (down, unreachable, bad/expired cert) + # this job builds from the upstream repos and Docker Hub exactly as before. + # The repo cache and the registry mirror are probed separately so either + # can fail back on its own. + - name: Probe package cache + id: pkgcache + # The registry mirror is only used for a digest-pinned base image. + env: + BASE_PINNED: ${{ steps.base.outputs.pinned }} + run: | + url=""; mirror="" + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi + if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi + echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" + [ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories" + echo "url=${url}" >> "$GITHUB_OUTPUT" + echo "mirror=${mirror}" >> "$GITHUB_OUTPUT" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + # BuildKit (docker-container driver) ignores dockerd's registry-mirrors, + # so the mirror is configured on the builder; BuildKit itself still falls + # back to Docker Hub if the mirror stops answering mid-run. + - name: Set up Docker Buildx (Docker Hub via pkg-cache mirror) + if: steps.pkgcache.outputs.mirror != '' + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["192.168.1.40:5000"] + [registry."192.168.1.40:5000"] + ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"] + - name: Set up Docker Buildx - uses: https://github.com/docker/setup-buildx-action@v3 - + if: steps.pkgcache.outputs.mirror == '' + uses: docker/setup-buildx-action@v3 + - name: Login to Gitea uses: docker/login-action@v3 with: registry: repo.anhonesthost.net username: ${{ secrets.CI_USER }} password: ${{ secrets.CI_TOKEN }} - + - name: Build Image uses: docker/build-push-action@v6 with: platforms: linux/amd64 push: true + build-args: | + BASE_IMAGE=${{ steps.base.outputs.image }} + PKG_CACHE=${{ steps.pkgcache.outputs.url }} tags: | repo.anhonesthost.net/cloud-hosting-platform/cmc:latest diff --git a/Dockerfile b/Dockerfile index 4cf2629..49b071e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,26 @@ -FROM almalinux/9-base -RUN dnf update -y && dnf upgrade -y -RUN dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm -y -RUN dnf update -y && dnf upgrade -y -RUN dnf install -y memcached procps-ng -RUN yum clean all +# Base image. CI passes BASE_IMAGE=almalinux/9-base@sha256:, resolved +# DIRECTLY from Docker Hub in the same run (.gitea/scripts/resolve-base-image.sh), +# so the pkg-cache registry mirror can only serve content-addressed blobs. +# Local builds use the tag. +ARG BASE_IMAGE=almalinux/9-base +FROM ${BASE_IMAGE} + +# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40 +# (CT130 pkg-cache, https only). Unset = upstream repositories. epel-release is +# GPG-verified against keys/pkg-cache/ either way. Repo files are restored at +# the end of the RUN, so no layer keeps the cache address. Same script as +# cloud-apache-container (scripts/pkg-cache-repos.sh, docs/pkg-cache.md there). +ARG PKG_CACHE= +COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh +COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/ +RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \ + /usr/local/sbin/pkg-cache-repos.sh release-install epel && \ + /usr/local/sbin/pkg-cache-repos.sh rpm && \ + dnf update -y && \ + dnf install -y memcached procps-ng && \ + /usr/local/sbin/pkg-cache-repos.sh restore && \ + dnf clean all && \ + rm -rf /var/cache/dnf /var/cache/yum COPY ./scripts/entrypoint.sh / RUN chmod +x /entrypoint.sh ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/keys/pkg-cache/README b/keys/pkg-cache/README new file mode 100644 index 0000000..c8edeec --- /dev/null +++ b/keys/pkg-cache/README @@ -0,0 +1,32 @@ +Trust material for the OPT-IN internal package cache (scripts/pkg-cache-repos.sh, +docs/pkg-cache.md). Copied into images at /usr/local/share/pkg-cache/; nothing +here is added to the system trust store. + +ca.crt "pkg-cache internal CA (CT130)", EC P-256, valid to 2036-10-18. + NAME-CONSTRAINED to IP 192.168.1.40 and DNS + pkg-cache.internal.cloud-hosting.io / pkg-cache.geekhome.local. + SHA-256 fingerprint BB:5B:9A:08:05:ED:2F:58:83:92:D0:99:9D:11:0A:30:14:88:68:F0:F0:88:65:5E:94:0B:75:EB:C2:A2:BD:F3 + (private key lives only on CT130: /etc/pkgcache-ca/ca.key) +RPM-GPG-KEY-EPEL-10 Fedora (epel10) 7D8D 15CB FC4E 6268 8591 FB26 33D9 8517 E37E D158 + fetched 2026-10-21 over HTTPS from dl.fedoraproject.org and + mirror.math.princeton.edu (identical); fingerprint matches + https://fedoraproject.org/security/ +RPM-GPG-KEY-remi2024 Remi's RPM repository 2024 CF1D F005 7CE8 5DFF 5B2F 2A37 C2FD 3B2C 2A09 48E4 + fetched 2026-10-21 over HTTPS from rpms.remirepo.net; byte-identical + to the key shipped in remi-release-10.2-1.el10.remi. Not found on + keys.openpgp.org / keyserver.ubuntu.com - confirm out of band. + Remi rotates keys (remi2025, ...): when remi-release starts being + signed by a new key, `release-install remi` fails loudly - add the + new key here (and to pkg-cache-repos.sh). +RPM-GPG-KEY-EPEL-9 Fedora (epel9) FF8A D134 4597 106E CE81 3B91 8A38 72BF 3228 467C + fetched 2026-10-22 over HTTPS from dl.fedoraproject.org and + mirror.math.princeton.edu (identical); used by `release-install epel` + on EL9 images (cloud-memcached-container, cloud-haproxy-container). +RPM-GPG-KEY-nodesource Nodesource Operations + 242B 8138 31AF 0956 2B6C 46F7 6B88 DA4E 3AF2 8A14 (rsa4096, 2023-12-22) + fetched 2026-10-22 over HTTPS from + https://rpm.nodesource.com/gpgkey/ns-operations-public.key (the key + NodeSource's own setup_N.x scripts reference); verified by dnf + gpgcheck against the nodejs RPMs. Not on keys.openpgp.org / + keyserver.ubuntu.com - confirm out of band. Used by + `pkg-cache-repos.sh nodesource MAJOR` (cloud-node-container). diff --git a/keys/pkg-cache/RPM-GPG-KEY-EPEL-10 b/keys/pkg-cache/RPM-GPG-KEY-EPEL-10 new file mode 100644 index 0000000..adf4f88 --- /dev/null +++ b/keys/pkg-cache/RPM-GPG-KEY-EPEL-10 @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGV4X6kBEAC3eQxgiWuo08uc3mHo4ELux++uqTnYz/tJzEf9Ou3h36WnhumA +Nvs+Ts5h8PBx879Y9/aIX1Z20p1kf6tBCinZnEJu59n+TAAsph0+XQlV1l5YkleK +Z2ff/Fg65k8QcLXWaIGykA/FaKznRiSurGuD6tRGhJw7DawEwBJr8QZSkRUpnH1L +URW97Q/iKrRPiE5VEayE0y8eAL28jIIiFvR+4oJMzvCsRRB/2wYZ2MlJOW91hcYf +mbUoXKOBD5UzsJylu7kj25K/ge8rEJ7KicOOwcdYddxsU3DxGSSfwF8AMagENcm2 +XROeXknjm84A8sNlUkFZBJwfuc7eRTiZGJrnQQVYLrkKj8Mxpq9Ts7hU51TqAWNI +uvGDlJdYNE3D2RMqjMEsZ8ej08Thrib6xslu4NzTBkt+6QNnXL4E3hEgYtoyio60 +GswSz2ulogKg7X4JrNdJYE8/qNowyF3hoVgj5TG1/wQRq+5HlMMOLjgGu9wzLUix +fnVfEUnzaofbrUf4/GabCaeY8xRe4tFQrvzigQ4g+kgwKKnfAeqBmPov0yljkw9z +BYJWR5zvaw0ffg9Ing00KUSaXBXA5jSlgk1603Y+LefY1SlXsTyqohiRvGH6FI77 +HNMo72DwoJfFcYjncZUzKgXWJECR4nhVsdj6pKoOjcQ4aSuyVxtsR86ASQARAQAB +tChGZWRvcmEgKGVwZWwxMCkgPGVwZWxAZmVkb3JhcHJvamVjdC5vcmc+iQJOBBMB +CAA4FiEEfY0Vy/xOYmiFkfsmM9mFF+N+0VgFAmV4X6kCGw8FCwkIBwIGFQoJCAsC +BBYCAwECHgECF4AACgkQM9mFF+N+0Vhv/A/+PlhPLSctGRCUEahE+cN4764Acc3p +l40ZYzXRhqR0/Tc1/cSDjlA3qVTc8SPohi5OJXwCyr9EiMqKoyoDN097euqbYpyp +yN/Pj0lBjsXwcpdDtZ21WGeQU0Khb04N68bMtJbDaxeBciTvDDQravZuPPh0m4Rg +Z6myEoa6Aa6EK0hI1Qwi1qIWeRiuEkVT671IaKVETBW5XiUpNBXDAB/L+6DzUF9u +scBzfsUDiPO6NrpYDtV3jwq22y6gWluIct/Ka8brwPbqK2sBfFzrHboRhfqlTGjs +7F9qUGwIQZn/A8iozXZYQ0+JG1bhQyvjA8eN1GOcRpT+O7H7JXN49o6IG2As4+iK +F04+qjqAu2sVfpD8mzM2VubFNllcKKiyCzRYHhSbObRCPzsudDL9GPiXeGGaCuWg +sDkiA1MESvf2tLETAGBs/TziO4GwmXUtlKbRiq1FYm90mVq9mBxPZ/Idn+yZusNB +0O5SXIbI8lYZw5n4XTK4b+byHRBYsOTHiTsGvjTF2Y7oSwW2CVUmL6RZ23mI4qoY +1p5kzRS+GjT1acnTei/FTsOlIKCsjfeHx7uxCkX6xpAD8P3UtLQqfsgH0CL4vSZt +TGO6L1InQlp4ZG3OYIomTKbD3/R0wod3U3dTqdulQMXL895u6OLTY3spY2m2MO2k +p9Dfd2pKuxK9Mys= +=mhQZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keys/pkg-cache/RPM-GPG-KEY-EPEL-9 b/keys/pkg-cache/RPM-GPG-KEY-EPEL-9 new file mode 100644 index 0000000..234c12f --- /dev/null +++ b/keys/pkg-cache/RPM-GPG-KEY-EPEL-9 @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGE3mOsBEACsU+XwJWDJVkItBaugXhXIIkb9oe+7aadELuVo0kBmc3HXt/Yp +CJW9hHEiGZ6z2jwgPqyJjZhCvcAWvgzKcvqE+9i0NItV1rzfxrBe2BtUtZmVcuE6 +2b+SPfxQ2Hr8llaawRjt8BCFX/ZzM4/1Qk+EzlfTcEcpkMf6wdO7kD6ulBk/tbsW +DHX2lNcxszTf+XP9HXHWJlA2xBfP+Dk4gl4DnO2Y1xR0OSywE/QtvEbN5cY94ieu +n7CBy29AleMhmbnx9pw3NyxcFIAsEZHJoU4ZW9ulAJ/ogttSyAWeacW7eJGW31/Z +39cS+I4KXJgeGRI20RmpqfH0tuT+X5Da59YpjYxkbhSK3HYBVnNPhoJFUc2j5iKy +XLgkapu1xRnEJhw05kr4LCbud0NTvfecqSqa+59kuVc+zWmfTnGTYc0PXZ6Oa3rK +44UOmE6eAT5zd/ToleDO0VesN+EO7CXfRsm7HWGpABF5wNK3vIEF2uRr2VJMvgqS +9eNwhJyOzoca4xFSwCkc6dACGGkV+CqhufdFBhmcAsUotSxe3zmrBjqA0B/nxIvH +DVgOAMnVCe+Lmv8T0mFgqZSJdIUdKjnOLu/GRFhjDKIak4jeMBMTYpVnU+HhMHLq +uDiZkNEvEEGhBQmZuI8J55F/a6UURnxUwT3piyi3Pmr2IFD7ahBxPzOBCQARAQAB +tCdGZWRvcmEgKGVwZWw5KSA8ZXBlbEBmZWRvcmFwcm9qZWN0Lm9yZz6JAk4EEwEI +ADgWIQT/itE0RZcQbs6BO5GKOHK/MihGfAUCYTeY6wIbDwULCQgHAgYVCgkICwIE +FgIDAQIeAQIXgAAKCRCKOHK/MihGfFX/EACBPWv20+ttYu1A5WvtHJPzwbj0U4yF +3zTQpBglQ2UfkRpYdipTlT3Ih6j5h2VmgRPtINCc/ZE28adrWpBoeFIS2YAKOCLC +nZYtHl2nCoLq1U7FSttUGsZ/t8uGCBgnugTfnIYcmlP1jKKA6RJAclK89evDQX5n +R9ZD+Cq3CBMlttvSTCht0qQVlwycedH8iWyYgP/mF0W35BIn7NuuZwWhgR00n/VG +4nbKPOzTWbsP45awcmivdrS74P6mL84WfkghipdmcoyVb1B8ZP4Y/Ke0RXOnLhNe +CfrXXvuW+Pvg2RTfwRDtehGQPAgXbmLmz2ZkV69RGIr54HJv84NDbqZovRTMr7gL +9k3ciCzXCiYQgM8yAyGHV0KEhFSQ1HV7gMnt9UmxbxBE2pGU7vu3CwjYga5DpwU7 +w5wu1TmM5KgZtZvuWOTDnqDLf0cKoIbW8FeeCOn24elcj32bnQDuF9DPey1mqcvT +/yEo/Ushyz6CVYxN8DGgcy2M9JOsnmjDx02h6qgWGWDuKgb9jZrvRedpAQCeemEd +fhEs6ihqVxRFl16HxC4EVijybhAL76SsM2nbtIqW1apBQJQpXWtQwwdvgTVpdEtE +r4ArVJYX5LrswnWEQMOelugUG6S3ZjMfcyOa/O0364iY73vyVgaYK+2XtT2usMux +VL469Kj5m13T6w== +=Mjs/ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keys/pkg-cache/RPM-GPG-KEY-nodesource b/keys/pkg-cache/RPM-GPG-KEY-nodesource new file mode 100644 index 0000000..0618885 --- /dev/null +++ b/keys/pkg-cache/RPM-GPG-KEY-nodesource @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +oSVkzT/Mwp9zFVQu5w== +=drMP +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keys/pkg-cache/RPM-GPG-KEY-remi2024 b/keys/pkg-cache/RPM-GPG-KEY-remi2024 new file mode 100644 index 0000000..fe82d62 --- /dev/null +++ b/keys/pkg-cache/RPM-GPG-KEY-remi2024 @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=meaU +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keys/pkg-cache/ca.crt b/keys/pkg-cache/ca.crt new file mode 100644 index 0000000..4413e94 --- /dev/null +++ b/keys/pkg-cache/ca.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICPzCCAeagAwIBAgIUJI0QJZ+PCrSQ/nKZFENwwcp8Zr0wCgYIKoZIzj0EAwIw +TDEmMCQGA1UEAwwdcGtnLWNhY2hlIGludGVybmFsIENBIChDVDEzMCkxIjAgBgNV +BAoMGWNsb3VkLWhvc3RpbmcuaW8gaW50ZXJuYWwwHhcNMjYxMDIxMTkzNjIyWhcN +MzYxMDE4MTkzNjIyWjBMMSYwJAYDVQQDDB1wa2ctY2FjaGUgaW50ZXJuYWwgQ0Eg +KENUMTMwKTEiMCAGA1UECgwZY2xvdWQtaG9zdGluZy5pbyBpbnRlcm5hbDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABBJh75sC5vSzTnkYl0CYYEA/TKUHEXhbtanV +OVNW4NlD3za5+bu5YuKjpbFAva4TCZqWsaS9IMd/JfsaFbf5XVejgaUwgaIwEgYD +VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP7UCTf/ +v7KU1KBrs1vqa71/TZl0MF0GA1UdHgEB/wRTMFGgTzAKhwjAqAEo/////zAlgiNw +a2ctY2FjaGUuaW50ZXJuYWwuY2xvdWQtaG9zdGluZy5pbzAaghhwa2ctY2FjaGUu +Z2Vla2hvbWUubG9jYWwwCgYIKoZIzj0EAwIDRwAwRAIgOod4wPHu0kAzYAw8KeGg +cIrFMwQ4ATKVTkDioiSFOqICIEhY+sGj/YZ4oBMst3b/AAz9zMEJAXpnCkSTI6tM +YAIl +-----END CERTIFICATE----- diff --git a/scripts/pkg-cache-repos.sh b/scripts/pkg-cache-repos.sh new file mode 100755 index 0000000..ee2bd7c --- /dev/null +++ b/scripts/pkg-cache-repos.sh @@ -0,0 +1,296 @@ +#!/bin/bash +## pkg-cache-repos.sh — BUILD-TIME ONLY, OPT-IN. Points dnf/apt at the internal +## package cache (nginx proxy_cache, CT130 "pkg-cache" on the Proxmox cluster) +## when PKG_CACHE is set; without it the repo files are left alone and builds +## use the upstream repositories as before. +## +## PKG_CACHE=https://192.168.1.40 pkg-cache-repos.sh rpm # AlmaLinux/EPEL/Remi/NodeSource +## PKG_CACHE=https://192.168.1.40 pkg-cache-repos.sh apt # Ubuntu, Debian, LiteSpeed +## pkg-cache-repos.sh release-install epel [remi] # GPG-verified release RPMs (EL9/EL10) +## pkg-cache-repos.sh nodesource MAJOR # NodeSource repo file + pinned key +## eval "$(pkg-cache-repos.sh go-env)" # GOPROXY via the cache (go.sum/sumdb verified) +## pkg-cache-repos.sh restore # undo everything above +## +## CANONICAL copy: cloud-apache-container/scripts/pkg-cache-repos.sh (docs/pkg-cache.md +## there). Other repos carry byte-identical copies - change it there first. +## +## Every RUN that rewrites must `restore` before it ends: it puts rewritten files +## back, deletes files this script created, deletes any NEW repo/apt file that +## still references the cache, then FAILS if any reference is left — so no shipped +## layer carries the cache's LAN address (images run on hosts that can't reach it). +## +## Why: on 2026-10-21 (PHP security release) builds failed on Remi mirrors lagging +## hours behind origin, Remi's origin timing out over HTTPS, and an EPEL mirror +## with a broken certificate. The cache fetches from the ORIGINS over HTTPS (with +## origin/mirror fallbacks and stale-on-error), keeps metadata ~3 minutes and +## immutable .rpm/.deb files long-term. +## +## Integrity (security review 2026-10-21): +## * PKG_CACHE must be https://. The cache's certificate is issued by the +## name-constrained "pkg-cache internal CA" (keys/pkg-cache/ca.crt, valid only +## for 192.168.1.40 / pkg-cache.*). That CA is trusted ONLY by the rewritten +## repos (dnf sslcacert= bundle, apt per-host CaInfo) and only during the RUN — +## never added to the system trust store. +## * gpgcheck=1 is forced on every rewritten repo; repo_gpgcheck=1 on Remi and +## AlmaLinux, which sign repomd.xml (measured: repomd.xml.asc 200). EPEL +## publishes no repomd signature (404), so its metadata relies on HTTPS end +## to end (client->cache->dl.fedoraproject.org/mirrors, all verified). +## * release-install downloads epel-release / remi-release (from the cache, or +## from the HTTPS origin without PKG_CACHE), checks the package name, imports +## the signing keys committed in keys/pkg-cache/ and requires +## `rpm -K` "signatures OK" before installing with localpkg_gpgcheck=1. +## * The php.net version gates (EXPECT_PHP_MIN / EXPECT_PHP_VERSION) still fail +## a build that gets old PHP (replay of old-but-signed metadata). +## +## rpm: each rewritten baseurl keeps the upstream origin as a SECOND url, so if +## the cache dies mid-build dnf fails over to the origin by itself. +## apt: apt has no per-source failover; CI only sets PKG_CACHE after probing the +## cache (with the CA), and a build without PKG_CACHE uses upstream sources. +set -euo pipefail + +MODE="${1:?usage: pkg-cache-repos.sh rpm|apt|release-install NAME...|nodesource MAJOR|go-env|restore}" +shift || true +STATE=/var/lib/pkg-cache-repos # saved originals + manifest + created files +SHARE="${PKG_CACHE_SHARE:-/usr/local/share/pkg-cache}" # ca.crt + RPM-GPG-KEY-* +C="${PKG_CACHE:-}" +C="${C%/}" + +die() { echo "FATAL(pkg-cache-repos): $*" >&2; exit 1; } + +if [ -n "$C" ]; then + case "$C" in + https://*) ;; + *) die "PKG_CACHE must be an https:// URL (plain http is refused), got '$C'" ;; + esac + [[ "$C" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._/-]*)?$ ]] || die "PKG_CACHE has unexpected characters: '$C'" + HOSTPORT="${C#https://}"; HOSTPORT="${HOSTPORT%%/*}" + [ -s "$SHARE/ca.crt" ] || die "PKG_CACHE set but $SHARE/ca.crt (pkg-cache CA) is missing" +fi + +# System CA bundle (EL vs Debian/Ubuntu path); the pkg-cache CA is appended to a +# copy in the state dir, never to the system store. +sys_bundle() { + local b + for b in /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt; do + [ -s "$b" ] && { echo "$b"; return 0; } + done + die "no system CA bundle found" +} +make_bundle() { cat "$(sys_bundle)" "$SHARE/ca.crt" > "$STATE/ca-bundle.pem"; } + +# Files that may hold repo configuration; restore re-checks all of them. +repo_files() { + find /etc/yum.repos.d /etc/dnf /etc/apt -type f 2>/dev/null | sort || true +} + +start_session() { + [ -d "$STATE" ] && return 0 + mkdir -p "$STATE/orig" + repo_files > "$STATE/manifest" # what existed before this RUN touched anything + : > "$STATE/created" + printf '%s\n' "$HOSTPORT" > "$STATE/host" +} +save() { # keep the first-seen (pre-rewrite) copy + [ -e "$STATE/orig$1" ] && return 0 + mkdir -p "$STATE/orig$(dirname "$1")" + cp -p "$1" "$STATE/orig$1" +} +created() { printf '%s\n' "$1" >> "$STATE/created"; } + +if [ "$MODE" = restore ]; then + [ -d "$STATE" ] || exit 0 + host=$(cat "$STATE/host") + # mapfile from a process substitution, not `cmd | while read`: no pipeline, + # so nothing for pipefail to misreport (scripts/tests/lsphp-info-probe.test.sh). + mapfile -t saved < <(cd "$STATE/orig" && find . -type f) + for rel in "${saved[@]}"; do + cat "$STATE/orig/$rel" > "/${rel#./}" + done + while read -r f; do [ -n "$f" ] && rm -f -- "$f"; done < "$STATE/created" + # A repo/apt file that appeared during this RUN and still names the cache + # (e.g. written by a package from the rewritten repos) is removed too. + mapfile -t current < <(repo_files) + for f in "${current[@]}"; do + if ! grep -qxF -- "$f" "$STATE/manifest" && grep -qF -- "$host" "$f"; then + echo "pkg-cache-repos: removing new file that references the cache: $f" + rm -f -- "$f" + fi + done + left=$(repo_files | xargs -r grep -lF -- "$host" 2>/dev/null || true) + rm -rf "$STATE" + [ -z "$left" ] || die "cache address $host still present after restore in: $left" + echo "pkg-cache-repos: original repository files restored (no reference to $host left)" + exit 0 +fi + +if [ "$MODE" = release-install ]; then + [ $# -gt 0 ] || die "release-install needs: epel and/or remi" + tmp=$(mktemp -d) + curl_opts=(-fsS --proto '=https' --retry 5 --retry-all-errors --max-time 120) + if [ -n "$C" ]; then + start_session + make_bundle + curl_opts+=(--cacert "$STATE/ca-bundle.pem") + fi + files=() + el=$(rpm -E '%{?rhel}' 2>/dev/null || true) + case "$el" in 9|10) ;; *) die "release-install supports EL9/EL10 only (rpm -E %rhel = '$el')" ;; esac + for name in "$@"; do + case "$name" in + epel) pkg=epel-release key=RPM-GPG-KEY-EPEL-$el + path=/epel/epel-release-latest-$el.noarch.rpm + origin=https://dl.fedoraproject.org/pub/epel/epel-release-latest-$el.noarch.rpm ;; + remi) pkg=remi-release key=RPM-GPG-KEY-remi2024 + path=/remi/enterprise/remi-release-$el.rpm + origin=https://rpms.remirepo.net/enterprise/remi-release-$el.rpm ;; + *) die "unknown release package '$name'" ;; + esac + url="$origin"; [ -n "$C" ] && url="$C$path" + f="$tmp/$pkg.rpm" + curl "${curl_opts[@]}" -o "$f" "$url" || die "download failed: $url" + rpm --import "$SHARE/$key" || die "cannot import $SHARE/$key" + got=$(rpm -qp --nosignature --qf '%{NAME}' "$f" 2>/dev/null || true) + [ "$got" = "$pkg" ] || die "$url is '$got', expected $pkg" + chk=$(rpm -K "$f" 2>&1 || true) + [[ "$chk" == *": digests signatures OK" ]] || die "signature check failed for $url: $chk" + echo "pkg-cache-repos: $pkg verified ($chk) from $url" + files+=("$f") + done + dnf install -y --setopt=localpkg_gpgcheck=1 "${files[@]}" + rm -rf "$tmp" + exit 0 +fi + +if [ -z "$C" ] && [ "$MODE" != nodesource ] && [ "$MODE" != go-env ]; then + echo "pkg-cache-repos($MODE): PKG_CACHE not set - using upstream repositories" + exit 0 +fi + +rewrite_rpm() { + local f="$1" + grep -q -E '^#? ?baseurl=https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net|rpm\.nodesource\.com)/' "$f" || return 0 + save "$f" + # Per [section]: a (commented or live) baseurl on a known origin becomes + # "baseurl=// /"; that section's + # mirrorlist/metalink is commented out (dnf prefers them over baseurl) and it + # gets sslcacert=, gpgcheck=1 and, for repos + # that sign repomd.xml, repo_gpgcheck=1. Other sections are untouched. + awk -v C="$C" -v BUNDLE="$STATE/ca-bundle.pem" ' + function flush( i) { + for (i = 1; i <= n; i++) { + if (hit && buf[i] ~ /^(mirrorlist|metalink)=/) buf[i] = "#" buf[i] + if (hit && buf[i] ~ /^(sslcacert|gpgcheck|repo_gpgcheck)[ \t]*=/) continue + print buf[i] + } + if (hit) { + print "sslcacert=" BUNDLE + print "gpgcheck=1" + if (signed) print "repo_gpgcheck=1" + } + n = 0; hit = 0; signed = 0 + } + function rw(line, re, prefix, origin, sig, p) { + if (match(line, re)) { + p = substr(line, RSTART + RLENGTH); sub(/[ \t].*$/, "", p) + hit = 1; if (sig) signed = 1 + return "baseurl=" C "/" prefix "/" p " " origin "/" p + } + return line + } + /^\[/ { flush() } + { + l = $0 + if (l ~ /^#? ?baseurl=/) { + l = rw(l, "^#? ?baseurl=https?://repo\\.almalinux\\.org/almalinux/", "almalinux", "https://repo.almalinux.org/almalinux", 1) + l = rw(l, "^#? ?baseurl=https?://download\\.example/pub/epel/", "epel", "https://dl.fedoraproject.org/pub/epel", 0) + l = rw(l, "^#? ?baseurl=https?://rpms\\.remirepo\\.net/", "remi", "https://rpms.remirepo.net", 1) + # NodeSource signs its RPMs but not repomd.xml (404): gpgcheck only. + l = rw(l, "^#? ?baseurl=https?://rpm\\.nodesource\\.com/", "nodesource", "https://rpm.nodesource.com", 0) + } + buf[++n] = l + } + END { flush() } + ' "$f" > "$f.pkgcache" && cat "$f.pkgcache" > "$f" && rm -f "$f.pkgcache" +} + +rewrite_apt() { + local f="$1" + grep -q -E 'https?://([a-z]{2}\.)?(archive|security)\.ubuntu\.com/|https?://rpms\.litespeedtech\.com/|https?://(deb|security)\.debian\.org/' "$f" || return 0 + save "$f" + # Debian: debian-security first, and "debian" only as a whole path segment, + # so .../debian-security is never rewritten as /debian/. + sed -i -E \ + -e "s%https?://(archive|[a-z]{2}\.archive)\.ubuntu\.com/ubuntu/?%$C/ubuntu/%g" \ + -e "s%https?://security\.ubuntu\.com/ubuntu/?%$C/ubuntu-security/%g" \ + -e "s%https?://rpms\.litespeedtech\.com/%$C/litespeed/%g" \ + -e "s%https?://(deb|security)\.debian\.org/debian-security/?([[:space:]]|\$)%$C/debian-security/\\2%g" \ + -e "s%https?://security\.debian\.org/?([[:space:]]|\$)%$C/debian-security/\\1%g" \ + -e "s%https?://deb\.debian\.org/debian/?([[:space:]]|\$)%$C/debian/\\1%g" \ + "$f" +} + +if [ "$MODE" = go-env ]; then + # Prints shell exports for `eval`. Without PKG_CACHE: nothing (Go defaults). + # GOPROXY "a|b" falls through on ANY error, so a dead cache costs one + # failed request, not the build. GOSUMDB stays the default sum.golang.org, + # reached directly: modules are still verified against go.sum + the sumdb. + [ -n "$C" ] || { echo "pkg-cache-repos(go-env): PKG_CACHE not set - default GOPROXY" >&2; exit 0; } + start_session + make_bundle + printf "export GOPROXY='%s/goproxy|https://proxy.golang.org|direct' SSL_CERT_FILE='%s'\n" "$C" "$STATE/ca-bundle.pem" + echo "pkg-cache-repos(go-env): GOPROXY via $C/goproxy (fallback proxy.golang.org)" >&2 + exit 0 +fi + +if [ "$MODE" = nodesource ]; then + # Replaces `curl https://rpm.nodesource.com/setup_N.x | bash` (unverified + # remote script): writes the same nodesource-nodejs repo (origin URL, so the + # shipped image keeps a normal repo file) with gpgcheck=1 and the signing key + # committed in keys/pkg-cache/; with PKG_CACHE it is then rewritten like any + # other repo and `restore` puts the origin version back. + major="${1:?usage: pkg-cache-repos.sh nodesource MAJOR}" + [[ "$major" =~ ^[0-9]{1,3}$ ]] || die "bad Node major '$major'" + key="$SHARE/RPM-GPG-KEY-nodesource" + [ -s "$key" ] || die "missing $key" + rpm --import "$key" || die "cannot import $key" + rm -f /etc/yum.repos.d/nodesource*.repo + f=/etc/yum.repos.d/nodesource-nodejs.repo + printf '%s\n' "[nodesource-nodejs]" \ + "name=Node.js Packages for Linux RPM based distros - \$basearch" \ + "baseurl=https://rpm.nodesource.com/pub_${major}.x/nodistro/nodejs/\$basearch" \ + "priority=9" "enabled=1" "gpgcheck=1" \ + "gpgkey=file://$key" "module_hotfixes=1" > "$f" + echo "pkg-cache-repos: wrote $f (Node ${major}.x, key $(basename "$key"))" + if [ -n "$C" ]; then + start_session + make_bundle + rewrite_rpm "$f" + grep -h '^baseurl=' "$f" | sed 's/^/ /' + fi + exit 0 +fi + +case "$MODE" in + rpm) + start_session + make_bundle + for f in /etc/yum.repos.d/*.repo; do [ -f "$f" ] && rewrite_rpm "$f"; done + echo "pkg-cache-repos(rpm): repos now via $C:" + grep -h '^baseurl=' /etc/yum.repos.d/*.repo | grep -F "$C/" | sort -u | sed 's/^/ /' + ;; + apt) + start_session + conf=/etc/apt/apt.conf.d/99pkg-cache + # Trust the pkg-cache CA for the cache host ONLY (apt per-host option). + printf 'Acquire::https::%s::CaInfo "%s";\n' "${HOSTPORT%%:*}" "$SHARE/ca.crt" > "$conf" + created "$conf" + for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do + [ -f "$f" ] && rewrite_apt "$f" + done + echo "pkg-cache-repos(apt): sources now via $C:" + # (|| true: Debian 13 images have no /etc/apt/sources.list; grep's exit 2 + # for the missing file must not fail the build under pipefail) + { grep -h -E '^(deb|URIs:)' /etc/apt/sources.list /etc/apt/sources.list.d/* 2>/dev/null || true; } | grep -F "$C/" | sort -u | sed 's/^/ /' || true + ;; + *) die "unknown mode '$MODE'" ;; +esac