OpenShift: Using oc-mirror to create image mirrors for air gapped environments

OpenShift mirror air gapped container image ImageContentSourcePolicy

4 min read | by Jordi Prats

Combining oc-mirror with ImageContentSourcePolicy we can configure image mirrors for container images in OpenShift. We can use it to setup air gapped environments: The images won't be available for the source repository, just from the internal mirror. This way we can audit them before allowing our cluster to use them

To do so first we'll have to populate our internal mirror. To do so we can use oc-mirror

Populate internal mirror

We can download oc-mirror from RedHat, or use it's repository to compile it: openshift/oc-mirror

There are several ways to configure oc-mirror to copy container images, making it easy to download images from the different operators we might need. For this example we are going to use mirror.additionalImages to specify an arbitrary image from docker hub:

kind: ImageSetConfiguration
apiVersion: mirror.openshift.io/v1alpha2
storageConfig:
  registry:
    imageURL: repo.pet2cattle.com/os-mirror
    skipTLS: false
mirror:
  additionalImages:
  - name: registry.hub.docker.com/jordiprats/flask-pet2cattle:5.30

This is a configuration file for oc-mirror, not an actual Kubernetes object. The OpenShift cluster is not going to accept this object.

Using oc mirror with this configuration and the target repository, we can sync the container images. We'll need to run it somewhere that can download the images from the source registry and push them to our internal mirror. This tool can be used to download images locally and then push them to the internal mirror using a different computer, but it's out of the scope of this post.

$ oc-mirror --config=configuration.yaml docker://repo.pet2cattle.com/os-mirror

This command is going to create a folder named oc-mirror-workspace that will contain some files that we can use as reference:

$ find oc-mirror-workspace
oc-mirror-workspace
oc-mirror-workspace/publish
oc-mirror-workspace/publish/.metadata.json
oc-mirror-workspace/results-1687463421
oc-mirror-workspace/results-1687463421/release-signatures
oc-mirror-workspace/results-1687463421/charts
oc-mirror-workspace/results-1687463421/imageContentSourcePolicy.yaml
oc-mirror-workspace/results-1687463421/mapping.txt
oc-mirror-workspace/mapping.txt

In the mapping.txt file we are going to see the list of container images that has sync, with it's internal equivalent:

registry.hub.docker.com/jordiprats/flask-pet2cattle@sha256:bf5823de6c97c10ced8e25787a355a92f08c2d83fbf91b3922bbf4d399510337=repo.pet2cattle.com/os-mirror/jordiprats/flask-pet2cattle:5.30

Configure container image mirrors

The imageContentSourcePolicy.yaml contains the object that we need to persist into the OpenShift cluster to be able to tell it where it to look for container images.

apiVersion: operator.openshift.io/v1alpha1
kind: ImageContentSourcePolicy
metadata:
  name: generic-0
spec:
  repositoryDigestMirrors:
  - mirrors:
    - repo.pet2cattle.com/os-mirror
    source: registry.hub.docker.com/jordiprats

To demostrate that it is using the mirrors, we can update it so we tell OpenShift that a certain image container from a non-existing registry can be found in our mirror: This way the only way it can fetch the image is using our mirror:

apiVersion: operator.openshift.io/v1alpha1
kind: ImageContentSourcePolicy
metadata:
  name: generic-0
spec:
  repositoryDigestMirrors:
  - mirrors:
    - repo.pet2cattle.com/os-mirror
    source: non-existing-repo.com/jordiprats

To test it, we can use kubectl run using the cryptographic hash of the image, it won't work with tags:

kubectl run testpull \
  --image='non-existing-repo.com/jordiprats/flask-pet2cattle@sha256:bf5823de6c97c10ced8e25787a355a92f08c2d83fbf91b3922bbf4d399510337' 
  --command -- sleep 24h

We can now check that it has been able to download the image anyway using kubectl describe:

$ kubectl describe pod testpull
Name:             testpull
Namespace:        test-jordi
(...)
Events:
  Type    Reason          Age   From               Message
  ----    ------          ----  ----               -------
  Normal  Scheduled       10m   default-scheduler  Successfully assigned test-jordi/testpull to ip-100-75-36-20.eu-central-1.compute.internal
  Normal  AddedInterface  10m   multus             Add eth0 [10.128.2.67/23] from openshift-sdn
  Normal  Pulling         10m   kubelet            Pulling image "non-existing-repo.com/jordiprats/flask-pet2cattle@sha256:bf5823de6c97c10ced8e25787a355a92f08c2d83fbf91b3922bbf4d399510337"
  Normal  Pulled          10m   kubelet            Successfully pulled image "non-existing-repo.com/jordiprats/flask-pet2cattle@sha256:bf5823de6c97c10ced8e25787a355a92f08c2d83fbf91b3922bbf4d399510337" in 6.181838823s
  Normal  Created         10m   kubelet            Created container testpull
  Normal  Started         10m   kubelet            Started container testpull

We can check what's the mirror we are using by checking the status.containerStatuses attribute. When we are using a container image fetched from the source, the image and imageID are going to point to the same registry:

$ kubectl get pod
apiVersion: v1
kind: Pod
(...)
    - containerID: cri-o://807336c48df5ed81c3ea970303c2a06d47cb4f51bf719086f21d0311850f4dc2
      image: registry.hub.docker.com/jordiprats/flask-pet2cattle:5.30
      imageID: registry.hub.docker.com/jordiprats/flask-pet2cattle@sha256:1590c0b9c38d565b3e5f65e78a3e05e47d8eab8d81cc5e7a04793fc1d9e62f67
(...)

When we have a ImageContentSourcePolicy configured and the Pod is using a cryptographic hash, they won't match: The imageID is going to show us the actual source it used to download the container:

$ kubectl get pod
apiVersion: v1
kind: Pod
(...)
  containerStatuses:
  - containerID: cri-o://5e6f1aa90d0f63f5ebd74a804b750e02b0f6ad60aa419eca84acb5fa9838d11f
    image: non-existing-repo.com/jordiprats/flask-pet2cattle@sha256:bf5823de6c97c10ced8e25787a355a92f08c2d83fbf91b3922bbf4d399510337
    imageID: repo.pet2cattle.com/os-mirror/jordiprats/flask-pet2cattle@sha256:1590c0b9c38d565b3e5f65e78a3e05e47d8eab8d81cc5e7a04793fc1d9e62f67
(...)

Posted on 16/01/2023