Skip to content

Install in an air-gapped environment

Install, update, and support Dreadnode on a disconnected cluster — prerequisites, the procedure for each install path, and support-bundle transfer.

Terminal window
sudo ./dreadnode install --license license.yaml --airgap-bundle dreadnode.airgap

On Embedded Cluster that flag is the whole difference. Everything else on this page is about the two things you need before it will run: a license entitled to download air-gap bundles, and a release that has one built.

Air gap is not a switch you flip on an existing license. Two things have to be in place, and both come from us:

Air-gap download on your license. The entitlement differs by install path — Embedded Cluster and KOTS use one, the Helm CLI uses another. Tell us which path you’re installing and we’ll enable the right one.

A release with a built bundle. Bundles are built per release, per channel. A version that shipped before your entitlement existed has no bundle until it’s rebuilt. Name the version you intend to install so we can confirm its bundle exists before you plan the transfer.

Reach out to us to arrange both. Your path’s normal requirements still apply — VM requirements for Embedded Cluster, cluster requirements otherwise. Air gap changes how images arrive, not what the platform needs to run.

Confirm those requirements before you disconnect rather than during the install. A default StorageClass in particular is easy to assume and is not present by default on every managed distribution; inside the enclave a missing one costs you a teardown and a second attempt.

On the KOTS existing-cluster and Helm CLI paths, download the support-bundle binary before you cross the boundary and carry it in alongside the release bundle. It’s how you collect diagnostics for us later, it isn’t part of the air-gap bundle, and there’s no way to fetch it once you’re disconnected. Embedded Cluster installs it on control plane nodes for you, so that path doesn’t need this.

Terminal window
curl -L https://github.com/replicatedhq/troubleshoot/releases/latest/download/support-bundle_linux_amd64.tar.gz \
| tar xzvf -
sudo mv ./support-bundle /usr/local/bin/kubectl-support_bundle

The binary checks for its own updates on every run, and kubectl kots version calls out too. Neither can succeed once you are disconnected and neither is required, but both show up as blocked outbound attempts if you monitor egress. Pass --auto-update=false when you collect a bundle, and --no-uri to keep the spec from being re-fetched.

PathBundleIngressImage handling
Embedded ClusterYes — includes k0s, Traefik, and the Admin ConsoleIncludedAutomatic, into an in-cluster registry
Existing cluster with KOTSYesYou supply itKOTS re-tags and pushes to your registry
Helm CLINo — bundles are a KOTS featureYou supply itYou mirror images yourself

Embedded Cluster is the shortest path and the one to choose unless you already run a cluster you must deploy into.

Embedded Cluster’s air-gap bundle carries the infrastructure as well as the application: k0s, the Admin Console, Traefik, and every platform image. During installation it stands up a registry inside the cluster to hold the application images, and preloads the infrastructure images and Helm charts onto each node.

1. Download the bundle on a machine with internet access.

Terminal window
curl -f 'https://replicated.app/embedded/dreadnode/stable?airgap=true' \
-H 'Authorization: <license-id>' -o dreadnode.tgz

Your license ID is the licenseID: field inside your license file. For Beta releases, replace stable with beta.

2. Extract it and confirm the bundle is there.

Terminal window
tar -xvzf dreadnode.tgz
ls dreadnode.airgap

3. Transfer all three files to the target machine, then install.

Terminal window
sudo ./dreadnode install --license license.yaml --airgap-bundle dreadnode.airgap

The installer prompts for an Admin Console password, runs its host preflight checks, and prints the Admin Console URL on port 30000. Installation takes longer than an online install — it loads every image from the bundle before the application starts.

4. Configure and deploy through the Admin Console. From here the flow matches an online install: open the console, complete the config screen, and click Deploy. See Configure via the Admin Console for what each group covers, and Build a ConfigValues file if you’d rather supply configuration on the command line with --config-values.

