Repository navigation
The second public release. It publishes the same images, chart and yj CLI as 0.1.0, now signed, each image with an SBOM and build provenance.
Before upgrading from 0.1.0, check these changes to default behaviour:
-
Package uploads (
npm publish,twine upload,mvn deploy) are refused under the defaultFW_WRITE_POLICY=block(#184). -
Every package from a PyPI index with no JSON API (pypiserver, a Nexus PyPI proxy) is held for approval under the default
FW_UNSCORABLE_POLICY=block(#221). -
On npm and PyPI, a package the gate was told does not exist, but which the registry serves to the client, is withheld as temporarily unavailable (#204, #221).
-
A known-malware snapshot that removes many entries at once is held until
FW_MALWARE_FEED_RELEASEreleases it (#216). -
A request path still holding
%after one decode is refused with a 400 (#208). -
Listeners are plain TCP, no longer Multipath TCP;
GODEBUG=multipathtcp=1restores it. -
A scanner launched outside the shipped compose file needs
SCHEDULER_DOCKER_NETWORKpointed at a network the approval service is not on. -
Containers: the release cooldown can hold back new images from Docker Hub, Quay and Google's registries (#127). These registries record when each image was pushed, and an image gate now reads that time. Docker Hub's comes from its tag API on
hub.docker.com, a new destination (docs/EGRESS.md). Quay's comes from its tag API, and that of gcr.io,*-docker.pkg.devand registry.k8s.io from the registry's own tag listing. The date inside an image is still never used, because whoever built the image writes it. A time counts only for the exact image the gate serves. On Quay and Docker Hub, a pull by digest is dated only as the platform image of a multi-arch pull by tag. Any other pull by digest is held while the window is on. It is off by default for containers (FW_MIN_RELEASE_AGE_DAYSis 0 on an image gate). Popular tags are re-pushed every few days when their base image changes: on 2026-10-31, 10 of 10 popular official tags were under 14 days old. A held tag is a failed pull, because there is no older version for the client to fall back to. Behind any other registry (ghcr.io,registry:2) the setting is still not enforced, and the gate says so at startup and on/policy. -
Security: no Multipath TCP on any listener (CVE-2026-98432). Go has opened every Linux listener as Multipath TCP since 1.24 where the kernel supports it, and the one demonstrated remote-root exploit for CVE-2026-98432, a use-after-free in the kernel's IPv4 TCP connection setup, targets MPTCP listeners. Every Yellow Jack binary is now built with
multipathtcp=0, so its listeners are plain TCP. This removes the exploit path that was demonstrated, not the kernel bug: patch the host kernel (fixed in 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56 and 7.2.10, and in 7.3; Ubuntu and RHEL had no fixed kernel as of 2026-10-30). An operator who needs MPTCP setsGODEBUG=multipathtcp=1. Containers share the host kernel, so this applies to the images as well. -
Security: scanners run sandboxed, on a network of their own. A scanner runs Scorecard against a repository anyone can publish, so a parsing bug in it, or in the 18-month-old Go runtime the bundled Scorecard v5.2.1 is built with (#224), would be hostile input running in that container. That container ran with the daemon's default capabilities and a writable root, with no limits, and on the stack's network, where the approval service approves packages without authentication (D77). Now, under either launcher, each scanner container:
- runs with no capabilities and
no-new-privileges; - has a read-only root, with
/tmpas an anonymous volume deleted along with the container; - is limited to 2 GiB of memory (the largest scan measured, torvalds/linux, peaked at 1,153 MiB) and 256 processes;
- runs on
${YJ_NETWORK:-yellowjack}-scan, which only the scheduler also joins, so it cannot reach approval, postgres or the gates (checked on a running stack bye2e/async_local.shleg 33).
With the real scanner image, kubernetes/kubernetes (6.2) and torvalds/linux (2.3) scored the same sandboxed and unsandboxed. A memory-backed
/tmpwas tried and rejected: when too small, it raised torvalds/linux's score from 2.3 to 2.7, because the file checks read fewer files. Anyone launching scanners outside the shipped compose file must pointSCHEDULER_DOCKER_NETWORKat a network approval is not on. - runs with no capabilities and
-
Go: the gate serves the checksum database, and Go modules work under TLS interception (paid build) (#218). The go command verifies every module against
sum.golang.org, and goes to it directly when its proxy does not offer to serve it, which proxy.golang.org does not. The Go gate now offers it: it answers the go command's check itself and relays the database's signed answers verbatim, so a client needs no route of its own tosum.golang.org. Verification is unchanged, because the go command checks those answers against a key built into the toolchain.sum.golang.orgis a new destination for the Go gate (docs/EGRESS.md). With that, TLS interception was measured with a realgo getthrough the paid build's intercept port: the module is evaluated, the checksum lookup stays on the one host, and a refused module failsgo getwith the gate's reason. -
Docs: a CA reaches a systemd-managed Docker daemon after
systemctl restart docker(#143). The "install the CA, then restart the Docker daemon" instruction indocs/CLIENT_TRUST.mdwas measured with a daemon run as a bare process. It now holds, measured, for the daemon a Debian or Ubuntu server runs under systemd: still not trusted before the restart, trusted aftersystemctl restart docker. -
Security: a package the gate could not look up is no longer recorded as simply "allowed" on PyPI, Maven and Go (#221). The gate's lookup reads a resource the client never asks for: PyPI's JSON API, Maven's
maven-metadata.xml, a Go proxy's@latest. Some registries, measured, do not serve it: pypiserver and a Nexus PyPI proxy serve project pages but no JSON API, and a Go proxy served from a module cache directory has no@latest. Before this change, a lookup "not found" on those registries meant the package was served with no evaluation.- PyPI: the lookup now asks the project page, the page pip itself asks for, before it says a package is unknown. A project page served to a developer's credential but not to the gate's own is withheld as on npm. A project served by an index with no JSON API cannot be evaluated, so it now takes
FW_UNSCORABLE_POLICY. Under the defaultblock, every package from such an index (pypiserver, a Nexus PyPI proxy) is held for approval, where before it was served unevaluated. An operator who accepts that setsFW_UNSCORABLE_POLICY=allow-but-log, and each such serve is recorded as not evaluated. - Go: the lookup falls back to the version list when the proxy has no
@latest, so those modules are now evaluated. - Maven and Go: where the lookup found nothing but the registry served the request, the request is still served, because withholding might never clear, and the record says it was not evaluated.
- Where to see it: these cases show in the console's audit log as "not evaluated" and are counted on
yellowjack_served_unevaluated_total.
- PyPI: the lookup now asks the project page, the page pip itself asks for, before it says a package is unknown. A project page served to a developer's credential but not to the gate's own is withheld as on npm. A project served by an index with no JSON API cannot be evaluated, so it now takes
-
Docs: client trust on Ubuntu holds for an ordinary user, not only root (#143).
docs/CLIENT_TRUST.md's Ubuntu results were measured as root. Every client was measured again as an ordinary account with a cleared environment, with the same results: pip in a developer's own venv and every apt-installed client are reached by a CA installed the ordinary Debian way, whileuv, a Node or JDK release tarball unpacked into the user's home, and code using the venv's owncertifiare not. The page now says so. -
Licences: every image and release carries the third-party notices (#213). The binaries statically link the Go standard library and, in the approval service, the PostgreSQL driver and its dependencies, whose MIT and BSD licences require their notices to travel with the binary.
THIRD_PARTY_NOTICES.mdlists each module, its version and licence, and which binary links it, followed by each licence in full. It is generated from what the binaries actually link, and a test fails when a dependency changes without it. Every image now has/licensesholding it and Yellow Jack's ownLICENSE, the GitHub release attaches it, and the optional scanner image also carries OpenSSF Scorecard's licence. The file also states the terms of the data the product uses: the known-malware feed is derived from OpenSSF Malicious Packages (Apache License 2.0), and each feed snapshot now begins with a comment line saying so; the gate queries deps.dev from your network, and Yellow Jack does not redistribute deps.dev data. -
Release: the images and the chart are signed, and every image carries an SBOM and build provenance (#212). The release workflow signs the three images and the Helm chart with Sigstore keyless signing, each by digest. It attaches to each image an SPDX SBOM, made by syft from the published image and signed as a cosign attestation by the same workflow identity, and GitHub's SLSA build provenance for the run that built it. Both are stored beside the image, so the image index a cluster pulls is unchanged. The release checks every signature and attestation as a user would (
cosign verify,cosign verify-attestation,gh attestation verify) before it creates the GitHub release;docs/RELEASING.mdgives the commands. 0.1.0 is not signed; verify it by the digests in its release notes. -
Contributing: the guide says what is true (#211). Until a Contributor License Agreement exists, this repository cannot merge outside code. CONTRIBUTING.md says so, asks for issues instead, and explains that the repository is published from the development repository one commit per release. A pull request template says the same.
-
Security: package uploads are refused in npm, PyPI and Maven, as OCI pushes already were (#184). Under
FW_WRITE_POLICY=block, the default,npm publish,twine uploadandmvn deploythrough the gate get a 405 naming what was refused, and nothing reaches the upstream. npm's other writes (npm login, tokens,dist-tag,deprecate,unpublish) andnpm auditare relayed as before: an npm publish is recognised by its content, a package document carrying a tarball, not by its method.allow-but-logandallowrelay uploads as they do OCI pushes. -
Console: a held known-malware feed update is shown, not only logged (#220). Each gate that pulls the signed feed now reports its last pull on its heartbeat: installed, up to date, failed with the reason, or held. Gate health shows it under the feed the gate enforces. A held update is in the warning style, with the serial still enforcing and the
FW_MALWARE_FEED_RELEASE=<serial>that releases it, and the Overview's known-malware tile points to Gate health. The report sits outside the policy, so a hold does not make a gate look as if its policy diverged. A gate that pulls nothing sends the heartbeat it always did. -
Console: the approval queue shows who is waiting on each release (#206). Every waiting release shows how many requests reached it since it entered the queue, and from how many distinct source addresses: "3 requests from 3 addresses". The waiting list can be ordered by that (
?sort=demand, or "most addresses" on the page). Addresses rank first, so a single host requesting a package in a loop cannot push down a release many hosts are waiting on. The default order is still longest waiting first. The counts come from the audit records the gate already writes, so they cost the request path nothing and limit nothing. An address is a host, not a person: everyone behind one CI egress or NAT counts once. -
Helm:
helm testproves the approval service is unreachable from other pods (#210). Approval takes no credential, and the chart's NetworkPolicy is its only protection. A NetworkPolicy does nothing on a CNI that does not enforce it, and then any pod in the cluster, a CI runner included, can approve a held package.helm test <release>now runs two pods: one the policy does not admit, which must fail to connect, and a control the policy admits, which must connect, so a failure without contact cannot read as isolation. The install notes say to run it, and the chart no longer calls an unenforced policy "harmless". -
Security: an operator can distrust a certificate on the gate's own TLS (#215).
FW_UPSTREAM_DISTRUSTtakes SHA-256 digests of a certificate's public key (SPKI) or of the certificate. A connection the gate makes (registry, artifact CDN, deps.dev, feed, scanner) is refused when every chain the verifier built passes through a listed one. The setting closes a compromised CA right away, without waiting for the pinned base image's bundle to change; Go checks no revocation. A malformed entry refuses startup, and the startup log lists what is distrusted. The roots the product images trust are now recorded indeploy/base-image-roots.txt, tied by test to the pinned base image, so a re-pin shows which CAs it adds or removes. On the October eMudhra compromise: its eight intermediates were cross-signed byAAA Certificate Services, which our base image trusts, and Go 1.26.10, which every 0.1.0 image is built with, rejects chains through them (CVE-2026-111943).docs/RELEASING.mddescribes the procedure for the next such event. -
Security (npm): a package the gate was told does not exist is not served unevaluated (#204). When the gate's own lookup gets a 404, it lets the request through so the registry's 404 answers a typo. The lookup and the client's request are two requests, though, and a private registry can answer 404 to the gate's credential and 200 to the developer's. That package used to be served with no lists, score or repo check applied. Now, if the registry answers the client's request with the package (200, or 304 to a conditional request), the gate withholds it as temporarily unavailable, not cached, and counts it on
yellowjack_probe_contradicted_total. The usual cause is anFW_UPSTREAM_AUTHthat sees fewer packages than your developers do. A real 404 relays as before. PyPI does the same since #221, below; on Maven and Go, whose lookups read a different document than the client's request, such a serve is named as not evaluated instead. -
Security: a pulled known-malware snapshot cannot silently stop blocking packages (#216). When a snapshot installs, each entry it stops blocking is logged by name. A snapshot that removes more than 25 entries at once, or 0.1% of the feed if that is more, is held: the feed in force keeps enforcing, the pull logs a
SECURITYline naming the removals, and onlyFW_MALWARE_FEED_RELEASE=<serial>installs it. A restart does not release it. This stops one stolen feed-signing key from widening what every gate admits. Additions are never held, and a snapshot an operator places on the mount directly is unaffected. -
Security: query values never reach the log (#207). Every URL a log line contains now has its query parameter values masked (
?private_token=***), with the names kept. That covers the relay lines and Go's own client errors, which quote the whole URL. A credential carried in a query, such as GitLab'sprivate_tokenor a presigned URL's signature, used to be written to the log in full. The request sent upstream is unchanged. -
Security: a double-encoded request path is refused (#208). A path segment that still contains
%after its one decode is refused with a 400, in every ecosystem. Before this,/lodas%2568was judged aslodashwhile the upstream was asked forlodas%68, because the npm parser decoded an already-decoded path again. No package name, tag, version or filename in the ecosystems served contains%, so real clients are unaffected. -
OCI:
FW_ATTESTATION_POLICY=requireserves only cosign-signed images (#200). An image's manifest digest must carry a keyless cosign signature (sha256-<digest>.sig), verified with the same Sigstore checks as the other ecosystems. Rekor's signed timestamp is accepted without an inclusion proof for this layout (D474). The manifest bytes served must hash to the signed digest. A signed index covers its platform manifests, and a layer is served only if a verified manifest lists it.input, the default, still reads no signatures on OCI. Docker Hub's official images are not cosign-signed, sorequirerefuses them. -
Maven:
FW_ATTESTATION_POLICY=requireserves only files with a valid Sigstore bundle (#201). Each file's<file>.sigstore.jsonis read fromFW_UPSTREAMand verified with the same Sigstore checks as npm and PyPI: a Fulcio certificate signed the file's SHA-256, and Rekor logged it. The bytes served must match that digest as they stream. A file without a valid bundle is refused before any byte, and that includes Maven's own plugins, since few artifacts publish a bundle (about 3% of widely used ones).input, the default, still checks nothing on Maven. PGP.ascsignatures are not verified (D472b, D474). -
PyPI build provenance is verified (#199).
FW_ATTESTATION_POLICYnow covers PyPI as well as npm. A wheel's or sdist's PEP 740 attestation is read fromFW_UPSTREAM's Integrity API and checked with the same Sigstore verifier as npm. Both PyPI's publish attestation and SLSA provenance count.validmeans the attestation names this file and its SHA-256, and the bytes served match it. Underinput(the default) each file is checked off the request path and the result logged. Underrequirea file without valid provenance is refused before any byte, with the status in the reason; bytes other than the attested ones abort the transfer. An upstream that does not answer the Integrity API (a mirror without PEP 740) reportsunsupported, sorequirethere refuses every file. The artifact record'sprovenance_statusnow covers PyPI files too.
Images
ghcr.io/flyyellowjack/yellowjack-firewall:0.2.0@sha256:8728d8696b008a603156467693ce770b0cd3f218d2f8c2868778ab3b65dc7127ghcr.io/flyyellowjack/yellowjack-approval:0.2.0@sha256:576205bfc702c871e72c6b3ebff7622564b475ed83e44e902224e55cf2508980ghcr.io/flyyellowjack/yellowjack-console:0.2.0@sha256:a044c89efb7577d0afb1cabf64833520396c4a74535c304f61bde991e470ce13
Each image and the chart is signed with Sigstore keyless signing by this release workflow:
cosign verify ghcr.io/flyyellowjack/yellowjack-firewall:0.2.0 \
--certificate-identity https://github.com/flyyellowjack/yellowjack/.github/workflows/release.yml@refs/tags/v0.2.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Each image also carries an SPDX SBOM and GitHub build provenance, both attested by this workflow:
cosign verify-attestation --type spdxjson ghcr.io/flyyellowjack/yellowjack-firewall:0.2.0 \
--certificate-identity https://github.com/flyyellowjack/yellowjack/.github/workflows/release.yml@refs/tags/v0.2.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/flyyellowjack/yellowjack-firewall:0.2.0 -R flyyellowjack/yellowjack
Install
helm install yellowjack oci://ghcr.io/flyyellowjack/charts/yellowjack --version 0.2.0
yj
The dry-run CLI for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 and windows/amd64.
SHA256SUMS lists every binary and is signed with Sigstore keyless signing by this
release workflow. Verify the signature, then the binary:
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/flyyellowjack/yellowjack/.github/workflows/release.yml@refs/tags/v0.2.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
Licences
Yellow Jack is licensed under the AGPLv3 (LICENSE). THIRD_PARTY_NOTICES.md lists the third-party
code the binaries contain, with each licence in full; every image carries both in /licenses.