Offline (Air-Gapped) Installation¶
Add execution or hop nodes without SSH access from the control host to the target nodes.
When to Use¶
- Control host cannot SSH to execution nodes (security policy)
- Execution nodes in isolated network segment
- Strict egress-only policies on EN networks
- Compliance requirements prevent direct access
- Hybrid cloud: controller in cloud, ENs on-premises
- Customer policy prohibits SSH from external hosts
Workflow Overview¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONTROL HOST │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ ansible-playbook generate_bundle.yml │ │
│ │ - SSH to controller (fetch CA, pre-register instance) │ │
│ │ - Mint TLS certificates for target node │ │
│ │ - Create self-contained archive │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ offline-bundle-exec1.tar.gz │
└─────────────────────────────────────│───────────────────────────────────────┘
│
[Transfer via approved channel]
(USB, secure file transfer, etc.)
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ EXECUTION NODE │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ tar -xzf offline-bundle-exec1.tar.gz │ │
│ │ cd offline-bundle-exec1 │ │
│ │ ansible-playbook -c local install.yml │ │
│ │ - Install receptor from bundle │ │
│ │ - Configure TLS from pre-minted certs │ │
│ │ - Start receptor service │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Preflight Checks¶
Offline bundle generation runs controller-side preflight only: - SSH to controller - Controller task container running - AIO disruption warning (if local-only) - Required variables provided
Not checked (no SSH to target): - Target node SSH access - Target node DNS resolution - Target node firewall/connectivity - Machine-ID uniqueness
Skip preflight: -e aap_add_node_preflight_enabled=false
Prerequisites¶
Control Host¶
- SSH access to controller (to fetch mesh CA and pre-register instance)
aap_setup_dirpointing to installer directory- Python cryptography library (
pip install cryptography)
Target Execution Node¶
- RHEL 9 or 10 with valid subscription
podmaninstalledansible-coreinstalled (to run the install playbook)- Receptor container image available:
- Pre-pulled:
podman pull registry.redhat.io/ansible-automation-platform-26/receptor-rhel9:latest - Or included in bundle with
aap_add_node_bundle_include_images=true
Note: Image path varies by AAP version:
- AAP 2.5: ansible-automation-platform-25/receptor-rhel8
- AAP 2.6: ansible-automation-platform-26/receptor-rhel9
Step 1: Generate Bundle¶
Run on the control host (which has SSH to the controller):
cd /path/to/aap_add_node/collection
ansible-playbook playbooks/generate_bundle.yml \
-i inventory.yml \
-e aap_setup_dir=/path/to/ansible-automation-platform-containerized-setup-2.x \
-e _target_hostname=exec1.example.com \
-e _receptor_peers='["controller.example.com"]'
Variables¶
| Variable | Required | Default | Description |
|---|---|---|---|
_target_hostname |
Yes | - | FQDN of target execution node |
_receptor_peers |
Yes | - | JSON list of peer hostnames to connect to |
_target_type |
No | execution |
Node type: execution or hop |
_receptor_port |
No | 27199 |
Receptor TCP port |
aap_add_node_bundle_include_images |
No | false |
Include receptor container image (~500MB) |
aap_add_node_bundle_output_dir |
No | ./offline-bundles |
Output directory |
Output¶
Step 2: Transfer Bundle¶
Transfer the bundle to the target node via your approved secure channel:
- USB drive
- Secure file transfer (SFTP, SCP via jump host)
- Internal file share
- Sneakernet
Security note: The bundle contains TLS private keys. Handle with care and delete after installation.
Step 3: Install on Target Node¶
On the target execution node:
# Extract bundle
tar -xzf offline-bundle-exec1.example.com-2026-08-11.tar.gz
cd offline-bundle-exec1.example.com-2026-08-11
# Review README and vars.yml
cat README.md
cat vars.yml
# Run install (requires root or sudo)
sudo ansible-playbook -c local install.yml
Step 4: Verify¶
Wait 1-2 minutes for heartbeat, then verify:
# On the execution node
systemctl status receptor
journalctl -u receptor -f
podman logs receptor
# On the controller
podman exec automation-controller-task awx-manage list_instances
Look for green output with capacity=16 and recent heartbeat timestamp.
Check Controller UI: Administration → Topology — node should appear with "Ready" status.
Bundle Contents¶
offline-bundle-<hostname>-<date>/
├── README.md # Installation instructions
├── install.yml # Local install playbook
├── vars.yml # Configuration variables
├── receptor/
│ ├── receptor.conf # Receptor configuration
│ ├── work_public_key.pem # Work signature verification
│ └── tls/
│ ├── receptor.crt # Node TLS certificate
│ ├── receptor.key # Node TLS private key (SENSITIVE)
│ └── ca/
│ └── mesh-CA.crt # Mesh CA certificate
└── images/ # (Optional) Container images
├── receptor.tar.gz # Receptor image
└── README.txt # Image loading instructions
Including Container Images¶
For fully air-gapped environments where the target node cannot pull from a registry:
ansible-playbook playbooks/generate_bundle.yml \
-i inventory.yml \
-e aap_setup_dir=/path/to/setup \
-e _target_hostname=exec1.example.com \
-e _receptor_peers='["controller.example.com"]' \
-e aap_add_node_bundle_include_images=true
This increases bundle size from ~50KB to ~500MB but includes the receptor container image.
On the target node, the install playbook will automatically load the image from the bundle.
Hybrid Cloud Example¶
Controller in cloud, ENs on-premises, no SSH from control host to ENs:
# Generate bundle for hop node (install first)
ansible-playbook playbooks/generate_bundle.yml \
-i inventory.yml \
-e aap_setup_dir=/path/to/setup \
-e _target_hostname=hop-onprem.example.com \
-e _target_type=hop \
-e _receptor_peers='["aap-controller.cloud.example.com"]' \
-e aap_add_node_bundle_include_images=true
# Generate bundle for each on-prem EN
ansible-playbook playbooks/generate_bundle.yml \
-i inventory.yml \
-e aap_setup_dir=/path/to/setup \
-e _target_hostname=exec-onprem-01.example.com \
-e _receptor_peers='["hop-onprem.example.com"]' \
-e aap_add_node_bundle_include_images=true
Installation order: Transfer bundles to on-prem site, install hop first, then ENs.
Split Network with Jumpbox¶
When control host cannot directly SSH to both controller and execution node (different network segments, each behind a jumpbox):
Option 1: ProxyJump Inventory¶
If control host can reach jumpbox(es) that can reach target nodes:
# inventory-proxyjump.yml
all:
vars:
ansible_user: ansible
registry_username: "{{ lookup('env', 'REGISTRY_USERNAME') }}"
registry_password: "{{ lookup('env', 'REGISTRY_PASSWORD') }}"
children:
automationcontroller:
hosts:
controller.networkA.com:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ProxyJump=ansible@jumpboxA'
ansible_python_interpreter: /usr/bin/python3
execution_nodes:
hosts:
exec1.networkB.com:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ProxyJump=ansible@jumpboxB'
receptor_peers:
- controller.networkA.com
Tested: 2026-08-12 with macOS control host, RHEL9 jumpbox.
See also: examples/inventory-proxyjump.yml
Option 2: Offline Bundle (Recommended)¶
Better for strict network separation:
-
Generate bundle via JumpboxA to controller:
-
Transfer bundle through approved channel to EN
-
Install locally on EN (no SSH needed):
Note: Receptor mesh connectivity (TCP 27199) must still work between EN and controller at runtime.
Troubleshooting¶
Bundle generation fails¶
# Check SSH to controller
ssh controller.example.com "podman ps | grep controller"
# Check mesh material exists
ssh controller.example.com "ls ~/aap/receptor/etc/certs/"
Install playbook fails¶
# Check ansible is available
ansible --version
# Check podman is available
podman --version
# Check receptor image (AAP 2.6)
podman image exists registry.redhat.io/ansible-automation-platform-26/receptor-rhel9:latest
# List available receptor images
podman images | grep receptor
capacity=0 after installation¶
Check controller task logs for errors:
Common causes: - PermissionError on /.ansible_runner_uuid — Container HOME not set correctly - Unknown instance — Instance deprovisioned after bundle generation
No heartbeat after installation¶
# Check receptor is running
systemctl status receptor
podman logs receptor
journalctl -u receptor -f
# Check mesh routing tables in logs (should show peers)
podman logs receptor 2>&1 | grep -i "routing table"
# Check firewall
firewall-cmd --list-ports | grep 27199
Security Considerations¶
- TLS private keys in bundle — Handle bundle as sensitive material
- Delete after installation — Remove bundle from target node after successful install
- Secure transfer — Use approved secure channels for transfer
- Audit trail — Bundle generation is logged; document transfers in your change management
- Pre-registration — Instance is registered on controller during bundle generation
Related Documentation¶
- TOPOLOGY.md — Topology patterns and firewall requirements
- REQ-005 — Requirement specification
- REQ-006 — Hybrid cloud topology