# Airgap Install

## Prerequisites

### Hardware/OS

* Ubuntu 22.04 LTS on all host nodes.
* Airgap policy: Hosts cannot reach the internet but must be able to resolve DNS.

### Network and DNS

* Hostnames and IPs for all nodes.
* VIP for the control plane.
* Ensure local DNS or add /etc/hosts entry for the private registry on all nodes.

### Size Estimates

* **Images:** \~40 GB
* **Packages:** \~266 MB

Steps to download scripts and package/image lists required for later phases.

{% hint style="info" %}
**Info**

The following scripts and lists must be downloaded on the node that has internet access and will be used to create the APT repository and the private image repository.
{% endhint %}

1. Script to create an apt repo:

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<USER_AGENT_TOKEN>" https://pf9-airctl.s3.us-west-1.amazonaws.com/latest/sample_scripts/create_apt_repo.sh > create_apt_repo.sh
```

{% endtab %}
{% endtabs %}

2. Script to download all the required APT dependencies

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<USER_AGENT_TOKEN>" https://pf9-airctl.s3.us-west-1.amazonaws.com/latest/sample_scripts/download_all_deps.sh > download_all_deps.sh
```

{% endtab %}
{% endtabs %}

3. Script to push the images to private repo

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<USER_AGENT_TOKEN>" https://pf9-airctl.s3.us-west-1.amazonaws.com/latest/sample_scripts/push-images.sh > push-images.sh
```

{% endtab %}
{% endtabs %}

List of images required

1. PCD-V images list:

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<YOUR_USER_AGENT_TOKEN>" https://pf9-airctl.s3-accelerate.amazonaws.com/latest/pcdv-images.txt > pcdv-list.txt
```

{% endtab %}
{% endtabs %}

2. PCD-K images list

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<YOUR_USER_AGENT_TOKEN>" https://pf9-airctl.s3-accelerate.amazonaws.com/latest/pcdk-images.txt > pcdk-list.txt
```

{% endtab %}
{% endtabs %}

### DNS Prerequisite for Image and Repo

If the registry hostname is **not resolvable** in customer DNS, add it to /etc/hosts on each node.

## NTP Configuration (Client-Side)

{% hint style="info" %}
**Info**

An NTP server must already be available and reachable in the customer’s network.

All nodes must be configured as NTP clients to sync time with this server.
{% endhint %}

If NTP is not already configured on the client nodes, follow the steps below to point them to the NTP server.

Point node to the existing NTP server:

{% tabs %}
{% tab title="Bash" %}

```bash
sudo mkdir -p /etc/systemd/timesyncd.conf.d echo "[Time] NTP=<ntp-server-ip-or-fqdn>" | sudo tee /etc/systemd/timesyncd.conf.d/custom.conf
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Bash" %}

```bash
sudo mkdir -p /etc/systemd/timesyncd.conf.d echo "[Time] NTP=<ntp-server-ip-or-fqdn>" | sudo tee /etc/systemd/timesyncd.conf.d/custom.conf
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Bash" %}

```bash
sudo mkdir -p /etc/systemd/timesyncd.conf.d echo "[Time] NTP=<ntp-server-ip-or-fqdn>" | sudo tee /etc/systemd/timesyncd.conf.d/custom.conf
```

{% endtab %}
{% endtabs %}

Restart service:

{% tabs %}
{% tab title="Bash" %}

```bash
sudo systemctl restart systemd-timesyncd sudo systemctl enable systemd-timesyncd
```

{% endtab %}
{% endtabs %}

Verify sync:

{% tabs %}
{% tab title="Bash" %}

```bash
timedatectl status timedatectl show-timesync --all
```

{% endtab %}
{% endtabs %}

## Download Required APT Package Dependencies

Download and prepare all required package dependencies on an **Ubuntu 22.04 node** where your APT repo will run.

### Script and Dependency List

The script for downloading the required packages and dependency list should be retrieved as part of the prerequisites.

### Steps:

{% tabs %}
{% tab title="Bash" %}

```bash
chmod +x download_all_deps.sh ./download_all_deps.sh <path-to-dependency_list.txt>
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**Info**\
If using HTTPS with a custom CA, install the CA into /usr/local/share/ca-certificates and run update-ca-certificates.
{% endhint %}

