Documentation

Packages and Releases

Pahlevan publishes three artifacts per release: a container image, a Helm chart, and a single-file Kubernetes manifest. Release notes live in CHANGELOG.md and on the GitHub releases page.

Container image

The image is published to the GitHub Container Registry:

ghcr.io/obsernetics/pahlevan
Tag Meaning
latest Most recent build of the default branch
v3.6.0 Immutable release tag (recommended for production)
main Rolling tag for the default branch
main-<sha> Per-commit build of the default branch, useful for bisecting
docker pull ghcr.io/obsernetics/pahlevan:v3.6.0

One image ships all three binaries:

  • pahlevan-agent - the privileged DaemonSet that loads and attaches the eBPF programs and enforces locally in the kernel.
  • pahlevan-operator - the leader-elected Deployment that drives policy lifecycle, aggregates status, and owns admission.
  • pahlevan - the CLI.

The runtime layer is distroless: no shell, no package manager, and no userland beyond the binaries themselves.

Verifying the image

docker pull ghcr.io/obsernetics/pahlevan:v3.6.0

# Inspect the manifest, digest, architecture, and labels
docker inspect ghcr.io/obsernetics/pahlevan:v3.6.0

# Digest only (pin this in air-gapped or regulated environments)
docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/obsernetics/pahlevan:v3.6.0

# Confirm the entrypoints exist without a shell in the image
docker run --rm --entrypoint /pahlevan-operator ghcr.io/obsernetics/pahlevan:v3.6.0 --help

Pin by digest rather than by tag when you need a byte-for-byte reproducible deployment:

image: ghcr.io/obsernetics/pahlevan@sha256:<digest>

Helm chart

The chart is served from the project's GitHub Pages site.

helm repo add pahlevan https://obsernetics.github.io/pahlevan/charts
helm repo update
helm install pahlevan pahlevan/pahlevan-operator \
  -n pahlevan-system --create-namespace

Useful overrides:

helm install pahlevan pahlevan/pahlevan-operator \
  -n pahlevan-system --create-namespace \
  --set image.repository=ghcr.io/obsernetics/pahlevan \
  --set image.tag=v3.6.0

Show the resolved values and the available versions:

helm search repo pahlevan --versions
helm show values pahlevan/pahlevan-operator

Uninstall:

helm uninstall pahlevan -n pahlevan-system

CRDs are not removed by helm uninstall. Delete them explicitly if you want a clean slate:

kubectl delete crd pahlevanpolicies.policy.pahlevan.io \
  containerprofiles.policy.pahlevan.io \
  attacksurfaces.policy.pahlevan.io

Raw manifest

For clusters without Helm, each release attaches a rendered install.yaml containing the CRDs, RBAC, the operator Deployment, and the agent DaemonSet:

kubectl apply -f https://github.com/obsernetics/pahlevan/releases/latest/download/install.yaml

Pin to a specific release instead of latest:

kubectl apply -f https://github.com/obsernetics/pahlevan/releases/download/v3.6.0/install.yaml

Review before applying:

curl -sSL https://github.com/obsernetics/pahlevan/releases/download/v3.6.0/install.yaml | less

Verifying the supply chain

The agent runs privileged on every node with CAP_BPF, and install.yaml is applied with cluster-admin-shaped permissions. Neither should be trusted because it happens to sit at a ghcr.io or github.com URL: anyone who obtains a registry or repository token can put something there. Every release is therefore signed, shipped with an SBOM, and accompanied by build provenance.

Signing is keyless. There is no Pahlevan public key to fetch and no long-lived private key in repository secrets - a key like that would be as useful to an attacker as the token it is supposed to defend against, and its signatures would never expire. Instead the release workflow exchanges a short-lived GitHub OIDC token for a Sigstore certificate valid for ten minutes, and that certificate records which workflow in which repository produced the artifact. So verification asks about identity, not possession: was this built by this repository's release workflow?

Install cosign v3.0.6 or newer - that is the version the release workflow signs with - and set the two values that every command below shares:

VERSION=v3.6.0
IDENTITY="https://github.com/obsernetics/pahlevan/.github/workflows/ci.yml@refs/tags/${VERSION}"
ISSUER="https://token.actions.githubusercontent.com"

IDENTITY is the workflow file that does the signing, at the exact tag being released. It is not a name somebody chose; it is what GitHub puts in the certificate, and cosign fails the verification if the artifact was signed by any other workflow, any other repository, or any other tag.

Image signature

cosign verify \
  --certificate-identity "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  ghcr.io/obsernetics/pahlevan:${VERSION}

The image is signed by digest, so this also works - and is the better habit - against a digest pin:

cosign verify \
  --certificate-identity "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  ghcr.io/obsernetics/pahlevan@sha256:<digest>

A successful run prints the verified payload. A tag that was moved onto an image nobody signed fails with no signatures found, and an artifact signed by a different workflow fails with a certificate identity mismatch.

SBOM

The SBOM is SPDX 2.3 JSON, attached to the image as a Sigstore attestation and also attached to the GitHub release as sbom.spdx.json.

Verify and read the copy attached to the image:

cosign verify-attestation --type spdxjson \
  --certificate-identity "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  ghcr.io/obsernetics/pahlevan:${VERSION} \
  | jq -r '.payload | @base64d | fromjson | .predicate' > sbom.spdx.json

It describes the linux/amd64 image. The published image is a multi-platform index, and the SBOM is generated from the variant the release runner resolves, so an arm64 deployment should treat it as the package inventory of the same source build rather than a byte-level statement about the image it pulls.

Build provenance

Provenance is a SLSA statement generated and signed by GitHub from the release run's own metadata: which commit, which workflow, which run built the artifact. It is pushed to the registry alongside the image and recorded for the release assets.

gh attestation verify \
  oci://ghcr.io/obsernetics/pahlevan:${VERSION} \
  --repo obsernetics/pahlevan

gh attestation needs GitHub CLI v2.49 or newer.

Release assets

Each release carries SHA256SUMS covering install.yaml and sbom.spdx.json, plus SHA256SUMS.cosign.bundle, the keyless signature over that checksum file. Verifying the signature and then the checksums covers every asset with one identity check:

base="https://github.com/obsernetics/pahlevan/releases/download/${VERSION}"
curl -sSLO "${base}/install.yaml"
curl -sSLO "${base}/sbom.spdx.json"
curl -sSLO "${base}/SHA256SUMS"
curl -sSLO "${base}/SHA256SUMS.cosign.bundle"

cosign verify-blob \
  --bundle SHA256SUMS.cosign.bundle \
  --certificate-identity "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  SHA256SUMS

sha256sum -c SHA256SUMS

Only apply the manifest once both commands have passed:

kubectl apply -f install.yaml

Provenance for install.yaml can be checked the same way as the image's:

gh attestation verify install.yaml --repo obsernetics/pahlevan

Signatures, SBOMs and provenance are produced by the release workflow, so they exist for releases published after it gained those steps. Tags older than that have no signature to verify, and cosign verify will say so rather than pass quietly.

After installing

Verify both workloads are up, then apply a policy:

kubectl -n pahlevan-system get pods
kubectl get crd | grep pahlevan.io

See quick-start.md for the first PahlevanPolicy, and system-requirements.md plus lsm-support.md for the kernel prerequisites of in-kernel enforcement.