Building and Tagging the OCI artefact

Vendor publishes artefact with BoB

  • extract a json conformant with predicate format from the ebpf-recording
  • if this is a Container: pretend use docker --bob=true build ...
  • if this is another type of artefact like helm: have a means to append the predicate

No well-established API should change, else people are not going to use it.


1) Predicate Format

Highlevel:

Header: 
Executables: Paths and arguments of executables that are expected to run.
Network Connections: Expected network connections (IP addresses, DNS names, ports, protocols).
File Access: Expected file access patterns (paths, read/write). 
System Calls: Expected system calls.
Capabilities: Expected Linux capabilities. 
Image information: Image ID, Image Tag.

Header (in the assumption we can use kubescape directly):

apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ApplicationProfile
metadata:
  annotations:
    kubescape.io/completion: complete
    kubescape.io/instance-id: apiVersion-apps/v1/namespace-$values.namespace/kind-$values.camelinstancekind/name-$values.name-$values.templatehash
    kubescape.io/status: completed
    kubescape.io/wlid: wlid://cluster-$values.clustername/namespace-$values.namespace/$values.workloadkind-$values.name
  labels:
    kubescape.io/workload-api-group: apps
    kubescape.io/workload-api-version: v1
    kubescape.io/workload-kind: $values.camelworkloadkind
    kubescape.io/workload-name: $values.name
    kubescape.io/workload-namespace: $values.namespace
  name: $values.instancekind-$values.name-$values.templatehash
  namespace: $values.namespace
  resourceVersion: "1"

Ideal Header

apiVersion: 
kind: BillOfBehavior
metadata:
  annotations:
  labels:
  name:
  namespace:

2) Building the BoB including a test

Lets take our ApplicationProfile and create a very simply bob

git clone https://github.com/k8sstormcenter/honeycluster.git
cd honeycluster
git checkout 162-write-bob-testscript-for-anyone-to-contribute-a-bob-for-the-pingapps
cd traces/kubescape-verify/attacks/bob
ls
cat bob.values

First, as a vendor, I need to choose what will be substitutable by a customer and what tests I can give to the customer.

The content of the bob is:

  • bob.yaml
  • bob.values
  • bob.test

Here and Back Again

A bob's tale:

sudo apt -y install python3-yaml
python3 bob.py

Well, rather minimalistic, but a sketch how to extract the values and substitute them back in:

==> bob_generated.values <==
namespace=default
name=webapp
clustername=honeycluster
templatehash=d87cdd796
workloadkind=deployment
camelworkloadkind=Deployment
instancekind=replicaset
camelinstancekind=ApplicationProfile

==> bob_generated.yaml <==
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ApplicationProfile
metadata:
  annotations:
    kubescape.io/completion: complete
    kubescape.io/instance-id: apiVersion-apps/v1/namespace-$values.namespace/kind-$values.camelinstancekind/name-$values.name-$values.templatehash
    kubescape.io/resource-size: '245'
    kubescape.io/status: completed
    kubescape.io/wlid: wlid://cluster-$values.clustername/namespace-$values.namespace/$values.workloadkind-$values.name
  creationTimestamp: '2025-05-12T12:45:42Z'

==> processed_bob_generated.yaml <==
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ApplicationProfile
metadata:
  annotations:
    kubescape.io/completion: complete
    kubescape.io/instance-id: apiVersion-apps/v1/namespace-default/kind-ApplicationProfile/name-webapp-d87cdd796
    kubescape.io/resource-size: '245'
    kubescape.io/status: completed
    kubescape.io/wlid: wlid://cluster-honeycluster/namespace-default/deployment-webapp
  creationTimestamp: '2025-05-12T12:45:42Z'

TODO: during parsing, remove labels and annotations that include timestamps

The template_hash will actually remain the same if the pod spec on customer side is identical. Which can be the case, but, in general, is not.

Now, we have simply substituted out the CRD header so we can transfer it . I suggest, we have variables and defaults with a well-defined precedence.

Ideally, on the other side (the customer side), we will have a composable way to unionize the runtimeAspects (making multiple BoBs additive ). So, I suspect the bobctl will need to be able to merge bob.yaml files.