Installing into a cluster you already run needs an image registry inside the network that KOTS can push to. KOTS rewrites every image reference in the application to read from that registry, then re-tags and pushes the images from the bundle into it. You supply two sets of registry credentials: read-write ones to push with, which KOTS does not store, and read-only ones the cluster keeps for pulling.

The application expects a single namespace in that registry, and most registries need the namespace to exist before anything can be pushed into it. Amazon ECR is the exception — it doesn’t use namespaces.

ECR also does not create repositories on push. KOTS keeps only the last path segment of each image reference, so create one repository per image name, with no namespace prefix, before you disconnect. Derive the list from the bundle you’re installing rather than copying one, since it changes release to release:

Terminal window
tar xzOf <release>.airgap airgap.yaml \
| awk '/^ savedImages:/{inlist=1; next} /^ [a-z]/{inlist=0} inlist' \
| sed 's/^ - //; s/@sha256:.*//; s/:[^:/]*$//' \
| awk -F/ '{print $NF}' | sort -u

That covers the application images — fifteen on a current release. The Admin Console carries its own, so repeat it against kotsadm.tar.gz and create those too; the two sets overlap on minio, so expect around twenty repositories in total.

Push fails partway through if a repository is missing. Whether you can recover in place depends on the registry’s control plane still answering from inside the enclave: an ECR registry reached over an ecr.api interface endpoint does, so the missing repository can be created on the spot — provided the role you’re using carries ecr:CreateRepository. Grant it before you disconnect. Without it you get an AccessDeniedException from a registry that is otherwise reachable, and a half-pushed registry you cannot finish.

You’ll need the application bundle for your version from us, plus the KOTS Admin Console bundle (kotsadm.tar.gz) from the kots releases page. Its version has to match your kots CLI — check with kubectl kots version — and be compatible with the application bundle. Push the Admin Console images first, then install:

Terminal window
kubectl kots admin-console push-images ./kotsadm.tar.gz <registry-host> \
--registry-username <rw-username> \
--registry-password <rw-password>
kubectl kots install dreadnode \
--kotsadm-registry <registry-host> \
--registry-username <ro-username> \
--registry-password <ro-password>

Then open the Admin Console, upload your license and the .airgap application bundle, complete the config screen, and deploy. Replicated’s air gap installation in existing clusters covers the registry compatibility requirements and the full prompt-by-prompt sequence.

This path does not bring an ingress controller — see Ingress outside Embedded Cluster.

Replicated does not build air-gap bundles for Helm CLI installs; bundles are a KOTS feature. An air-gapped Helm install means you mirror the platform images into your own registry and install the chart against that mirror. We provide the image list and the pull, tag, and push commands for your target version — ask us for them alongside the entitlement, since the list changes release to release.

Once your images are mirrored, point the chart at your registry:

global:
domain: dreadnode.example.com
imageRegistry: registry.internal.example.com
imagePullSecrets:
- name: mirror-pull-secret
airgapped: true
sandbox:
egressEnabled: true
replicated:
isAirgap: true

global.imageRegistry replaces the registry host on every image the chart renders and leaves the repository path untouched, so mirror each image to the same path under your own host. The chart refuses to install if you set it without global.imagePullSecrets, because every workload would otherwise fail to pull. Create that Secret before you install.

global.sandbox.egressEnabled: true belongs with global.airgapped here for the same reason replicated.isAirgap does — nothing on this path can detect air gap on your behalf. Without it the API fails preflight and does not start: an air-gapped deployment that runs sandboxes with unrestricted outbound access is a misconfiguration rather than a default. Embedded Cluster and KOTS derive it themselves. Mirror the egress sidecar image along with the rest, and see Restrict sandbox egress for what it enforces.

Traefik ships only as an Embedded Cluster extension. On the KOTS existing-cluster and Helm CLI paths you supply your own ingress controller — and in an air-gapped network, you mirror its images too. The chart emits standard networking.k8s.io/v1 Ingress resources and does not install a controller.

