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.,
kustomizeYAML 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