Skip to content

OpenZiti Inventory Autodiscovery: Overview

Steven A. Broderick Elias edited this page Jan 5, 2023 · 5 revisions

Overview:

The community.openziti.connect_autodiscovery.py dynamic inventory plugin relies on a specific set of OpenZiti constructs, objects, and conventions to function correctly. There is some initial setup overhead involved when using this integration, and we outline here what is needed end-to-end that makes it all work.

The plugin's function is to provide the community.openziti.* connection plugins with the necessary variables used to automatically configure the SSH connection for targets discovered from OpenZiti's Edge API. In this setup, a ziti tunneling software (e.g. the ziti-edge-tunnel), proxies connections to the SSH server, which is configured to listen on localhost only.

Needed Conventions:

  • The OpenZiti identity name for the ansible target MUST match the inventory_hostname derived from other inventory sources.
  • An OpenZiti config-type named ansible-target-discovery.v1 must be defined, and a config that implements the schema must be assigned to the service that is hosted by the ziti tunneler.
  • An OpenZiti host.v1 config that fronts traffic to the SSH server. You may change the config to match your setup, but this integration relies on binding the name of the identity as known to the edge (that is, the inventory_hostname as described above). The option "bindUsingEdgeIdentity": true can only be substituted to the equivalent "identity": "$tunneler_id.name"for the purposes of this integration.
  • Identities, assigned to Ansible controller users (generally, sysadmins), must have Dial permissions to the Openziti service. This is accomplished through a service-policy in OpenZiti.
  • The comunity.openziti.connect_autodiscovery plugin should be last in the list of enabled_plugins in the [inventory] section of the ansible.cfg
  • The magic string openziti_connection_autodiscovery must be in the list of inventory sources defined in the [defaults] section of the ansible.cfg
  • The plugin option ziti_identities must be satisfied with at least 1 ziti identity on the Ansible controller, which is used to discover the list of inventory targets. See: ansible-doc -t inventory community.openziti.connection_autodiscovery for details.

Needed Object Definitions:

  1. New OpenZiti config-type object named ansible-target-discovery.v1:
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "connection_plugin": {
      "type": "string",
      "enum": ["paramiko", "libssh"]
    }
  },
  "required": [
    "connection_plugin"
  ]
}
  1. Concrete config object from above:

Sample name: impl-ansible-target-discovery.v1

{
  "connection_plugin": "paramiko"
}
  1. Define host.v1 config object:

ansible-ssh-host.v1

{
  "address": "localhost",
  "port": 22,
  "protocol": "tcp",
  "listenOptions": {
  	"bindUsingEdgeIdentity": true
   }
}

Sample ansible.cfg snippet:

[defaults]
inventory = ./hosts.yaml, openziti_connection_autodiscovery

...

[inventory]
enable_plugins = host_list, script, auto, yaml, ini, toml, community.openziti.connection_autodiscovery

...

[openziti_connection]
ziti_identities = ./identities/site-0-ansible-admin-identity.json, ./identities/site-N-ansible-admin-identity.json

...

[openziti]
ziti_log_level = 0

Setup Guides:

Additional Information (Advanced):

This section is meant to give an account of the inner workings of this integration. It involves certain "advanced" topics in OpenZiti and Ansible. We describe the high level functionality of the integration, not the topics themselves.

The connect_autodiscovery.py is loaded when the magic string openziti_connection_autodiscovery is received by the BaseInventoryPlugin.verify_file() function implemented in our plugin class.

The plugin reads Ansible options to determine which ziti identities to read for the purposes of target discovery. Each identity is then used by the OpenAPI generated python module to communicate with the OpenZiti Edge API. The plugin uses a scoped session to identity services an identity has permission to Dial, which also have a config assigned to them of type ansible-target-discovery.v1. For these services, a dictionary is created for each identity that Binds the service. This dictionary yields information to each of the community.openziti.* connection plugins as follows:

Example (only relevant fields):

{
  "ansible-target-1": {
    "ansible_connection": "community.openziti.paramiko",
    "ziti_connection_dial_service": {
      "ziti_connection_identity_file": "./identities/site-0-ansible-admin-identity.json",
      "ziti_connection_service": "ansible-ssh",
      "ziti_connection_service_terminator": "ansible-target-1"
    }
  }
}

When the community.openziti.* connection plugins load, and the ziti_connection_dial_service varaible is defined, the plugin will instantiate a ZitiContext using the ziti-sdk-py library. This socket is then used for the connection to the target by leveraging OpenZiti addressable terminators. In order to accomplish this with minimal configuration, the connection plugins set the remote_addr option to something that is always resolvable, like 0.0.0.1, so that hostname resolution need not be congruent. Alternatively, connections to inventory_hostnames which happen to be OpenZiti intercept addresses are still intercepted normally.

Generally, the connection plugin classes utilize monkeypatched versions of the original connection plugins, which extend their functionality to hold some needed state, and operate over the Ziti socket for transport. This is a deliberate choice, so that all normal configurations for these plugins continue to work without modification or duplication.