Set global.ingress.className if your controller isn’t the cluster default. Traefik is what we test against; other controllers work in principle but may need controller-specific annotations via global.ingress.annotations.

Air-gapped updates carry a new bundle in and deploy it from the Admin Console. Migrations are forward-only and rollback stays disabled — see Upgrades for what runs during an upgrade and how recovery works.

On Embedded Cluster, download the new version with the same curl command you used to install, extract it, and check for the .airgap file as before. Then either upload it from the command line:

Terminal window
./dreadnode update --airgap-bundle dreadnode.airgap

Or use Version history → Upload new version in the Admin Console and choose the .airgap file. Either way the version becomes available but is not live: click Deploy next to it, review the config and preflight screens, and confirm.

Check the registry before an existing-cluster update

Section titled “Check the registry before an existing-cluster update”

On an existing cluster with KOTS, the image list in the bundle changes release to release, and a release that adds an image needs a repository your registry doesn’t have yet. On a registry that creates repositories on push this is invisible. On ECR it stops the update. KOTS does validate the registry first, but that check only confirms the endpoint answers and accepts your credentials — it never looks at repositories. Re-derive the list from the new bundle and check every name yourself before you upload anything:

Terminal window
for repo in $(tar xzOf <new-release>.airgap airgap.yaml \
| awk '/^ savedImages:/{inlist=1; next} /^ [a-z]/{inlist=0} inlist' \
| sed 's/^ - //; s/@sha256:.*//; s/:[^:/]*$//' \
| awk -F/ '{print $NF}' | sort -u); do
aws ecr describe-repositories --repository-names "$repo" >/dev/null 2>&1 \
|| echo "missing: $repo"
done
# missing: opensandbox-egress

Create anything it names, then upload the bundle through the Admin Console the same way you did at install, or push it from the command line:

Terminal window
kubectl kots upstream upgrade dreadnode -n dreadnode \
--airgap-bundle dreadnode.airgap \
--kotsadm-registry <registry-host> \
--registry-username <rw-username> \
--registry-password <rw-password> \
--deploy --wait

Then confirm the version moved, because the exit code won’t tell you:

Terminal window
kubectl kots get versions dreadnode -n dreadnode
# VERSION SEQUENCE STATUS SOURCE
# 1.2.0 3 deployed Airgap Update

If you script the upgrade, add -o json and gate on .success instead.

Mirror new images before a Helm CLI update

Section titled “Mirror new images before a Helm CLI update”

The Helm CLI path has no bundle, so nothing pushes images for you. Mirror the new version’s images into your registry before helm upgrade — ask us for that version’s list, the same way you did for the install, since it changes release to release.

A support bundle is how anything gets out of an air-gapped install — including the instance telemetry we’d otherwise receive continuously.

Generate one from the Admin Console under Troubleshoot, or with the support-bundle binary you carried in during setup. See Support bundles for both commands. If you didn’t stage that binary, there’s no way to install it from inside the enclave, and the Admin Console is your only route.

Then transfer the archive out of the enclave the same way you carried the bundle in, and send it to us. Review the contents first if your environment requires it — the bundle collects logs, cluster state, and the telemetry Secrets.

An air-gapped install has no outbound path, and the platform doesn’t need one. Two things are worth knowing when you plan a network review.

The outbound endpoints an online install uses go unused: images come from the bundle or your mirror, and the SDK stops calling out once air-gap mode is on. On the Helm CLI path that last part depends on you setting replicated.isAirgap: true — until you do, the SDK keeps trying.

Marking the deployment air-gapped turns off the features that can only reach a public host, so configuration left over from a connected install doesn’t produce outbound attempts. That covers GitHub and Google authentication — use OpenID Connect against an identity provider inside your network instead — along with the Linear and HackerOne integrations and the model catalog.