Later work: we need to create functions to replace sections. I see mostly networking being cumbersome. For example; for the following scenario:

    endpoints:
    - direction: inbound
      endpoint: :8080/ping.php
      headers:
        Host:
        - localhost:8080
      internal: false
      methods:
      - GET

it is not currently clear to me, if this fails open or fails closed. Given, that port-forwarding/api-gw/ingress will be vastly different, the endpoint section should maybe be discovered. But, again, be added in. It is unlikely we can pre-determine it.

The Test

Lets start simple and add to our deployment.yaml a second deployment that executes our curl given that the port is different, this might already trigger an alert.

==> deployment.yaml <==
...
        app: testapp
        kubescape.io/ignore: "true"
    spec:
      containers:
      - name: curl-container
        image: curlimages/curl:latest
        command: ["/bin/sh", "-c"]
        args:
        - |
          for i in $(seq 1 20); do
            curl webapp.default.svc.cluster.local:8080/ping.php?ip=172.16.0.2
            echo
          done
...

Result

We now have

  • bob.yaml # the parametrized ApplicationProfile
  • bob.values # the Parameters
  • bob.test # sample Deployment incl a LoadTest

We will now pretend a helm like management of these artefacts: next step -> stuff them into an OCI registry

Publishing the artefact

Idea: WIP

  • use co-sign or oras
  • determine choice of key (can we use keyless?)
  • attestation: choose predicate type
  • verification: can tools like OPA verfiy predicate-type= bob.spdx.json
  • transparency: do we need public signing records, like recor?

Diagram of the publication of a BoB

Huge thanks to the OpenSource communities!

Sketch

Authoring artifacts - Ideas for bobctl

On the client side, bobctl provides commands to bundle and sign security artifacts into OCI artifacts and pushing these artifact to container registries.

The bobctl CLI commands for managing BoB artifacts are:

  • bobctl push artifact
  • bobctl pull artifact
  • bobctl tag artifact
  • bobctl list artifact
  • bobctl describe artifact
    • List bob manifest
    • Meta informations (annotations)
  • bobctl link artifact <bob artifact> <image artifact>
    • Reference which image
  • bobctl create secret

The OCI artifacts produced with bobctl push artifact have the following custom media types:

Question: C to P: Do we need that many different types?

  • artifact media type application/vnd.oci.image.manifest.v1+json
  • config media type application/vnd.k8sstormcenter.bob.config.v1+json
  • content media type application/vnd.k8sstormcenter.bob.content.v1.tar+gzip

This shows how the BoB OCI artifact is structured:

  • A manifest links to a config object and one or more layers
  • The config describes metadata (here, BoB-specific).
  • Each layer contains actual content (e.g., kustomize YAML files, BoB manifest (values + yaml), BoB test).

    Question: C to P: kustomize doesnt work as far as I tested, helm could work

  • An annotation provide extra metadata for humans and tools.
  • A subject specifies a descriptor of another manifest to attach a signature (cosign or notary).

Question: C to P: I dont understand this diagram

WIP: Definition of BoB Artifact Manifest

To support platform-specific BoB (Bill of Behaviour) artifacts within a single OCI-compliant image, the OCI Image Index (a.k.a. manifest list) can be leveraged to define a set of platform-targeted manifests. Each manifest can correspond to a specific OS/architecture combination, allowing fine-grained association of artifacts per platform. Furthermore, the config object within each image manifest can be utilized to transport structured metadata (e.g., media type, labels, annotations, or custom JSON data). This mechanism enables embedding auxiliary information (e.g., BoB version, build metadata, artifact links) directly within the OCI image specification in a platform-aware manner. The official OCI Image Annotations specification provides a standardized way to embed metadata within OCI image manifests and config objects.

NOTE: At present, the config object in the OCI image manifest is mainly utilized to support image integrity checks, serving as part of the content-addressable structure of the image.

Config needs a hash to test integrity of the content of bob artifact folder .rootfs.diff_ids[0].

{
  "created": "2025-05-04T00:42:21Z",
  "config": {
  }
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:9803b474e9216bc407ccec87418061bf084954d9752136e0b2d12c328fe5b7ba"
    ]
  },
}

