> ## Documentation Index
> Fetch the complete documentation index at: https://docs.siderolabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Run Omni Air-Gapped

> Set up the full Sidero stack in a fully offline environment

export const k8s_release = '1.36.0';

export const release = 'v1.13.2';

export const version = 'v1.13';

export const omni_release = 'v1.7.3';

<iframe width="560" height="315" src="https://www.youtube.com/embed/ExpIWRdrKEQ?si=EbRKeGkRplmZVaLv" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen />

This document will walk through each component to run the "Sidero stack" in an offline environment which includes the following components.

* Omni
* [Image Factory](./run-image-factory-on-prem)
* Container registry
* Authentication service

> When running Talos with Omni you do not need to run additional services such as the [Discovery Service](../../talos/v1.12/configure-your-talos-cluster/system-configuration/discovery) because discovery functionality is built in to Omni.

If you already have services such as a container registry, authentication service (SAML or OIDC), or a trusted certificate authority you can skip those sections of the guide. This guide will set up proof-of-concept deployment to get you started. We recommend [talking with the Sidero team](https://www.siderolabs.com/contact/) for production deployments.

<Note>Omni is licensed under the [Business Source License](https://github.com/siderolabs/omni/blob/main/LICENSE) and requires a support contract for production use.</Note>

<iframe
  width="560"
  height="315"
  src="https://www.youtube.com/embed/ExpIWRdrKEQ"
  title="Talos Omni, Airgapped"
  frameborder="0"
  allow="accelerometer;
autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen
/>

## Prerequisites

There are some expectations your environment has the following supporting services for the services to run. These include:

* Networks that can route Talos nodes to the Omni service endpoints
* Basic networking services such as DNS, DHCP, and NTP
* An admin system that can connect to the internet to download assets
* A Linux server (e.g. RHEL, Ubuntu) to run the Sidero stack

> The Sidero stack can be run on a single server or multiple servers, but we don't recommend running Omni inside of Kubernetes for air gapped environments.

In addition to these services you'll need the following tools installed on the administrator machine and server.

* [`talosctl`](../../talos/v1.12/getting-started/talosctl)
* `docker` or `podman`
* [`cfssl`](https://github.com/cloudflare/cfssl) and `cfssljson`
* [`yq`](https://github.com/mikefarah/yq)
* [`crane`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md)
* `htpasswd`

> Podman is known to work but has some flags that are different than docker and you may have to translate them for your version of podman.

<Accordion title="Download required tools">
  Download static binaries of the required tools. `Docker` and `htpasswd` should be provided by your distributions package manager.

  `talosctl`

  <CodeBlock lang="sh">
    {`curl -L -o talosctl https://github.com/siderolabs/talos/releases/download/${release}/talosctl-linux-amd64\nchmod +x talosctl`}
  </CodeBlock>

  `cfssl`

  ```bash theme={null}
  CFSSL_VERSION=$(curl -sI https://github.com/cloudflare/cfssl/releases/latest | grep -i location | awk -F '/' '{print $NF}' | tr -d '\r')
  curl -L -o cfssl https://github.com/cloudflare/cfssl/releases/download/${CFSSL_VERSION}/cfssl_${CFSSL_VERSION#v}_linux_amd64
  curl -L -o cfssljson https://github.com/cloudflare/cfssl/releases/download/${CFSSL_VERSION}/cfssljson_${CFSSL_VERSION#v}_linux_amd64
  chmod +x cfssl cfssljson
  ```

  `yq`

  ```bash theme={null}
  YQ_VERSION=$(curl -sI https://github.com/mikefarah/yq/releases/latest | grep -i location | awk -F '/' '{print $NF}' | tr -d '\r')
  curl -L -o yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64"
  chmod +x yq
  ```

  `crane`

  ```bash theme={null}
  CRANE_VERSION=$(curl -sI https://github.com/google/go-containerregistry/releases/latest | grep -i location | cut -d/ -f8 | tr -d '\r')
  curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | tar -xz crane
  chmod +x crane
  ```
</Accordion>

### Export endpoints

To make this guide easier to follow we will set global variables for each of the endpoints and ports we will use. Update the hostnames and ports if you change any of them from the defaults.

If you don't have DNS on your network it will be easiest to set these endpoints to the IP address of the machine running the services.

```bash theme={null}
export REGISTRY_ENDPOINT=registry.internal:5000
export FACTORY_ENDPOINT=factory.internal:8080
export AUTH_ENDPOINT=auth.internal:5556
export OMNI_ENDPOINT=omni.internal
```

### Open firewall ports

You'll need to open all of the above firewall ports on the machine you'll be running them on.

<Tabs>
  <Tab title="Linux (RHEL)">
    Red Hat and enterprise linux derivatives use `firewall-cmd`

    ```bash theme={null}
    sudo firewall-cmd --permanent --add-port=8100/tcp
    sudo firewall-cmd --permanent --add-port=5000/tcp
    sudo firewall-cmd --permanent --add-port=8080/tcp
    sudo firewall-cmd --permanent --add-port=5556/tcp
    sudo firewall-cmd --permanent --add-port=443/tcp
    sudo firewall-cmd --permanent --add-port=8090/udp
    sudo firewall-cmd --permanent --add-port=8091/tcp
    ```
  </Tab>

  <Tab title="Linux (Ubuntu)">
    Debian and Ubuntu use `ufw` for firewall commands.

    ```bash theme={null}
    sudo ufw allow 8100/tcp
    sudo ufw allow 5000/tcp
    sudo ufw allow 8080/tcp
    sudo ufw allow 5556/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 8090/udp
    sudo ufw allow 8091/tcp
    ```
  </Tab>
</Tabs>

## 1. Generate certificates

In order to run services securely, even in an air gapped environment, you should run with encrypted data in transit and at rest. There are multiple certificates and keys needed to secure your infrastructure.

* CA certificate (root of trust)
* Domain certificates for the following endpoints
  * Omni
  * Authentication
  * Image factory
  * Container registry
* Container signing certificate
* Omni database encryption key

### Create Root CA certificate (optional)

<Note>If you already have a trusted, internal root CA you can skip generating the CA (root of trust). You will need to use your existing CA to create certificates for the services in this guide. Skip to [Generate endpoint certificates](#generate-endpoint-certificates)</Note>

We will use the `cfssl` command to make the CA and certificate signing easier, but `openssl` can be used if you have existing CA infrastructure.

```bash theme={null}
cat <<EOF > ca-csr.json
{
  "CN": "Internal Root CA",
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C": "US",
      "O": "Internal Infrastructure",
      "OU": "Security"
    }
  ]
}
EOF
```

With the configuration create a CA certificate.

```bash theme={null}
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
```

This will give you a private key, `ca-key.pem`, a public key `ca.pem`, and a signing request `ca.csr`.

For any client that will be calling the internal services you will need to install the ca.pem file into your trusted store.

<Tabs>
  <Tab title="Linux (RHEL)">
    On Red Hat and Fedora based distros you can copy the `ca.pem` file into the `/etc/pki/ca-trust/source/anchors/` folder and then run the following command to generate the trusted root store:

    ```bash theme={null}
    sudo cp ca.pem /etc/pki/ca-trust/source/anchors/
    sudo update-ca-trust
    ```
  </Tab>

  <Tab title="Linux (Ubuntu)">
    On Ubuntu and Debian based Linux distros you can copy the `ca.pem` file into the `/usr/local/share/ca-certificates/` directory and rename it to `ca.crt`

    ```bash theme={null}
    sudo cp ca.pem /usr/local/share/ca-certificates/ca.crt
    sudo update-ca-certificates
    ```
  </Tab>

  <Tab title="macOS">
    For macOS you should open the *Keychain Access* application and drag the `ca.pem` file into the window to install it.
  </Tab>

  <Tab title="Windows">
    On Windows you should rename the certificate extension from `.pem` to `.crt`. You can then double click on the file and select *Install Certificate* -> *Local Machine*. You then need to select "Place all certificates in the following store" and select the *Trusted Root Certification Authorities*.

    If you're using Windows Subsystem for Linux (WSL) you should follow the Linux guide for installing the certificate.
  </Tab>
</Tabs>

### Generate endpoint certificates

Generate a single certificate that all services can use based on the CA we just created. For a production deployment you should generate individual certificates for each service.

Create a signing configuration to let `cfssl` know we want a web server certificate that should expire in 1 year.

```bash theme={null}
cat <<EOF > ca-config.json
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "web-server": {
        "usages": ["signing", "key encipherment", "server auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF
```

> When using `.internal` domains you will need to update your DNS server or `/etc/hosts` file to make sure the endpoints resolve properly. The file should look something like this.

```text theme={null}
# Example config in /etc/hosts
127.0.0.1 localhost registry.internal factory.internal auth.internal omni.internal
```

Now create a wildcard certificate for your services. We'll be using the ICANN reserved `.internal` TLD for service domains, but you can use any domain if you have it registered.

```bash theme={null}
cat <<EOF > wildcard-csr.json
{
  "CN": "Internal Wildcard",
  "hosts": [
    "internal",
    "${AUTH_ENDPOINT%:*}",
    "${REGISTRY_ENDPOINT%:*}",
    "${FACTORY_ENDPOINT%:*}",
    "${OMNI_ENDPOINT%:*}",
    "127.0.0.1",
    "$(hostname -I | awk '{print $1}')"
  ],
  "key": {
    "algo": "rsa",
    "size": 4096
  }
}
EOF
```

Generate the wildcard certificates for services.

```bash theme={null}
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=web-server wildcard-csr.json \
  | cfssljson -bare server
```

Create a certificate chain with server and CA.

```bash theme={null}
cat server.pem ca.pem > server-chain.pem
```

This will create a private server key, `server-key.pem`, a public server key, `server.pem`, a server signing request, `server.csr`, and a `server-chain.pem`, a server certificate with CA.

Services in this guide run with different user IDs so we will update the certificates to allow all users to read them. This is not suitable or secure for a production environment.

```bash theme={null}
chmod 644 server*.pem
```

## 2. Image factory and container registry

Using the certificates we just created, follow the guide [Deploy Image Factory On-prem](./run-image-factory-on-prem). This will
create a container registry and host the Image Factory in your environment. It will also sign container images with an offline key for verification.

<Warning>If you do not have a working Image Factory with Talos images and extensions seeded do not continue with the guide. That is a pre-requisite for running Omni in an air gapped environment.</Warning>

## 3. Authentication

If you have existing SAML or OIDC authentication available you can use that with Omni. Please see the configuration guides in the [Authentication and Authorization](../security-and-authentication/authentication-and-authorization) section.

For a PoC environment we will run [dex](https://dexidp.io/docs/) with static users configured. Dex can be used for static configuration or to communicate with upstream providers. For this guide we will configure static users.

### Deploy dex (optional)

Because Omni does not have any user authentication Dex will be configured so we can log in to Omni with a static user. You will need to download the dex container from a machine that has internet access and push it to your internal registry.

#### Download dex

Download the dex container image.

```bash theme={null}
docker pull ghcr.io/dexidp/dex:v2.41.1
```

If your machine has access to the internal registry you can push the image directly.

```bash theme={null}
docker tag ghcr.io/dexidp/dex:v2.41.1 ${REGISTRY_ENDPOINT}/dexidp/dex:v2.41.1
docker push ${REGISTRY_ENDPOINT}/dexidp/dex:v2.41.1
```

If you need to send the image to a remote machine or transfer it via an offline method you can export the container image.

```bash theme={null}
docker save -o dex.tar ghcr.io/dexidp/dex:v2.41.1
```

On the remote machine load the archive into the local image storage and then push it to the registry.

```bash theme={null}
docker load -i dex.tar
docker push ${REGISTRY_ENDPOINT}/dexidp/dex:v2.41.1
```

#### Configure dex

We will create an example dex configuration to use with Omni.

Create a password. Make sure you remember this password for logging in later.

```bash theme={null}
export OMNI_USER_PASSWORD=$(htpasswd -BnC 15 user42 | cut --delimiter=: --fields=1 --complement)
```

Create a dex configuration file.

```bash theme={null}
cat <<EOF > dex.yaml
issuer: https://${AUTH_ENDPOINT}
storage:
  type: memory

web:
  https: 0.0.0.0:5556
  tlsCert: /etc/dex/tls/server-chain.pem
  tlsKey: /etc/dex/tls/server-key.pem

enablePasswordDB: true

staticClients:
  - name: Omni
    id: omni
    secret: aW50ZXJuYWwtc2lkZXJvLXN0YWNrCg== #internal-sidero-stack
    redirectURIs: [https://omni.internal/oidc/consume]

staticPasswords:
  - email: "admin@omni.internal"
    username: "admin"
    preferredUsername: "admin"
    hash: "$OMNI_USER_PASSWORD"
EOF
```

#### Run dex

Run dex with the provided configuration and certificate.

```bash theme={null}
docker run -d \
  --name dex \
  -p 5556:5556 \
  -v $(pwd)/dex.yaml:/etc/dex/dex.yaml:ro,Z \
  -v $(pwd)/server-key.pem:/etc/dex/tls/server-key.pem:ro,Z \
  -v $(pwd)/server-chain.pem:/etc/dex/tls/server-chain.pem:ro,Z \
  ${REGISTRY_ENDPOINT}/dexidp/dex:v2.41.1 \
    dex serve /etc/dex/dex.yaml
```

> If your machine has SELinux in enforcing mode you may need to add `:Z` to the volume mounts in the docker command.

## 4. Run Omni

Omni will depend on the following URLs. If these services are not running or the hostnames do not resolve you may need to add them to your /etc/hosts file.

* omni.internal
* factory.internal
* registry.internal
* auth.internal

### Create etcd encryption key

Data in Omni's database is encrypted and we need an encryption key to provide to Omni.

Generate a GPG key:

```bash theme={null}
gpg --batch --passphrase '' \
  --quick-generate-key "Omni (Used for etcd data encryption) how-to-guide@siderolabs.com" \
  rsa4096 cert never
FINGERPRINT=$(gpg --with-colons --list-keys "how-to-guide@siderolabs.com" \
  | awk -F: '$1 == "fpr" {print $10; exit}')
gpg --batch --passphrase '' \
  --quick-add-key ${FINGERPRINT} rsa4096 encr never
gpg --export-secret-key --armor how-to-guide@siderolabs.com > omni.asc
```

> Note: Do not add passphrases to keys during creation.

### Download Omni

Download the Omni container image.

<CodeBlock lang="sh">
  {`docker pull ghcr.io/siderolabs/omni:${omni_release}\ndocker tag ghcr.io/siderolabs/omni:${omni_release} \\\n  \${REGISTRY_ENDPOINT}/siderolabs/omni:${omni_release}`}
</CodeBlock>

If your machine has access to the internal registry you can push the image directly.

<CodeBlock lang="sh">
  {`docker push \${REGISTRY_ENDPOINT}/siderolabs/omni:${omni_release}`}
</CodeBlock>

If you need to send the image to a remote machine or transfer it via an offline method you can export the container image.

<CodeBlock lang="sh">
  {`docker save -o omni.tar ghcr.io/siderolabs/omni:${omni_release}`}
</CodeBlock>

On the remote machine load the archive into the local image storage and then push it to the registry.

<CodeBlock lang="sh">
  {`docker load -i omni.tar\ndocker push \${REGISTRY_ENDPOINT}/siderolabs/omni:${omni_release}`}
</CodeBlock>

### Start Omni container

This will run Omni with an embedded etcd database mounted to the host. It is not recommended for production use cases. The command assumes the certificates generated earlier are available in the local directory where you run this command.

<Info>If running Omni with podman you'll need to use `sudo` so the wireguard endpoints and low level ports can be mapped</Info>

<CodeBlock lang="sh">
  {`docker run \\\n  --name omni \\\n  -d --net=host \\\n  --cap-add=NET_ADMIN \\\n  --device /dev/net/tun:/dev/net/tun \\\n  -v "\${PWD}/ca.pem:/etc/ssl/certs/ca-certificates.crt:ro,Z" \\\n  -v "\${PWD}/etcd:/_out/etcd:rw,Z" \\\n  -v "\${PWD}/sqlite:/_out/sqlite:rw,Z" \\\n  -v "\${PWD}/server-key.pem:/server-key.pem:ro,Z" \\\n  -v "\${PWD}/server-chain.pem:/server-chain.pem:ro,Z" \\\n  -v "\${PWD}/omni.asc:/omni.asc:ro,Z" \\\n  \${REGISTRY_ENDPOINT}/siderolabs/omni:${omni_release} \\\n    --name=air-gap-omni \\\n    --cert=/server-chain.pem \\\n    --key=/server-key.pem \\\n    --siderolink-api-cert=/server-chain.pem \\\n    --siderolink-api-key=/server-key.pem \\\n    --private-key-source=file:///omni.asc \\\n    --event-sink-port=8091 \\\n    --bind-addr=0.0.0.0:443 \\\n    --siderolink-api-bind-addr=0.0.0.0:8090 \\\n    --k8s-proxy-bind-addr=0.0.0.0:8100 \\\n    --advertised-api-url=https://omni.internal \\\n    --siderolink-api-advertised-url=https://omni.internal:8090 \\\n    --siderolink-wireguard-advertised-addr=\$(hostname -I | awk '{print \$1}'):50180 \\\n    --advertised-kubernetes-proxy-url=https://omni.internal:8100 \\\n    --auth-auth0-enabled=false \\\n    --auth-oidc-enabled=true \\\n    --auth-oidc-client-secret=aW50ZXJuYWwtc2lkZXJvLXN0YWNrCg== \\\n    --auth-oidc-provider-url=https://\${AUTH_ENDPOINT} \\\n    --auth-oidc-client-id=omni \\\n    --auth-oidc-scopes=openid,profile,email \\\n    --image-factory-address=https://\${FACTORY_ENDPOINT} \\\n    --initial-users=admin@omni.internal \\\n    --kubernetes-registry=\${REGISTRY_ENDPOINT}/siderolabs/kubelet \\\n    --sqlite-storage-path=/_out/sqlite/omni.db \\\n    --talos-installer-registry=\${REGISTRY_ENDPOINT}/siderolabs/installer \\\n    --workload-proxying-enabled=false \\\n    --metrics-bind-addr=0.0.0.0:2123`}
</CodeBlock>

> We changed the `--metrics-bind-addr` to use port `:2123` to avoid port conflicts with Image Factory (if it's running on the same host).

These flags mount the files and directories needed by Omni (e.g. certificates, etcd storage) and set flags to connect Omni to the upstream services (e.g. factory, authentication).

<Info>If you are running with SELinux enforcing you'll need to use `audit2allow` to allow the Omni container to create wireguard tunnels. Something like `sudo ausearch -m avc -ts recent | grep -v mlsconstrain | audit2allow -M omni-container` should work.</Info>

## 5. Create a cluster

Before you create a cluster you will need to generate installation media. When creating a cluster you'll need to make sure you patch the machine config to redirect container registries to your internal registry.

### Download Kubernetes containers

Seed the internal registry with Kubernetes container images.

```bash theme={null}
talosctl images k8s-bundle > k8s-images.txt
```

Download the images and push them into the internal registry.

<Tabs>
  <Tab title="Internet available">
    If your machine can reach the public internet and the internal registry at the same time you can copy the images internally with this command.

    ```bash theme={null}
    for SOURCE_IMAGE in $(cat k8s-images.txt)
      do
        IMAGE_WITHOUT_DIGEST=${SOURCE_IMAGE%%@*}
        IMAGE_WITH_NEW_REG="${REGISTRY_ENDPOINT}/${IMAGE_WITHOUT_DIGEST#*/}"
        crane copy \
          $SOURCE_IMAGE \
          $IMAGE_WITH_NEW_REG
    done
    ```
  </Tab>

  <Tab title="Air-gapped">
    If you don't have direct access to an internal container registry (e.g. air gapped environment) you need to download the container images while connected to the internet with this command:

    ```bash theme={null}
    cat k8s-images.txt \
      | talosctl images cache-create \
          --layout flat \
          --image-cache-path ./k8s-image-cache \
          --images=-
    ```

    Move the `image-cache` folder to an air gapped machine and serve the images on a read only, temporary container registry with:

    ```bash theme={null}
    export IP=$(hostname -I | awk '{print $1}')

    talosctl image cache-cert-gen \
      --advertised-address $IP

    talosctl image cache-serve \
      --address $IP:5000 \
      --image-cache-path ./k8s-image-cache \
      --tls-cert-file tls.crt \
      --tls-key-file tls.key
    ```

    A temporary image registry will run on your local machine IP address port 5000 with self-signed certificates. Copy the images to an internal, permanent container registry.

    ```bash theme={null}
    for SOURCE_IMAGE in $(cat k8s-images.txt)
      do
        IMAGE_WITHOUT_DIGEST=${SOURCE_IMAGE%%@*}
        IMAGE_WITH_NEW_REG="${REGISTRY_ENDPOINT}/${IMAGE_WITHOUT_DIGEST#*/}"
        LOCALHOST_IMAGE="${IP}:5000/${IMAGE_WITHOUT_DIGEST#*/}"
        crane copy --insecure \
          $LOCALHOST_IMAGE \
          $IMAGE_WITH_NEW_REG
    done
    ```
  </Tab>
</Tabs>

### Create installation media

The first step to create a cluster is to boot machines and connect them to Omni. In order to do that we will need to embed the self-signed CA certificate into Talos.

Create a configuration for a TrustedRootsConfig:

```bash theme={null}
yq eval --null-input '
.apiVersion = "v1alpha1" |
.kind = "TrustedRootsConfig" |
.name = "internal-ca" |
.certificates = load_str("ca.pem")
' > trustedrootsconfig.yaml
```

You can use this config two different ways. Use kernel arguments for Talos 1.11 and older and use embedded config for Talos 1.12.

<Tabs>
  <Tab title="Embedded config (Talos 1.12+)">
    Create an output directory for installation media and config.

    ```bash theme={null}
    mkdir _out
    mv trustedrootsconfig.yaml _out/machine-config.yaml
    echo "---" >> _out/machine-config.yaml
    yq eval --null-input '
    .apiVersion = "v1alpha1" |
    .kind = "TimeSyncConfig" |
    .enabled = false
    ' >> _out/machine-config.yaml
    echo "---" >> _out/machine-config.yaml
    ```

    Download machine join configuration from Omni. You can do this from the Omni web interface home page by clicking on the **Download Machine Join Config** button or if you have `omnictl` installed you can download it with

    > If your `omni.internal` endpoint is not resolvable via DNS, update your machine config to set the SideroLink endpoint to the IP address of the Omni machine instead of a hostname.

    > If you're shell still has `$OMNI_ENDPOINT` configured you may need to `unset OMNI_ENDPOINT` for `omnictl` commands to work

    ```bash theme={null}
    omnictl jointoken machine-config >> _out/machine-config.yaml
    ```

    Embed both configurations and create an installation ISO with `imager`.

    <Warning>Imager cannot be run with SELinux in enforcing mode.</Warning>

    <CodeBlock lang="sh">
      {`docker run --rm -t \\\n  -v "\${PWD}/_out:/out" \\\n   \${REGISTRY_ENDPOINT}/siderolabs/imager:${release} \\\n    iso \\\n    --embedded-config-path=/out/machine-config.yaml`}
    </CodeBlock>
  </Tab>

  <Tab title="Kernel args (Talos 1.11)">
    Get the kernel arguments from Omni with `omnictl`. You can also copy them from the Omni web interface with the **Copy Kernel Parameters** button on the home page.

    > If you're shell still has `$OMNI_ENDPOINT` configured you may need to `unset OMNI_ENDPOINT` for `omnictl` commands to work

    ```bash theme={null}
    OMNI_KERNEL_ARGS=$(omnictl jointoken kernel-args)
    ```

    > If your `omni.internal` endpoint is not resolvable via DNS, update your machine config to set the SideroLink endpoint to the IP address of the Omni machine instead of a hostname.

    Compress and base64 encode the configuration for a kernel argument.

    ```bash theme={null}
    TRUSTED_ROOT_CONFIG=$(cat trustedrootsconfig.yaml | zstd --compress --ultra -21 | base64 -w 0)
    ```

    Create an ISO with `imager` with both of the kernel arguments.

    <Warning>Imager cannot be run with SELinux in enforcing mode.</Warning>

    <CodeBlock lang="sh">
      {`docker run --rm -t \\\n  -v "\${PWD}/_out:/out" \\\n  --privileged \\\n  \${REGISTRY_ENDPOINT}/siderolabs/imager:${release} \\\n    iso \\\n    --extra-kernel-arg "talos.config.early=\$TRUSTED_ROOT_CONFIG $OMNI_KERNEL_ARGS"`}
    </CodeBlock>
  </Tab>
</Tabs>

No matter which version of Talos you use you should have a Talos iso file in the `_out` directory. You can use this to boot a machine and it will connect to Omni and trust the self-signed CA certificate.

### Create cluster in Omni

Guides on creating a cluster on Omni can be found at [creating an Omni cluster](../getting-started/create-a-cluster).

Because we're working in an airgapped environment we will need the following values added to our cluster configs so they know where to pull images from. We also need to provide the CA certificate to the node so it will trust the certificate that signed `omni.internal` endpoint.

> **NOTE:** In this example, cluster discovery is also disabled. You may also configure cluster discovery via Omni. More information on the Discovery Service can be found <a href="../../talos/v1.12/configure-your-talos-cluster/system-configuration/discovery">here</a>.

Export the air-gap configuration patch to a file:

```bash theme={null}
(yq eval --null-input '
.machine.registries.mirrors."docker.io".endpoints = ["https://" + env(REGISTRY_ENDPOINT)] |
.machine.registries.mirrors."gcr.io".endpoints = ["https://" + env(REGISTRY_ENDPOINT)] |
.machine.registries.mirrors."ghcr.io".endpoints = ["https://" + env(REGISTRY_ENDPOINT)] |
.machine.registries.mirrors."registry.k8s.io".endpoints = ["https://" + env(REGISTRY_ENDPOINT)]
' && echo "---" && yq eval --null-input '
.apiVersion = "v1alpha1" |
.kind = "TimeSyncConfig" |
.enabled = false
' && echo "---" && yq eval --null-input '
.apiVersion = "v1alpha1" |
.kind = "RegistryTLSConfig" |
.name = env(REGISTRY_ENDPOINT) |
.ca = load_str("ca.pem")
' && echo "---" && yq eval --null-input '
.apiVersion = "v1alpha1" |
.kind = "TrustedRootsConfig" |
.name = "internal-ca" |
.certificates = load_str("ca.pem")
') > air-gap-patch.yaml
```

Get the first machine UUID from Omni:

```bash theme={null}
export OMNI_CP_MACHINE=$(omnictl get machines -o yaml \
  | yq '.metadata.id')
```

This will list all available machine UUIDs. Note down the UUIDs you want to use for your cluster (you'll need at least one control plane node and optionally worker nodes).

Generate a cluster template using the patch file and machine UUIDs:

<CodeBlock lang="bash">
  {`cat <<EOF > cluster-template.yaml\nkind: Cluster\nname: air-gap-cluster\nkubernetes:\n  version: v${k8s_release}\ntalos:\n  version: ${release}\npatches:\n  - file: air-gap-patch.yaml\n---\nkind: ControlPlane\nmachines:\n  - $OMNI_CP_MACHINE\nEOF`}
</CodeBlock>

Create the cluster using the template:

```bash theme={null}
omnictl cluster template sync --file cluster-template.yaml
```

The machine patch will be applied cluster-wide to all nodes.