Services you point somewhere yourself are a different case, and the platform leaves them alone: E2B sandboxes, external SMTP, and Logfire reach whatever you configure. See Optional external services. E2B runs sandboxes in E2B’s cloud and cannot work without outbound access; leave the sandbox provider on its default to keep execution inside your cluster.

Most of the platform works the same disconnected as connected. The features that need a public host are switched off when the deployment is marked air-gapped, rather than left to fail in front of a user:

FeatureWhy it needs the networkDisconnected behaviour
Analytics → NotebookIts Python kernel is a WebAssembly runtime the browser downloads from a public CDN when the notebook opensThe tab is hidden. Analytics → Query covers the same project data
Web-search result iconsSite icons come from a public favicon serviceResults show a generic icon
Agent web searchThe web_search tool answers from a search provider, and the hosted one needs a public APIThe tool reports that search is unavailable. Point it at an internal provider to restore it
GitHub and Google sign-inBoth authenticate against the provider’s own public endpointsNeither appears on the sign-in page. Use OpenID Connect or local passwords
Linear and HackerOneBoth submit findings to a fixed vendor APINeither can be selected. Webhook connections still work — see below
Model pricing and limitsCost, context window, and capability metadata come from a public model catalogServed from a copy shipped in the release. Accurate as of that release

Agent web search deserves a note. An agent’s web_search tool normally answers from the hosted search backend, which reaches a public API. Disconnected, that backend is unavailable, and the tool is barred from falling back to a public search engine — so the call returns no results and says why, rather than reaching out and failing. Tasks written against a connected deployment that assume search works will still run; their searches come back empty with the reason in the tool’s warnings.

If you run a search provider inside your network, you can give the tool a real backend. A provider you have pointed at your own host is not restricted — only the one that needs no credentials and answers from a public engine is. Set the provider’s credentials and internal URL through config.sandboxExtraEnv, along with DREADNODE_WEB_SEARCH_MODE: auto to let the tool use it.

An agent can still reach a URL a task hands it. The fetch and web_extract tools connect to whatever the task or model asks for, which is what makes them useful against services inside your enclave, and they are not gated by this setting. Where an agent may connect is governed by the sandbox egress floor instead — on an air-gapped deployment that floor denies by default, so an agent reaches the platform’s own services, the mirrors you configured, and whatever a task declares. See Restrict sandbox egress.

Model pricing deserves a note. Disconnected, the platform reads cost and context data from a catalog bundled into the image rather than fetching a current one, so a model released after your version won’t be in it. Deployments you configure with explicit pricing are unaffected — that pricing is what the platform bills against. If you add a model the bundled catalog doesn’t recognize, set its costs on the deployment.

Findings can still leave the deployment through a webhook connection, which posts to whatever URL you give it. Point it at a ticketing system inside your network to keep the submission workflow intact.

Embedded Cluster and KOTS detect air-gapped installs and set this for you. On the Helm CLI path nothing can detect it, so set global.airgapped: true yourself — the same division of labour as replicated.isAirgap.

Left unset on a disconnected deployment, neither feature breaks loudly. The notebook sits on Initializing Python… and never finishes, and result icons silently fail to load. If you are seeing either, this value is the first thing to check.

Fonts are served by the platform itself and need no configuration. Earlier versions loaded them from a public font service on every page load, which failed in a disconnected network and left the interface in a fallback typeface.

This documentation ships inside the release. The dreadnode-docs workload serves the pages for the version you installed at /docs on the platform’s own hostname, and every help link in the application points there, so a reader on an isolated network gets the same docs as a connected one — including the API and CLI reference and full-text search, which run entirely in the browser. Nothing in the bundled copy fetches from a public host.

Two things the public site has are left out, because they are indexes of absolute URLs and the hostname is not known when the release is built: the sitemap, and the llms.txt exports on the homepage.

The bundled copy is on by default and needs no configuration. Turning it off in favour of another documentation site, and the routing it needs under your own ingress, are covered in Configure → Documentation.

