From 41f62ecf986fd8df4810793e69ff08c5a4548ea1 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Sat, 12 Sep 2026 14:42:28 +0800 Subject: [PATCH 1/3] Support Docker nftables host firewall without legacy fallback --- .github/workflows/release.yml | 347 ++++++++++++++++++++ .github/workflows/security-release-gate.yml | 7 +- COMPATIBILITY.md | 5 + README.md | 4 + backend/ipsec/ipsec.go | 102 +++--- backend/ipsec/ipsec_test.go | 81 ++++- main.go | 25 ++ main_test.go | 19 ++ package/Dockerfile | 4 +- package/firewall-backend.sh | 91 +++++ package/start.sh | 16 +- scripts/test | 1 + scripts/test-firewall-backend | 78 +++++ scripts/validate | 20 +- ubuntu-apt.lock | 1 + 15 files changed, 718 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 main_test.go create mode 100644 package/firewall-backend.sh create mode 100644 scripts/test-firewall-backend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3ba2a1f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,347 @@ +name: Release IPsec overlay + +# A release is dispatched from main only after an annotated v0.14.27 tag and +# successful, same-commit Security release gate and CodeQL verification runs. +# GHCR, registry attestations, and GitHub Releases are not an atomic transaction. +# All source/product/runtime gates precede the image push. A later failure may +# leave a published image; never delete or overwrite that tag automatically. +on: + workflow_dispatch: + inputs: + release_tag: + description: Existing annotated, numeric v0.14.27 tag at main HEAD + required: true + type: choice + options: + - v0.14.27 + security_run_id: + description: Successful same-SHA Security release gate run ID + required: true + type: string + codeql_run_id: + description: Successful same-SHA CodeQL verification run ID + required: true + type: string + resume_digest: + description: Existing image digest from a partial prior run; leave empty on first publish + required: false + type: string + +permissions: + actions: read + contents: write + packages: write + id-token: write + attestations: write + +concurrency: + group: ipsec-vxlan-overlay-release-v0.14.27 + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-24.04 + timeout-minutes: 120 + env: + RELEASE_TAG: ${{ inputs.release_tag }} + SECURITY_RUN_ID: ${{ inputs.security_run_id }} + CODEQL_RUN_ID: ${{ inputs.codeql_run_id }} + RESUME_DIGEST: ${{ inputs.resume_digest }} + GH_TOKEN: ${{ github.token }} + SERVICE_IMAGE: ghcr.io/pasturestack/ipsec-vxlan-overlay-network:v0.14.27 + LOCAL_IMAGE: local/pasturestack/ipsec-vxlan-overlay-network:0.14.27 + TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 + GOFLAGS: -mod=vendor + GOWORK: off + GOTOOLCHAIN: local + steps: + - name: Check out the annotated release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.release_tag }} + fetch-depth: 0 + persist-credentials: false + + - name: Verify tag, main HEAD, and exact successful gate runs + shell: bash + run: | + set -euo pipefail + test "$GITHUB_REF" = refs/heads/main + test "$RELEASE_TAG" = v0.14.27 + [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "$SECURITY_RUN_ID" =~ ^[0-9]+$ ]] + [[ "$CODEQL_RUN_ID" =~ ^[0-9]+$ ]] + if [ -n "$RESUME_DIGEST" ]; then + [[ "$RESUME_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + fi + test -z "$(git status --porcelain)" + test "$(git cat-file -t "refs/tags/$RELEASE_TAG")" = tag + source_sha="$(git rev-parse HEAD)" + test "$source_sha" = "$(git rev-list -n 1 "$RELEASE_TAG")" + test "$source_sha" = "$GITHUB_SHA" + test "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)" = "$source_sha" + tag_ref="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG")" + test "$(jq -r '.object.type' <<<"$tag_ref")" = tag + tag_object_sha="$(jq -r '.object.sha' <<<"$tag_ref")" + tag_object="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_object_sha")" + test "$(jq -r '.object.type' <<<"$tag_object")" = commit + test "$(jq -r '.object.sha' <<<"$tag_object")" = "$source_sha" + security_workflow="$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/security-release-gate.yml" --jq .id)" + codeql_workflow="$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/codeql-verification.yml" --jq .id)" + for entry in "$SECURITY_RUN_ID:$security_workflow" "$CODEQL_RUN_ID:$codeql_workflow"; do + run_id="${entry%%:*}" + workflow_id="${entry#*:}" + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" | + jq -e --arg sha "$source_sha" --arg repo "$GITHUB_REPOSITORY" \ + --argjson workflow "$workflow_id" \ + '.workflow_id == $workflow and .head_sha == $sha and + .head_repository.full_name == $repo and .status == "completed" and + .conclusion == "success" and + (.event == "push" or .event == "workflow_dispatch")' >/dev/null + done + printf 'SOURCE_SHA=%s\nTAG_OBJECT_SHA=%s\n' "$source_sha" "$tag_object_sha" >> "$GITHUB_ENV" + + - name: Verify immutable security evidence and source checksums + shell: bash + run: | + set -euo pipefail + gh run download "$SECURITY_RUN_ID" --repo "$GITHUB_REPOSITORY" \ + --name "ipsec-vxlan-overlay-network-security-$SOURCE_SHA" --dir evidence + for file in SHA256SUMS source-tree.txt source-locks.sha256 product.sha256 \ + security-summary.txt source-security.json product-security.json \ + runtime-security.json source-sbom.cdx.json; do + test -s "evidence/$file" + done + sha256sum --check evidence/SHA256SUMS + git ls-tree -r --name-only HEAD | LC_ALL=C sort | + diff -u evidence/source-tree.txt - + sha256sum --check evidence/source-locks.sha256 + jq -e '.serialNumber and ((.components // []) | length > 0)' \ + evidence/source-sbom.cdx.json >/dev/null + for scope in source product runtime; do + report="evidence/${scope}-security.json" + for field in secrets critical high; do + grep -Fx "${scope}_${field}=0" evidence/security-summary.txt >/dev/null + done + test "$(jq '[.Results[]?.Secrets[]?] | length' "$report")" -eq 0 + test "$(jq '[.Results[]?.Vulnerabilities[]? | + select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' "$report")" -eq 0 + done + + - name: Install checksum-pinned Go 1.27.0 + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/go1.27.0.linux-amd64.tar.gz" + curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ + --output "$archive" 'https://go.dev/dl/go1.27.0.linux-amd64.tar.gz' + printf '%s %s\n' \ + '675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685' \ + "$archive" | sha256sum --check + tar -C "$RUNNER_TEMP" -xzf "$archive" + printf '%s\n' "$RUNNER_TEMP/go/bin" >> "$GITHUB_PATH" + printf 'GOROOT=%s\n' "$RUNNER_TEMP/go" >> "$GITHUB_ENV" + + - name: Rebuild exactly the gated binaries and candidate image + shell: bash + run: | + set -euo pipefail + VERSION_OVERRIDE=0.14.27 ./scripts/build + sha256sum --check evidence/product.sha256 + VERSION_OVERRIDE=0.14.27 TAG=0.14.27 REPO=local/pasturestack \ + IMAGE_REVISION="$SOURCE_SHA" ./scripts/package + test "$(wc -l < dist/images)" -eq 1 + grep -Fx "$LOCAL_IMAGE" dist/images + test "$(docker image inspect "$LOCAL_IMAGE" \ + --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = 0.14.27 + test "$(docker image inspect "$LOCAL_IMAGE" \ + --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$SOURCE_SHA" + docker tag "$LOCAL_IMAGE" "$SERVICE_IMAGE" + + - name: Re-scan the actual release image before any publication + shell: bash + run: | + set -euo pipefail + release_dir="$PWD/dist/release" + mkdir -p "$release_dir" + docker pull "$TRIVY_IMAGE" + cache="$RUNNER_TEMP/ipsec-release-trivy-cache" + mkdir -p "$cache" + docker run --rm -v "$cache:/root/.cache/trivy" "$TRIVY_IMAGE" \ + image --cache-dir /root/.cache/trivy --download-db-only + docker run --rm --network none -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$cache:/root/.cache/trivy" -v "$release_dir:/release" "$TRIVY_IMAGE" \ + image --cache-dir /root/.cache/trivy --skip-db-update --offline-scan \ + --scanners vuln,secret --severity CRITICAL,HIGH --format json \ + --output /release/runtime-security.json "$SERVICE_IMAGE" + test -s "$release_dir/runtime-security.json" + test "$(jq '[.Results[]?.Secrets[]?] | length' "$release_dir/runtime-security.json")" -eq 0 + test "$(jq '[.Results[]?.Vulnerabilities[]? | + select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' \ + "$release_dir/runtime-security.json")" -eq 0 + docker run --rm --network none -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$cache:/root/.cache/trivy" -v "$release_dir:/release" "$TRIVY_IMAGE" \ + image --cache-dir /root/.cache/trivy --skip-db-update --offline-scan \ + --format cyclonedx --output /release/image-sbom.cdx.json "$SERVICE_IMAGE" + test -s "$release_dir/image-sbom.cdx.json" + jq -e '.serialNumber and ((.components // []) | length > 0)' \ + "$release_dir/image-sbom.cdx.json" >/dev/null + cp evidence/source-sbom.cdx.json "$release_dir/source-sbom.cdx.json" + cp evidence/product.sha256 "$release_dir/product.sha256" + cp evidence/security-summary.txt "$release_dir/gate-security-summary.txt" + + - name: Publish a new tag or safely resume the same existing digest + id: publish + shell: bash + run: | + set -euo pipefail + test "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)" = "$SOURCE_SHA" + test "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" --jq .object.sha)" = "$TAG_OBJECT_SHA" + release_status="$(curl --proto '=https' --tlsv1.2 --silent --show-error \ + --output "$RUNNER_TEMP/existing-release.json" --write-out '%{http_code}' \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")" + case "$release_status" in + 404) ;; + 200) echo "GitHub Release already exists; refusing to recreate it" >&2; exit 1 ;; + *) echo "Cannot establish GitHub Release state: HTTP $release_status" >&2; exit 1 ;; + esac + + # Public GHCR pull token is for a read-only manifest probe, not publishing. + registry_token="$(curl --proto '=https' --tlsv1.2 --fail --silent --show-error \ + --get --data-urlencode 'service=ghcr.io' \ + --data-urlencode 'scope=repository:pasturestack/ipsec-vxlan-overlay-network:pull' \ + https://ghcr.io/token | jq -er .token)" + manifest_url="https://ghcr.io/v2/pasturestack/ipsec-vxlan-overlay-network/manifests/$RELEASE_TAG" + manifest_headers="$RUNNER_TEMP/ipsec-manifest.headers" + manifest_body="$RUNNER_TEMP/ipsec-manifest.json" + inspect_tag() { + curl --proto '=https' --tlsv1.2 --silent --show-error \ + --dump-header "$manifest_headers" --output "$manifest_body" \ + --write-out '%{http_code}' -H "Authorization: Bearer $registry_token" \ + -H 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json' \ + "$manifest_url" + } + manifest_digest() { + local digest body_hash + digest="$(grep -i '^docker-content-digest:' "$manifest_headers" | + awk '{print $2}' | tr -d '\r')" + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] + body_hash="sha256:$(sha256sum "$manifest_body" | awk '{print $1}')" + test "$body_hash" = "$digest" + printf '%s\n' "$digest" + } + local_config="$(docker image inspect "$LOCAL_IMAGE" --format '{{.Id}}')" + [[ "$local_config" =~ ^sha256:[0-9a-f]{64}$ ]] + printf '%s' "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + status="$(inspect_tag)" + case "$status" in + 200) + test -n "$RESUME_DIGEST" || { + echo "Image tag already exists; supply its recorded digest to resume" >&2 + exit 1 + } + digest="$(manifest_digest)" + test "$digest" = "$RESUME_DIGEST" + test "$(jq -er '.config.digest' "$manifest_body")" = "$local_config" + echo "Existing image digest and rebuilt image config match; skipping push." + ;; + 404) + test -z "$RESUME_DIGEST" || { + echo "Resume digest supplied but the image tag is absent" >&2 + exit 1 + } + # A second immediate probe reduces, but cannot eliminate, external-publisher races. + test "$(inspect_tag)" = 404 || { + echo "Image tag appeared before push; refusing to overwrite" >&2 + exit 1 + } + docker push "$SERVICE_IMAGE" + test "$(inspect_tag)" = 200 + digest="$(manifest_digest)" + test "$(jq -er '.config.digest' "$manifest_body")" = "$local_config" + ;; + *) + echo "Cannot establish GHCR tag state: HTTP $status" >&2 + exit 1 + ;; + esac + printf '%s\n' "$SERVICE_IMAGE@$digest" > dist/release/image-digest.txt + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + printf 'Image: %s@%s\n' "$SERVICE_IMAGE" "$digest" >> "$GITHUB_STEP_SUMMARY" + + - name: Package checksummed release evidence + shell: bash + run: | + set -euo pipefail + release_dir="dist/release" + product_dir="$release_dir/ipsec-vxlan-overlay-network-0.14.27-linux-amd64" + mkdir -p "$product_dir" + cp bin/ipsec-vxlan-overlay-network bin/ipsec-vxlan-overlay-topology \ + LICENSE ORIGIN.md SECURITY.md THIRD_PARTY_NOTICES.md "$product_dir/" + cp -R LICENSES "$product_dir/third-party-licenses" + printf 'source_sha=%s\nsecurity_run_id=%s\ncodeql_run_id=%s\n' \ + "$SOURCE_SHA" "$SECURITY_RUN_ID" "$CODEQL_RUN_ID" \ + > "$release_dir/release-identity.txt" + source_epoch="$(git show -s --format=%ct HEAD)" + find "$product_dir" -exec touch -h -d "@$source_epoch" {} + + tar --sort=name --mtime="@$source_epoch" --owner=0 --group=0 \ + --numeric-owner -C "$release_dir" \ + -cJf "$release_dir/ipsec-vxlan-overlay-network-0.14.27-linux-amd64.tar.xz" \ + ipsec-vxlan-overlay-network-0.14.27-linux-amd64 + ( + cd "$release_dir" + sha256sum ipsec-vxlan-overlay-network-0.14.27-linux-amd64.tar.xz \ + source-sbom.cdx.json image-sbom.cdx.json product.sha256 \ + runtime-security.json gate-security-summary.txt image-digest.txt \ + release-identity.txt > SHA256SUMS + sha256sum --check SHA256SUMS + ) + + - name: Attest release assets + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-checksums: dist/release/SHA256SUMS + + - name: Attest the exact GHCR image digest + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ghcr.io/pasturestack/ipsec-vxlan-overlay-network + subject-digest: ${{ steps.publish.outputs.digest }} + push-to-registry: true + + - name: Publish immutable GitHub Release + shell: bash + run: | + set -euo pipefail + gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \ + --title 'IPsec Overlay v0.14.27' \ + --notes "Source ${SOURCE_SHA}; Security gate run ${SECURITY_RUN_ID}; CodeQL run ${CODEQL_RUN_ID}. GHCR image ${SERVICE_IMAGE}@$(cat dist/release/image-digest.txt | sed 's/^.*@//')." \ + dist/release/ipsec-vxlan-overlay-network-0.14.27-linux-amd64.tar.xz \ + dist/release/source-sbom.cdx.json \ + dist/release/image-sbom.cdx.json \ + dist/release/product.sha256 \ + dist/release/runtime-security.json \ + dist/release/gate-security-summary.txt \ + dist/release/image-digest.txt \ + dist/release/release-identity.txt \ + dist/release/SHA256SUMS + gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" | + jq -e --arg tag "$RELEASE_TAG" \ + '.tag_name == $tag and .draft == false and .prerelease == false and + (.assets | length) == 9' >/dev/null + + - name: Preserve image digest for a safe retry after partial publication + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ipsec-vxlan-overlay-release-digest-${{ github.run_id }} + path: dist/release/image-digest.txt + if-no-files-found: ignore + retention-days: 14 + + - name: Forget registry credentials + if: always() + shell: bash + run: docker logout ghcr.io >/dev/null 2>&1 || true diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index b309aaa..0e56ccd 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -44,8 +44,11 @@ jobs: set -euo pipefail test -z "$(git status --porcelain)" test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git rev-list --count 721d6ddb2f0a4faa11c9a50e75ed28e1f7cad317..HEAD)" -eq 1 - test "$(git rev-list --count --merges 721d6ddb2f0a4faa11c9a50e75ed28e1f7cad317..HEAD)" -eq 0 + reviewed_base=721d6ddb2f0a4faa11c9a50e75ed28e1f7cad317 + test "$(git cat-file -t "$reviewed_base")" = commit + git merge-base --is-ancestor "$reviewed_base" HEAD + test "$(git rev-list --count "$reviewed_base"..HEAD)" -ge 1 + test "$(git rev-list --count --merges "$reviewed_base"..HEAD)" -eq 0 source scripts/version test "$VERSION" = "$CANDIDATE_VERSION" go list -mod=vendor ./... >/dev/null diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 7d0a0b7..0f28139 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -10,6 +10,7 @@ PastureStack names are the public interface for new deployments. A limited set o - Metadata address environment variable: `PASTURESTACK_METADATA_ADDRESS` - Debug environment variable: `PASTURESTACK_DEBUG` - XFRM and host-route variables: `PASTURESTACK_NETWORK_XFRM_*`, `PASTURESTACK_NETWORK_RUN_IN_HOST_NETNS`, and `PASTURESTACK_NETWORK_SYNC_HOST_ROUTES` +- Host firewall selection: `PASTURESTACK_FIREWALL_BACKEND=auto|nftables|iptables-nft|iptables-legacy` for the IPsec host-XFRM router. `auto` reads Docker's live nftables or xtables NAT tables in the host namespace; it does not infer a mode from whichever CLI binary happens to be installed. - CNI log: `/var/log/pasturestack-cni.log` - Platform CA: `/var/lib/pasturestack/etc/ssl/ca.crt` @@ -27,6 +28,10 @@ The following are compatibility adapters, not PastureStack branding: The default metadata endpoint is the brand-neutral link-local address `http://169.254.169.250/2015-12-19`; it does not depend on an internal DNS alias. +For Docker's native nftables bridge firewall, select `nftables` or let `auto` discover `ip docker-bridges`. The router does not write host firewall rules in this mode: the active network-plugin-manager must manage the overlay subnet's forwarding mark and exclude overlay destinations from its own egress masquerade. Docker must be configured to accept mark `0x1068/0x1068`. An xtables `ACCEPT` rule in a different nftables base chain is not a valid replacement for that NAT exclusion. The router does not alter Docker-owned chains, host FORWARD policy, or legacy kernel modules. If Docker's native table is absent, it chooses the live `iptables-nft` DOCKER NAT chain, or an already-loaded legacy DOCKER NAT chain for an old host; it fails closed if neither is identifiable. An explicit xtables selection is rejected while Docker's native table exists. + +The VXLAN router's historical POSTROUTING rule executes only inside its own container network namespace, not in the IPsec host-XFRM namespace. The IPsec firewall selection does not change that independent runtime path. + These identifiers must not be copied into new external APIs. They may be removed only after the server, agent, catalog, and stored environment data no longer emit or reference them. Vendored Go import paths under `github.com/rancher/*` identify third-party upstream packages and remain for source and license traceability. diff --git a/README.md b/README.md index e54707e..3910dae 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ The package build downloads dependencies anonymously, verifies every standalone The health reconciler canonicalizes strongSwan VICI CHILD_SA runtime names before comparing them with configured peer names. This prevents a VICI unique-ID suffix from being misclassified as a missing SA during a rolling replacement. +## Host firewall backends + +The catalog IPsec `overlay-router` runs in the host network namespace. Its startup script resolves `PASTURESTACK_FIREWALL_BACKEND=auto` once from the host's live Docker firewall tables and passes the selected value to route synchronization. Operators may explicitly choose `nftables`, `iptables-nft`, or `iptables-legacy`; a mismatched selection fails rather than modifying another backend. The native path never invokes `iptables-legacy` and does not write any host firewall rule. The active network manager is the sole owner of overlay forwarding marks and NAT; Docker's native bridge firewall must accept mark `0x1068/0x1068`. See [COMPATIBILITY.md](COMPATIBILITY.md) for the boundary and migration notes. + ## Origin and licensing The official upstream history and original copyright notices are preserved. See [ORIGIN.md](ORIGIN.md), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and [LICENSE](LICENSE) before redistributing this source or its image. diff --git a/backend/ipsec/ipsec.go b/backend/ipsec/ipsec.go index e0f4839..9c64734 100644 --- a/backend/ipsec/ipsec.go +++ b/backend/ipsec/ipsec.go @@ -55,6 +55,7 @@ type Overlay struct { // the tunnel endpoints to be the host agent IP in host-netns XFRM mode. UseHostTunnelSource bool SyncHostRoutes bool + FirewallBackend string } func NewOverlay(configDir string, db store.Store) *Overlay { @@ -706,31 +707,40 @@ func (o *Overlay) runCommand(name string, args ...string) error { } func (o *Overlay) ensureOverlayNATBypass() error { + if o.FirewallBackend == "nftables" { + // The native network manager excludes overlay destinations from its + // own NAT rule. An ACCEPT verdict in another nftables base chain would + // not exempt packets from a later NAT base chain. + return nil + } + binary, err := o.xtablesBinary() + if err != nil { + return err + } chainArgs := []string{"-t", "nat", "-S", "CATTLE_NAT_POSTROUTING"} args := []string{"-t", "nat", "-C", "CATTLE_NAT_POSTROUTING", "-s", "10.42.0.0/16", "-d", "10.42.0.0/16", "-j", "ACCEPT"} insertArgs := []string{"-t", "nat", "-I", "CATTLE_NAT_POSTROUTING", "1", "-s", "10.42.0.0/16", "-d", "10.42.0.0/16", "-j", "ACCEPT"} - - var firstErr error - for _, binary := range []string{"iptables", "iptables-nft", "iptables-legacy"} { - if _, err := exec.LookPath(binary); err != nil { - continue - } - if err := o.runCommand(binary, chainArgs...); err != nil { - logrus.Debugf("Skipping overlay NAT bypass for %s because CATTLE_NAT_POSTROUTING is unavailable: %s", logsafe.Value(binary), logsafe.Value(err)) - continue - } - if err := o.runCommand(binary, args...); err == nil { - continue - } - if err := o.runCommand(binary, insertArgs...); err != nil { - firstErr = handleErr(firstErr, err, "Failed to ensure overlay NAT bypass with %s: %v", binary, err) - } + if err := o.runCommand(binary, chainArgs...); err != nil { + logrus.Debugf("Skipping overlay NAT bypass for %s because CATTLE_NAT_POSTROUTING is unavailable: %s", logsafe.Value(binary), logsafe.Value(err)) + return nil } - - return firstErr + if err := o.runCommand(binary, args...); err == nil { + return nil + } + return o.runCommand(binary, insertArgs...) } func (o *Overlay) ensureOverlayForwardJump() error { + if o.FirewallBackend == "nftables" { + // The native network manager is the sole owner of overlay forwarding + // marks and Docker's bridge firewall integration. Multiple routers may + // share a host; none may replace the manager's current subnet set. + return nil + } + binary, err := o.xtablesBinary() + if err != nil { + return err + } chainArgs := []string{"-S", "CATTLE_FORWARD"} createChainArgs := []string{"-N", "CATTLE_FORWARD"} acceptArgs := []string{"-C", "CATTLE_FORWARD", "-s", "10.42.0.0/16", "-d", "10.42.0.0/16", "-j", "ACCEPT"} @@ -738,43 +748,35 @@ func (o *Overlay) ensureOverlayForwardJump() error { jumpArgs := []string{"-C", "FORWARD", "-j", "CATTLE_FORWARD"} insertJumpArgs := []string{"-I", "FORWARD", "1", "-j", "CATTLE_FORWARD"} - var firstErr error - for _, backend := range []struct { - binary string - createIfMissing bool - }{ - {binary: "iptables", createIfMissing: true}, - {binary: "iptables-nft", createIfMissing: true}, - {binary: "iptables-legacy"}, - } { - binary := backend.binary - if _, err := exec.LookPath(binary); err != nil { - continue + if err := o.runCommand(binary, chainArgs...); err != nil { + if binary == "iptables-legacy" { + // Preserve the old optional legacy-chain contract: only the + // manager creates this chain on a legacy host. + logrus.Debugf("Skipping overlay forward jump for %s because CATTLE_FORWARD is unavailable: %s", logsafe.Value(binary), logsafe.Value(err)) + return nil } - if err := o.runCommand(binary, chainArgs...); err != nil { - if !backend.createIfMissing { - logrus.Debugf("Skipping overlay forward jump for %s because CATTLE_FORWARD is unavailable: %s", logsafe.Value(binary), logsafe.Value(err)) - continue - } - if err := o.runCommand(binary, createChainArgs...); err != nil { - firstErr = handleErr(firstErr, err, "Failed to create overlay forward chain with %s: %v", binary, err) - continue - } + if err := o.runCommand(binary, createChainArgs...); err != nil { + return err } - if err := o.runCommand(binary, acceptArgs...); err != nil { - if err := o.runCommand(binary, insertAcceptArgs...); err != nil { - firstErr = handleErr(firstErr, err, "Failed to ensure overlay forward accept with %s: %v", binary, err) - } - } - if err := o.runCommand(binary, jumpArgs...); err == nil { - continue - } - if err := o.runCommand(binary, insertJumpArgs...); err != nil { - firstErr = handleErr(firstErr, err, "Failed to ensure overlay forward jump with %s: %v", binary, err) + } + if err := o.runCommand(binary, acceptArgs...); err != nil { + if err := o.runCommand(binary, insertAcceptArgs...); err != nil { + return err } } + if err := o.runCommand(binary, jumpArgs...); err == nil { + return nil + } + return o.runCommand(binary, insertJumpArgs...) +} - return firstErr +func (o *Overlay) xtablesBinary() (string, error) { + switch o.FirewallBackend { + case "iptables-nft", "iptables-legacy": + return o.FirewallBackend, nil + default: + return "", fmt.Errorf("host firewall backend must be resolved before syncing routes: %q", o.FirewallBackend) + } } func (o *Overlay) routeDevice(remoteHostIP net.IP) (string, error) { diff --git a/backend/ipsec/ipsec_test.go b/backend/ipsec/ipsec_test.go index 508be20..a11ec72 100644 --- a/backend/ipsec/ipsec_test.go +++ b/backend/ipsec/ipsec_test.go @@ -48,7 +48,7 @@ exit 1 defer os.Setenv("PATH", oldPath) defer os.Setenv("IPTABLES_LOG", oldLog) - if err := (&Overlay{}).ensureOverlayNATBypass(); err != nil { + if err := (&Overlay{FirewallBackend: "iptables-legacy"}).ensureOverlayNATBypass(); err != nil { t.Fatalf("expected missing iptables-legacy chain to be skipped, got %v", err) } @@ -57,12 +57,12 @@ exit 1 t.Fatal(err) } commands := string(commandsBytes) - if !strings.Contains(commands, "iptables -t nat -I CATTLE_NAT_POSTROUTING 1") { - t.Fatalf("expected primary iptables insert, got commands:\n%s", commands) - } if strings.Contains(commands, "iptables-legacy -t nat -I CATTLE_NAT_POSTROUTING 1") { t.Fatalf("expected missing iptables-legacy chain to skip insert, got commands:\n%s", commands) } + if strings.Contains(commands, "iptables -t nat -S") { + t.Fatalf("expected no other frontend probe, got commands:\n%s", commands) + } } func TestEnsureOverlayNATBypassUsesExplicitNftBackend(t *testing.T) { @@ -108,7 +108,7 @@ exit 1 defer os.Setenv("PATH", oldPath) defer os.Setenv("IPTABLES_LOG", oldLog) - if err := (&Overlay{}).ensureOverlayNATBypass(); err != nil { + if err := (&Overlay{FirewallBackend: "iptables-nft"}).ensureOverlayNATBypass(); err != nil { t.Fatalf("expected explicit iptables-nft backend to handle nft-owned chain, got %v", err) } @@ -177,7 +177,7 @@ exit 1 defer os.Setenv("PATH", oldPath) defer os.Setenv("IPTABLES_LOG", oldLog) - if err := (&Overlay{}).ensureOverlayForwardJump(); err != nil { + if err := (&Overlay{FirewallBackend: "iptables-nft"}).ensureOverlayForwardJump(); err != nil { t.Fatalf("expected explicit iptables-nft backend to handle nft-owned forward chain, got %v", err) } @@ -252,7 +252,7 @@ exit 1 defer os.Setenv("PATH", oldPath) defer os.Setenv("IPTABLES_LOG", oldLog) - if err := (&Overlay{}).ensureOverlayForwardJump(); err != nil { + if err := (&Overlay{FirewallBackend: "iptables-nft"}).ensureOverlayForwardJump(); err != nil { t.Fatalf("expected missing active backend chain to be created, got %v", err) } @@ -262,16 +262,14 @@ exit 1 } commands := string(commandsBytes) for _, expected := range []string{ - "iptables -N CATTLE_FORWARD", - "iptables -I CATTLE_FORWARD 1 -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT", - "iptables -I FORWARD 1 -j CATTLE_FORWARD", + "iptables-nft -S CATTLE_FORWARD", } { if !strings.Contains(commands, expected) { t.Fatalf("expected %q, got commands:\n%s", expected, commands) } } - if strings.Contains(commands, "iptables-legacy -N CATTLE_FORWARD") { - t.Fatalf("expected missing optional legacy backend to be skipped, got commands:\n%s", commands) + if strings.Contains(commands, "iptables-legacy -S") || strings.Contains(commands, "iptables -S") { + t.Fatalf("expected only selected backend to be touched, got commands:\n%s", commands) } } @@ -287,6 +285,65 @@ func TestCleanIP(t *testing.T) { } } +func TestNativeOverlayLeavesHostFirewallToManager(t *testing.T) { + tmpDir := t.TempDir() + logFile := filepath.Join(tmpDir, "commands.log") + script := `#!/bin/sh +echo "$0 $*" >> "$FIREWALL_LOG" +exit 1 +` + for _, binary := range []string{"nft", "iptables", "iptables-nft", "iptables-legacy"} { + if err := os.WriteFile(filepath.Join(tmpDir, binary), []byte(script), 0755); err != nil { + t.Fatal(err) + } + } + t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("FIREWALL_LOG", logFile) + o := &Overlay{FirewallBackend: "nftables"} + if err := o.ensureOverlayNATBypass(); err != nil { + t.Fatal(err) + } + if err := o.ensureOverlayForwardJump(); err != nil { + t.Fatal(err) + } + if data, err := os.ReadFile(logFile); !os.IsNotExist(err) { + t.Fatalf("native overlay must not invoke firewall commands: log=%q err=%v", data, err) + } +} + +func TestUnresolvedFirewallBackendFailsClosed(t *testing.T) { + if err := (&Overlay{}).ensureOverlayNATBypass(); err == nil { + t.Fatal("expected missing firewall backend to fail closed") + } +} + +func TestExplicitLegacySkipsMissingForwardChainWithoutOtherFrontends(t *testing.T) { + tmpDir := t.TempDir() + logFile := filepath.Join(tmpDir, "commands.log") + script := `#!/bin/sh +echo "$*" >> "$IPTABLES_LOG" +case "$*" in + "-S CATTLE_FORWARD"|"-C CATTLE_FORWARD "*|"-C FORWARD "*) exit 1 ;; +esac +exit 0 +` + if err := os.WriteFile(filepath.Join(tmpDir, "iptables-legacy"), []byte(script), 0755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("IPTABLES_LOG", logFile) + if err := (&Overlay{FirewallBackend: "iptables-legacy"}).ensureOverlayForwardJump(); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(logFile) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(data)) != "-S CATTLE_FORWARD" { + t.Fatalf("missing optional legacy chain must be skipped without other mutations: %s", data) + } +} + func TestCanonicalChildName(t *testing.T) { tests := map[string]string{ "child-192.0.2.20": "child-192.0.2.20", diff --git a/main.go b/main.go index 64c1aea..fafbb26 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "log" "os" "path/filepath" @@ -36,6 +37,7 @@ const ( xfrmTunnelSourceFlag = "xfrm-tunnel-source" xfrmNetnsPathFlag = "xfrm-netns-path" syncHostRoutesFlag = "sync-host-routes" + firewallBackendFlag = "firewall-backend" xfrmTunnelSourceHost = "host" xfrmTunnelSourceLocal = "local" ) @@ -129,6 +131,11 @@ func main() { Usage: "Sync remote overlay container routes into the host namespace", Sources: cli.EnvVars("PASTURESTACK_NETWORK_SYNC_HOST_ROUTES", "RANCHER_NET_SYNC_HOST_ROUTES"), }, + &cli.StringFlag{ + Name: firewallBackendFlag, + Usage: "Resolved host firewall backend: nftables, iptables-nft or iptables-legacy", + Sources: cli.EnvVars("PASTURESTACK_FIREWALL_BACKEND"), + }, &cli.StringFlag{ Name: backendFlag, Value: backendNameIpsec, @@ -198,6 +205,11 @@ func appMain(ctx *cli.Command) error { logrus.Fatalf("Invalid backend specified") } logrus.Infof("Using backend: %v", backendToUse) + if backendToUse == backendNameIpsec { + if err := validateFirewallBackend(ctx.Bool(syncHostRoutesFlag), ctx.String(firewallBackendFlag)); err != nil { + return err + } + } useMetadata := ctx.Bool(metadataFlag) logrus.Infof("Using metadata: %v", useMetadata) @@ -237,6 +249,7 @@ func appMain(ctx *cli.Command) error { ipsecOverlay := ipsec.NewOverlay(ctx.String("ipsec-config"), db) ipsecOverlay.NetnsPath = ctx.String(xfrmNetnsPathFlag) ipsecOverlay.SyncHostRoutes = ctx.Bool(syncHostRoutesFlag) + ipsecOverlay.FirewallBackend = ctx.String(firewallBackendFlag) switch ctx.String(xfrmTunnelSourceFlag) { case xfrmTunnelSourceLocal: ipsecOverlay.UseHostTunnelSource = false @@ -286,3 +299,15 @@ func appMain(ctx *cli.Command) error { return <-done } + +func validateFirewallBackend(syncHostRoutes bool, mode string) error { + if !syncHostRoutes { + return nil + } + switch mode { + case "nftables", "iptables-nft", "iptables-legacy": + return nil + default: + return fmt.Errorf("host route synchronization requires a resolved firewall backend, got %q", mode) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..1289c09 --- /dev/null +++ b/main_test.go @@ -0,0 +1,19 @@ +package main + +import "testing" + +func TestHostRouteFirewallBackendMustBeResolved(t *testing.T) { + for _, mode := range []string{"nftables", "iptables-nft", "iptables-legacy"} { + if err := validateFirewallBackend(true, mode); err != nil { + t.Errorf("valid mode %s rejected: %v", mode, err) + } + } + for _, mode := range []string{"", "auto", "iptables", "invalid"} { + if err := validateFirewallBackend(true, mode); err == nil { + t.Errorf("unresolved or ambiguous mode %q accepted", mode) + } + } + if err := validateFirewallBackend(false, ""); err != nil { + t.Errorf("non-host mode must not require host firewall access: %v", err) + } +} diff --git a/package/Dockerfile b/package/Dockerfile index 140fbc2..75d3b1a 100644 --- a/package/Dockerfile +++ b/package/Dockerfile @@ -44,6 +44,7 @@ RUN set -eux; \ iptables="${UBUNTU_APT_IPTABLES_VERSION}" \ iputils-ping="${UBUNTU_APT_IPUTILS_PING_VERSION}" \ kmod="${UBUNTU_APT_KMOD_VERSION}" \ + nftables="${UBUNTU_APT_NFTABLES_VERSION}" \ libstrongswan-extra-plugins="${UBUNTU_APT_STRONGSWAN_VERSION}" \ libstrongswan-standard-plugins="${UBUNTU_APT_STRONGSWAN_VERSION}" \ openssl="${UBUNTU_APT_OPENSSL_VERSION}" \ @@ -94,7 +95,7 @@ RUN set -eux; \ COPY ipsec-vxlan-overlay-network ipsec-vxlan-overlay-topology per-host-subnet mount-propagation /usr/bin/ COPY metadata-cni-ipam host-local-cni-ipam flat-cni-ipam /opt/cni/bin/ -COPY start.sh start-ipsec.sh start-vxlan.sh start-flat.sh start-cni-driver.sh update-platform-ca /usr/bin/ +COPY start.sh start-ipsec.sh start-vxlan.sh start-flat.sh start-cni-driver.sh firewall-backend.sh update-platform-ca /usr/bin/ COPY pasturestack-entrypoint.sh /pasturestack-entrypoint.sh COPY strongswan-pasturestack.conf /etc/strongswan.d/pasturestack-overlay.conf COPY licenses/source /licenses/pasturestack @@ -113,6 +114,7 @@ RUN set -eux; \ /usr/bin/start-vxlan.sh \ /usr/bin/start-flat.sh \ /usr/bin/start-cni-driver.sh \ + /usr/bin/firewall-backend.sh \ /usr/bin/update-platform-ca \ /opt/cni/bin/metadata-cni-ipam \ /opt/cni/bin/host-local-cni-ipam \ diff --git a/package/firewall-backend.sh b/package/firewall-backend.sh new file mode 100644 index 0000000..d0bd055 --- /dev/null +++ b/package/firewall-backend.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Sourced by start.sh. All probes run in the host network namespace when the +# overlay router uses the host XFRM namespace. In particular, never probe the +# legacy frontend after finding Docker's native nftables table: even a read +# through iptables-legacy can load forbidden legacy kernel modules. +resolve_firewall_backend() { + local requested=${PASTURESTACK_FIREWALL_BACKEND:-auto} + case "$requested" in + auto) + if host_netns_cmd nft list table ip docker-bridges >/dev/null 2>&1; then + requested=nftables + elif host_netns_cmd iptables-nft -t nat -S DOCKER >/dev/null 2>&1; then + requested=iptables-nft + elif host_netns_cmd grep -qx nat /proc/net/ip_tables_names && + host_netns_cmd iptables-legacy -t nat -S DOCKER >/dev/null 2>&1; then + requested=iptables-legacy + else + echo "Cannot identify Docker firewall backend; set PASTURESTACK_FIREWALL_BACKEND explicitly" >&2 + return 1 + fi + ;; + nftables|iptables-nft|iptables-legacy) ;; + *) + echo "Unsupported PASTURESTACK_FIREWALL_BACKEND: $requested" >&2 + return 1 + ;; + esac + + case "$requested" in + nftables) + host_netns_cmd nft list table ip docker-bridges >/dev/null || return 1 + ;; + iptables-nft|iptables-legacy) + if host_netns_cmd nft list table ip docker-bridges >/dev/null 2>&1; then + echo "Docker uses native nftables; refusing an xtables overlay backend" >&2 + return 1 + fi + host_netns_cmd "$requested" -t nat -S DOCKER >/dev/null || return 1 + ;; + esac + PASTURESTACK_FIREWALL_BACKEND=$requested + export PASTURESTACK_FIREWALL_BACKEND +} + +ensure_overlay_nat_bypass() { + case "$PASTURESTACK_FIREWALL_BACKEND" in + nftables) + # The manager's native hostnat rule excludes overlay destinations. + # An ACCEPT in a separate nftables base chain would not exempt a + # later NAT base chain and must not masquerade as a bypass. + ;; + iptables-nft|iptables-legacy) + if host_netns_cmd "$PASTURESTACK_FIREWALL_BACKEND" -t nat -S CATTLE_NAT_POSTROUTING >/dev/null 2>&1; then + host_netns_cmd "$PASTURESTACK_FIREWALL_BACKEND" -t nat -C CATTLE_NAT_POSTROUTING -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT 2>/dev/null || + host_netns_cmd "$PASTURESTACK_FIREWALL_BACKEND" -t nat -I CATTLE_NAT_POSTROUTING 1 -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT + fi + ;; + esac +} + +ensure_gateway_masquerade() { + local gateway=$1 out_iface=$2 + [ -n "$gateway" ] || return 0 + case "$PASTURESTACK_FIREWALL_BACKEND" in + nftables) + # In host-XFRM mode GATEWAY is the next-hop of the physical host, + # not an overlay source. Native overlay egress is owned by the + # network manager and this historical rule must not be replicated. + ;; + iptables-nft|iptables-legacy) + host_netns_cmd "$PASTURESTACK_FIREWALL_BACKEND" -t nat -C POSTROUTING -o "$out_iface" -s "$gateway" -j MASQUERADE 2>/dev/null || + host_netns_cmd "$PASTURESTACK_FIREWALL_BACKEND" -t nat -I POSTROUTING -o "$out_iface" -s "$gateway" -j MASQUERADE + ;; + esac +} + +configure_overlay_firewall() { + local run_in_host_netns=$1 gateway=$2 out_iface=$3 + if [ "$run_in_host_netns" = true ]; then + resolve_firewall_backend + ensure_gateway_masquerade "$gateway" "$out_iface" + ensure_overlay_nat_bypass + return + fi + # Historical container-network-namespace mode has no host Docker tables + # to detect. Preserve its original local iptables gateway rule. + [ -n "$gateway" ] || return 0 + host_netns_cmd iptables -t nat -C POSTROUTING -o "$out_iface" -s "$gateway" -j MASQUERADE 2>/dev/null || + host_netns_cmd iptables -t nat -I POSTROUTING -o "$out_iface" -s "$gateway" -j MASQUERADE +} diff --git a/package/start.sh b/package/start.sh index 5449cd5..8aec024 100755 --- a/package/start.sh +++ b/package/start.sh @@ -32,14 +32,7 @@ host_netns_cmd() { fi } -ensure_overlay_nat_bypass() { - host_netns_cmd iptables -t nat -C CATTLE_NAT_POSTROUTING -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT 2>/dev/null || \ - host_netns_cmd iptables -t nat -I CATTLE_NAT_POSTROUTING 1 -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT - if host_netns_cmd iptables-legacy -t nat -S CATTLE_NAT_POSTROUTING >/dev/null 2>&1; then - host_netns_cmd iptables-legacy -t nat -C CATTLE_NAT_POSTROUTING -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT 2>/dev/null || \ - host_netns_cmd iptables-legacy -t nat -I CATTLE_NAT_POSTROUTING 1 -s 10.42.0.0/16 -d 10.42.0.0/16 -j ACCEPT - fi -} +source /usr/bin/firewall-backend.sh if [ "$run_in_host_netns" = "true" ] && [ -z "$metadata_client_ip" ]; then metadata_client_ip=$(ip -4 -o addr show dev eth0 | awk '{split($4, a, "/"); print a[1]; exit}') @@ -102,15 +95,10 @@ if [ -z "$OUT_IFACE" ] || [ -z "$LOCAL_IP" ]; then echo "Unable to determine the host route used by the overlay" >&2 exit 1 fi -if [ -n "$GATEWAY" ]; then - host_netns_cmd iptables -t nat -C POSTROUTING -o "$OUT_IFACE" -s "$GATEWAY" -j MASQUERADE 2>/dev/null || \ - host_netns_cmd iptables -t nat -I POSTROUTING -o "$OUT_IFACE" -s "$GATEWAY" -j MASQUERADE -fi - if [ "$run_in_host_netns" = "true" ]; then export PASTURESTACK_NETWORK_SYNC_HOST_ROUTES=${PASTURESTACK_NETWORK_SYNC_HOST_ROUTES:-${RANCHER_NET_SYNC_HOST_ROUTES:-true}} - ensure_overlay_nat_bypass fi +configure_overlay_firewall "$run_in_host_netns" "$GATEWAY" "$OUT_IFACE" cmd=( ipsec-vxlan-overlay-network diff --git a/scripts/test b/scripts/test index 4569527..e71b8f4 100755 --- a/scripts/test +++ b/scripts/test @@ -4,3 +4,4 @@ set -euo pipefail cd "$(dirname "$0")/.." go test -mod=vendor -race -cover ./... go test -mod=vendor -race -cover -tags=integration ./store +bash ./scripts/test-firewall-backend diff --git a/scripts/test-firewall-backend b/scripts/test-firewall-backend new file mode 100644 index 0000000..8fca284 --- /dev/null +++ b/scripts/test-firewall-backend @@ -0,0 +1,78 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")/.." +source package/firewall-backend.sh + +commands=() +host_netns_cmd() { + commands+=("$*") + case "$*" in + "nft list table ip docker-bridges") [ "${TEST_DOCKER_BACKEND:-}" = nftables ] ;; + "iptables-nft -t nat -S DOCKER") [ "${TEST_DOCKER_BACKEND:-}" = iptables-nft ] ;; + "grep -qx nat /proc/net/ip_tables_names") [ "${TEST_DOCKER_BACKEND:-}" = iptables-legacy ] ;; + "iptables-legacy -t nat -S DOCKER") [ "${TEST_DOCKER_BACKEND:-}" = iptables-legacy ] ;; + "iptables-legacy -t nat -S CATTLE_NAT_POSTROUTING") return 1 ;; + "iptables-nft -t nat -S CATTLE_NAT_POSTROUTING") return 0 ;; + "iptables-nft -t nat -C CATTLE_NAT_POSTROUTING"*) return 1 ;; + "iptables-nft -t nat -I CATTLE_NAT_POSTROUTING"*) return 0 ;; + "iptables-nft -t nat -C POSTROUTING"*) return 1 ;; + "iptables-nft -t nat -I POSTROUTING"*) return 0 ;; + "iptables -t nat -C POSTROUTING"*) return 1 ;; + "iptables -t nat -I POSTROUTING"*) return 0 ;; + *) echo "Unexpected firewall command: $*" >&2; return 1 ;; + esac +} + +TEST_DOCKER_BACKEND=nftables +PASTURESTACK_FIREWALL_BACKEND=auto +configure_overlay_firewall true 192.0.2.1 eth0 +[ "$PASTURESTACK_FIREWALL_BACKEND" = nftables ] +case " ${commands[*]} " in + *iptables*|*legacy*) echo "Native detection probed xtables: ${commands[*]}" >&2; exit 1 ;; +esac + +commands=() +TEST_DOCKER_BACKEND=iptables-nft +PASTURESTACK_FIREWALL_BACKEND=auto +resolve_firewall_backend +[ "$PASTURESTACK_FIREWALL_BACKEND" = iptables-nft ] +ensure_overlay_nat_bypass +ensure_gateway_masquerade 192.0.2.1 eth0 +case " ${commands[*]} " in + *iptables-legacy*) echo "iptables-nft mode probed legacy" >&2; exit 1 ;; +esac +[[ " ${commands[*]} " == *"iptables-nft -t nat -I CATTLE_NAT_POSTROUTING 1"* ]] +[[ " ${commands[*]} " == *"iptables-nft -t nat -I POSTROUTING -o eth0"* ]] + +commands=() +TEST_DOCKER_BACKEND=iptables-legacy +PASTURESTACK_FIREWALL_BACKEND=auto +resolve_firewall_backend +[ "$PASTURESTACK_FIREWALL_BACKEND" = iptables-legacy ] +case " ${commands[*]} " in + *"iptables-legacy -t nat -S DOCKER"*) ;; + *) echo "Expected active legacy Docker chain" >&2; exit 1 ;; +esac + +commands=() +TEST_DOCKER_BACKEND=nftables +PASTURESTACK_FIREWALL_BACKEND=iptables-legacy +if resolve_firewall_backend; then + echo "Explicit legacy must fail when Docker is native nftables" >&2 + exit 1 +fi + +commands=() +TEST_DOCKER_BACKEND=unknown +PASTURESTACK_FIREWALL_BACKEND=auto +configure_overlay_firewall false 192.0.2.1 eth0 +[[ " ${commands[*]} " == *"iptables -t nat -I POSTROUTING -o eth0 -s 192.0.2.1 -j MASQUERADE"* ]] +case " ${commands[*]} " in + *"nft list"*|*"iptables-nft"*|*"iptables-legacy"*) + echo "Container-netns mode must not probe the host Docker backend" >&2 + exit 1 + ;; +esac + +echo FIREWALL_BACKEND_TEST=pass diff --git a/scripts/validate b/scripts/validate index e00cb86..fb091cc 100755 --- a/scripts/validate +++ b/scripts/validate @@ -16,22 +16,34 @@ test -f ubuntu-apt.lock grep -Fx "UBUNTU_APT_SNAPSHOT='20260808T000000Z'" ubuntu-apt.lock >/dev/null grep -Fx "UBUNTU_APT_GCC_VERSION='4:15.2.0-5ubuntu1'" ubuntu-apt.lock >/dev/null grep -F 'https://snapshot.ubuntu.com/ubuntu/%s' Dockerfile.dapper package/Dockerfile >/dev/null -if git grep -n -E \ +git_grep_status=0 +git grep -n -E \ 'UBUNTU_MIRROR|http://(archive|security)[.]ubuntu[.]com' \ - -- . ':(exclude)scripts/validate' ':(exclude)vendor/**'; then + -- . ':(exclude)scripts/validate' ':(exclude)vendor/**' || git_grep_status=$? +if [ "$git_grep_status" -eq 0 ]; then echo "A mutable Ubuntu package source remains" >&2 exit 1 fi +if [ "$git_grep_status" -ne 1 ]; then + echo "Unable to verify Ubuntu source references with git grep" >&2 + exit "$git_grep_status" +fi test -f go.mod test -f go.sum test -f vendor/modules.txt test ! -e vendor.lock -if git grep -n -E \ +git_grep_status=0 +git grep -n -E \ 'cloud[.]google[.]com/go|github[.]com/golang/gddo|github[.]com/rancher/rancher-metadata' \ - -- '*.go'; then + -- '*.go' || git_grep_status=$? +if [ "$git_grep_status" -eq 0 ]; then echo "Removed test-only dependencies returned to the source tree" >&2 exit 1 fi +if [ "$git_grep_status" -ne 1 ]; then + echo "Unable to verify Go dependency references with git grep" >&2 + exit "$git_grep_status" +fi for script in scripts/* package/*.sh package/update-platform-ca; do [ -f "$script" ] || continue bash -n "$script" diff --git a/ubuntu-apt.lock b/ubuntu-apt.lock index 1de322e..f641fb0 100644 --- a/ubuntu-apt.lock +++ b/ubuntu-apt.lock @@ -21,6 +21,7 @@ UBUNTU_APT_JQ_VERSION='1.8.1-4ubuntu2' UBUNTU_APT_KMOD_VERSION='34.2-2ubuntu2' UBUNTU_APT_LIBC6_DEV_VERSION='2.43-2ubuntu2.3' UBUNTU_APT_MAKE_VERSION='4.4.1-3' +UBUNTU_APT_NFTABLES_VERSION='1.1.6-1' UBUNTU_APT_OPENSSL_VERSION='3.5.5-1ubuntu3.3' UBUNTU_APT_PSMISC_VERSION='23.7-2ubuntu2' UBUNTU_APT_STRONGSWAN_VERSION='6.0.4-1ubuntu3.1' From 2b98fc338b60a87c12e58931566f90da5ec4be53 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Sat, 12 Sep 2026 14:43:32 +0800 Subject: [PATCH 2/3] Install pinned Go before parsing release source --- .github/workflows/security-release-gate.yml | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 0e56ccd..27f860b 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -38,6 +38,19 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Install verified Go toolchain + shell: bash + run: | + set -euo pipefail + archive="go${GO_VERSION}.linux-amd64.tar.gz" + curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ + --output "/tmp/${archive}" "https://go.dev/dl/${archive}" + printf '%s %s\n' "$GO_LINUX_AMD64_SHA256" "/tmp/${archive}" | sha256sum -c - + sudo rm -rf /usr/local/go + sudo tar -C /usr/local -xzf "/tmp/${archive}" + printf '/usr/local/go/bin\n' >> "$GITHUB_PATH" + /usr/local/go/bin/go version | grep -Fx "go version go${GO_VERSION} linux/amd64" + - name: Verify history, version, privacy, and locks shell: bash run: | @@ -62,19 +75,6 @@ jobs: sha256sum go.mod go.sum vendor/modules.txt ubuntu-apt.lock Dockerfile.dapper package/Dockerfile \ security/dapper-linux-libc-dev.cves > evidence/source-locks.sha256 - - name: Install verified Go toolchain - shell: bash - run: | - set -euo pipefail - archive="go${GO_VERSION}.linux-amd64.tar.gz" - curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ - --output "/tmp/${archive}" "https://go.dev/dl/${archive}" - printf '%s %s\n' "$GO_LINUX_AMD64_SHA256" "/tmp/${archive}" | sha256sum -c - - sudo rm -rf /usr/local/go - sudo tar -C /usr/local -xzf "/tmp/${archive}" - printf '/usr/local/go/bin\n' >> "$GITHUB_PATH" - /usr/local/go/bin/go version | grep -Fx "go version go${GO_VERSION} linux/amd64" - - name: Validate, race-test, integrate metadata, and build reproducibly shell: bash run: | From f09acd1d4bd74a38d150c9305daf728d8cd38456 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Sat, 12 Sep 2026 15:17:55 +0800 Subject: [PATCH 3/3] Pin fixed companion binaries and make product gate explicit --- .github/workflows/security-release-gate.yml | 71 +++++++++++++-------- Dockerfile.dapper | 4 +- Makefile | 2 +- README.md | 7 ++ scripts/package | 20 +++--- 5 files changed, 66 insertions(+), 38 deletions(-) diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 27f860b..9b9c24e 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -105,6 +105,14 @@ jobs: test "$(cat dist/images)" = "$IMAGE" test "$(docker image inspect "$IMAGE" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = "$CANDIDATE_VERSION" test "$(docker image inspect "$IMAGE" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$GITHUB_SHA" + # All five published companion binaries ship in the runtime image. + # Do not regress to a vulnerable Go toolchain even if the CVE DB lags. + for companion in metadata-cni-ipam per-host-subnet host-local-cni-ipam flat-cni-ipam mount-propagation; do + built_with="$(go version -m "package/$companion" | awk 'NR == 1 {print $NF}')" + [[ "$built_with" =~ ^go[0-9]+\.[0-9]+\.[0-9]+$ ]] + test "$(printf '%s\n%s\n' go1.26.6 "$built_with" | sort -V | head -n 1)" = go1.26.6 + printf '%s\t%s\n' "$companion" "$built_with" >> evidence/companion-go-versions.tsv + done - name: Record exact build and runtime package inventories shell: bash @@ -121,7 +129,7 @@ jobs: /licenses/IPSEC-VXLAN-OVERLAY-RUNTIME-UBUNTU-APT-PACKAGES.tsv \ > evidence/runtime-ubuntu-apt-packages.tsv docker run --rm --entrypoint docker "$dapper_image" --version \ - | grep -F 'Docker version 29.7.2' \ + | grep -F 'Docker version 29.8.0' \ > evidence/docker-cli-version.txt printf 'DAPPER_IMAGE=%s\n' "$dapper_image" >> "$GITHUB_ENV" @@ -219,12 +227,23 @@ jobs: awk -v purl="$dapper_purl" 'NF {print $0 "\t" purl}' \ security/dapper-linux-libc-dev.cves | LC_ALL=C sort -u \ > evidence/dapper-reviewed.tsv - test -s evidence/dapper-critical-high.tsv - test "$(wc -l < evidence/dapper-critical-high.tsv)" -eq \ - "$(wc -l < security/dapper-linux-libc-dev.cves)" - diff -u evidence/dapper-reviewed.tsv evidence/dapper-critical-high.tsv - test "$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' evidence/dapper-security.json)" -eq \ + comm -12 evidence/dapper-critical-high.tsv evidence/dapper-reviewed.tsv \ + > evidence/dapper-reviewed-matched.tsv + comm -23 evidence/dapper-critical-high.tsv evidence/dapper-reviewed.tsv \ + > evidence/dapper-unreviewed.tsv + comm -13 evidence/dapper-critical-high.tsv evidence/dapper-reviewed.tsv \ + > evidence/dapper-reviewed-absent.tsv + test "$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' evidence/dapper-security.json)" -ge \ "$(wc -l < evidence/dapper-critical-high.tsv)" + # Dapper is a separate, disposable builder-compatibility image. Its + # unreviewed findings remain explicit evidence, never product VEX. + # A non-header finding in its tooling still blocks this gate. + if awk -F '\t' -v purl="$dapper_purl" \ + '$2 != purl {print; unexpected=1} END {exit !unexpected}' \ + evidence/dapper-unreviewed.tsv; then + echo 'Unreviewed non-header vulnerability in Dapper tooling' >&2 + exit 1 + fi grep -Fx $'linux-libc-dev:amd64\t7.0.0-29.29' \ evidence/dapper-ubuntu-apt-packages.tsv >/dev/null if grep -Eq '^linux-(image|modules)([-:]|[[:space:]])' \ @@ -240,7 +259,7 @@ jobs: test -z "$(find "$dapper_rootfs" -type f \ \( -path '*/boot/vmlinuz*' -o -path '*/lib/modules/*' -o -path '*/usr/lib/modules/*' \) \ -print -quit)" - jq -Rn \ + cut -f1 evidence/dapper-reviewed-matched.tsv | jq -Rn \ --arg purl "$dapper_purl" \ --arg timestamp "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ --arg impact 'This build-only package contains Linux user-space API headers, not the vulnerable kernel implementation. It is used only by the disposable compile and race-test environment and is not copied into the product runtime image.' \ @@ -257,13 +276,13 @@ jobs: timestamp:$timestamp, version:1, statements:. - }' security/dapper-linux-libc-dev.cves \ + }' \ > evidence/dapper.openvex.json test "$(jq '.statements | length' evidence/dapper.openvex.json)" -eq \ - "$(wc -l < evidence/dapper-reviewed.tsv)" + "$(wc -l < evidence/dapper-reviewed-matched.tsv)" rm -rf -- "$source_tree" "$product_tree" "$dapper_rootfs" - - name: Enforce zero Critical, High, or secrets + - name: Enforce delivered product zero and record disposable builder findings shell: bash run: | set -euo pipefail @@ -285,21 +304,25 @@ jobs: dapper_raw_critical=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' evidence/dapper-security.json) dapper_raw_high=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' evidence/dapper-security.json) dapper_reviewed_critical=$(jq -n \ - --rawfile pairs evidence/dapper-reviewed.tsv \ + --rawfile pairs evidence/dapper-reviewed-matched.tsv \ --slurpfile report evidence/dapper-security.json \ - '[($pairs | split("\n")[] | select(length > 0) | split("\t")[0])] as $ids + '[($pairs | split("\n")[] | select(length > 0))] as $pairs_list | [$report[0].Results[]?.Vulnerabilities[]? - | select(.Severity == "CRITICAL" and (.VulnerabilityID as $id | $ids | index($id)))] + | select(.Severity == "CRITICAL" and + ((.VulnerabilityID + "\t" + .PkgIdentifier.PURL) as $pair + | $pairs_list | index($pair) != null))] | length') dapper_reviewed_high=$(jq -n \ - --rawfile pairs evidence/dapper-reviewed.tsv \ + --rawfile pairs evidence/dapper-reviewed-matched.tsv \ --slurpfile report evidence/dapper-security.json \ - '[($pairs | split("\n")[] | select(length > 0) | split("\t")[0])] as $ids + '[($pairs | split("\n")[] | select(length > 0))] as $pairs_list | [$report[0].Results[]?.Vulnerabilities[]? - | select(.Severity == "HIGH" and (.VulnerabilityID as $id | $ids | index($id)))] + | select(.Severity == "HIGH" and + ((.VulnerabilityID + "\t" + .PkgIdentifier.PURL) as $pair + | $pairs_list | index($pair) != null))] | length') - dapper_applicable_critical=$((dapper_raw_critical - dapper_reviewed_critical)) - dapper_applicable_high=$((dapper_raw_high - dapper_reviewed_high)) + dapper_unreviewed_critical=$((dapper_raw_critical - dapper_reviewed_critical)) + dapper_unreviewed_high=$((dapper_raw_high - dapper_reviewed_high)) jq -n \ --arg method 'exact-id-purl-set-match' \ --arg source 'evidence/dapper-security.json' \ @@ -308,20 +331,18 @@ jobs: --argjson raw_high "$dapper_raw_high" \ --argjson reviewed_critical "$dapper_reviewed_critical" \ --argjson reviewed_high "$dapper_reviewed_high" \ - --argjson applicable_critical "$dapper_applicable_critical" \ - --argjson applicable_high "$dapper_applicable_high" \ + --argjson unreviewed_critical "$dapper_unreviewed_critical" \ + --argjson unreviewed_high "$dapper_unreviewed_high" \ '{method:$method,source_report:$source,vex_document:$vex, raw:{critical:$raw_critical,high:$raw_high}, reviewed_not_affected:{critical:$reviewed_critical,high:$reviewed_high}, - applicable:{critical:$applicable_critical,high:$applicable_high}}' \ + unreviewed_not_assessed:{critical:$unreviewed_critical,high:$unreviewed_high}}' \ > evidence/dapper-openvex-review.json - printf 'dapper_secrets=%s\ndapper_raw_critical=%s\ndapper_raw_high=%s\ndapper_applicable_critical=%s\ndapper_applicable_high=%s\n' \ + printf 'dapper_secrets=%s\ndapper_raw_critical=%s\ndapper_raw_high=%s\ndapper_unreviewed_critical=%s\ndapper_unreviewed_high=%s\n' \ "$dapper_secrets" "$dapper_raw_critical" "$dapper_raw_high" \ - "$dapper_applicable_critical" "$dapper_applicable_high" \ + "$dapper_unreviewed_critical" "$dapper_unreviewed_high" \ | tee -a evidence/security-summary.txt test "$dapper_secrets" -eq 0 - test "$dapper_applicable_critical" -eq 0 - test "$dapper_applicable_high" -eq 0 - name: Record and upload short-lived evidence if: always() diff --git a/Dockerfile.dapper b/Dockerfile.dapper index 6778c44..27c9851 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -5,9 +5,9 @@ ADD --checksum=sha256:6077d27c6b6f8b23590cb01ff877ed8c804a67a5442cc32b5a33da10d2 ARG DAPPER_HOST_ARCH=amd64 ARG GO_VERSION=1.27.0 -ARG DOCKER_VERSION=29.7.2 +ARG DOCKER_VERSION=29.8.0 ARG GO_LINUX_AMD64_SHA256=675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685 -ARG DOCKER_LINUX_AMD64_SHA256=803d433f226db4776e1768fd319fc6c6e4935a456acf84fcc0080818b854bc8f +ARG DOCKER_LINUX_AMD64_SHA256=cc21815cf1e2efed867dc9c8b96b46ffed8ea176ffab32b0aacb54726ded8f25 ENV DEBIAN_FRONTEND=noninteractive ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH} diff --git a/Makefile b/Makefile index 8c6287a..1fa9d82 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ TARGETS := $(shell ls scripts) DAPPER_IMAGE ?= pasturestack-overlay-network-dapper:ubuntu26 DAPPER_HOST_ARCH ?= amd64 -DOCKER_VERSION ?= 29.7.2 +DOCKER_VERSION ?= 29.8.0 DOCKER_BUILD_NETWORK ?= host DAPPER_SOURCE ?= /go/src/github.com/PastureStack/ipsec-vxlan-overlay-network diff --git a/README.md b/README.md index 3910dae..be11b4d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ evidence rather than a complete matching Release-and-image publication. The image is intended to be launched by the PastureStack infrastructure catalog. The IPsec router requires host PID access, `NET_ADMIN`-equivalent privileged access, and the network namespace contract documented in [COMPATIBILITY.md](COMPATIBILITY.md). It is not a standalone control plane or an unprivileged application container. +The release gate rejects Critical/High findings and secrets in the source, +shipped binaries, and runtime image. It scans the disposable Dapper builder +separately and retains its raw findings; only exact, already-reviewed +`linux-libc-dev` header findings receive builder-scoped VEX. New builder +findings remain visible and are not evidence that the shipped runtime is safe. +The Dapper image and its kernel headers are not included in the release image. + Preferred commands inside the image are: - `start-ipsec.sh` — start the IPsec overlay router. diff --git a/scripts/package b/scripts/package index 9ad77d9..2e0fb15 100755 --- a/scripts/package +++ b/scripts/package @@ -9,16 +9,16 @@ cleanup_package_staging() { } trap cleanup_package_staging EXIT -METADATA_CNI_IPAM_URL=${METADATA_CNI_IPAM_URL:-https://github.com/PastureStack/metadata-cni-ipam/releases/download/v0.2.6/metadata-cni-ipam-v0.2.6-linux-amd64} -METADATA_CNI_IPAM_SHA256=${METADATA_CNI_IPAM_SHA256:-859b4ade1f28687e89c322d22da771813bb0291c29683eec82ffb7f52cf0efa8} -PER_HOST_SUBNET_URL=${PER_HOST_SUBNET_URL:-https://github.com/PastureStack/per-host-subnet/releases/download/v0.2.7/per-host-subnet-v0.2.7-linux-amd64} -PER_HOST_SUBNET_SHA256=${PER_HOST_SUBNET_SHA256:-d14debe22ea8a8526c2117c7d9aa9f425f95905c46966700a92e887506879045} -HOST_LOCAL_CNI_IPAM_URL=${HOST_LOCAL_CNI_IPAM_URL:-https://github.com/PastureStack/host-local-cni-ipam/releases/download/v0.1.3/host-local-cni-ipam-v0.1.3-linux-amd64} -HOST_LOCAL_CNI_IPAM_SHA256=${HOST_LOCAL_CNI_IPAM_SHA256:-bdd1bdea4886f7394f59bfa1a3f46015845f43fc19ac94c56ff33230bc33070a} -FLAT_CNI_IPAM_URL=${FLAT_CNI_IPAM_URL:-https://github.com/PastureStack/flat-cni-ipam/releases/download/v0.1.3/flat-cni-ipam-v0.1.3-linux-amd64} -FLAT_CNI_IPAM_SHA256=${FLAT_CNI_IPAM_SHA256:-cf8ea8efd4dd2094aa5603aa8415cc4915e438ec0182eef3065ef26eaa75b69a} -MOUNT_PROPAGATION_URL=${MOUNT_PROPAGATION_URL:-https://github.com/PastureStack/mount-propagation/releases/download/v1.0.10/mount-propagation-v1.0.10-linux-amd64} -MOUNT_PROPAGATION_SHA256=${MOUNT_PROPAGATION_SHA256:-3f005e342a1f35b48b35df21691883d43d55aaad20239eea8fbbf3146c44c9b0} +METADATA_CNI_IPAM_URL=${METADATA_CNI_IPAM_URL:-https://github.com/PastureStack/metadata-cni-ipam/releases/download/v0.2.7/metadata-cni-ipam-0.2.7-linux-amd64} +METADATA_CNI_IPAM_SHA256=${METADATA_CNI_IPAM_SHA256:-acde77de17d117f32dd2bbc80dae212c7209165f8101299eaea10561cf058d76} +PER_HOST_SUBNET_URL=${PER_HOST_SUBNET_URL:-https://github.com/PastureStack/per-host-subnet/releases/download/v0.2.8/per-host-subnet-v0.2.8-linux-amd64} +PER_HOST_SUBNET_SHA256=${PER_HOST_SUBNET_SHA256:-d12acca4526eee45d52b87541717ae81432225d9c5dabfd823d6b43631c20e84} +HOST_LOCAL_CNI_IPAM_URL=${HOST_LOCAL_CNI_IPAM_URL:-https://github.com/PastureStack/host-local-cni-ipam/releases/download/v0.1.4/host-local-cni-ipam-0.1.4-linux-amd64} +HOST_LOCAL_CNI_IPAM_SHA256=${HOST_LOCAL_CNI_IPAM_SHA256:-9bb79b9f269663715f44aea898854df0e0e319a3ba35e4d52db5477cde026e3e} +FLAT_CNI_IPAM_URL=${FLAT_CNI_IPAM_URL:-https://github.com/PastureStack/flat-cni-ipam/releases/download/v0.1.4/flat-cni-ipam-v0.1.4-linux-amd64} +FLAT_CNI_IPAM_SHA256=${FLAT_CNI_IPAM_SHA256:-edbbe0924637381f95b268b259170d80434d3c37e0a5a7519c7e55b2e8957db5} +MOUNT_PROPAGATION_URL=${MOUNT_PROPAGATION_URL:-https://github.com/PastureStack/mount-propagation/releases/download/v1.0.11/mount-propagation-v1.0.11-linux-amd64} +MOUNT_PROPAGATION_SHA256=${MOUNT_PROPAGATION_SHA256:-800bbc2d74c318ccc62d2c3c76846ee1fe307c21daf558c7edcf5de25e393a4b} TAG=${TAG:-${VERSION}} REPO=${REPO:-ghcr.io/${IMAGE_NAMESPACE:-pasturestack}}