NOTE: Add support for different architectures and os's needed? CR: Multi-Arch definitely required

The checksum calculated of the layer.tar.gz:

computed_digest = sha256(layer.tar.gz)

FORMAT: application/vnd.k8sstormcenter.bob.content.v1.tar+gzip

bobctl push artifact oci://registry.iximiuz.com/k8sstormcenter/manifests/honey:$(git rev-parse --short HEAD) \
    --path="./traces/kubescape-verify/attacks/bob" \
    --source="$(git config --get remote.origin.url)" \
    --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)"

bobctl has the ability to convert the bob.yaml together with the bob.values and the deployment.yaml into a mini helm-chart WIP CR I didnt yet write it as HELM, but the test function woudl make a lot of sense. The most crucial thing we need are loop-like structures (either list or range) that can deal with network blocks. I suspect that on customer-side, the bob.yaml will need to be modified quite a bit.

Reference BoB Artifact to an OCI Image

Key metadata associated with OCI images that enhance security and traceability, such as SBOMs, vulnerability attestations, signatures (e.g., Cosign), and provenance data (e.g., SLSA).

WIP: Useful Security References from OCI Image Artifacts.

  • OCI Image: Central artifact being published.
  • SBOM: Linked via OCI manifest annotations (typically as attestations or referrers). (NOT IN SCOPE)
  • CVE Attestation: Security scan results, often attached via in-toto. (NOT IN SCOPE)
  • Cosign Signature: Verifies integrity and authorship of the image.
  • Bob Artifact: A custom reference (e.g., bob://...) pointing to further metadata or build context.
  • Build Provenance: Describes the build process, tools, and environment used to produce the artifact, following the SLAS -framework. (NOT IN SCOPE)

Note: All artifacts that are references form the OCI Image should be signed! CR we must also allow unsigned BoBs for people to get started.

POC: Create and bob artifact manually

Prepare

mkdir ~/downloads
cd ~/downloads
curl -s https://api.github.com/repos/oras-project/oras/releases/latest \
| grep "browser_download_url.*linux_amd64.tar.gz" \
| cut -d '"' -f 4 \
| wget -i -
tar -xvzf oras_*_linux_amd64.tar.gz
sudo mv oras /usr/local/bin/
cd $HOME
oras version
cd $HOME/honeycluster
mkdir -p bob-artifact 
cd bob-artifact
# upload applicationprofile
cp /home/laborant/honeycluster/traces/kubescape-verify/attacks/bob/bob_generated.yaml .
cp /home/laborant/honeycluster/traces/kubescape-verify/attacks/bob/bob.test .
cp /home/laborant/honeycluster/traces/kubescape-verify/attacks/bob/bob_generated.values .

# Add metafile with License and author? CR: so you mean to state who wrote the bob, rather than who wrote the software?
# https://specs.opencontainers.org/image-spec/annotations/
# https://spdx.org/licenses/
# cat >manifest.yaml <<EOF
# apiVersion: k8sstormcenter.io/v1alpha1
# kind: BoBManifest
# spec
#   author: Peter Rossbach
#   licenses: Apache-2.0
# EOF


# package with tar und zip
BOB_PACKAGE=app-profile-webapp.tar.gz 
tar czf ../${BOB_PACKAGE} .
cd ..

ARTIFACT_PACKAGE=registry.iximiuz.com/k8sstormcenter/manifest/honey

# create Config checksum
cat >config.json <<EOF
{
  "created": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:$(sha256sum ${BOB_PACKAGE} | cut -d' ' -f1)"
    ]
  },
}
EOF

# review manifest

CONFIG_DIGEST="sha256:$(sha256sum config.json | cut -d' ' -f1)"
CONFIG_SIZE=$(wc -c < config.json)

# Digest for layer
LAYER_DIGEST="sha256:$(sha256sum ${BOB_PACKAGE} | cut -d' ' -f1)"
LAYER_SIZE=$(wc -c < ${BOB_PACKAGE})

VERSION=$((git describe --tags --abbrev=0 2>/dev/null || echo "main") && exit 0)
COMMIT=$(git rev-parse HEAD)

