441 Redirect SHH to jails

Use case

Create multiple jails with auto UUID names. In the inventory, compose the variables ansible_host and ansible_port to connect the jails via the redirected SSH ports. See the example 440 Configure DHCP and pf how pf is configured.

Tree

shell> tree .
.
├── ansible.cfg
├── hosts
│   ├── 05_iocage.yml
│   └── 99_constructed.yml
├── host_vars
│   └── iocage_05.yml
├── iocage.ini
└── pb-test.yml

Synopsis

  • At a managed node:

    In the playbook vbotka.freebsd.pb_iocage_ansible_clients.yml:

    • create jails

    • start jails

    • optionally, stop and destroy the jails.

  • Create dynamic inventory to redirect SSH to the jails.

  • At all created jails:

    In the playbook pb-test.yml:

    • connect to the created jails

    • display basic configuration of the jails.

Requirements

Notes

The only difference between this example and the example 442 Connection jailexec instead of ssh are the following two lines in the file hosts/05_iocage.yml

ansible_host: dict(iocage_properties.notes | regex_findall('(\w+)=([\w\-]+)')).vmm
ansible_port: iocage_ip4 | split('.') | last | int - 100 + 2200

See also

example 050 Connection jailexec

Templates at iocage_05

[iocage_05]# iocage list -lt
+------+-----------------------+------+-------+----------+--------------+--------------------+-----+----------+----------+
| JID  |         NAME          | BOOT | STATE |   TYPE   |   RELEASE    |        IP4         | IP6 | TEMPLATE | BASEJAIL |
+======+=======================+======+=======+==========+==============+====================+=====+==========+==========+
| None | ansible_client        | off  | down  | template | 15.0-RELEASE | DHCP (not running) | -   | -        | no       |
+------+-----------------------+------+-------+----------+--------------+--------------------+-----+----------+----------+
| None | ansible_client_apache | off  | down  | template | 15.0-RELEASE | DHCP (not running) | -   | -        | no       |
+------+-----------------------+------+-------+----------+--------------+--------------------+-----+----------+----------+
| None | ansible_client_pull   | off  | down  | template | 15.0-RELEASE | DHCP (not running) | -   | -        | no       |
+------+-----------------------+------+-------+----------+--------------+--------------------+-----+----------+----------+

ansible.cfg

[defaults]
callback_result_format = yaml
deprecation_warnings = false
display_skipped_hosts = false
gathering = explicit

[connection]
pipelining = true

Inventory iocage.ini

iocage_05

[iocage]
iocage_05

[iocage:vars]
ansible_user=admin
ansible_become=true
ansible_python_interpreter=auto_silent

host_vars

host_vars/iocage_05.yml
properties:
  bpf: 1
  dhcp: 1
  vnet: 1
  notes: "vmm={{ inventory_hostname }}"
  defaultrouter: 10.10.99.1

swarms:
  sw_01:
    count: 3
    template: ansible_client

Hint

If the default iocage option defaultrouter=auto doesn’t work set it. This may be needed if the jails are provided with the DHCP on the bridge. In this case, the defaultrouter for the jails is the IP address of the bridge. pf must provide NAT and redirection. See example 440 Configure DHCP and pf.

Playbook output - Create and start swarms

(env) > ansible-playbook vbotka.freebsd.pb_iocage_ansible_clients.yml \
                         -i iocage.ini \
                         -t swarm \
                         -e swarm=true \
                         -e debug=true
PLAY [Create and start jails. Optionally stop and destroy jails.] **************

TASK [Get iocage facts] ********************************************************
ok: [iocage_05]

TASK [Reject jails with empty notes] *******************************************
ok: [iocage_05]

TASK [Debug cmd_create debug=true] *********************************************
ok: [iocage_05] => (item=sw_01) => 
    msg: |-
        iocage create --short --template ansible_client --count 3 bpf=1 defaultrouter=10.10.99.1 dhcp=1 notes="vmm=iocage_05 swarm=sw_01" vnet=1

TASK [Create swarms] ***********************************************************
ok: [iocage_05] => (item=sw_01)

