Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions UPGRADES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,52 @@ 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 `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:

1. Read the keyring hash from the pinned base image:
`docker run --rm --entrypoint sha256sum kalilinux/kali-rolling@sha256:<digest> /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
Expand Down
25 changes: 17 additions & 8 deletions containers/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
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.
RUN printf 'deb http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \
# 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. 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 [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 && \
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 && \
Expand Down Expand Up @@ -46,12 +54,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
RUN printf 'deb http://archive.kali.org/kali %s main contrib non-free non-free-firmware\n' "$KALI_APT_SUITE" > /etc/apt/sources.list && \
ARG KALI_APT_KEYRING_SHA256=42a247ff5a26869e3739b6d3ad125938bb5da8e7bb43ed0791d2a190cd09c64f
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 && \
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
Expand Down
72 changes: 67 additions & 5 deletions tests/test_sandbox_dockerfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,12 +29,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
Expand All @@ -59,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<opts>\[[^\]]*\])? ?[^\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(
Expand Down