# echo "\"org.opencontainers.image.revision\": \"$VERSION/$COMMIT\""

# How you can manually create a manifest?
cat >manifest.json <<EOF
{
    "schemaVersion": 2,
    "mediaType": "application/vnd.oci.image.manifest.v1+json",
    "config": {
        "mediaType": "application/vnd.k8sstormcenter.bob.config.v1+json",
        "size": $CONFIG_SIZE,
        "digest": "$CONFIG_DIGEST"
    },
    "layers": [
        {
        "mediaType": "application/vnd.k8sstormcenter.bob.content.v1.tar+gzip",
        "size": $LAYER_SIZE,
        "digest": "$LAYER_DIGEST"
        }
    ],
    "annotations": {
        "org.opencontainers.image.created": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
        "org.opencontainers.image.revision": "$VERSION/$COMMIT",
        "org.opencontainers.image.source": "https://github.com/k8sstormcenter/honeycluster"
    }
}
EOF

# https://oras.land/docs/how_to_guides/manifest_annotations/


# but better options is:

cat >annotations.json <<EOF
{
  "\$manifest": {
     "org.opencontainers.image.created": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
     "org.opencontainers.image.revision": "$VERSION/$COMMIT",
     "org.opencontainers.image.source": "https://github.com/k8sstormcenter/honeycluster"
  }
}
EOF

# upload image manifest
# upload blobs layer tar
# upload config manifest

oras push --annotation-file annotations.json \
  ${ARTIFACT_PACKAGE}:latest \
  --config config.json:application/vnd.k8sstormcenter.bob.config.v1+json \
  ${BOB_PACKAGE}:application/vnd.k8sstormcenter.bob.content.v1.tar+gzip

__NOTE__ CR: at this point we have a bit of a chicken egg problem: cause inside the bob, there is the SHA
and but to get the SHA we need to push it? I think, we have at least two hashes here.
So: we first push the image (multiarch) so it gets a unique SHA and then we use that and push the signed metastuff?

# check upload
oras discover ${ARTIFACT_PACKAGE}:latest
registry.iximiuz.com/k8sstormcenter/manifest/honey@sha256:9fafbb7da542abf2e1f04832b5cd39a3225d7381328e144401a7db230cc1dc63

oras manifest fetch ${ARTIFACT_PACKAGE}:latest --pretty  

# fetch layer
ARTIFACT_BLOB=${ARTIFACT_PACKAGE}@$(oras manifest fetch ${ARTIFACT_PACKAGE}:latest --pretty | jq -r .layers[0].digest)
oras blob fetch --output - \
  $ARTIFACT_BLOB \
  >registry-${BOB_PACKAGE}

# happyness check
WORKDIR=$(mktemp -d)
IMG1_DIR="$WORKDIR/img1"
IMG2_DIR="$WORKDIR/img2"

mkdir -p "$IMG1_DIR" "$IMG2_DIR"

# Unpack image layers
mkdir -p $IMG1_DIR/unpacked $IMG2_DIR/unpacked

tar -xzf ${BOB_PACKAGE} -C $IMG1_DIR/unpacked
tar -xzf registry-${BOB_PACKAGE} -C $IMG2_DIR/unpacked

# Diff the contents
diff -urN $IMG1_DIR/unpacked $IMG2_DIR/unpacked > $WORKDIR/diff.txt

echo "Binary diff stored at: $WORKDIR/diff.txt"

rmdir $WORKDIR

# fetch manifest + config

oras manifest fetch-config ${ARTIFACT_PACKAGE}:latest

this should be moved to unit-1?

POC: reference BoB Artifact to OCI Image

Let's dive into OCI (Open Container Initiative) referrers, especially in the context of the OCI Artifacts v1.1 spec which introduced the referrers API. OCI referrers are a standardized way to attach related artifacts (like SBOMs, signatures, or policies) to an OCI image or artifact in a registry — without modifying the original image.

  • OCI Referrers let you attach related artifacts to an image without modifying it.
  • They're discoverable via a referrers API.
  • They’re a key building block for modern supply chain tooling — signatures, SBOMs, provenance.