TASK [Debug create swarms debug=true] ******************************************
ok: [iocage_05] => 
    out:
        changed: false
        msg: All items completed
        results:
        -   ansible_loop_var: item
            changed: false
            cmd:
            - iocage
            - create
            - --short
            - --template
            - ansible_client
            - --count
            - '3'
            - bpf=1
            - defaultrouter=10.10.99.1
            - dhcp=1
            - notes=vmm=iocage_05 swarm=sw_01
            - vnet=1
            delta: '0:00:00.830750'
            end: '2026-05-13 11:04:13.243889'
            failed: false
            invocation:
                module_args:
                    _raw_params: null
                    _uses_shell: false
                    argv: null
                    chdir: null
                    cmd: |-
                        iocage create --short --template ansible_client --count 3 bpf=1 defaultrouter=10.10.99.1 dhcp=1 notes="vmm=iocage_05 swarm=sw_01" vnet=1
                    creates: null
                    executable: null
                    expand_argument_vars: true
                    removes: null
                    stdin: null
                    stdin_add_newline: true
                    strip_empty_ends: true
            item:
                key: sw_01
                value:
                    count: 3
                    template: ansible_client
            msg: ''
            rc: 0
            start: '2026-05-13 11:04:12.413139'
            stderr: ''
            stderr_lines: []
            stdout: |-
                cbe505b1 successfully created!
                f4073d63 successfully created!
                4a8426e6 successfully created!
            stdout_lines:
            - cbe505b1 successfully created!
            - f4073d63 successfully created!
            - 4a8426e6 successfully created!
        skipped: false

TASK [Get iocage facts] ********************************************************
ok: [iocage_05]

TASK [Reject jails with empty notes] *******************************************
ok: [iocage_05]

TASK [Debug cmd_start debug=true] **********************************************
ok: [iocage_05] => (item=sw_01) => 
    msg: |-
        iocage start 4a8426e6 cbe505b1 f4073d63

TASK [Start swarms] ************************************************************
ok: [iocage_05] => (item=sw_01)

TASK [Debug start swarms debug=true] *******************************************
ok: [iocage_05] => 
    out:
        changed: false
        msg: All items completed
        results:
        -   ansible_loop_var: item
            changed: false
            cmd:
            - iocage
            - start
            - 4a8426e6
            - cbe505b1
            - f4073d63
            delta: '0:00:08.396317'
            end: '2026-05-13 11:04:27.719685'
            failed: false
            invocation:
                module_args:
                    _raw_params: null
                    _uses_shell: false
                    argv: null
                    chdir: null
                    cmd: |-
                        iocage start 4a8426e6 cbe505b1 f4073d63
                    creates: null
                    executable: null
                    expand_argument_vars: true
                    removes: null
                    stdin: null
                    stdin_add_newline: true
                    strip_empty_ends: true
            item:
                key: sw_01
                value:
                    count: 3
                    template: ansible_client
            msg: ''
            rc: 0
            start: '2026-05-13 11:04:19.323368'
            stderr: |-
                No default gateway found for ipv6.
                No default gateway found for ipv6.
                No default gateway found for ipv6.
            stderr_lines:
            - No default gateway found for ipv6.
            - No default gateway found for ipv6.
            - No default gateway found for ipv6.
            stdout: |-
                * Starting 4a8426e6
                  + Started OK
                  + Using devfs_ruleset: 1000 (iocage generated default)
                  + Configuring VNET OK
                  + Using IP options: vnet
                  + Starting services OK
                  + Executing poststart OK
                  + DHCP Address: 10.10.99.130/24
                * Starting cbe505b1
                  + Started OK
                  + Using devfs_ruleset: 1001 (iocage generated default)
                  + Configuring VNET OK
                  + Using IP options: vnet
                  + Starting services OK
                  + Executing poststart OK
                  + DHCP Address: 10.10.99.161/24
                * Starting f4073d63
                  + Started OK
                  + Using devfs_ruleset: 1002 (iocage generated default)
                  + Configuring VNET OK
                  + Using IP options: vnet
                  + Starting services OK
                  + Executing poststart OK
                  + DHCP Address: 10.10.99.162/24
            stdout_lines:
            - '* Starting 4a8426e6'
            - '  + Started OK'
            - '  + Using devfs_ruleset: 1000 (iocage generated default)'
            - '  + Configuring VNET OK'
            - '  + Using IP options: vnet'
            - '  + Starting services OK'
            - '  + Executing poststart OK'
            - '  + DHCP Address: 10.10.99.130/24'
            - '* Starting cbe505b1'
            - '  + Started OK'
            - '  + Using devfs_ruleset: 1001 (iocage generated default)'
            - '  + Configuring VNET OK'
            - '  + Using IP options: vnet'
            - '  + Starting services OK'
            - '  + Executing poststart OK'
            - '  + DHCP Address: 10.10.99.161/24'
            - '* Starting f4073d63'
            - '  + Started OK'
            - '  + Using devfs_ruleset: 1002 (iocage generated default)'
            - '  + Configuring VNET OK'
            - '  + Using IP options: vnet'
            - '  + Starting services OK'
            - '  + Executing poststart OK'
            - '  + DHCP Address: 10.10.99.162/24'
        skipped: false

PLAY RECAP *********************************************************************
iocage_05                  : ok=10   changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0   

Jails at iocage_05

