Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a329000
serve: honour Forwarded / X-Forwarded-* and --external-url for downlo…
ephico2real2 Sep 13, 2026
2eb0f6b
review pass: Forwarded beats X-Forwarded-* as documented, a forwarded…
ephico2real2 Sep 13, 2026
bcf06c8
the policy name is a function of the whole selector; the selector's l…
ephico2real2 Sep 13, 2026
17b6a83
page: the flow summary names a workload the way the policy is named —…
ephico2real2 Sep 13, 2026
ea87ee8
E1: cluster-aware selectors for ClusterMesh flows — a peer in another…
ephico2real2 Sep 13, 2026
49f55eb
E3: --dns-visibility / ?dnsVisibility=true — a world policy without n…
ephico2real2 Sep 13, 2026
7f74ca1
E2: --l7 / ?l7=true — HTTP method+path (anchored, escaped) and DNS na…
ephico2real2 Sep 13, 2026
03de96f
E4: review before generate — ?exclude=key=value (repeatable) drops fl…
ephico2real2 Sep 13, 2026
ad7fd2d
E5: cf2cnp merge — fold rules generated from flows into an existing C…
ephico2real2 Sep 13, 2026
be59889
E6: hardening of a shared /generate — --allowed-origins (CORS allow-l…
ephico2real2 Sep 13, 2026
f84c7ce
E10: binary release workflow — on a v* tag, linux/darwin amd64/arm64 …
ephico2real2 Sep 13, 2026
c14604b
E1: cluster-aware selectors for ClusterMesh flows — the peer in anoth…
ephico2real2 Sep 13, 2026
b400270
E6: the pod's policy admits reserved:host too — the kubelet's probes …
ephico2real2 Sep 13, 2026
e8be5ca
E1 (review): the cluster falls back to the flow's io.cilium.k8s.polic…
ephico2real2 Sep 13, 2026
f757a3a
E2 (review): a DNS query that is a resolver search-list expansion (th…
ephico2real2 Sep 13, 2026
aeaf34e
E5 (review): rule equality compares scalars as strings — a hand-writt…
ephico2real2 Sep 13, 2026
f0cdb97
E6 (review): networkPolicy.fromEndpoints defaults to none — the Grafa…
ephico2real2 Sep 13, 2026
3c74e93
E4 (review): the page's peer key follows the parser's label priority …
ephico2real2 Sep 13, 2026
1f0d30f
E4 (review, second pass): a peer is its whole identifying label set —…
ephico2real2 Sep 13, 2026
e9bc684
E6 (review, second pass): the bearer scheme is case-insensitive and t…
ephico2real2 Sep 13, 2026
2847eb8
E2 (review, second pass): L7 records live on the port they were seen …
ephico2real2 Sep 13, 2026
de1866a
merge enh/E1-cluster-aware-selectors into develop (enhancement 001)
ephico2real2 Sep 13, 2026
3f79e5f
merge enh/E2-l7-rules into develop (enhancement 001) — additive confl…
ephico2real2 Sep 13, 2026
4a82eff
merge enh/E3-dns-visibility into develop (enhancement 001) — re-appli…
ephico2real2 Sep 13, 2026
fa8f048
merge enh/E4-exclude-peers into develop (enhancement 001)
ephico2real2 Sep 13, 2026
a47dfe5
merge enh/E5-merge into develop (enhancement 001)
ephico2real2 Sep 13, 2026
1dcf3a5
merge enh/E6-hardening into develop (enhancement 001)
ephico2real2 Sep 13, 2026
e8b357b
merge enh/E10-binary-release into develop (enhancement 001)
ephico2real2 Sep 13, 2026
2dbb866
release 0.6.0: enhancement 001 — cluster-aware selectors (E1), L7 rul…
ephico2real2 Sep 13, 2026
6dc41d7
0.6.1: merge keeps the document as it was (a YAML node edit: key orde…
ephico2real2 Sep 13, 2026
92ad962
0.6.2: the description is written from the policy's own rules — subje…
ephico2real2 Sep 13, 2026
ade2855
0.6.3: the description names its subject the way it names the peers (…
ephico2real2 Sep 13, 2026
4fa3585
E12 forensics: cf2cnp's 20 fields against the 291 schema paths of the…
ephico2real2 Sep 13, 2026
8854edb
forensics: the rendering decision researched (sigs.k8s.io/yaml is alp…
ephico2real2 Sep 13, 2026
d3819ca
0.7.0: cf2cnp consumes Cilium's own policy spec (E12) — the rule type…
ephico2real2 Sep 13, 2026
97630be
E12: validation refusals wrapped in ErrInvalidPolicy and answered wit…
ephico2real2 Sep 13, 2026
7be44d6
ci: the dependency guard names the client machinery itself (two jsonp…
ephico2real2 Sep 13, 2026
4a0aa90
review(ENH-003): Cursor pass 1 — dual-stack world labels, the DNS res…
ephico2real2 Sep 13, 2026
70a9571
review(ENH-003): TestEmbeddedCRDVersionMatchesModule reads go.mod — t…
ephico2real2 Sep 13, 2026
cc01b46
review(ENH-003): Codex pass 1 — validate matches admission (ObjectMet…
ephico2real2 Sep 13, 2026
051419a
review(ENH-003): Cursor pass 2 — the resolver is recognised by the la…
ephico2real2 Sep 13, 2026
3144152
review(ENH-003): Codex pass 2 — merge takes --l7 (the policy-PR templ…
ephico2real2 Sep 13, 2026
f6544d2
CHANGELOG.md — the fork's releases 0.5.0 → 0.7.0, each with its demo …
ephico2real2 Sep 13, 2026
cc99fcb
docs: link the ci-captures running-test screenshots (cf2cnp output, g…
ephico2real2 Sep 15, 2026
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
39 changes: 39 additions & 0 deletions .github/workflows/binary-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Release binaries

# On a v* tag, build the CLI for linux (amd64, arm64) and darwin (amd64, arm64) and attach the archives to
# the GitHub release of that tag — so a pipeline (E10: policy as code) can `curl` the binary instead of building
# it. No goreleaser: four `go build`s and `gh release upload`.
on:
push:
tags: ['v*']

permissions:
contents: write

jobs:
binaries:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build
run: |
set -eu
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p dist
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
os="${target%/*}"; arch="${target#*/}"
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags="-s -w -X main.version=${GITHUB_REF_NAME#v}" -o "dist/cf2cnp" ./cmd/cf2cnp
tar -C dist -czf "dist/cf2cnp_${VERSION}_${os}_${arch}.tar.gz" cf2cnp
rm dist/cf2cnp
done
(cd dist && sha256sum *.tar.gz > "cf2cnp_${VERSION}_checksums.txt")
ls -l dist
- name: Attach to the release of this tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --title "cf2cnp $GITHUB_REF_NAME" --notes "cf2cnp $GITHUB_REF_NAME — see the chart release for Helm."
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
32 changes: 32 additions & 0 deletions .github/workflows/chart-releaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Release Helm Chart (GitHub Pages)

# Publishes helm/cf2cnp as a classic Helm repository on the gh-pages branch (index.yaml) and as a GitHub
# release carrying the chart .tgz, whenever the chart version on the default branch changes. This is the
# fork's release path while the upstream pull request is pending; the OCI push (helm-publish.yml) stays.
on:
push:
branches: [develop, main]
paths: ['helm/**', '.github/workflows/chart-releaser.yml']
workflow_dispatch:

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- uses: azure/setup-helm@v4
- uses: helm/chart-releaser-action@v1.7.0
with:
charts_dir: helm
skip_existing: true
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
96 changes: 96 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CI — the checks a change must pass before it is released. Three layers guard the spec:
# 1. `go test ./...`: unit tests, and TestGolden — the /generate answer for 14 real Hubble captures must be byte for
# byte what 0.6.3 answered (a deliberate output change regenerates the fixtures in the same commit).
# 2. Cilium's own Sanitize() runs inside those tests (internal/policy.Validate) — the agent's admission checks.
# 3. The CRD schema of the pinned Cilium module: the embedded copy must be identical to the module's, and every
# golden output must validate against it offline (kubectl-validate --local-crds, the same mechanism the PoC's
# policy-PR template uses).
# And one line that must never appear: the Kubernetes client machinery in the dependency graph (18 MB → 94 MB).
name: CI

on:
push:
branches: [develop, main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Vet and test (unit, golden, Sanitize) — and say what ran, in the job summary
run: |
go vet ./...
set -o pipefail
go test ./... -count=1 -json | tee /tmp/go-test.json | grep -E '"Action":"(fail)"' || true
python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY"
import json, collections
pk = collections.defaultdict(lambda: collections.Counter()); golden = []; failed = []
for l in open('/tmp/go-test.json'):
try: e = json.loads(l)
except ValueError: continue
if e.get('Test') and e.get('Action') in ('pass', 'fail', 'skip'):
pk[e['Package'].split('/')[-1]][e['Action']] += 1
if e['Action'] == 'fail': failed.append(e['Package'].split('/')[-1] + ' ' + e['Test'])
if e['Test'].startswith('TestGolden/'): golden.append((e['Test'][len('TestGolden/'):], e['Action']))
print('## Tests\n'); print('| package | passed | failed | skipped |'); print('|---|---|---|---|')
for k, c in sorted(pk.items()): print(f"| {k} | {c['pass']} | {c['fail']} | {c['skip']} |")
print(f"\n**{sum(c['pass'] for c in pk.values())} passed, {sum(c['fail'] for c in pk.values())} failed.**")
if failed: print('\nFailed: ' + ', '.join(failed))
print('\n## The 14 golden captures (byte-for-byte against 0.6.3, deliberate changes named in internal/testdata/golden/README.md)\n')
print('| capture | result |'); print('|---|---|')
for n, a in sorted(golden): print(f"| {n} | {'✅ identical' if a == 'pass' else '❌ differs'} |")
PY
! grep -q '"Action":"fail"' /tmp/go-test.json
- name: The embedded CRD is the pinned module's, byte for byte
run: |
set -eu
dir=$(go list -m -f '{{.Dir}}' github.com/cilium/cilium)
cmp internal/crd/ciliumnetworkpolicies.yaml "$dir/pkg/k8s/apis/cilium.io/client/crds/v2/ciliumnetworkpolicies.yaml"
test "$(cat internal/crd/VERSION)" = "$(go list -m -f '{{.Version}}' github.com/cilium/cilium)"
echo "CRD: cilium $(cat internal/crd/VERSION)"
- name: Every golden output validates against the CRD schema (offline)
run: |
set -eu
crds=$(mktemp -d); cp internal/crd/ciliumnetworkpolicies.yaml "$crds/"
echo "## Every golden output against the CRD schema (kubectl-validate, cilium $(cat internal/crd/VERSION))" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"; echo "| file | documents | schema |" >> "$GITHUB_STEP_SUMMARY"; echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY"
for f in internal/testdata/golden/*/expected.yaml; do
n=$(grep -c '^kind: CiliumNetworkPolicy' "$f")
if go run sigs.k8s.io/kubectl-validate@v0.0.4 --local-crds "$crds" "$f"; then r="✅ valid"; else r="❌ INVALID"; fi
echo "| $(basename "$(dirname "$f")") | $n | $r |" >> "$GITHUB_STEP_SUMMARY"
done
- name: The Kubernetes client machinery stays out of the binary
# Two small helpers ride along with Cilium's policy API (k8s.io/client-go/util/jsonpath and its forked template,
# via github.com/cilium/cilium/pkg/command); the line not to cross is the client itself and Cilium's k8s client
# package, which pull the apiserver machinery in (18 MB → 94 MB, docs/CRD-SPEC-FORENSICS.md §3). The size ceiling
# is the real guard.
run: |
set -eu
if go list -deps ./cmd/cf2cnp | grep -E '^k8s.io/client-go/(kubernetes|rest|tools/clientcmd|dynamic|discovery)|^github.com/cilium/cilium/pkg/k8s/client($|/)'; then
echo "the client packages are imported — see docs/CRD-SPEC-FORENSICS.md §3"; exit 1
fi
go build -trimpath -ldflags="-s -w" -o /tmp/cf2cnp-size ./cmd/cf2cnp
size=$(stat -c %s /tmp/cf2cnp-size)
echo "binary: $size bytes"
test "$size" -lt 40000000 || { echo "the binary passed 40 MB: something heavy joined the graph"; exit 1; }
- name: The binary names the spec it supports, and validates every golden the way the agent would
run: |
go build -ldflags="-X main.version=ci" -o /tmp/cf2cnp ./cmd/cf2cnp
/tmp/cf2cnp version
/tmp/cf2cnp validate internal/testdata/golden/*/expected.yaml | tee /tmp/validate.txt
{ echo "## cf2cnp validate (Sanitize, ObjectMeta, the protocol enum, apiVersion/kind, nodeSelector, specs)"; echo; echo '```'; /tmp/cf2cnp version; echo "$(grep -c ' ok$' /tmp/validate.txt) documents accepted, $(grep -vc ' ok$' /tmp/validate.txt) refused"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
- name: The policy-PR template's calls work (merge takes --l7)
run: |
set -eu
out=$(mktemp -d)
/tmp/cf2cnp generate --input internal/testdata/l7-http-request.json --output "$out"
existing=$(find "$out" -name '*.yaml' | head -1)
/tmp/cf2cnp merge --l7 --existing "$existing" --input internal/testdata/l7-http-request.json --output "$out/merged.yaml"
grep -q 'http:' "$out/merged.yaml"
2 changes: 2 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || github.sha }}

73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Changelog — the fork's releases

Every release below was built for a Cilium 1.20.1 ClusterMesh lab and proven there before it was tagged; each row
names the demo of [cilium-implementation-poc](https://github.com/ephico2real2/cilium-implementation-poc) that
exercises it (its README quotes the recorded transcript, the Hubble verdicts and the screenshots), and the review
record where two independent reviewers (Cursor, Codex) went through the claims. The CI of this repository prints a
test summary on every push (the workflow's job summary): the unit tests by package, the 14 golden captures that must
answer byte for byte, the CRD check, and `cf2cnp validate` over every golden output.

## 0.7.0 — 2026-09-13 · Cilium's own policy types, validated the way the agent validates

- **The policy document is `github.com/cilium/cilium/pkg/policy/api` at the pinned Cilium version (v1.20.1).** Every
field of the CiliumNetworkPolicy spec exists (the 0.6.x hand-written structs modelled 20 of the schema's 291
paths); rendering keeps cf2cnp's field order through a precedence table (the kustomize/kyaml pattern); the CRD of
the same version is embedded and CI compares it byte for byte with the module's; `cf2cnp version` prints the spec
version the build supports. Output for the 14 recorded captures is byte-identical to 0.6.3 except two deliberate
changes named in `internal/testdata/golden/README.md`.
- **Validation:** Cilium's own `Sanitize()` on every generated policy (on a copy — the file stays as a user writes
it), plus the object's checks: ObjectMeta (DNS-1123 names, label keys), the CRD's protocol enum, `apiVersion` and
`kind`, no `nodeSelector` on a namespaced policy, no namespace on a clusterwide one, `specs` rule by rule. A
refusal is a 400 on the API and exit 1 on the CLI. New: `cf2cnp validate <files…>`.
- **`fromCIDR` for external sources:** an ingress flow from `reserved:world` (or `world-ipv4` / `world-ipv6` on a
dual-stack cluster) with an address becomes `fromCIDR: [<addr>/32]` (`/128` IPv6), what the receiver saw, instead
of `fromEntities: [world]`.
- **The DNS resolver rule from the flows:** the rule `toFQDNs` and `--dns-visibility` need names the resolver the
workload actually asked — kube-dns by `k8s-app`, the CoreDNS Helm chart by `app.kubernetes.io/name`, NodeLocal
DNSCache, OpenShift's `openshift-dns` on 5353 without a label (Cilium's DNS guide) — on protocol `ANY`, as Cilium's
examples write it (a truncated UDP answer retries over TCP). Profiles `--dns-profile auto|kubernetes|openshift`,
any resolver with `--dns-resolver <ns>[/<label>=<v>]:<port>[/<proto>]`, the same on the API (`?dnsProfile=`,
`?dnsResolver=`), the page, and as a cluster-wide default on `serve` and in the chart (`dns.profile`,
`dns.resolver`, `networkPolicy.dnsEgress`).
- **`merge --l7`**, the flag the policy-PR template already passed.
- **Chart 0.7.0:** the DNS values above; the chart's own policy's DNS egress follows the profile and refuses to
render when a custom resolver is named without a matching `dnsEgress`.
- Fixed: an IPv6 world peer got a `/32`; a dual-stack world peer was neither world nor an entity (an egress to it
produced no peer at all); the hard-coded kube-dns `53/UDP` rule selected nothing on OpenShift and with the CoreDNS
Helm chart and denied the TCP retry; a `specs`-only document was accepted as empty and merged into silently.
- Tests: 104 (46 at 0.6.3); the goldens; `kubectl-validate` over every golden in CI; the dependency guard (no
Kubernetes client machinery, a 40 MB ceiling). Reviews: two passes,
[REVIEW_ENH-003](https://github.com/ephico2real2/cilium-implementation-poc/blob/main/docs/REVIEW_ENH-003.md);
forensics: [docs/CRD-SPEC-FORENSICS.md](docs/CRD-SPEC-FORENSICS.md). Demos: 35 (regenerated), the policy-PR
template on 0.7.0.

## 0.6.3 — 2026-09-13 · descriptions from the rules

- The description names its subject the way it names the peers (name / component / instance from the selector), the
peer's namespace or cluster when it differs, the ports and L7 rules; cut with "and N more" past six peers
(0.6.2 wrote it from the rules; 0.6.3 named the subject). Demo 35 Part 4.

## 0.6.1 — 2026-09-13 · merge keeps the file as it was

- `merge` edits the YAML node tree: key order, comments, quoting and indentation kept, so a pull request shows the
added rule only (0.6.0 re-serialised the whole file). One kube-dns rule when a pod's own lookups plus the
DNS-visibility rule met. Demo 32 Part 2b, demo 31 Part 1.

## 0.6.0 — 2026-09-12 · enhancement 001, the enterprise set

- Cluster-aware selectors for ClusterMesh flows (`io.cilium.k8s.policy.cluster`, demo 29); layer-7 rules from flows
that carry them, one port rule per port (demo 30); DNS visibility on demand (demo 31); review before generating
with `?exclude=` and the page's peer checklist (demo 32); `cf2cnp merge` (demo 32); CORS allow-list, a bearer
token, a CiliumNetworkPolicy for the pod (demo 33); release binaries for every `v*` tag (demo 32 Part 0).
Review: [REVIEW_ENH-001](https://github.com/ephico2real2/cilium-implementation-poc/blob/main/docs/REVIEW_ENH-001.md).

## 0.5.1 — 2026-09-12

- A `{}` body is refused; the proxy's `Forwarded` host is sanitised (review findings).

## 0.5.0 — 2026-09-12 · the first fork release

- `download_url` correct behind a TLS-terminating proxy (`--external-url`, `Forwarded`, `X-Forwarded-*` — the bug
of upstream #2); many flows per request merged per workload; policy names that cannot collide (a function of the
whole selector); `app.kubernetes.io/managed-by: cf2cnp` and the selector's labels on every policy; `?name=`.
Demos 26 and 27.
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
FROM golang:1.25-alpine AS builder
FROM golang:1.26-alpine AS builder

WORKDIR /app

Expand All @@ -11,7 +11,8 @@ RUN go mod download
COPY . .

# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o cf2cnp ./cmd/cf2cnp
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X main.version=${VERSION}" -o cf2cnp ./cmd/cf2cnp

# Runtime stage - using distroless for minimal attack surface
FROM gcr.io/distroless/static-debian12:nonroot
Expand Down
Loading