# 0. Prepare
mkdir -p  bob-artifact 
cd bob-artifact

# 1. Define the base image you're referencing
IMAGE=ghcr.io/k8sstormcenter/webapp@sha256:e323014ec9befb76bc551f8cc3bf158120150e2e277bae11844c2da6c56c0a2b

IMAGE=k8sstormcenter/webapp@sha256:e323014ec9befb76bc551f8cc3bf158120150e2e277bae11844c2da6c56c0a2b

docker run -d \
  --name zot \
  -p 5000:5000 \
  ghcr.io/project-zot/zot:v2.1.2

# 2. Download to local registry
docker pull ghcr.io/$IMAGE
#docker tag ghcr.io/$IMAGE \
#  registry.iximiuz.com/k8sstormcenter/webapp:latest
#docker push registry.iximiuz.com/k8sstormcenter/webapp:latest

docker tag ghcr.io/$IMAGE \
  127.0.0.1:5000/k8sstormcenter/webapp:latest
docker push 127.0.0.1:5000/k8sstormcenter/webapp:latest

# 3. Prepare the Bob artifact (e.g., JSON file)
#echo '{ "build": "bob", "arch": "amd64", "url": "https://bob.artifact/reference" }' > bob-artifact.json

#cp kustomize/app-profile-webapp.yaml bob-artifact.yaml CR TODO
# 4. Attach the artifact using oras (as an OCI artifact referrer)

# zot
oras attach 127.0.0.1:5000/k8sstormcenter/webapp:latest --plain-http  \
  --artifact-type "application/vnd.bob.artifact+yaml" \
  --annotation "org.opencontainers.artifact.description=BoB Security Policy" \
   \
 bob_generated.yaml:application/yaml \
 bob.test:application/yaml \
 bob_generated.values:application/text 

# ixi registry
oras attach registry.iximiuz.com/k8sstormcenter/webapp:latest  \
  --artifact-type "application/vnd.bob.artifact+yaml" \
  --annotation "org.opencontainers.artifact.description=BoB Security Policy" \
   \
  bob-artifact.yaml:application/yaml
 Exists    application/vnd.oci.empty.v1+json                   2/2  B 100.00%     0s
  └─ sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
 Uploaded  bob.test                                      1.62/1.62 KB 100.00%    5ms
  └─ sha256:16b90fdcca596952bfb053b5454eef720a2dde8b7278f28eff43520d3cb6b60e
 Uploaded  bob_generated.values                            192/192  B 100.00%    6ms
  └─ sha256:6dabf2e0e01a3eb39fbea2df74445842e3a73d7429f27fb7339b70e890541e3e
 Exists    bob_generated.yaml                            18.2/18.2 KB 100.00%     0s
  └─ sha256:8f956060effdd6df20c9c49b5f6f4c80e3b7efdf7c49cb69fdbd403530c1a4c0
 Uploaded  application/vnd.oci.image.manifest.v1+json    1.17/1.17 KB 100.00%    5ms
  └─ sha256:52456e38289837346a7d6eb4647fb1efd1b7efb2000e3f64a9ddab049f946b4e
Attached to [registry] registry.iximiuz.com/k8sstormcenter/webapp@sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e
Digest: sha256:191a03218f7f1200232265b7cde605daf6070f0d3e873e34f6ed9399d62d0322

What's happening here:

  • A new artifact with a reference is create and push to the registry
  • --artifact-type: Custom media type for the artifact (application/vnd.bob.artifact+json).
  • --plain-http: Optional if use local registry with plain http.
  • registry.iximiuz.com/k8sstormcenter/webapp:latest: A new OCI reference storing the metadata.
  • bob-artifact.yaml:application/yaml: Links this artifact as a referrer to the OCI image.

This attaches your artifact in a way that tools like oras discover or cosign can find it under the base image.

Discovering referrers:

oras discover ghcr.io/$IMAGE          
ghcr.io/k8sstormcenter/webapp@sha256:e323014ec9befb76bc551f8cc3bf158120150e2e277bae11844c2da6c56c0a2b