## Set Up APT Repository

The script for setting up the APT repo should be retrieved as part of the prerequisites.

Steps to create an APT repo:

{% tabs %}
{% tab title="YAML" %}

```yaml
chmod +x create_apt_repo.sh
```

{% endtab %}
{% endtabs %}

### Option 1 — HTTP (Insecure)

#### Initialize the Repository

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./scripts/create_apt_repo.sh init
```

{% endtab %}
{% endtabs %}

#### Add Packages

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./scripts/create_apt_repo.sh add-bulk ./deb_packages
```

{% endtab %}
{% endtabs %}

#### Configure Clients (on each node)

{% tabs %}
{% tab title="Bash" %}

```bash
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak sudo mkdir -p /etc/apt/sources.list.d.bak sudo mv /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d.bak/ 2>/dev/null || true sudo rm /etc/apt/sources.list echo "deb [trusted=yes] http://<repo-host>/ stable main" | sudo tee /etc/apt/sources.list.d/private-repo.list sudo apt update
```

{% endtab %}
{% endtabs %}

### Option 2 — HTTPS (Secure, Recommended)

#### Initialize with Self-Signed Cert

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./scripts/create_apt_repo.sh init-https repo.local
```

{% endtab %}
{% endtabs %}

#### OR

Initialize with Custom Cert + Key

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./scripts/create_apt_repo.sh init-https repo.example.com /path/to/cert.pem /path/to/key.pem
```

{% endtab %}
{% endtabs %}

#### Add Packages

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./scripts/create_apt_repo.sh add-bulk ./deb_packages
```

{% endtab %}
{% endtabs %}

### Configure Clients (Install CA First If Required)

If the repository was created with a **self-signed certificate**, the generated cert will be available at:

`/etc/nginx/ssl/apt_repo.crt`

Copy this certificate to each client node and update the certificate store. If you used your own certificate when creating the APT repo, copy that certificate instead.

{% tabs %}
{% tab title="Bash" %}

```bash
# Update CA certificates 
sudo cp /path/to/apt_repo.crt /usr/local/share/ca-certificates/ 
sudo update-ca-certificates 
# Backup existing sources list
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak sudo mkdir -p /etc/apt/sources.list.d.bak sudo mv /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d.bak/ 2>/dev/null || true sudo rm /etc/apt/sources.list 
# Add private repo over HTTPS 
echo "deb https://<repo-host> stable main" | sudo tee /etc/apt/sources.list.d/private-repo.list 
# Refresh repo 
sudo apt update
```

{% endtab %}
{% endtabs %}

## Setup Image Repository

Script to setup a Sample Image Repository on a Node:

{% tabs %}
{% tab title="Bash" %}

```bash
#!/bin/bash
set -e

# === Functions ===

function get_input() {
  local PROMPT="$1"
  local VARIABLE_NAME="$2"
  local OUTPUT

  read -r -p "$PROMPT " OUTPUT
  eval "$VARIABLE_NAME='$OUTPUT'"
}

# === Variables ===

get_input "Enter the registry host IP address" "REGISTRY_HOST"
get_input "Enter the registry domain" "REGISTRY_DOMAIN"
get_input "Enter the username" "USERNAME"
get_input "Enter the password" "PASSWORD"

CERT_DIR="$HOME/certs"
AUTH_DIR="$HOME/auth"
DATA_DIR="$HOME/registry-data"
OPENSSL_CONF="$CERT_DIR/openssl.conf"

echo "[INFO] Using registry password: $PASSWORD"

echo "[Step 1] Installing dependencies..."
sudo apt update && sudo apt -y install docker.io jq apache2-utils
sudo chown "$USER" /var/run/docker.sock

echo "[Step 2] Creating certs directory..."
mkdir -p "$CERT_DIR"
cd "$CERT_DIR"

echo "[Step 2.1] Writing OpenSSL config..."
cat << EOF > "$OPENSSL_CONF"
[req]
distinguished_name = req_distinguished_name
prompt             = no
x509_extensions    = ca_x509_extensions