[iocage_05]# iocage list -l
+-----+----------+------+-------+------+--------------+----------------------+-----+----------------+----------+
| JID |   NAME   | BOOT | STATE | TYPE |   RELEASE    |         IP4          | IP6 |    TEMPLATE    | BASEJAIL |
+=====+==========+======+=======+======+==============+======================+=====+================+==========+
| 31  | 4a8426e6 | off  | up    | jail | 15.0-RELEASE | epair0b|10.10.99.130 | -   | ansible_client | no       |
+-----+----------+------+-------+------+--------------+----------------------+-----+----------------+----------+
| 32  | cbe505b1 | off  | up    | jail | 15.0-RELEASE | epair0b|10.10.99.161 | -   | ansible_client | no       |
+-----+----------+------+-------+------+--------------+----------------------+-----+----------------+----------+
| 33  | f4073d63 | off  | up    | jail | 15.0-RELEASE | epair0b|10.10.99.162 | -   | ansible_client | no       |
+-----+----------+------+-------+------+--------------+----------------------+-----+----------------+----------+

Inventory hosts

hosts/05_iocage.yml
plugin: vbotka.freebsd.iocage
host: iocage_05
user: admin
sudo: true
get_properties: true
hooks_results:
  - /var/db/dhclient-hook.address.epair0b
compose:
  ansible_host: dict(iocage_properties.notes | regex_findall('(\w+)=([\w\-]+)')).vmm | d('none')
  ansible_port: iocage_ip4 | split('.') | last | int - 100 + 2200
  ansible_user: "'admin'"
  ansible_python_interpreter: "'auto_silent'"
  iocage_tags: dict(iocage_properties.notes | regex_findall('(\w+)=([\w\-]+)'))

Note

  • In the example 440 Configure DHCP and pf, the variables ssh_rdr_start=2200 and dhcp_ip_start=100 are used in the playbook pb-pf-setup.yml to calculate the ports to redirect SSH from, and to create the file pf-rdr-ssh.conf.

  • For example, from the controller, the following command connects to the jail at <bsd_dhcpd_subnet>.106:

    shell> ssh -p 2206 admin@iocage_05
    
  • See the variable bsd_dhcpd_subnet in the example 440 Configure DHCP and pf

hosts/99_constructed.yml
plugin: ansible.builtin.constructed
keyed_groups:
  - prefix: swarm
    key: iocage_tags.swarm
  - prefix: vmm
    key: iocage_tags.vmm

Display inventory

(env) > ansible-inventory -i hosts --graph
@all:
  |--@ungrouped:
  |--@swarm_sw_01:
  |  |--4a8426e6
  |  |--cbe505b1
  |  |--f4073d63
  |--@vmm_iocage_05:
  |  |--4a8426e6
  |  |--cbe505b1
  |  |--f4073d63

Playbook pb-test.yml

- name: Test the SSH redirection to the jails.
  hosts: swarm_sw_01
  gather_facts: false
    
  tasks:
    
    - ansible.builtin.debug:
        msg: |
          ansible_connection: {{ ansible_connection }}
          ansible_host: {{ ansible_host }}
          ansible_port: {{ ansible_port }}
          ansible_user: {{ ansible_user }}

          iocage_ip4: {{ iocage_ip4 }}
          iocage_tags: {{ iocage_tags }}

Playbook output - Test SSH redirection

(env) > ansible-playbook pb-test.yml -i hosts
PLAY [Test the SSH redirection to the jails.] **********************************

TASK [ansible.builtin.debug] ***************************************************
ok: [4a8426e6] => 
    msg: |-
        ansible_connection: ssh
        ansible_host: iocage_05
        ansible_port: 2230
        ansible_user: admin

        iocage_ip4: 10.10.99.130
        iocage_tags: {'vmm': 'iocage_05', 'swarm': 'sw_01'}
ok: [cbe505b1] => 
    msg: |-
        ansible_connection: ssh
        ansible_host: iocage_05
        ansible_port: 2261
        ansible_user: admin

        iocage_ip4: 10.10.99.161
        iocage_tags: {'vmm': 'iocage_05', 'swarm': 'sw_01'}
ok: [f4073d63] => 
    msg: |-
        ansible_connection: ssh
        ansible_host: iocage_05
        ansible_port: 2262
        ansible_user: admin

        iocage_ip4: 10.10.99.162
        iocage_tags: {'vmm': 'iocage_05', 'swarm': 'sw_01'}

PLAY RECAP *********************************************************************
4a8426e6                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
cbe505b1                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
f4073d63                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Hint

The below play stops and destroys the jails in swarms

ansible-playbook vbotka.freebsd.pb_iocage_ansible_clients.yml \
                 -i iocage.ini \
                 -t swarm_destroy \
                 -e swarm_destroy=true