ci: enable PKG_CACHE via a TLS probe that falls back to upstream; host repo tool
- build-push.yaml: Build-and-Push, Build-FPM-Images, Build-Shared-httpd and Build-Shared-OLS probe https://192.168.1.40 and :5000 with the pinned pkg-cache CA (5 s timeouts). When the cache answers they pass PKG_CACHE and configure the BuildKit Docker Hub mirror (ca=). When it doesn't, they build from upstream exactly as before and emit a warning. Fallback measured from the runner bridge: refused <1 s, silent drop 10 s. - scripts/host/pkgcache-host-repos.sh: apply/revert/status for WHP hosts. Each rewritten repo uses baseurl=<cache> <origin> as the real fallback; mirrorlist/metalink stay commented because measured dnf ignores baseurl when they are present. Keeps gpgcheck=1 / repo_gpgcheck=1 and backs up every file. Tested on 192.168.1.148, including cache-down and silent-drop fallback. - docs: runner firewall rule (applied), measured fallback results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
18f65c6d84
commit
2b9e7f302c
3 files changed
+301
-51
No files matched your search
@@ -85,10 +85,40 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). 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
|
||||
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 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:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||
[ -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
|
||||
if: steps.pkgcache.outputs.mirror == ''
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||||
@@ -135,6 +165,7 @@ jobs:
|
||||
build-args: |
|
||||
PHPVER=${{ matrix.phpver }}
|
||||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
||||
@@ -152,10 +183,40 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). 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
|
||||
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 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:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||
[ -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
|
||||
if: steps.pkgcache.outputs.mirror == ''
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||||
@@ -203,6 +264,7 @@ jobs:
|
||||
build-args: |
|
||||
PHPVER=${{ matrix.phpver }}
|
||||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
|
||||
@@ -417,10 +479,40 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). 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
|
||||
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 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:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||
[ -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
|
||||
if: steps.pkgcache.outputs.mirror == ''
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea
|
||||
@@ -436,6 +528,8 @@ jobs:
|
||||
file: ./Dockerfile.shared-httpd
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
||||
|
||||
@@ -445,10 +539,40 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). 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
|
||||
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 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:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||
[ -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
|
||||
if: steps.pkgcache.outputs.mirror == ''
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea
|
||||
@@ -469,5 +593,6 @@ jobs:
|
||||
build-args: |
|
||||
PHPVER=83
|
||||
OLS_VERSION=1.8.4
|
||||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
|
||||
+69
-51
@@ -95,72 +95,90 @@ Remaining risks:
|
||||
- It passes the results as `EXPECT_OPENSSL_MIN` / `EXPECT_HTTPD_MIN` / `EXPECT_IMAGEMAGICK_MIN`.
|
||||
- The Dockerfile asserts `rpm -q --qf '%{EVR}'` is not older (`rpm --eval "%{lua: print(rpm.vercmp('A','B'))}"`).
|
||||
|
||||
## CI integration plan (NOT applied — needs Josh)
|
||||
## CI integration (APPLIED 2026-10-21)
|
||||
|
||||
1. **Network (blocker).** The ephemeral runners are NATed, and `iptables FORWARD` **drops all RFC1918 destinations**, including 192.168.1.40:
|
||||
1. **Runner network.** Ephemeral runners are NATed and `iptables FORWARD` drops
|
||||
all RFC1918 destinations:
|
||||
- `eph-pve-*` on `pve`, vmbr2, 10.99.98.0/24;
|
||||
- `eph-pve3-*` on `pve3`, vmbr1, 10.99.99.0/24.
|
||||
|
||||
A narrow exception is needed, placed before the DROP lines:
|
||||
One rule per node now sits **above** those DROPs. It is persistent through a
|
||||
`post-up`/`post-down` pair placed before the first DROP line in
|
||||
`/etc/network/interfaces.d/vmbr2-ci-isolated` (pve) and
|
||||
`/etc/network/interfaces.d/vmbr1-ci-isolated` (pve3). The Proxmox firewall is
|
||||
disabled on both nodes, so its reloads do not touch these chains. Backups:
|
||||
`/root/vmbr{2,1}-ci-isolated.bak-pkgcache-20261021`.
|
||||
```
|
||||
# pve, /etc/network/interfaces.d/ vmbr2 stanza, BEFORE the 192.168.0.0/16 DROP:
|
||||
post-up iptables -A FORWARD -i vmbr2 -o vmbr0 -d 192.168.1.40 -p tcp -m multiport --dports 443,5000 -j ACCEPT
|
||||
post-down iptables -D FORWARD -i vmbr2 -o vmbr0 -d 192.168.1.40 -p tcp -m multiport --dports 443,5000 -j ACCEPT
|
||||
# live: iptables -I FORWARD 1 -i vmbr2 -o vmbr0 -d 192.168.1.40 -p tcp -m multiport --dports 443,5000 -j ACCEPT
|
||||
# pve3: the same with vmbr1
|
||||
iptables -I FORWARD 1 -i vmbr2 -o vmbr0 -d 192.168.1.40/32 -p tcp -m multiport --dports 443,5000 \
|
||||
-m comment --comment pkg-cache-ct130 -j ACCEPT
|
||||
```
|
||||
`gitea-runner.internal.cloud-hosting.io` resolves to 10.10.14.20, which is not on this LAN. Whether it can reach the cache is untested.
|
||||
2. **Probe with the CA, then opt in.** Keep `${{ }}` out of `run:`. The probe fails when the cache is down **or its certificate is expired or untrusted**, and the build then uses upstream.
|
||||
```yaml
|
||||
- name: Probe package cache
|
||||
id: pkgcache
|
||||
run: |
|
||||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/ ; then
|
||||
echo "url=https://192.168.1.40" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "url=" >> "$GITHUB_OUTPUT"; echo "::warning::pkg-cache unreachable - building from upstream"
|
||||
fi
|
||||
# docker/build-push-action `build-args:` add PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||
```
|
||||
3. **Docker Hub mirror for BuildKit**, over TLS and without `insecure`. `setup-buildx-action` uses the docker-container driver, which **ignores dockerd's `registry-mirrors`**. buildx copies the `ca` file into the builder.
|
||||
```yaml
|
||||
- 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 = ["keys/pkg-cache/ca.crt"]
|
||||
```
|
||||
The exact equivalent (absolute `ca` path) was measured on 192.168.1.193. BuildKit falls back to Docker Hub when the mirror is down; this was measured too, with the registry stopped. The relative `ca` path inside a workflow is **untested**; use `${{ github.workspace }}/keys/pkg-cache/ca.crt` if it doesn't resolve.
|
||||
4. **dockerd clients** (plain `docker pull`). Untested, because it needs a daemon restart. Use TLS, no `insecure-registries`:
|
||||
Verified from throwaway CTs on vmbr2 and vmbr1:
|
||||
- :443 and :5000 work with TLS verified;
|
||||
- CT130 :22/:80, pve :8006, .193 :22, the router's :53 and 10.10.14.20 stay **blocked**;
|
||||
- internet access is unchanged.
|
||||
|
||||
The LAN runner VM *Git-Builders* (VM103, 192.168.1.126 on vmbr0) needs no
|
||||
rule. `gitea-runner.internal.cloud-hosting.io` resolves to 10.10.14.20, which
|
||||
is unroutable from both the LAN and the DC (stale record?).
|
||||
2. **Workflow** (`.gitea/workflows/build-push.yaml`). Jobs Build-and-Push,
|
||||
Build-FPM-Images, Build-Shared-httpd and Build-Shared-OLS run a
|
||||
**"Probe package cache"** step. It curls `https://192.168.1.40/` and
|
||||
`https://192.168.1.40:5000/v2/` with `--cacert keys/pkg-cache/ca.crt
|
||||
--max-time 5`.
|
||||
- If both answer: `PKG_CACHE=https://192.168.1.40` is passed as a build arg,
|
||||
and buildx is set up with the Docker Hub mirror (`buildkitd-config-inline`,
|
||||
`ca=`).
|
||||
- If either fails: that output is empty, the build uses upstream repos /
|
||||
Docker Hub, and a `::warning::` is emitted.
|
||||
|
||||
Litespeed and lsphp jobs are unchanged (their Dockerfiles are not wired).
|
||||
**Fail-back measured** with the workflow's own step code, from a runner-bridge CT:
|
||||
|
||||
| cache state | probe result | time |
|
||||
|---|---|---|
|
||||
| up | url + mirror set | <1 s |
|
||||
| nginx + registry stopped | both empty, warning | <1 s (connection refused) |
|
||||
| silently dropped | both empty, warning | 10 s (2 x 5 s) |
|
||||
|
||||
Mid-build cache loss is also covered: dnf has the origin as a 2nd baseurl, and
|
||||
BuildKit falls back from the mirror to Docker Hub (measured with the registry stopped).
|
||||
3. **dockerd clients** (plain `docker pull`). Untested, because it needs a daemon restart.
|
||||
Use TLS, no `insecure-registries`:
|
||||
```
|
||||
install -D -m0644 keys/pkg-cache/ca.crt /etc/docker/certs.d/192.168.1.40:5000/ca.crt
|
||||
# /etc/docker/daemon.json: { "registry-mirrors": ["https://192.168.1.40:5000"] } then restart docker
|
||||
```
|
||||
|
||||
## WHP hosts (plan only)
|
||||
## WHP hosts (`scripts/host/pkgcache-host-repos.sh`)
|
||||
|
||||
The production hosts (whp01, whp02, sdbees) are in the datacenter and **cannot reach 192.168.1.40**. Only LAN hosts can, e.g. 192.168.1.148. Options for Josh:
|
||||
- (a) a second identical cache in the datacenter, with its own name-constrained CA;
|
||||
- (b) expose this one over NetBird;
|
||||
- (c) LAN hosts only.
|
||||
`pkgcache-host-repos.sh apply https://192.168.1.40` does the following:
|
||||
- backs up every `/etc/yum.repos.d/*.repo` to `/root/pkgcache-repo-backup-<UTC>/`;
|
||||
- installs the name-constrained CA as a system anchor;
|
||||
- rewrites the AlmaLinux, EPEL and Remi sections to `baseurl=<cache>/... <origin>/...`;
|
||||
- comments out mirrorlist/metalink in those sections;
|
||||
- sets `gpgcheck=1`, plus `repo_gpgcheck=1` for AlmaLinux and Remi.
|
||||
|
||||
For a host that can reach a cache:
|
||||
- install the CA at `/etc/pki/pkg-cache/ca.crt`;
|
||||
- create a bundle (`cat /etc/pki/tls/certs/ca-bundle.crt /etc/pki/pkg-cache/ca.crt > /etc/pki/pkg-cache/bundle.pem`);
|
||||
- in each repo section, keep the origin as the second URL:
|
||||
`revert [DIR]` restores byte-identical files (verified on AL9 and AL10) and
|
||||
removes the anchor. `status` shows the current state.
|
||||
|
||||
```ini
|
||||
[remi-safe]
|
||||
baseurl=https://<cache>/remi/enterprise/$releasever_major/safe/$basearch/ https://rpms.remirepo.net/enterprise/$releasever_major/safe/$basearch/
|
||||
#mirrorlist=...
|
||||
sslcacert=/etc/pki/pkg-cache/bundle.pem
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=1 # Remi + AlmaLinux sign repomd; EPEL does not
|
||||
```
|
||||
**Why not "cache first, mirrorlist as fallback":** measured on AL10, with
|
||||
mirrorlist/metalink left in place dnf **ignores `baseurl` completely** (0 cache
|
||||
requests). The origin as second baseurl is the fallback dnf really performs.
|
||||
|
||||
Hosts have no php.net gate. On a security-release day, run `pkgcache-refresh` on the cache and check `dnf info php-cli` against php.net.
|
||||
**Measured on 192.168.1.148** (AlmaLinux 10.2), 2026-10-21:
|
||||
|
||||
| test | result |
|
||||
|---|---|
|
||||
| baseline `dnf makecache --refresh` (original mirrorlists) | rc=0, **1209 s** |
|
||||
| via cache `makecache --refresh` | rc=0, **22 s** (2nd run, non-interactive: 2 s) |
|
||||
| via cache `dnf download php-cli` | rc=0, 1 s |
|
||||
| `dnf -y --downloadonly update php-cli` | "Nothing to do" (already 8.5.12) |
|
||||
| cache **down** (nginx stopped): makecache --refresh / download | rc=0 **16 s** / rc=0 24 s, served by origin |
|
||||
| cache **silently dropped** (iptables DROP): makecache --refresh / download | rc=0 **225 s** / rc=0 59 s (dnf's 30 s connect timeout per attempt) |
|
||||
|
||||
**Datacenter hosts** (whp01, whp02, sdbees, TrueSelfCA) cannot reach
|
||||
192.168.1.40 and are **not configured**. That needs CT130 on NetBird (see the
|
||||
report: setup key / route pending).
|
||||
|
||||
## Operating the cache (CT130)
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
## pkgcache-host-repos.sh — point a WHP HOST's dnf repos (AlmaLinux 9/10 BaseOS/
|
||||
## AppStream/CRB/extras/..., EPEL, Remi) at the internal package cache, with the
|
||||
## ORIGIN as automatic fallback. Not used by image builds (see pkg-cache-repos.sh).
|
||||
##
|
||||
## pkgcache-host-repos.sh apply https://192.168.1.40 # backs up, then rewrites
|
||||
## pkgcache-host-repos.sh status
|
||||
## pkgcache-host-repos.sh revert [BACKUP_DIR] # default: newest backup
|
||||
##
|
||||
## Fallback design (measured 2026-10-21 on AlmaLinux 10.2):
|
||||
## * each rewritten section gets baseurl=<cache>/<prefix>/<path> <origin>/<path>
|
||||
## — dnf/librepo treats the second URL as a failover mirror.
|
||||
## * the section's mirrorlist=/metalink= is COMMENTED OUT. Measured: with a
|
||||
## mirrorlist/metalink present dnf ignores baseurl entirely (0 cache requests),
|
||||
## so "cache first, mirrorlist as fallback" is not something dnf can do.
|
||||
## * gpgcheck=1 everywhere; repo_gpgcheck=1 for AlmaLinux + Remi (both sign
|
||||
## repomd.xml); EPEL has no repomd signature and relies on HTTPS end to end.
|
||||
## * trust: the name-constrained pkg-cache CA is installed as a system anchor
|
||||
## (/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt). Its nameConstraints
|
||||
## (IP 192.168.1.40, pkg-cache.* DNS) are enforced by OpenSSL — a cert it
|
||||
## signs for any other name fails ("permitted subtree violation").
|
||||
## Every changed file is copied to /root/pkgcache-repo-backup-<UTC>/ first.
|
||||
set -euo pipefail
|
||||
|
||||
MODE="${1:-status}"
|
||||
CA_SRC="$(dirname "$0")/pkg-cache-ca.crt"
|
||||
ANCHOR=/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt
|
||||
die() { echo "FATAL(pkgcache-host-repos): $*" >&2; exit 1; }
|
||||
same() { [ "$(sha256sum < "$1")" = "$(sha256sum < "$2")" ]; } # cmp/diffutils is not on minimal hosts
|
||||
|
||||
rewrite() { # $1 file $2 cache url
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import re, sys
|
||||
f, C = sys.argv[1], sys.argv[2].rstrip('/')
|
||||
maps = [ # regex on a (commented or live) baseurl, cache prefix, origin base, repomd signed?
|
||||
(r'^#?\s*baseurl\s*=\s*https?://repo\.almalinux\.org/almalinux/(\S+)', 'almalinux', 'https://repo.almalinux.org/almalinux/', True),
|
||||
(r'^#?\s*baseurl\s*=\s*https?://download\.example/pub/epel/(\S+)', 'epel', 'https://dl.fedoraproject.org/pub/epel/', False),
|
||||
(r'^#?\s*baseurl\s*=\s*https?://rpms\.remirepo\.net/(\S+)', 'remi', 'https://rpms.remirepo.net/', True),
|
||||
]
|
||||
out, sec = [], []
|
||||
state = {'hit': False, 'sig': False}
|
||||
def flush():
|
||||
for x in sec:
|
||||
if state['hit']:
|
||||
if re.match(r'^(mirrorlist|metalink)\s*=', x):
|
||||
x = '#' + x
|
||||
elif re.match(r'^(gpgcheck|repo_gpgcheck)\s*=', x):
|
||||
continue
|
||||
out.append(x)
|
||||
if state['hit']:
|
||||
# keep blank-line layout: insert before trailing blanks
|
||||
tail = []
|
||||
while out and out[-1].strip() == '':
|
||||
tail.append(out.pop())
|
||||
out.append('gpgcheck=1')
|
||||
if state['sig']:
|
||||
out.append('repo_gpgcheck=1')
|
||||
out.extend(tail)
|
||||
sec.clear(); state['hit'] = state['sig'] = False
|
||||
for line in open(f).read().split('\n'):
|
||||
if line.startswith('['):
|
||||
flush()
|
||||
for rx, prefix, origin, sig in maps:
|
||||
m = re.match(rx, line)
|
||||
if m:
|
||||
p = m.group(1)
|
||||
line = f'baseurl={C}/{prefix}/{p} {origin}{p}'
|
||||
state['hit'] = True; state['sig'] = state['sig'] or sig
|
||||
break
|
||||
sec.append(line)
|
||||
flush()
|
||||
open(f, 'w').write('\n'.join(out))
|
||||
PY
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
apply)
|
||||
C="${2:?usage: apply https://<cache>}"; C="${C%/}"
|
||||
case "$C" in https://*) ;; *) die "cache URL must be https://" ;; esac
|
||||
[ -s "$CA_SRC" ] || die "missing $CA_SRC (copy keys/pkg-cache/ca.crt next to this script)"
|
||||
B=/root/pkgcache-repo-backup-$(date -u +%Y%m%dT%H%M%SZ)
|
||||
mkdir -p "$B/yum.repos.d"
|
||||
cp -a /etc/yum.repos.d/*.repo "$B/yum.repos.d/"
|
||||
[ -e "$ANCHOR" ] && cp -a "$ANCHOR" "$B/" || touch "$B/.no-anchor-before"
|
||||
install -m 0644 "$CA_SRC" "$ANCHOR" && update-ca-trust
|
||||
for f in /etc/yum.repos.d/almalinux*.repo /etc/yum.repos.d/epel*.repo /etc/yum.repos.d/remi*.repo; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -q -E '^#?\s*baseurl\s*=\s*https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net)/' "$f" && rewrite "$f" "$C"
|
||||
done
|
||||
echo "backup: $B"
|
||||
echo "changed files:"; for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || echo " /etc/yum.repos.d/$(basename "$f")"; done
|
||||
;;
|
||||
revert)
|
||||
B="${2:-$(ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sort | tail -1)}"
|
||||
[ -d "$B/yum.repos.d" ] || die "no backup dir ($B)"
|
||||
for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || { cp -a "$f" /etc/yum.repos.d/; echo "restored /etc/yum.repos.d/$(basename "$f")"; }; done
|
||||
if [ -e "$B/.no-anchor-before" ]; then rm -f "$ANCHOR"; update-ca-trust; echo "removed $ANCHOR"; fi
|
||||
dnf clean metadata -q || true
|
||||
echo "reverted from $B"
|
||||
;;
|
||||
status)
|
||||
grep -l -E '^baseurl=https://[^ ]+/(almalinux|epel|remi)/' /etc/yum.repos.d/*.repo 2>/dev/null | sed 's/^/via cache: /' || echo "no repo uses the cache"
|
||||
ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sed 's/^/backup: /' || true
|
||||
[ -e "$ANCHOR" ] && echo "CA anchor: $ANCHOR" || echo "CA anchor: absent"
|
||||
;;
|
||||
*) die "usage: apply URL | status | revert [DIR]" ;;
|
||||
esac
|
||||
Reference in new issue
Block a user