[ca_x509_extensions]
basicConstraints = CA:TRUE
keyUsage         = cRLSign, keyCertSign

[req_distinguished_name]
C   = US
ST  = Washington
L   = Seattle
CN  = CA

[registry]
distinguished_name = registry_distinguished_name
prompt             = no
req_extensions     = registry_req_extensions

[registry_distinguished_name]
C   = US
ST  = Washington
L   = Seattle
CN  = $REGISTRY_DOMAIN

[registry_req_extensions]
basicConstraints     = CA:FALSE
extendedKeyUsage     = clientAuth, serverAuth
keyUsage             = critical, digitalSignature, keyEncipherment
nsCertType           = client, server
nsComment            = "Docker Registry Certificate"
subjectAltName       = DNS:$REGISTRY_DOMAIN
subjectKeyIdentifier = hash
EOF

echo "[Step 2.2] Generating CA key and cert..."
openssl genrsa -out ca.key 4096
openssl req -x509 -new -sha512 -noenc -key ca.key -days 3653 -config "$OPENSSL_CONF" -out ca.crt

echo "[Step 2.3] Generating registry key and cert signed by CA..."
openssl genrsa -out registry.key 4096
openssl req -new -key registry.key -sha256 -config "$OPENSSL_CONF" -section registry -out registry.csr
openssl x509 -req -days 3653 -in registry.csr -copy_extensions copyall -sha256 \
  -CA ca.crt -CAkey ca.key -CAcreateserial -out registry.crt

cd ~

echo "[Step 3] Setting up authentication..."
mkdir -p "$AUTH_DIR"
htpasswd -Bc "$AUTH_DIR/htpasswd" "$USERNAME" <<< "$PASSWORD"

echo "[Step 4] Creating data directory..."
mkdir -p "$DATA_DIR"

echo "[Step 5] Running Docker registry..."
docker rm -f registry || true
docker run -d \
  --restart=always \
  --name registry \
  -v "$CERT_DIR:/certs" \
  -v "$AUTH_DIR:/auth" \
  -v "$DATA_DIR:/var/lib/registry" \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
  -p 443:443 \
  registry:2

echo "[Step 6] Adding CA cert to system trust..."
sudo cp "$CERT_DIR/ca.crt" /usr/local/share/ca-certificates
sudo update-ca-certificates

echo "[Step 7] Verifying registry..."
sleep 3
curl -u "$USERNAME:$PASSWORD" "https://$REGISTRY_DOMAIN/v2/_catalog" || true

echo "✅ Private Docker Registry setup complete!"
echo "   URL: https://$REGISTRY_DOMAIN"
echo "   User: $USERNAME"
echo "   Password: $PASSWORD"
```

{% endtab %}
{% endtabs %}

Testing the Registry Setup (on the same registry node):

{% tabs %}
{% tab title="Bash" %}

```bash
docker ps -a cat << EOF >> /etc/hosts <ip> registry.pf9.io EOF curl -u admin:Welcome@PF9123 https://registry.pf9.io/v2/_catalog -k sudo cp /home/ubuntu/certs/ca.crt /usr/local/share/ca-certificates sudo update-ca-certificates curl -u admin:<password> https://registry.pf9.io/v2/_catalog
```

{% endtab %}
{% endtabs %}

## Upload Packages to APT Repo

If you’re hosting your own APT repo:

1. Ensure all packages in dependency\_list.txt are present.
2. Regenerate repo metadata (Packages.gz, Release, InRelease) after any changes.

If using the provided script:

From the repo host, at the repo project root

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./create_apt_repo.sh add-bulk ./deb_packages
```

{% endtab %}
{% endtabs %}

