Air-gapped installation

Nothing in Nazeel reaches the internet. An air-gapped install therefore needs three things carried across the boundary: the chart, the images, and the licence key.

1. Carry one file across

Ask for the air-gap bundle attached to the release, or build it on a connected machine:

make bundle          # dist/bundle/nazeel-airgap-0.1.0.tar.gz, plus its .sha256

It carries the images, the packaged chart, the CRDs, the CLI binaries for every platform, the SBOMs and signatures (on release bundles), and the mirror.sh that pushes the images into your registry. One file crosses the boundary instead of a list of steps to follow correctly on a machine with no internet to check against.

Take the bundle and your licence key (a single line beginning NZL2.) into the isolated network, and check the tarball arrived intact:

shasum -a 256 -c nazeel-airgap-0.1.0.tar.gz.sha256
tar -xzf nazeel-airgap-0.1.0.tar.gz && cd nazeel-airgap-0.1.0

2. Mirror the images

./mirror.sh registry.bank.internal
# a registry that requires a project or path prefix:
./mirror.sh harbor.bank.internal platform

It loads each image and pushes it, using docker or podman, whichever the host has. It fails loudly if an image is missing from the bundle rather than leaving you to find out at install time.

One image carries the operator and the router; the API image carries the dashboard. The bundle also carries the images that only some installations use — the bundled registry, and rootless BuildKit, the git image and the Paketo builder for building from source — because the alternative is discovering one is missing after the bundle has crossed the boundary. make images-list prints the same list outside the bundle, and make images-check fails the build if the chart can deploy an image the list does not name.

The one exception is the vulnerability scanner. imageScan.image is empty by default, because the scanner and its database are your choice; if you enable scanning, mirror that image yourself along with the database PVC it reads.

What was tested, and why the bundle is the durable copy

Third-party images are pinned to exact versions so a moved tag cannot change what an assessed installation runs. These are the digests of the 0.1.0 bundle, so you can verify that what you mirrored is what we shipped:

Image Digest
distribution/distribution:3.0.0 sha256:4ba3adf47f5c866e9a29288c758c5328ef03396cb8f5f6454463655fa8bc83e2
moby/buildkit:v0.17.2-rootless sha256:5b45405a38c579692f6fcd47ceef2002fe4fa61bb04ef0c2c644cf74cbbd57b8
alpine/git:2.45.2 sha256:16ad8e788e1d3b0c30f18da8dde5c0ace3b187445a62d8af893b003ca1e70592
paketobuildpacks/builder-jammy-base:latest sha256:baacac1ea303146e5c86f7b947eb7f814163c798c6801247b0ba4d5f6bd3e32d

Check one after mirroring:

docker image inspect <image> --format '{{index .RepoDigests 0}}'

The Paketo builder is the one image with no version tag to pin to — Paketo publishes it as a rolling latest. That matters for a product expected to work years after a sale, so:

The same reasoning applies to every image here: after mirror.sh has run, an installation pulls exclusively from your registry, and its ability to start and to build does not depend on any external registry continuing to exist.

3. Install

kubectl apply -f chart/crds/
kubectl create namespace nazeel-system
kubectl -n nazeel-system create secret generic nazeel-bootstrap --from-literal=token="$(openssl rand -hex 24)"
kubectl -n nazeel-system create secret generic nazeel-licence --from-file=licence=./licence.txt

helm install nazeel chart/nazeel-0.1.0.tgz -n nazeel-system \
  --set global.imageRegistry=registry.bank.internal \
  --set ingress.domain=dev.bank.internal \
  --set ingress.className=nginx \
  --set ingress.tlsSecret=wildcard-dev-bank-internal \
  --set ingress.dashboard.host=nazeel.bank.internal \
  --set ingress.dashboard.tlsSecret=nazeel-tls \
  --set api.bootstrap.existingSecret=nazeel-bootstrap \
  --set api.licence.existingSecret=nazeel-licence

Then check it answers, from inside the cluster:

helm test nazeel -n nazeel-system

global.imageRegistry rewrites every image reference; the operator passes it on to the router image it deploys. Application images stay wherever your Baselines point; use imageRegistry on a Baseline to rewrite a chart's images to your mirror.

What the admission webhook needs

Nothing. The operator issues its own CA and serving certificate, stores them in the Secret nazeel-webhook-cert in its namespace, renews them yearly and publishes the CA into its ValidatingWebhookConfiguration. If the cluster already runs cert-manager and policy requires it, set operator.webhook.certMode=cert-manager and an issuerRef.

Git access from the operator

Baselines pull from your internal git server with a Secret in the application namespace. The Secret must carry the label nazeel.sa/git-credentials=true: the operator only reads Secrets that carry it, so connecting an application never exposes your other application secrets to it.

kubectl -n payments create secret generic shop-git \
  --from-literal=token="$GIT_TOKEN" --from-literal=username=nazeel
kubectl -n payments label secret shop-git nazeel.sa/git-credentials=true
Key Purpose
token, username HTTPS token (username defaults to nazeel)
ssh-privatekey, known_hosts, passphrase SSH; known_hosts is mandatory
ca.crt PEM bundle of your private CA for HTTPS

Helm chart dependencies must be vendored under charts/ in the repository; the operator never contacts a chart repository.

Verifying the install

kubectl -n nazeel-system get pods
kubectl get validatingwebhookconfiguration nazeel-validating-webhook -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | wc -c   # > 0
curl -k https://nazeel.bank.internal/api/v1/auth/providers

hack/e2e-chart.sh runs this whole flow on a kind cluster with images loaded from disk and no registry at all, which is the closest reproducible stand-in for an air-gapped site.