# discover referrer
# zot registry
oras discover 127.0.0.1:5000/k8sstormcenter/webapp:latest --plain-http
127.0.0.1:5000/k8sstormcenter/webapp@sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e
└── application/vnd.bob.artifact+yaml
    ├── sha256:ff905f36f27f3f82e62240b4d0ca4a7c22cb0dff3bc5039e9302d5591f628bfc
    ├── sha256:d61f3da3f2406061469718b3eb99b30b6a393673047ee4a493d4d94dc97b36a8
    ├── sha256:52456e38289837346a7d6eb4647fb1efd1b7efb2000e3f64a9ddab049f946b4e
    └── sha256:4c2e430fc6025297ac03653b761ee6f4f1d8612cd081bbeb986e0bd0e5776072

# # ixi registry
# oras discover registry.iximiuz.com/k8sstormcenter/webapp:latest 
# registry.iximiuz.com/k8sstormcenter/webapp@sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e
# └── application/vnd.bob.artifact+yaml
#     └── sha256:191a03218f7f1200232265b7cde605daf6070f0d3e873e34f6ed9399d62d0322

You’ll see your Bob artifact listed as a referrer to that image.

What’s happening behind the scenes?

The oras attach command:

  • Adds the artifact (e.g.,bob-artifact.json) as a new OCI artifact.
  • Links it to the registry.iximiuz.com/k8sstormcenter/webapp:latest image via a special referrers API.
  • Creates metadata (e.g., media type, annotations) so tools can discover and fetch these referrers later.

Show the manifest from the new bob artifact to refer the image.

 GET /v2/<name>/referrers/<digest>?artifactType=application/vnd.bob.artifact+yaml

bob-artifact.yaml is the artifact that is referenced to the image.

# # ixi registry
# # request bob-artifact
# oras discover  \
# --artifact-type application/yaml \
# registry.iximiuz.com/k8sstormcenter/webapp@sha256:191a03218f7f1200232265b7cde605daf6070f0d3e873e34f6ed9399d62d0322

# zot registry
oras discover --plain-http --artifact-type application/yaml 127.0.0.1:5000/k8sstormcenter/webapp:latest
127.0.0.1:5000/k8sstormcenter/webapp@sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e

# # request image
# # ixi registry
# curl -s -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
# https://registry.iximiuz.com/v2/k8sstormcenter/webapp/manifests/sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e |jq .

# zot registry
curl -s -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
http://127.0.0.1:5000/v2/k8sstormcenter/webapp/manifests/sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e |jq .

# ixi registry
curl -s -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
https://registry.iximiuz.com/v2/k8sstormcenter/webapp/manifests/sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e |jq .
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:191a03218f7f1200232265b7cde605daf6070f0d3e873e34f6ed9399d62d0322",
      "size": 813,
      "annotations": {
        "org.opencontainers.artifact.description": "BoB Security Policy",
        "org.opencontainers.image.created": "2025-05-09T10:40:22Z"
      },
      "artifactType": "application/vnd.bob.artifact+yaml"
    }
  ]
}


# only zot registry support referrers
curl -sL -H "Accept: application/json" \
 https://registry.iximiuz.com/k8sstormcenter/webapp/referrers/sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e?artifactType=application/vnd.bob.artifact+yaml
# 404


# I like it :)
curl -sL -H "Accept: application/vnd.oci.image.index.v1+json"   \
 127.0.0.1:5000/v2/k8sstormcenter/webapp/referrers/sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e ?artifactType=application/vnd.bob.artifact+yaml \
  | jq .


oras manifest fetch --plain-http 127.0.0.1:5000/k8sstormcenter/webapp@sha256:dab18b4d7784d33644f02f88e4df384c37cfc8a4c038e345af24d6d666d12e0e
# no subject!

# my wish
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.bob.artifact+json",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2,
    "data": "e30="
  },
  "layers": [
    {
      "mediaType": "application/yaml",
      "digest": "sha256:cd661a568dbe340f8ed7a542e0ba24bc47ff0106d2c219dc0ec93cc3a01ab7ea",
      "size": 77,
      "annotations": {
        "org.opencontainers.image.title": "bob-artifact.yaml"
      }
    }
  ],
  "subject": {
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "digest": "sha256:a46e19e923c1aebefbc6c561749230ff6f097ad89faee7c4206d916fab498119",
    "size": 739
  },
  "annotations": {
    "org.opencontainers.artifact.description": "BoB Security Policy",
    "org.opencontainers.image.created": "2025-04-19T08:12:58Z"
  }
}