If your .deb files are in another folder:

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ./create_apt_repo.sh add-bulk /path/to/your/deb_dir
```

{% endtab %}
{% endtabs %}

The script will:

* Copy all .deb files into /var/www/html/my-private-apt-repo/pool/main/
* Regenerate Packages.gz and Release

#### On all the client nodes:

{% tabs %}
{% tab title="Bash" %}

```bash
sudo apt update
```

{% endtab %}
{% endtabs %}

## Upload Images to Private Registry

The script used to pull the required images and push them to the custom registry is downloaded as part of the prerequisites. *(Note: Docker must be installed for this script to run.)*

Additionally, the PCD-V and PCD-K image lists are also downloaded during the prerequisite steps.

## Install OpenSSL

On a **jumphost with internet access**:

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<YOUR_USER_AGENT_KEY>" \ https://pf9-airctl.s3-accelerate.amazonaws.com/openssl-smcp-ubuntu/openssl_3.0.7-1_amd64.deb \ --output openssl_3.0.7-1_amd64.deb
```

{% endtab %}
{% endtabs %}

Copy to all nodes and install:

{% tabs %}
{% tab title="Bash" %}

```bash
sudo dpkg -i openssl_3.0.7-1_amd64.deb sudo tee -a /etc/ld.so.conf.d/openssl-3.0.7.conf << EOF /usr/local/ssl/lib64 EOF sudo ldconfig -v sudo sed -i 's#^PATH="\([^"]*\)"#PATH="\1:/usr/local/ssl/bin"#' /etc/environment source /etc/environment sudo ln -sf /usr/local/ssl/bin/openssl /usr/bin/openssl openssl version
```

{% endtab %}
{% endtabs %}

Expected output:

`OpenSSL 3.0.7 1 Nov 2022 (Library: OpenSSL 3.0.7 1 Nov 2022)`

## Download Airctl Artifacts

Run on a **jumphost with internet access**:

{% tabs %}
{% tab title="Bash" %}

```bash
curl --user-agent "<YOUR_USER_AGENT_KEY>" \ https://pf9-airctl.s3-accelerate.amazonaws.com/latest/index.txt | \ awk '{print "curl -sS --user-agent \"<YOUR_USER_AGENT_KEY>\" \"https://pf9-airctl.s3-accelerate.amazonaws.com/latest/" $NF "\" -o ${HOME}/" $NF}' | bash
```

{% endtab %}
{% endtabs %}

Copy all fetched artifacts to one of the master nodes.

## Configure and Deploy PCD

Run the installer script:

{% tabs %}
{% tab title="Bash" %}

```bash
chmod +x ./install-pcd.sh ./install-pcd.sh $(cat version.txt)
```

{% endtab %}
{% endtabs %}

Create a binary symlink:

{% tabs %}
{% tab title="Bash" %}

```bash
sudo ln -sf /opt/pf9/airctl/airctl /usr/local/bin/airctl airctl --version
```

{% endtab %}
{% endtabs %}

Configure Airctl:

{% tabs %}
{% tab title="Bash" %}

```bash
airctl configure \ -4 \ -f <fqdn> \ -e 10.149.101.50 \ -i 10.149.101.242,10.149.101.41,10.149.101.172 \ --master-vip4 10.149.101.193 \ -v ens3 \ -r Region1 \ -k 10.149.106.253 \ -n /mnt/gnocchi \ -p hostpath-provisioner \ --custom-registry-url https://registry.pf9.io \ --custom-registry-username admin \ --custom-registry-password "<password>" \ --custom-registry-ca-cert-path /home/ubuntu/jumphost/ca.crt \ --custom-registry-path-overrides \ --enable-pcd-chart-bundle \ --verbose
```

{% endtab %}
{% endtabs %}

Validate configuration:

{% tabs %}
{% tab title="Bash" %}

```bash
airctl check --config /opt/pf9/airctl/conf/airctl-config.yaml
```

{% endtab %}
{% endtabs %}

Deploy components:

{% tabs %}
{% tab title="Bash" %}

```bash
airctl create-cluster --config /opt/pf9/airctl/conf/airctl-config.yaml --verbose airctl start --config /opt/pf9/airctl/conf/airctl-config.yaml
```

{% endtab %}
{% endtabs %}

## Hypervisor Onboarding

Ensure each host has the private APT configured and prerequisites installed.

* In the DU, navigate to **Infrastructure → Cluster Hosts** and click **Add Hosts**.
* Follow the instructions to onboard.

####


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.platform9.com/private-cloud-director/2025.8/getting-started/self-hosted/self-hosted-airgap-install.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
