Disconnected Environments with Hauler¶
Big Bang releases ship bb-<tag>-images-charts.tar.zst, a
Hauler content archive holding every
container image Big Bang needs plus the OCI-published Big Bang Helm charts. It is
an alternative to images.tar.gz, which remains available and unchanged.
Use this guide if your environment has a registry (Harbor, Artifactory, Nexus, or any OCI registry) to import into — or if it has none, since hauler can serve the archive as a registry itself. For the general disconnected picture — what has to exist inside the boundary before any of this matters — see Disconnected Environments.
The work is in three parts: import the archive, point Big Bang at your registry, and verify what the cluster actually pulled.
Throughout, registry.example.mil stands for your registry and <tag> for the Big Bang
release you are installing. Substitute your own.
Prerequisites¶
- The
haulerCLI on the high side. Optional — see Without the hauler CLI if you cannot install it and would rather useskopeo. - A registry you can push to, and credentials for it — or none, if you serve the
archive with
hauler store serve registryinstead - Roughly 2x the archive size in free disk for the unpacked store
On the cluster side, how you get images out of that registry depends on what you can change — decide this before you start, because it affects the install order:
- Kubernetes 1.36+, and you can rewrite image references at admission. This is the recommended path and the only one available on managed control planes.
- Root access to every node, and you can configure a containerd registry mirror instead. Works on any Kubernetes version; not available on EKS managed node groups without launch template user data, or on Fargate at all.
1. Import the archive into your registry¶
Download and verify¶
Download bb-<tag>-images-charts.tar.zst and the release checksums file from
the release page.
The release page always lists the archive; on the rare release where it failed to
build, that link returns 404. Confirm the file actually appears in the checksums
manifest before trusting it — --ignore-missing reports success for a file it
never checked.
grep images-charts bigbang-<tag>_checksums.txt
sha256sum -c bigbang-<tag>_checksums.txt --ignore-missing
Load and push¶
The archive is named for its release, so -f is required — hauler store load
on its own looks for hauler’s default haul.tar.zst and will not find it.
hauler store load -f bb-<tag>-images-charts.tar.zst
hauler login registry.example.mil -u <username> --password-stdin < token.txt
hauler store copy registry://registry.example.mil
-p <password> works too, but puts the credential in your shell history.
Where the images land¶
Hauler strips the source registry host and preserves the rest of the repository path, so an image published as
registry1.dso.mil/ironbank/big-bang/base:2.1.0
lands in your registry as
registry.example.mil/ironbank/big-bang/base:2.1.0
Nothing in Big Bang points at that path yet — that is what
part 2 does. If you are migrating from the older
images.tar.gz flow, compare a few entries against your current registry before
switching over.
Harbor: create the projects first¶
Harbor treats the first path segment as a project and will not create one
automatically. Create the projects matching the top-level path segments in
images-v2-with-dependencies.txt and oci_package_list.txt — ironbank and
bigbang always, plus gitlab if you enable the GitLab package — before
running hauler store copy, or the pushes will fail with permission errors.
If you need everything under a single Harbor project instead, see Harbor under a single project; that layout only works with the registry-mirror approach.
ECR: create the repositories first¶
ECR does not create repositories on push. Unless a repository creation template
matches, the push fails. Before hauler store copy, either pre-create every repository
or add a repository creation template with Applied for: CREATE_ON_PUSH and prefix
ROOT. This is a one-time registry setting, and the archive contains roughly 190
repositories.
ECR also constrains which rewriting mechanism you can use later — see AWS: EKS and ECR.
Self-signed registry certificates¶
hauler store copy registry://registry.example.mil --insecure
No registry? Serve one from the archive¶
If there is no registry to import into, hauler will run one over the store it just
loaded. hauler store serve registry copies the store into a
distribution registry and serves it on
port 5000, read-only:
hauler store load -f bb-<tag>-images-charts.tar.zst
hauler store serve registry \
--tls-cert /etc/ssl/certs/registry.crt \
--tls-key /etc/ssl/private/registry.key
The repository paths are identical to those in
Where the images land — it is the same registry:// copy — so
everything in part 2 applies unchanged, with the
serving host in place of registry.example.mil.
Four things to know before you rely on it:
- Pass the TLS flags for charts. They are optional, and without them the registry is plain HTTP, which Flux will not use for OCI charts. See TLS is required for an OCI chart registry. Images are less picky — a containerd mirror can be pointed at an HTTP endpoint.
- There is no authentication. Anything that can reach the port can pull the whole
archive. Put it where only the cluster can, and leave
registryCredentials: null. - It must be reachable from every node, so it belongs on a host inside the boundary rather than on the workstation you unpacked the archive on.
- It is one process serving local disk — no replication, no failover, and the store has to stay on that disk. That suits an appliance or edge install; for a long-lived cluster, import into a real registry — see Migrating to a Permanent Registry for moving to the Harbor Big Bang ships once the cluster is up.
2. Point Big Bang at your registry¶
Getting the archive into your registry is only half the job. Big Bang will not use it until you tell it to, and images and charts reach the registry by two different routes. Getting this wrong is the most common failure here:
| Fetched by | Configured with | |
|---|---|---|
| Images | kubelet/containerd, per node | admission-time rewriting, or a registry mirror |
| Charts | Flux source-controller, over HTTP | Big Bang values |
Neither substitutes for the other.
registryCredentialsdoes not rewrite image references. It only builds the imagePullSecret. SettingregistryCredentials.registryto your registry will not move a single image pull, and the pods will sit inImagePullBackOffwhile every rendered manifest looks correct.
Choosing an image mechanism¶
| Admission policy | Registry mirror | |
|---|---|---|
| Node-level configuration | none | required, on every node |
| Managed control planes, Fargate | works (Kubernetes 1.36+) | not available |
| Kyverno allowlist override | required | not needed |
| Image refs in pod specs | rewritten to your registry | unchanged |
| Apply order matters | yes — before any workload is admitted | no |
Images, option A: rewrite at admission (recommended)¶
MutatingAdmissionPolicy is stable and enabled by default in Kubernetes 1.36+. It
runs inside the API server, so there is no webhook to operate and no policy engine to
install, and it rewrites at pod admission — which is downstream of every way an image
reference is produced, including istio sidecar injection and operator-spawned pods.
apiVersion: admissionregistration.k8s.io/v1 # v1beta1 on Kubernetes 1.34-1.35
kind: MutatingAdmissionPolicy
metadata:
name: rewrite-image-registry
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
failurePolicy: Fail
# Required. Sidecars are injected by another webhook after this policy first runs;
# without IfNeeded the injected containers are never rewritten.
reinvocationPolicy: IfNeeded
variables:
- name: src
expression: '"registry1.dso.mil/"'
- name: dst
expression: '"registry.example.mil/"'
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: >
Object{
spec: Object.spec{
containers: object.spec.containers.map(c,
Object.spec.containers{
name: c.name,
image: c.image.startsWith(variables.src)
? variables.dst + c.image.substring(size(variables.src))
: c.image
})
}
}
- patchType: ApplyConfiguration
applyConfiguration:
expression: >
Object{
spec: Object.spec{
initContainers: object.spec.?initContainers.orValue([]).map(c,
Object.spec.initContainers{
name: c.name,
image: c.image.startsWith(variables.src)
? variables.dst + c.image.substring(size(variables.src))
: c.image
})
}
}
---
apiVersion: admissionregistration.k8s.io/v1 # v1beta1 on Kubernetes 1.34-1.35
kind: MutatingAdmissionPolicyBinding
metadata:
name: rewrite-image-registry
spec:
policyName: rewrite-image-registry
matchResources:
namespaceSelector: {}
Keep the startsWith guard. It confines the rewrite to Big Bang images and leaves the
cluster’s own components — cloud CNI plugins, CoreDNS, kube-proxy — untouched.
The two mutations cover both container fields you need. Native sidecars are
initContainers with restartPolicy: Always, so they are handled by the second one.
Ephemeral containers are not, and cannot be — they are only settable through the
pods/ephemeralcontainers subresource on update, which a CREATE rule never sees.
So name your registry explicitly when you run kubectl debug.
Apply the policy before anything else¶
The rule matches CREATE only, so it never touches pods that already exist, and a pod
admitted before the policy is in place keeps its original reference and fails to pull.
Kubernetes does not re-admit pods, so that pod stays broken until its owner recreates it.
The order is:
# 1. create the cluster
# 2. apply the policy
kubectl apply -f rewrite-image-registry.yaml
# 3. install Flux from base/flux -- its manifests still say registry1.dso.mil
# 4. install Big Bang
Because the policy is live first, Flux’s own controllers are rewritten as they are
admitted. Nothing needs pre-rewriting, including base/flux.
On Kubernetes 1.34 and 1.35¶
The feature exists but is beta and off by default. Both API-server flags are needed, and they fail differently:
--runtime-config=admissionregistration.k8s.io/v1beta1=true
--feature-gates=MutatingAdmissionPolicy=true
With only the first, the API accepts the policy and kubectl get mutatingadmissionpolicy
reports it live with MUTATIONS: 2 — and it silently rewrites nothing. If mutation
appears to do nothing, check the feature gate before debugging the CEL. Managed control
planes that do not expose API-server flags cannot use the beta version at all.
Kyverno: allowlist the destination, not the source¶
If you run kyvernoPolicies, its restrict-image-registries policy permits only
registry1.dso.mil and registry.dso.mil. MutatingAdmissionPolicy is a mutating
plugin and therefore runs before Kyverno’s validating webhook, so Kyverno inspects
the image after rewriting. You must allowlist the registry you rewrote to, even
though every manifest you authored names registry1.dso.mil:
kyvernoPolicies:
values:
policies:
restrict-image-registries:
parameters:
allow:
- registry.example.mil
If you have enabled the CEL-based policies, apply the same override to
restrict-image-registries-cel.
Without it, every pod is refused at admission:
admission webhook "validate.kyverno.svc-fail" denied the request:
restrict-image-registries: 'Image registry is not in the approved list.'
This failure is delayed and easy to misread. Pods admitted before Kyverno started keep running, so a cluster that already looks healthy stays healthy until something churns — the breakage surfaces on the next upgrade, node drain, or rollout, as workloads that will not reschedule. If you enable Kyverno after rewriting is already in place, apply this override in the same change.
Images, option B: a containerd registry mirror¶
Where you control node configuration and would rather not run an admission policy, a
registry mirror achieves the same result at the pull layer. Image references stay
registry1.dso.mil/... and containerd redirects them. Configure the appropriate file
on every node that pulls images, including schedulable server nodes:
- K3s:
/etc/rancher/k3s/registries.yaml - RKE2:
/etc/rancher/rke2/registries.yaml
mirrors:
"registry1.dso.mil":
endpoint:
- "https://registry.example.mil"
configs:
"registry.example.mil":
tls:
ca_file: /etc/ssl/certs/your-ca.crt
Configure this file before starting K3s or RKE2, or restart the corresponding service on each already-running node for the changes to take effect. See the K3s and RKE2 private registry documentation.
This works because the mirror and hauler are two halves of one convention. Hauler strips
the source registry host on push and keeps the repository path; the mirror substitutes
the host back and appends that same path, so
registry1.dso.mil/ironbank/big-bang/base:2.1.0 is served from
registry.example.mil/ironbank/big-bang/base:2.1.0 — exactly where hauler store copy
put it.
Two consequences worth knowing. Because references are unchanged, Kyverno’s
restrict-image-registries keeps working with its default allowlist and needs no
override. And because the redirect happens below Kubernetes entirely, it covers every
image regardless of how it was referenced, with nothing to apply in the right order.
The trade-off is reach: it requires node-level configuration on every node, which rules it out on managed control planes and on Fargate.
Harbor under a single project¶
Endpoint substitution appends the original path, so a mirror pointing at
https://harbor.example.mil yields harbor.example.mil/ironbank/... and needs an
ironbank project. If you require everything under one project instead
(harbor.example.mil/bigbang/ironbank/...), a plain endpoint will not do it — k3s
exposes a rewrite option taking regular expressions for that case.
Charts: Big Bang values¶
Charts are fetched by Flux over HTTP, so neither admission rewriting nor a registry mirror touches them — both operate on pods. Point the Helm repository at your registry and switch the packages onto it:
helmRepositories:
- name: "registry1"
repository: "oci://registry.example.mil/bigbang"
type: "oci"
username: ""
password: ""
istiod:
sourceType: "helmRepo"
# ...and every other package you enable
Two things that will trip you up:
- Every package defaults to
sourceType: "git". Without flipping them the OCI charts in the archive go unused and you still needrepositories.tar.gzplus a git server. - An unauthenticated registry is not directly expressible. The values schema requires
existingSecret, orusernameandpassword, or a non-genericprovider. Empty strings satisfy the schema and stay falsy in the template, so nosecretRefis rendered — that is theusername: ""/password: ""above.
TLS is required for an OCI chart registry¶
Flux’s insecure field — the one that allows a non-TLS registry — is only honoured when
.spec.type is oci, and Big Bang’s helmRepositories template emits neither insecure
nor certSecretRef. The archive ships OCI charts, so that is the path you are on: the
chart registry needs a real certificate. (A classic type: "default" HTTP chart
repository is unaffected by this, but the archive does not give you one.)
If the certificate is signed by a private CA, source-controller needs that CA — it runs as a pod, so the node trust store does not reach it. Two ways to get it there.
With values only. Create a Secret in the bigbang namespace and reference it as
existingSecret. Source-controller reads TLS material out of the auth secret and
extends the system pool with it. Use the format that matches the registry:
- For CA trust only, use an
OpaqueSecret containingca.crt. - For authentication plus CA trust, either use an
OpaqueSecret containingusername,password, andca.crt, or use akubernetes.io/dockerconfigjsonSecret containing.dockerconfigjsonandca.crt.
helmRepositories:
- name: "registry1"
repository: "oci://registry.example.mil/bigbang"
type: "oci"
existingSecret: "registry1-ca"
Flux logs this as deprecated (certSecretRef is the supported field), so it works today
but is worth watching. Big Bang has no certSecretRef passthrough, which is what forces
the choice.
With a patch to Flux. Longer-lived, at the cost of editing base/flux:
patches:
- target:
kind: Deployment
name: source-controller
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: source-controller
spec:
template:
spec:
containers:
- name: manager
env:
- name: SSL_CERT_FILE
value: /etc/ssl/certs/your-ca.crt
volumeMounts:
- name: registry-ca
mountPath: /etc/ssl/certs/your-ca.crt
subPath: ca.crt
readOnly: true
volumes:
- name: registry-ca
configMap:
name: registry-ca
Mount it read-only; base/flux sets readOnlyRootFilesystem: true. Note SSL_CERT_FILE
replaces Go’s trust store rather than adding to it, which is fine in a disconnected
environment. The symptom of getting this wrong is a consuming HelmChart reporting an
x509 error.
Flux’s own controller images are in the archive at the versions base/flux pins, and the
mirror covers them, so no image changes are needed there.
AWS: EKS and ECR¶
On EKS the choice of rewriting mechanism is made for you. (For getting the archive into ECR in the first place, see ECR: create the repositories first.)
- A registry mirror is not available. It needs node-level containerd configuration, which managed node groups allow only through launch template user data and Bottlerocket through settings — and Fargate not at all.
- Use
MutatingAdmissionPolicy. On Kubernetes 1.36+ it is stable and on by default, so it needs no API-server flags and works on managed control planes. On 1.34-1.35 it is beta and requires flags EKS does not expose, so it is unavailable there.
Rewriting to ECR has a further advantage: the node’s IAM role authenticates to ECR
natively, so no pull secret is required. Big Bang documents that case as
registryCredentials: null.
If you are on a Kubernetes version below 1.36 on EKS, neither a mirror nor an in-tree
policy is available, and the remaining option is a Kyverno mutating ClusterPolicy.
Note the bootstrap cost: Kyverno cannot mutate the pods that install Kyverno, so its own
images and the kubectl image in the kyverno-policies wait-job must be rewritten with
postRenderers first, and Flux’s four controller images with a kustomize images:
transformer in your base/flux overlay.
The values, all together¶
The pieces above are described where they matter; this is what they look like in one file. For the admission-policy path:
# Pull credentials for the registry the images were rewritten TO. This does not
# rewrite anything -- it only builds the imagePullSecret -- but without it a private
# registry answers the rewritten pulls with 401.
registryCredentials:
registry: registry.example.mil
username: <username>
password: <password>
# Charts. Flux fetches these itself, so they are configured here rather than rewritten.
# Chart-pull credentials are configured separately from registryCredentials above.
helmRepositories:
- name: "registry1"
repository: "oci://registry.example.mil/bigbang"
type: "oci"
username: "<username>"
password: "<password>"
# Kyverno validates AFTER the rewrite, so allow the destination.
kyvernoPolicies:
values:
policies:
restrict-image-registries:
parameters:
allow:
- registry.example.mil
# Every package defaults to sourceType "git". Flip each one you enable.
istiod:
sourceType: "helmRepo"
istioGateway:
sourceType: "helmRepo"
# ...and so on
With a registry mirror instead, drop the kyvernoPolicies override, leave
registryCredentials pointed at registry1.dso.mil (references are unchanged, so that
is the host the kubelet authenticates to), and give containerd the mirror’s own
credentials under configs: in registries.yaml. The helmRepositories and
sourceType values are the same either way — charts never go through the mirror.
3. Verify it came from your registry¶
OCI HelmRepository objects are static configuration and do not report a Ready
condition. Check the consuming HelmChart and HelmRelease conditions instead; both
should report Ready=True once reconciliation succeeds. See
Flux’s OCI HelmRepository documentation.
kubectl get helmcharts.source.toolkit.fluxcd.io -A # Ready=True: charts fetched
kubectl get helmreleases.helm.toolkit.fluxcd.io -A # Ready=True: releases reconciled
kubectl get po -A # no ImagePullBackOff
If you rewrote at admission, the pod specs are the evidence — they should name your registry, not the one in the manifests you applied. This should return zero:
kubectl get po -A -o json \
| jq '[.items[].spec | (.containers[]?, .initContainers[]?) | .image]
| map(select(startswith("registry1.dso.mil"))) | length'
Check a pod in an istio-injected namespace specifically. The sidecar is added by a
separate webhook, so it is the case most likely to be missed — if istio-proxy still
names registry1.dso.mil, reinvocationPolicy: IfNeeded is not set.
If you used a mirror, pod specs still say registry1.dso.mil by design, so a running
pod proves nothing on its own. The registry’s access log is the only evidence: if a pod
is Running and your registry logged nothing for that image, it reached upstream and the
mirror is not doing its job.
Without the hauler CLI¶
The archive is not a proprietary format — it is a zstd-compressed tar of a standard
OCI image layout, so the archive can be unpacked and pushed with any OCI-aware
tooling if you cannot install hauler on the high side.
mkdir haul && tar --zstd -xf bb-<tag>-images-charts.tar.zst -C haul
That yields index.json, manifest.json, and blobs/sha256/. Older hauler versions
did not write an oci-layout marker file, and tools that validate the layout strictly
will want one. Check, and add it if it is missing — it is a single line:
[ -f haul/oci-layout ] || printf '{"imageLayoutVersion":"1.0.0"}' > haul/oci-layout
Each image is addressable by the ref name recorded in index.json, so a push loop
is short. Refs repeat across entries (signatures and attestations share the name of
the image they cover), hence the sort -u:
jq -r '.manifests[].annotations."org.opencontainers.image.ref.name" | select(. != null)' \
haul/index.json | sort -u \
| xargs -P4 -I{} skopeo copy --retry-times 3 oci:haul:{} docker://registry.example.mil/{}
Two notes if you go this route:
crane pushreads the layout but refuses a multi-image one without--index, which fuses every image into a single index rather than pushing them to separate repositories. Useskopeo, ororasfor individual artifacts.- The ref names in
index.jsonhave the source registry host stripped (ironbank/big-bang/base:2.1.0), which is what makes the push loop above land images at the right paths. If you need to know where an image originally came from,manifest.jsonretains the full original reference in itsRepoTags.
Verifying signatures¶
Hauler carries cosign signatures, attestations, and SBOMs alongside each image by
default, so they arrive in your registry as the usual sha256-<digest>.sig,
.att, and .sbom tags. Signature verification with cosign works against your
internal registry without reaching back to the source.
What is not included¶
The archive contains images and OCI Helm charts only. If you deploy Big Bang
packages from git sources rather than the helmRepo (OCI) sources, you also
need repositories.tar.gz from the same release and a git server to host it.
Hauler has no git repository content type and does not replace that artifact.