Show the OCI image manifest!

NOTE: No subject refer the bob artifact!

oras manifest fetch $IMAGE --pretty --plain-http 
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 2376,
    "digest": "sha256:3594acf667b2bc448df45921c993042890042d7ecc53d5e70f0cc2da59322687"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 2207025,
      "digest": "sha256:cd784148e3483c2c86c50a48e535302ab0288bebd587accf40b714fffd0646b3"
    },
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 3030947,
      "digest": "sha256:5dd70daad9005c8316e9a6646645e53cd5748c9fe0cf4dd00ce04835d6b839d4"
    }
  ]
}

Signing the artefact

Authentication

Bob Artifacts works with Docker Hub, GitHub and GitLab Container Registry, ACR, ECR, GAR, Harbor, self-hosted Docker Registry and any other registry which supports custom OCI media types.

For authentication purposes, the bobctl <verb> artifact commands are using the ~/.docker/config.json config file and the Docker credential helpers.

NOTE: Login to GitHub Container Registry example:

echo ${GITHUB_PAT} | docker login ghcr.io -u ${GITHUB_USER} --password-stdin

Note: We use local lab registry registry.iximiuz.com!

To pull artifacts in Kubernetes clusters, BoB Operator can authenticate to container registries using image pull secrets or IAM role bindings to the bob-controller service account.

Generate an image pull secret for GitHub Container Registry example:

bobctl create secret oci ghcr-auth \
  --url=ghcr.io \
  --username=bob \
  --password=${GITHUB_PAT}

Then reference the secret in the OCIRepository with:

apiVersion: source.k8sstormcenter.io/v1alpha1
kind: OCIRepository
metadata:
  name: k8sstormcenter-honey
  namespace: default
spec:
  interval: 5m
  url: oci://registry.iximiuz.com/k8sstormcenter/manifests/honey
  provider: generic
  secretRef:
    name: ghcr-auth

Contextual Authorization

When running BoB on managed Kubernetes clusters like EKS, AKS or GKE, you can set the provider field to azure, aws or gcp and BoB will use the Kubernetes node credentials or an IAM Role binding to pull artifacts without needing an image pull secret.

For more details on how to setup contextual authorization for Azure, AWS and Google Cloud please see:

Signing and verification

BoB comes with support for verifying OCI artifacts signed with Sigstore Cosign or Notaryproject notation.

To secure your delivery pipeline, you can sign the artifacts and configure Flux to verify the artifacts' signatures before they are downloaded and reconciled in production.

Cosign Workflow example

COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d '"' -f4)
curl -sLo cosign "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64"
chmod +x cosign
sudo mv cosign /usr/local/bin/

Generate a Cosign key-pair and create a Kubernetes secret with the public key:

cosign version
cosign generate-key-pair

kubectl -n default create secret generic cosign-pub \
  --from-file=cosign.pub=cosign.pub

Push and sign the artifact using the Cosign private key:

bobctl push artifact oci://registry.iximiuz.com/k8sstormcenter/manifests/honey:$(git tag --points-at HEAD) \
    --path="./kustomize" \
    --source="$(git config --get remote.origin.url)" \
    --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)"

cosign sign --key=cosign.key registry.iximiuz.com//k8sstormcenter/manifests/honey:$(git tag --points-at HEAD)

Configure k8sstormcenter to verify the artifacts using the Cosign public key from the Kubernetes secret:

apiVersion: source.k8sstormcenter.io/v1alpha1
kind: OCIRepository
metadata:
  name: k8sstormcenter-honey
  namespace: default
spec:
  interval: 5m
  url: oci://registry.iximiuz.com/k8sstormcenter/manifests/honey
  ref:
    semver: "*"
  verify:
    provider: cosign
    secretRef:
      name: cosign-pub

For publicly available OCI artifacts, which are signed using the Cosign Keyless method, you can enable the verification by omitting the .verify.secretRef field.

