The usual way this goes wrong
You copy chart.tgz across, run helm install, and Helm reports
success. A minute later the pods are in ImagePullBackOff because the chart's
defaults point at a public registry. You mirror those images and try again. Now a
pre-install hook hangs, because its job uses a different image that only appears when a
certain value is set. Then the application starts and an operator it deploys tries to pull a
further image named in an environment variable.
Each of these costs a round trip through your transfer process, and in a truly disconnected environment that may mean a change ticket and a physical media handover. The goal of this walkthrough is to find everything on the connected side, once.
The steps assume two machines: a connected build host with Helm and an image copy tool, and a host inside the closed network with access to the cluster and an internal registry.
Step 1: fetch the chart and everything it depends on
On the connected side, pin an exact version. Never transfer whatever latest
happens to mean today.
helm repo add vendor https://charts.example.com
helm repo update
helm pull vendor/app --version 4.2.1
tar -xzf app-4.2.1.tgz
helm show values app-4.2.1.tgz > values.default.yaml
For a chart in an OCI registry, use helm pull oci://registry.example.com/charts/app
--version 4.2.1.
If you maintain your own chart with subcharts, run helm dependency build first
and check that the charts/ directory holds real archives. A packaged chart
should need no repository access at install time. To confirm this, list its contents with
tar -tzf app-4.2.1.tgz | grep charts/.
Also read the chart's crds/ directory if it has one. Helm installs CRDs from
that directory on first install and does not upgrade them afterwards, which matters when you
plan later upgrades through the same gap. helm show crds app-4.2.1.tgz prints
them.
Step 2: build the real image list
Render the chart with the values you intend to use in production. Image lists change with configuration, so a render using defaults can miss optional components you will enable.
helm template app ./app-4.2.1.tgz -n app -f values.prod.yaml \
--kube-version 1.31.0 > rendered.yaml
grep -E '^\s*(- )?image:' rendered.yaml \
| sed -E 's/^\s*(- )?image:\s*//; s/"//g' \
| sort -u > images.txt
The --kube-version flag matters for charts that branch on
.Capabilities. Use your cluster's real version.
That grep finds ordinary pod specs, including hooks, because helm template
renders them by default. It does not find these:
-
Images passed in as environment variables or container arguments, which operators often do
(
RELATED_IMAGE_*variables are a common convention) - Images in ConfigMaps or custom resources that a controller reads at runtime
-
Images that a chart looks up from a running cluster with
lookup, which returns nothing duringhelm template
So search for anything image-shaped too:
grep -Eo '[a-z0-9.-]+\.[a-z]{2,}(:[0-9]+)?/[a-z0-9._/-]+(:[A-Za-z0-9._-]+|@sha256:[a-f0-9]{64})' \
rendered.yaml | sort -u
Review the output by hand. It will contain some false positives such as URLs, and that is fine. The vendor's documentation may also publish an image list, and any difference between their list and yours should be explained.
Step 3: resolve tags to digests and pack the images
Tags can move. Digests cannot. Record the digest at the point of download, so what you tested on the connected side is what lands inside.
The examples use oc image mirror and oc image info from the
OpenShift client, which can write images to a directory and do not need a cluster connection
for this. Any OCI-capable copy tool follows the same steps.
mkdir -p bundle/images
while read -r image; do
digest=$(oc image info --filter-by-os=linux/amd64 -o json "$image" | jq -r .digest)
repo=${image%:*}
oc image mirror --filter-by-os=linux/amd64 --dir=bundle/images \
"$repo@$digest" "file://${image#*/}"
printf '%s %s\n' "$image" "$digest" >> bundle/images.lock
done < images.txt
cp app-4.2.1.tgz values.prod.yaml bundle/
(cd bundle && find . -type f ! -name SHA256SUMS -exec sha256sum {} + > SHA256SUMS)
The sketch assumes tagged image references with no registry port in the path, and one node architecture, so adjust it if yours differ. Mixed-architecture clusters need each platform copied.
Before the transfer, run your organisation's image scan on the connected side, and sign
SHA256SUMS with whichever signing tool your organisation approves. The person
receiving the bundle then has one small file to verify.
If the vendor signs its images, verify those signatures here as well, since the public key source or transparency log may be out of reach from inside the closed network. Offline verification needs some planning in advance.
Step 4: load into the internal registry
On the inside, verify first and load second.
cd bundle
sha256sum -c SHA256SUMS
oc image mirror --from-dir=images \
file://vendor/app:4.2.1 registry.internal/vendor/app:4.2.1
Verify the signature on SHA256SUMS first, using the same tool that created it.
Then compare the pushed digest with images.lock. If your registry uses a
private CA, install the CA on the host. Do not get into the habit of turning TLS
verification off, since this is the one step where you want integrity guarantees.
Push the chart too, so upgrades and rollbacks do not depend on a file on someone's laptop:
helm registry login registry.internal
helm push app-4.2.1.tgz oci://registry.internal/charts
Step 5: point the workloads at the mirror
There are two approaches, and they suit different environments.
Override image values. Most charts expose image.repository and
image.tag or a global.imageRegistry. Field names are
chart-specific, so read values.default.yaml rather than assuming them. This is
explicit and shows up in the release values, but a missed override fails only when that
component starts.
image:
repository: registry.internal/vendor/app
tag: "4.2.1"
imagePullSecrets:
- name: internal-registry
Mirror at the node level. The container runtime rewrites pulls, so
manifests can keep their original image names. On OpenShift, this is an
ImageDigestMirrorSet for digest references, with ImageTagMirrorSet
as its counterpart for tags:
apiVersion: config.openshift.io/v1
kind: ImageDigestMirrorSet
metadata:
name: vendor-mirror
spec:
imageDigestMirrors:
- source: registry.example.com/vendor
mirrors:
- registry.internal/vendor
Applying this causes a machine config rollout on OpenShift, so plan for it. On plain Kubernetes, the equivalent is the container runtime's registry mirror configuration, managed on every node. Node-level mirroring catches images referenced in places your values cannot reach, such as those deployed by an operator.
Node trust matters as well. The kubelet and container runtime must trust your registry's CA, which is a node configuration task separate from anything in Helm.
Step 6: install, then prove nothing reaches outward
Do a server-side dry run first, since it surfaces admission and API validation errors without changing anything:
helm install app ./app-4.2.1.tgz -n app --create-namespace \
-f values.prod.yaml --dry-run=server
helm install app ./app-4.2.1.tgz -n app --create-namespace \
-f values.prod.yaml --wait --timeout 10m
If --wait times out, Helm reports failure, but the resources remain in place
for inspection. These commands show where things stand:
kubectl get pods -n app
kubectl get events -n app --sort-by=.lastTimestamp | tail -30
helm get manifest app -n app | grep -E 'image:' | sort -u
Then verify the promise that makes air-gapping worthwhile. Check for images that did not come from your registry:
kubectl get pods -n app \
-o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{range .spec.initContainers[*]}{.image}{"\n"}{end}{end}' \
| sort -u | grep -v '^registry.internal/'
Runtime egress is the other half. An application can install cleanly and still try to reach a licence server, an update check or a model endpoint. Look at DNS query logs and firewall denials for the application's namespace during startup and for at least a day of normal running. The best time to do this is on a pre-production cluster with egress blocked and logged. If the vendor cannot tell you what their product connects to, this test will.
Things that catch people out after the install
- Time. Closed networks often have poor NTP. Tokens and certificates fail in confusing ways when clocks drift.
- Certificate chains. Applications with a bundled trust store will not automatically trust your private CA. Look for a value that mounts a CA bundle, and keep that CA under certificate expiry monitoring.
- Pod security. A chart that worked on a test cluster may need changes for OpenShift's restricted-v2 SCC. Check this before the transfer, since a new image build may be involved.
-
Upgrades. Repeat the whole procedure for each version. Script it from day
one, and keep the previous bundle so that
helm rollbackstill has its images available. - Vulnerability feeds. Scanners and similar tools need their databases refreshed through the same gap.
What this looks like with Wrentrix
Wrentrix is built for this case. It works
fully air-gapped, with no licence calls, no telemetry and no external model, and
installs with helm install wrentrix ./wrentrix.tgz -n wrentrix -f values.yaml
or through an OpenShift operator or Docker Compose. For closed networks there is also an
image-less agent install. Ask us for the image list and transfer details for the version you
plan to run, then apply the egress check above to it yourself.
Frequently asked questions
Can Helm install a chart without internet access?
Yes. Helm needs only the chart archive and access to the Kubernetes API, so helm
install name ./chart.tgz works offline as long as subcharts are packaged within it.
The real obstacle is container images, which the cluster has to be able to pull from an
internal registry.
How do I find all the images a Helm chart uses?
Render the chart with helm template and your production values, then extract
the image: lines. Also search the output for image-like strings, because
operators often pass images in environment variables or custom resources. For final
confirmation, run an install on an egress-blocked test cluster and see what fails.
Should I use image value overrides or a registry mirror?
Overrides are explicit and visible in the release, and they work on any cluster, but you
have to catch every image. Node-level mirroring, such as ImageDigestMirrorSet
on OpenShift, also catches images deployed by operators at runtime. Many teams use mirroring
as the base layer and override only when a chart requires it.
How do I move container images to an air-gapped registry?
Copy images to disk on a connected machine, for example with oc image mirror,
transfer them with a signed checksum file, then push them to the internal registry from the
closed side. The oc-mirror plugin covers a similar workflow on OpenShift,
especially for platform and operator content. Copying by digest protects you from tags
moving during the process.
How do I upgrade a Helm release in a disconnected cluster?
Repeat the bundling process for the new version, and diff the new image list against the old
one. Check the crds/ directory by hand, because Helm does not upgrade CRDs
placed there. Keep the previous version's images in your registry so that a helm
rollback can still pull them.