From 422d9a6f5f1032edb7b036155a30cf2e38a07a9d Mon Sep 17 00:00:00 2001 From: Ankit Das Date: Fri, 9 Oct 2026 00:19:57 +0530 Subject: [PATCH 1/2] fix(sandbox): pin the Kali keyring instead of the drifting InRelease hash `kali-last-snapshot` is a rolling pointer that Kali re-signs on every archive refresh, so the pinned InRelease hash broke `Build and smoke-test sandbox image` on each re-sign (main 9c6b9102). The archive publishes no immutable dated suite and snapshot.kali.org is not serving, so the index bytes cannot be pinned durably. The trust anchor is now the Kali keyring shipped by the digest-pinned base image, verified by content hash, with apt signature verification enforced fail-closed through 99lyrashield-signature-required. Package selection still pins the snapshot suite at Pin-Priority 1001 with --allow-downgrades. UPGRADES.md documents how to refresh the pin. --- UPGRADES.md | 31 +++++++++++++++++++++++++++++++ containers/Dockerfile | 18 ++++++++++++------ tests/test_sandbox_dockerfile.py | 22 +++++++++++++++++----- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/UPGRADES.md b/UPGRADES.md index 7e133d1a..08ca7e64 100644 --- a/UPGRADES.md +++ b/UPGRADES.md @@ -17,6 +17,37 @@ states, not the current patch inventory. ## History +## Sandbox apt trust anchor: keyring pin replaces the InRelease hash pin (2026-10-09) + +`containers/Dockerfile` pinned `KALI_APT_INRELEASE_SHA256` against the +`kali-last-snapshot` suite. That suite is a rolling pointer: Kali re-signs its +`InRelease` on every archive refresh, so the pin failed the build on each +re-sign. The archive carries no immutable dated suite, and the dated-snapshot +service at `snapshot.kali.org` is not serving. + +The durable anchor is the Kali keyring shipped by the digest-pinned base image +(`usr/share/keyrings/kali-archive-keyring.gpg`, currently +`42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f`, holding key +`827C8569F2518CC677FECA1AED65462EC8D5E4C5`). The build verifies that file's hash +and apt then verifies the index against it, fail-closed via +`/etc/apt/apt.conf.d/99lyrashield-signature-required` +(`AllowUnauthenticated`, `AllowInsecureRepositories` and +`AllowDowngradeToInsecureRepositories` all `false`). Package selection stays +pinned by the `Pin-Priority: 1001` snapshot pin and `--allow-downgrades`. + +To refresh the pin, when the base image digest advances or Kali rotates its +signing key: + +1. Read the keyring hash from the pinned base image: + `docker run --rm --entrypoint sha256sum kalilinux/kali-rolling@sha256: /usr/share/keyrings/kali-archive-keyring.gpg` +2. Update both `ARG KALI_APT_KEYRING_SHA256=` lines in `containers/Dockerfile` + to that value. +3. Rebuild the sandbox image (`ci.yml` job `docker-smoke`) and confirm the + `sha256sum -c -` step and both `apt-get update` calls still pass. + +The archive key rotation itself is the normal Kali maintenance path and needs +no code change beyond step 2. + ## DG-16 owned/upstream twin disposition (2026-09-28) CI now prints a sorted, informational CSV inventory of same-path Python modules diff --git a/containers/Dockerfile b/containers/Dockerfile index 4b9fcde7..bbf4b0e2 100644 --- a/containers/Dockerfile +++ b/containers/Dockerfile @@ -9,14 +9,19 @@ FROM kalilinux/kali-rolling@sha256:f49124869e4eee549315879c3bd7ef92f23b8753fec7544537bcec1493277096 AS gobuilder ARG KALI_APT_SUITE=kali-last-snapshot -ARG KALI_APT_INRELEASE_SHA256=5bc9a9bf729d7f9a0c360afe66488379a04d809cbaa7ad4fcefe1704266f5f51 -# The pinned InRelease hash and its APT signature authenticate this immutable -# archive snapshot; the base image predates the archive's current TLS chain. +# Supply-chain anchor for the Kali apt index. `kali-last-snapshot` is a +# re-signed rolling pointer, so its InRelease bytes change on every archive +# refresh and cannot be pinned durably. The trust anchor is instead the pinned +# base image's own Kali keyring, whose content hash is fixed by the +# digest-pinned FROM above; the apt signature verification that keyring +# performs is enforced fail-closed in the RUN below. +ARG KALI_APT_KEYRING_SHA256=42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f RUN printf 'deb http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \ printf 'Package: *\nPin: release n=%s\nPin-Priority: 1001\n' "$KALI_APT_SUITE" > /etc/apt/preferences.d/kali-snapshot && \ rm -f /etc/apt/sources.list.d/* && \ + printf 'APT::Get::AllowUnauthenticated "false";\nAcquire::AllowInsecureRepositories "false";\nAcquire::AllowDowngradeToInsecureRepositories "false";\n' > /etc/apt/apt.conf.d/99lyrashield-signature-required && \ + echo "${KALI_APT_KEYRING_SHA256} /usr/share/keyrings/kali-archive-keyring.gpg" | sha256sum -c - && \ apt-get update && \ - echo "${KALI_APT_INRELEASE_SHA256} /var/lib/apt/lists/archive.kali.org_kali_dists_${KALI_APT_SUITE}_InRelease" | sha256sum -c - && \ apt-get full-upgrade -y --allow-downgrades && \ apt-get install -y kali-archive-keyring && \ apt-get update && \ @@ -46,12 +51,13 @@ FROM kalilinux/kali-rolling@sha256:f49124869e4eee549315879c3bd7ef92f23b8753fec75 LABEL description="AI Agent Penetration Testing Environment with Comprehensive Automated Tools" ARG KALI_APT_SUITE=kali-last-snapshot -ARG KALI_APT_INRELEASE_SHA256=5bc9a9bf729d7f9a0c360afe66488379a04d809cbaa7ad4fcefe1704266f5f51 +ARG KALI_APT_KEYRING_SHA256=42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f RUN printf 'deb http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \ printf 'Package: *\nPin: release n=%s\nPin-Priority: 1001\n' "$KALI_APT_SUITE" > /etc/apt/preferences.d/kali-snapshot && \ rm -f /etc/apt/sources.list.d/* && \ + printf 'APT::Get::AllowUnauthenticated "false";\nAcquire::AllowInsecureRepositories "false";\nAcquire::AllowDowngradeToInsecureRepositories "false";\n' > /etc/apt/apt.conf.d/99lyrashield-signature-required && \ + echo "${KALI_APT_KEYRING_SHA256} /usr/share/keyrings/kali-archive-keyring.gpg" | sha256sum -c - && \ apt-get update && \ - echo "${KALI_APT_INRELEASE_SHA256} /var/lib/apt/lists/archive.kali.org_kali_dists_${KALI_APT_SUITE}_InRelease" | sha256sum -c - && \ apt-get full-upgrade -y --allow-downgrades && \ apt-get install -y kali-archive-keyring && \ apt-get update diff --git a/tests/test_sandbox_dockerfile.py b/tests/test_sandbox_dockerfile.py index f5a9c7a7..288ceaa2 100644 --- a/tests/test_sandbox_dockerfile.py +++ b/tests/test_sandbox_dockerfile.py @@ -27,12 +27,24 @@ def test_every_external_sandbox_input_is_immutable_or_hash_verified() -> None: assert "FROM kalilinux/kali-rolling@sha256:" in content assert "ARG KALI_APT_SUITE=kali-last-snapshot" in content - assert "ARG KALI_APT_INRELEASE_SHA256=" in content - kali_index_pins = re.findall( - r"^ARG KALI_APT_INRELEASE_SHA256=([0-9a-f]{64})$", content, flags=re.MULTILINE + # The Kali apt trust anchor is the keyring shipped by the digest-pinned base + # image, not the InRelease bytes. `kali-last-snapshot` is a rolling pointer + # that Kali re-signs on every archive refresh, so an index-hash pin broke the + # build on each re-sign; the keyring is the part that stays fixed. + assert "ARG KALI_APT_INRELEASE_SHA256=" not in content + kali_keyring_pins = re.findall( + r"^ARG KALI_APT_KEYRING_SHA256=([0-9a-f]{64})$", content, flags=re.MULTILINE ) - assert len(kali_index_pins) == 2 - assert kali_index_pins[0] == kali_index_pins[1] + assert len(kali_keyring_pins) == 2 + assert kali_keyring_pins[0] == kali_keyring_pins[1] + assert "sha256sum -c -" in content + # Signature verification must fail closed: no fallback to an unauthenticated + # or insecure repository, and no opt-out flags anywhere in the image. + assert 'APT::Get::AllowUnauthenticated "false"' in content + assert 'Acquire::AllowInsecureRepositories "false"' in content + assert 'Acquire::AllowDowngradeToInsecureRepositories "false"' in content + assert "--allow-unauthenticated" not in content + assert "trusted=yes" not in content assert "Pin-Priority: 1001" in content assert "apt-get full-upgrade -y --allow-downgrades" in content assert "archive.kali.org/kali" in content From 3d58f7bf4e366104d9b04a1547d342dffc27d345 Mon Sep 17 00:00:00 2001 From: LyraShield Developer Agent Date: Fri, 9 Oct 2026 01:14:42 +0530 Subject: [PATCH 2/2] fix(sandbox): pin apt to the hashed Kali keyring with signed-by The deb line named no keyring, so apt authenticated the Kali index with whatever trusted key the image happened to carry while the build separately hashed /usr/share/keyrings/kali-archive-keyring.gpg. Name that exact file with [signed-by=...] in both stages so the keyring whose sha256 the build checks is the file apt is told to trust, and keep the fail-closed apt conf. Verified the later 'apt-get install -y kali-archive-keyring' does not disturb that file: kali-archive-keyring_2025.2_all.deb unpacks the same keyring at the same path with an identical sha256 (42a247ff...). A future package that rotated the key would fail closed at the post-install 'apt-get update'. tests/test_sandbox_dockerfile.py gains an invariant that every Kali deb line carries signed-by pointing at the hashed keyring path, proven load-bearing by two negative controls (option removed, option pointing elsewhere). UPGRADES.md records signed-by and the keyring-package check. --- UPGRADES.md | 19 ++++++++++-- containers/Dockerfile | 9 ++++-- tests/test_sandbox_dockerfile.py | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/UPGRADES.md b/UPGRADES.md index 08ca7e64..4ed9369c 100644 --- a/UPGRADES.md +++ b/UPGRADES.md @@ -28,13 +28,28 @@ service at `snapshot.kali.org` is not serving. The durable anchor is the Kali keyring shipped by the digest-pinned base image (`usr/share/keyrings/kali-archive-keyring.gpg`, currently `42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f`, holding key -`827C8569F2518CC677FECA1AED65462EC8D5E4C5`). The build verifies that file's hash -and apt then verifies the index against it, fail-closed via +`827C8569F2518CC677FECA1AED65462EC8D5E4C5`). The `deb` line names that exact +file with `[signed-by=/usr/share/keyrings/kali-archive-keyring.gpg]`, so the +keyring whose sha256 the build checks is the same file apt is told to +authenticate the index with; without `signed-by` apt would fall back to +whatever trusted key the image happens to carry. The build verifies that +file's hash and apt then verifies the index against it, fail-closed via `/etc/apt/apt.conf.d/99lyrashield-signature-required` (`AllowUnauthenticated`, `AllowInsecureRepositories` and `AllowDowngradeToInsecureRepositories` all `false`). Package selection stays pinned by the `Pin-Priority: 1001` snapshot pin and `--allow-downgrades`. +The later `apt-get install -y kali-archive-keyring` in the same RUN does not +disturb that file: the package ships the same keyring at the same path +(`kali-archive-keyring_2025.2_all.deb` unpacks +`./usr/share/keyrings/kali-archive-keyring.gpg`, 3464 bytes, sha256 +`42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f` — identical +to the pin). If a future package version rotated the key or changed the path, +the hash check would still pass (it runs before the install) but `signed-by` +would point at a key apt could no longer match, and `apt-get update` after the +install would fail closed rather than silently trust a different key. Refresh +the pin and the `signed-by` path together if that ever happens. + To refresh the pin, when the base image digest advances or Kali rotates its signing key: diff --git a/containers/Dockerfile b/containers/Dockerfile index bbf4b0e2..84577029 100644 --- a/containers/Dockerfile +++ b/containers/Dockerfile @@ -14,9 +14,12 @@ ARG KALI_APT_SUITE=kali-last-snapshot # refresh and cannot be pinned durably. The trust anchor is instead the pinned # base image's own Kali keyring, whose content hash is fixed by the # digest-pinned FROM above; the apt signature verification that keyring -# performs is enforced fail-closed in the RUN below. +# performs is enforced fail-closed in the RUN below. The `deb` line names that +# same keyring with `signed-by`, so the file the hash check below pins is +# exactly the file apt trusts to authenticate the index; without it apt would +# fall back to whatever trusted key is present in the image. ARG KALI_APT_KEYRING_SHA256=42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f -RUN printf 'deb http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \ +RUN printf 'deb [signed-by=/usr/share/keyrings/kali-archive-keyring.gpg] http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \ printf 'Package: *\nPin: release n=%s\nPin-Priority: 1001\n' "$KALI_APT_SUITE" > /etc/apt/preferences.d/kali-snapshot && \ rm -f /etc/apt/sources.list.d/* && \ printf 'APT::Get::AllowUnauthenticated "false";\nAcquire::AllowInsecureRepositories "false";\nAcquire::AllowDowngradeToInsecureRepositories "false";\n' > /etc/apt/apt.conf.d/99lyrashield-signature-required && \ @@ -52,7 +55,7 @@ LABEL description="AI Agent Penetration Testing Environment with Comprehensive A ARG KALI_APT_SUITE=kali-last-snapshot ARG KALI_APT_KEYRING_SHA256=42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f -RUN printf 'deb http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \ +RUN printf 'deb [signed-by=/usr/share/keyrings/kali-archive-keyring.gpg] http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \ printf 'Package: *\nPin: release n=%s\nPin-Priority: 1001\n' "$KALI_APT_SUITE" > /etc/apt/preferences.d/kali-snapshot && \ rm -f /etc/apt/sources.list.d/* && \ printf 'APT::Get::AllowUnauthenticated "false";\nAcquire::AllowInsecureRepositories "false";\nAcquire::AllowDowngradeToInsecureRepositories "false";\n' > /etc/apt/apt.conf.d/99lyrashield-signature-required && \ diff --git a/tests/test_sandbox_dockerfile.py b/tests/test_sandbox_dockerfile.py index 288ceaa2..477918ae 100644 --- a/tests/test_sandbox_dockerfile.py +++ b/tests/test_sandbox_dockerfile.py @@ -6,6 +6,8 @@ import re from pathlib import Path +import pytest + DOCKERFILE = Path(__file__).parents[1] / "containers" / "Dockerfile" DOCKERIGNORE = Path(__file__).parents[1] / ".dockerignore" @@ -71,6 +73,54 @@ def test_every_external_sandbox_input_is_immutable_or_hash_verified() -> None: assert "npm install -g" not in content +KALI_KEYRING_PATH = "/usr/share/keyrings/kali-archive-keyring.gpg" +KALI_DEB_RE = re.compile(r"^.*printf 'deb (?P\[[^\]]*\])? ?[^\s']+ .*$", re.MULTILINE) + + +def assert_every_kali_deb_line_is_signed_by_the_pinned_keyring(content: str) -> None: + """Every Kali `deb` line must name the hashed keyring with `signed-by`. + + `signed-by` is what makes the file whose sha256 the build checks the same + file apt verifies the index against. Without it apt falls back to whatever + trusted key the image happens to carry, so the hash check would be pinning + a file that nothing forces apt to consult. + """ + deb_lines = [ + line + for line in content.splitlines() + if "printf 'deb " in line and "archive.kali.org/kali" in line + ] + assert len(deb_lines) == 2, f"expected one Kali deb line per stage, found {len(deb_lines)}" + for line in deb_lines: + match = KALI_DEB_RE.match(line) + assert match is not None, f"unparsable Kali deb line: {line}" + opts = match.group("opts") or "" + assert f"signed-by={KALI_KEYRING_PATH}" in opts, ( + f"Kali deb line is not signed by the pinned keyring: {line}" + ) + + +def test_every_kali_deb_line_is_signed_by_the_pinned_keyring() -> None: + content = DOCKERFILE.read_text(encoding="utf-8") + + assert_every_kali_deb_line_is_signed_by_the_pinned_keyring(content) + + # Negative control: the invariant must be load-bearing. Dropping the + # `signed-by` option from a copy of the real content has to fail it, both + # when the option is removed outright and when it points at another key. + unsigned = content.replace(f"deb [signed-by={KALI_KEYRING_PATH}] http://", "deb http://") + assert unsigned != content, "negative control did not modify the content" + with pytest.raises(AssertionError): + assert_every_kali_deb_line_is_signed_by_the_pinned_keyring(unsigned) + + wrong_key = content.replace( + f"deb [signed-by={KALI_KEYRING_PATH}] http://", + "deb [signed-by=/etc/apt/trusted.gpg.d/other.gpg] http://", + ) + with pytest.raises(AssertionError): + assert_every_kali_deb_line_is_signed_by_the_pinned_keyring(wrong_key) + + def test_caido_download_retries_transient_server_errors() -> None: content = DOCKERFILE.read_text(encoding="utf-8") download_command = next(