Note that keyless verification is an experimental feature, using custom root CAs or self-hosted Rekor instances are not currently supported.

Notary Workflow example

Generate a local signing key pair:

openssl req -x509 -sha256 -nodes -newkey rsa:2048 \
-keyout <name>.key \
-out <name>.crt \
-days 365 \
-subj "/C=DE/ST=NRW/L=Bochum/O=Notary/CN=<name>" \
-addext "basicConstraints=CA:false" \
-addext "keyUsage=critical,digitalSignature" \
-addext "extendedKeyUsage=codeSigning"

Configure notation to use the local key:

cat <<EOF > ~/.config/notation/signingkeys.json
{
    "default": "<key-name>"
    "keys": [
        {
            "name": "<key-name>",
            "keyPath": "<path-to-key>.key",
            "certPath": "<path-to-cert>.crt"
        }
    ]
}

You should now be able to list the keys:

notation key ls

It is also possible to generate a test certificate:

# Generate a certificate and RSA key pair
notation cert generate-test valid-example

The test certificate is not suitable for production use. Please visit the notation documentation for more information on how to use the notation plugin for production.

Push and sign the artifact using the certificate's private key:

bobctl push artifact oci://registry.iximiuz.com/k8sstormcenter/manifests/honey:$(git tag --points-at HEAD) \
    --path="./kustomize" \
    --source="$(git config --get remote.origin.url)" \
    --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)"

notation sign registry.iximiuz.com/k8sstormcenter/manifests/honey:$(git tag --points-at HEAD) -k <key-name>

Create a trustpolicy.json file:

{
    "version": "1.0",
    "trustPolicies": [
        {
            "name": "<policy-name>",
            "registryScopes": [ 
                "registry.iximiuz.com/k8sstormcenter/manifests/honey"
             ],
            "signatureVerification": {
                "level" : "strict" 
            },
            "trustStores": [ "ca:<store-name>" ],
            "trustedIdentities": [
                "x509.subject: C=DE, ST=NRW, L=Bochum, O=Notary, CN=<name>"
            ]
        }
    ]
}

For more details see trust policy spec

Generate a kubernetes secret with the certificate and trust policy:

bobctl create secret notation notation-cfg \
    --namespace=<namespace> \
    --trust-policy-file=<trust-policy-file-path> \
    --ca-cert-file=<ca-cert-file-path>

Configure Bob to verify the artifacts using the Notary trust policy and certificate:

apiVersion: source.k8sstormcenter.io/v1alpha1
kind: OCIRepository
metadata:
  name: k8sstormcenter-honey
  namespace: default
spec:
  interval: 5m
  url: oci://registry.iximiuz.com/k8sstormcenter/manifests/honey
  ref:
    semver: "*"
  verify:
    provider: notation
    secretRef:
      name: notation-cfg

Verification status

If the verification succeeds, BoB adds a condition with the following attributes to the OCIRepository's .status.conditions:

  • type: SourceVerified
  • status: "True"
  • reason: Succeeded

If the verification fails, BoB will set the SourceVerified status to False and will not fetch the artifact contents from the registry. The verification failure will report this to the OCIRepository ready status message.

$ kubectl -n default describe ocirepository.source.k8sstormcenter.io k8sstormcenter-honey

Status:                        
  Conditions:
    Last Transition Time:     2025-04-18T00:42:21Z
    Message:                  failed to verify the signature using provider 'cosign': no matching signatures were found
    Observed Generation:      1
    Reason:                   VerificationError
    Status:                   False
    Type:                     Ready

Verification failures are also visible when running bobctl get sources oci and in Kubernetes events.

Git commit status updates

Another important reason to specify the Git revision when publishing artifacts with bobctl push is for benefiting from BoB's integration with Git notification providers that support commit status updates:

bobctl push artifact oci://<repo url> --path=<manifests dir> \
    --source="$(git config --get remote.origin.url)" \
    --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)"

When bob-controller finds OCI artifacts containing a revision specified like in the example above, this origin revision is added on events.

Level up your Server Side game — Join 10,500 engineers who receive insightful learning materials straight to their inbox