The published install command downloads uv from one public host and the Dreadnode packages from another, so it cannot work on a disconnected machine. The same workload that serves the documentation also serves a complete offline install, and the command to run it is the one the application shows on its home page and in its documentation drawer:

Terminal window
curl -fsSL https://platform.example.com/docs/clients/install.sh | bash

Substitute your own platform hostname. The script notices it was fetched from a deployment and installs from that deployment, using the hostname you fetched it over — so it keeps working on networks where the platform’s own idea of its address differs from what your machine resolves. If you pass a source manually with --from, include the scheme: https://, http://, or file://. Bare hostnames are rejected.

It downloads one archive containing every Python package the CLI and SDK need, a uv binary, and a Python interpreter. Nothing outside your network is contacted at any point, and the machine needs no Python, no uv, and no package manager beforehand — only curl (or wget) and tar. The archive’s checksum is verified before anything is unpacked.

The version installed is the version your deployment is running. That is a property of where the archive comes from rather than a flag anyone has to remember, and it removes the class of support problem where a client is a few releases ahead of the platform it is talking to.

PlatformSupported
Linux x86-64 (glibc)Yes
Linux ARM64 (glibc)Yes
macOS on Apple siliconYes
macOS on IntelNo — required packages no longer publish macOS Intel builds
Alpine and other muslNo — the archives carry glibc builds
WindowsNo — use WSL, which is a supported Linux target

On anything outside this list the script stops and names the platform before doing any work, rather than failing partway through with a dependency resolution error.

Where a machine has no route to the deployment at all, transfer an archive to it and install from the file. Download the archive for the target platform and its SHA256SUMS from any machine that can reach the deployment:

Terminal window
curl -fsSLO https://platform.example.com/docs/clients/dreadnode-client-<version>-linux-x86_64.tar.gz
curl -fsSLO https://platform.example.com/docs/clients/SHA256SUMS
curl -fsSLO https://platform.example.com/docs/clients/install.sh

<version> is served at /docs/clients/VERSION. Copy all three files to the target machine, keeping them in the same directory so the checksum is verified, then:

Terminal window
bash install.sh --bundle ./dreadnode-client-<version>-linux-x86_64.tar.gz

Installing the SDK into your own environment

Section titled “Installing the SDK into your own environment”

install.sh installs the CLI as a standalone tool: an isolated environment with its own interpreter, exposing dn and dreadnode on your PATH. That is what you want for the TUI and the command line, but it does not make import dreadnode work inside a project of your own — a uv project, a Poetry project, or a plain virtualenv resolves against its own environment, not the CLI’s.

The same archive covers that case. Alongside the interpreter it carries a wheels/ directory holding the SDK and its entire transitive closure, so it installs into your project with no package index at all:

Terminal window
tar xzf dreadnode-client-<version>-linux-x86_64.tar.gz
uv pip install --no-index \
--find-links dreadnode-client-<version>-linux-x86_64/wheels \
dreadnode

Use pip install --no-index --find-links ... identically where uv is not in play. A resolved requirements.txt sits next to wheels/ if you would rather pin the exact closure the release was built against.

The bundle carries the base CLI and SDK. It does not carry the optional extras — dreadnode[airt-ml], [training], and [scoring] — and it will not: airt-ml and training both pull torch, whose Linux x86-64 wheel is 527 MB before its CUDA dependencies, and scoring pulls sentence-transformers, which pulls torch as well. Carrying them would multiply the size of the release several times over for a minority of users.

The base install covers every attack in this documentation that does not say otherwise, including all generative-AI red teaming. The extras matter for traditional-ML attacks, hosted training, and the heavier scorers.

Getting a Python dependency onto a disconnected machine is a problem your organization almost certainly already solves, and the right answer depends on your mirror and your platform rather than on anything specific to Dreadnode. Two that work:

An internal index. If you run one — Artifactory, Nexus, devpi, a pip proxy — point the client at it and install normally:

Terminal window
export UV_INDEX_URL=https://pypi.internal.example.com/simple
export PIP_INDEX_URL=https://pypi.internal.example.com/simple
pip install 'dreadnode[airt-ml]'

A wheelhouse. Otherwise, download the wheels on a connected machine, transfer the directory, and install from it with no index at all:

Terminal window
# On a connected machine, for the target platform:
pip download 'dreadnode[airt-ml]' --dest ./wheelhouse \
--only-binary=:all: --python-version 3.12 \
--platform manylinux_2_28_x86_64 \
--platform manylinux_2_17_x86_64 \
--platform manylinux2014_x86_64
# On the disconnected machine, after transferring ./wheelhouse:
pip install --no-index --find-links ./wheelhouse 'dreadnode[airt-ml]'

Once the CLI is installed, point it at your deployment: see Connect clients.

Capabilities are how agents get their tooling, and each one can declare system packages, Python packages, and setup scripts that the sandbox runs when it starts. Disconnected, none of that is fetched: the runtime image already contains everything the capabilities shipped with the release declare, so each step finds its work already done.

A deployment marked air-gapped goes further and installs without network access at all, rather than relying on those steps to be quiet. Python packages resolve from what is already installed and the local cache. System packages are not requested — if one is missing, the deployment reports it as an unmet prerequisite instead of contacting a package archive it cannot reach. A request that cannot succeed is not made.

Setup scripts still run, because a capability whose tools are present has to keep working. Their contents are the capability author’s responsibility, so a script that reaches for something it does not already have will fail. That failure is bounded and reported rather than left to hang, and it is recorded distinctly from an ordinary install error, so a network review can tell the difference between a capability that could not install and one that tried to reach a public host.

A capability whose prerequisites are missing loads with those tools marked unavailable. The agent is told, so it asks you rather than calling tools that cannot work.

Two tools are the exception, because we do not redistribute them.

Caido backs the web-security capability’s proxy tooling. The capability talks to a Caido instance over the network rather than driving a local binary, so run Caido somewhere inside your network and point the sandbox at it by setting CAIDO_URL. Until you do, the capability loads but its proxy tools cannot connect, and the agent is told the tool is unavailable rather than discovering it mid-task.

Burp Suite is optional for the same capability. Supply the Community or Professional jar at /opt/burp/burpsuite.jar in a runtime image derived from ours if you want it. A Java runtime is already included, so nothing else is needed.

The browser automation tool downloads its browser on first use and cannot do that disconnected. The rest of the capability is unaffected.

Pointing sandboxes at services inside your network

Section titled “Pointing sandboxes at services inside your network”

config.sandboxExtraEnv sets environment variables on every sandbox. Use it for the endpoints above, and for internal package mirrors if you run them:

dreadnode-api:
config:
sandboxExtraEnv:
CAIDO_URL: http://caido.internal.svc.cluster.local:8080
UV_INDEX_URL: https://nexus.internal/repository/pypi/simple
PIP_INDEX_URL: https://nexus.internal/repository/pypi/simple
HF_ENDPOINT: https://huggingface.internal

Mirrors are optional. Everything the built-in capabilities need is already in the image, and these are only useful if you have added capabilities of your own that declare dependencies we do not ship. The variables are the standard ones each tool already understands, so anything a resolver or client reads from its environment works here.

Variables the platform sets for the sandbox itself, such as its server address and credentials, cannot be overridden this way. Setting one stops the API at startup with a message naming it, rather than accepting a value that would be ignored.

Installation is not a finished deployment. Continue with the post-install steps, which are the same on every path:

  1. First admin account — sign in and claim platform ownership
  2. First model deployment — every model picker stays empty until you add one
  3. Install the CLI and SDK — offline, from the deployment itself
  4. Connect clients — point the TUI, CLI, and SDK at your deployment

Your Hub starts empty, and an air-gapped instance can’t pull from ours. See Loading Hub content for the export-and-carry flow that populates it.