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.
sudo ./dreadnode install --license license.yaml --airgap-bundle dreadnode.airgapOn 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.
Before you begin
Section titled “Before you begin”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.
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_bundleThe 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.
Pick your path
Section titled “Pick your path”| Path | Bundle | Ingress | Image handling |
|---|---|---|---|
| Embedded Cluster | Yes — includes k0s, Traefik, and the Admin Console | Included | Automatic, into an in-cluster registry |
| Existing cluster with KOTS | Yes | You supply it | KOTS re-tags and pushes to your registry |
| Helm CLI | No — bundles are a KOTS feature | You supply it | You 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
Section titled “Embedded Cluster”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.
curl -f 'https://replicated.app/embedded/dreadnode/stable?airgap=true' \ -H 'Authorization: <license-id>' -o dreadnode.tgzYour 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.
tar -xvzf dreadnode.tgzls dreadnode.airgap3. Transfer all three files to the target machine, then install.
sudo ./dreadnode install --license license.yaml --airgap-bundle dreadnode.airgapThe 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.
Existing cluster with KOTS
Section titled “Existing cluster with KOTS”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:
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 -uThat 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:
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.
Helm CLI
Section titled “Helm CLI”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: trueglobal.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.
Ingress outside Embedded Cluster
Section titled “Ingress outside Embedded Cluster”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.
Updates
Section titled “Updates”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:
./dreadnode update --airgap-bundle dreadnode.airgapOr 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:
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-egressCreate 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:
kubectl kots upstream upgrade dreadnode -n dreadnode \ --airgap-bundle dreadnode.airgap \ --kotsadm-registry <registry-host> \ --registry-username <rw-username> \ --registry-password <rw-password> \ --deploy --waitThen confirm the version moved, because the exit code won’t tell you:
kubectl kots get versions dreadnode -n dreadnode# VERSION SEQUENCE STATUS SOURCE# 1.2.0 3 deployed Airgap UpdateIf 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.
Support bundles and telemetry
Section titled “Support bundles and telemetry”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.
What still reaches the network
Section titled “What still reaches the network”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.
Features that need outbound access
Section titled “Features that need outbound access”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:
| Feature | Why it needs the network | Disconnected behaviour |
|---|---|---|
| Analytics → Notebook | Its Python kernel is a WebAssembly runtime the browser downloads from a public CDN when the notebook opens | The tab is hidden. Analytics → Query covers the same project data |
| Web-search result icons | Site icons come from a public favicon service | Results show a generic icon |
| Agent web search | The web_search tool answers from a search provider, and the hosted one needs a public API | The tool reports that search is unavailable. Point it at an internal provider to restore it |
| GitHub and Google sign-in | Both authenticate against the provider’s own public endpoints | Neither appears on the sign-in page. Use OpenID Connect or local passwords |
| Linear and HackerOne | Both submit findings to a fixed vendor API | Neither can be selected. Webhook connections still work — see below |
| Model pricing and limits | Cost, context window, and capability metadata come from a public model catalog | Served 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.
Documentation
Section titled “Documentation”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.
Installing the CLI and SDK
Section titled “Installing the CLI and SDK”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:
curl -fsSL https://platform.example.com/docs/clients/install.sh | bashSubstitute 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.
Supported platforms
Section titled “Supported platforms”| Platform | Supported |
|---|---|
| Linux x86-64 (glibc) | Yes |
| Linux ARM64 (glibc) | Yes |
| macOS on Apple silicon | Yes |
| macOS on Intel | No — required packages no longer publish macOS Intel builds |
| Alpine and other musl | No — the archives carry glibc builds |
| Windows | No — 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.
Machines that cannot reach the platform
Section titled “Machines that cannot reach the platform”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:
curl -fsSLO https://platform.example.com/docs/clients/dreadnode-client-<version>-linux-x86_64.tar.gzcurl -fsSLO https://platform.example.com/docs/clients/SHA256SUMScurl -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:
bash install.sh --bundle ./dreadnode-client-<version>-linux-x86_64.tar.gzInstalling 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:
tar xzf dreadnode-client-<version>-linux-x86_64.tar.gz
uv pip install --no-index \ --find-links dreadnode-client-<version>-linux-x86_64/wheels \ dreadnodeUse 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.
Optional extras
Section titled “Optional extras”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:
export UV_INDEX_URL=https://pypi.internal.example.com/simpleexport PIP_INDEX_URL=https://pypi.internal.example.com/simplepip 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:
# 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.
Agent capabilities and their tools
Section titled “Agent capabilities and their tools”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.internalMirrors 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.
Set up the platform
Section titled “Set up the platform”Installation is not a finished deployment. Continue with the post-install steps, which are the same on every path:
- First admin account — sign in and claim platform ownership
- First model deployment — every model picker stays empty until you add one
- Install the CLI and SDK — offline, from the deployment itself
- 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.