aoh

A tool for running Ansible playbooks with `curl <url> | python` commands
git clone https://git.ashermorgan.net/aoh/
Log | Files | Refs | README

Run an Ansible playbook with a single curl <aoh server url> | python command.

More specifically, this triggers an ansible-playbook run on the AoH server, with the local client fetching commands for its own tasks over HTTP. This is especially useful for machines that are behind a NAT or firewall (thus preventing the use of ansible-playbook directly), but also either run Windows or aren't trusted with full repository access (thus preventing the use of ansible-pull). For example, AoH can be used to easily run a dotfile installation playbook on new machines, anytime, anywhere.

Comparison with Traditional Ansible & Ansible Pull

Ansible AoH Ansible Pull
Architecture Push Pull Pull
Server requirements ansible-playbook aoh Any git server
Client requirements python python ansible-pull
Ansible controller Server Server Client
Client has full repository access No No Yes
Ansible connection method Usually SSH HTTP Local connection

Note that Ansible controllers don't support Windows, so the ansible-playbook, aoh, and ansible-pull programs all must be run on a Unix-based system.

Getting Started

First, run the AoH server docker image.

$ docker run --rm --detach --name aoh -p 8000:8000 ghcr.io/ashermorgan/aoh

Then run the demo playbook via the client Python script. The demo AoH password is hunter2.

$ curl -s 127.0.0.1:8000/run | python3
AoH password:

PLAY [Demo play] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [my-host]

TASK [Write message to ~/aoh-demo.txt] *****************************************
--- before
+++ after: /home/me/.ansible/tmp/ansible-local-23520hhz8owqd/tmpddlm2qex/message.j2
@@ -0,0 +1,8 @@
+Welcome to AoH!
+
+The server is running this playbook with ansible-playbook, and your client is
+fetching commands for its own tasks over HTTP.
+
+Username: me
+Hostname: my-host
+Timestamp: 2026-09-12 22:58:10 UTC

changed: [my-host]

PLAY RECAP *********************************************************************
my-host                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Clients can pass options to control which playbook is executed and what ansible-playbook arguments are used. Use the --help flag to view the CLI syntax.

$ curl -s 127.0.0.1:8000/run | python3 - --help
Usage: curl -s http://127.0.0.1:8000/run | python3 - [-h] [<playbook>] [<opts>...]

Runs Ansible playbooks on a remote server and fetches commands for the local
host's tasks over HTTP.

Arguments:
  playbook          The name of the playbook to run (defaults to "main.yml")

Options:
  -h, --help        Show this help message and exit
  <opts>            Any (server-approved) ansible-playbook(1) options

Windows clients are also supported. Just use the Invoke-RestMethod PowerShell cmdlet instead of curl.

PS> irm http://<server IP>:8000 | python3

Finally, stop the AoH server when you're done using it.

$ docker stop aoh

Next steps for deploying AoH include:

Configuration

Environment Variables

AoH supports the following configuration options, which are set via environment variables.

Playbooks

AoH only executes playbooks that are listed in the file specified by the $AOH_PLAYBOOKS_FILE option. Below is an example playbooks file that documents the required structure and available options.

main.yml: # The playbook name

  # The path to the playbook. Relative paths are interpreted as relative to
  # $AOH_PLAYBOOKS_FILE. Defaults to the playbook name.
  path: my-playbook.yml

  # The hostname to assign to clients. If set to null, clients are assigned the
  # hostname that they report for themselves, which could be spoofed. Defaults
  # to null.
  host: my-host

  # The groups to assign to clients, in addition to the "aoh" group and any
  # groups defined in inventory files.
  groups:
    - group1
    - group2

  # A dictionary of environment variables to pass to ansible-playbook. Note that
  # the ANSIBLE_FORCE_COLOR and ANSIBLE_NOCOLOR variables are not supported here
  # as they are parsed client-side.
  env:
    ANSIBLE_PIPELINING: 'true'

  # Whether to restrict play execution to the AoH client only using
  # ansible-playbook's --limit option. If enabled, the --limit option must not
  # be present in allow_opts or extra_args. Defaults to true.
  limit: true

  # Whether to require a password to execute the playbook. Passwords must be
  # specified separately in the $AOH_PASSWORDS_FILE file (see the playbook
  # passwords section below). Defaults to false.
  password: true

  # Whether ansible-playbook output is sent to the client. Defaults to true.
  output: true

  # A list of ansible-playbooks options that users are allowed to invoke. Short
  # and long option forms must be specified separately. The following options
  # are allowed by default:
  #   --ask-become-pass / -K
  #   --ask-pass / -k
  #   --ask-vault-password / --ask-vault-pass / -J
  #   --check / -C
  #   --diff / -D
  #   --list-tags
  #   --skip-tags
  #   --start-at-task
  #   --tags / -t
  allow_opts:
    - '--become-method'

  # A list of ansible-playbook options that users are not allowed to invoke,
  # overriding the default allowed options.
  block_opts:
    - '--list-tags'

  # Whether to allow clients to use Jinja expressions in ansible-playbook
  # arguments. Defaults to false.
  jinja_args: false

  # A list of additional arguments to pass to ansible-playbook. These are not
  # subject to the allow_opts/block_opts/jinja_args restrictions. Note that the
  # --inventory option is not supported and the env.ANSIBLE_INVENTORY playbook
  # option should be used instead for specifying inventory files.
  extra_args:
    - '--diff'

  # The description displayed on the playbook web page, which is located at
  # /<playbook name>. Defaults to null, which disables the playbook web page.
  web_description: 'Run the main.yml playbook with a single command:'

another-playbook.yml:
  # and so on...

Playbook Passwords

Passwords for password-protected playbooks must be included as bcrypt hashes in the file specified by the $AOH_PASSWORDS_FILE option. For example:

# Require the password "hunter2" to execute the main.yml playbook
main.yml: $2a$14$LfC6Bcczc0WM.OObqgQPYeVvYa10g1Z4Z8C.eERyd.GarPmNa5/ve

There are many tools for generating bcrypt hashes. Here is a one-liner that uses the same bcrypt Python library that AoH depends on internally:

$ python3 -c 'import bcrypt, getpass; print(bcrypt.hashpw(getpass.getpass().encode(), bcrypt.gensalt()).decode())'

Security

Like both Ansible and Ansible Pull, AoH clients must completely trust the server and its playbooks, since they are allowed to execute arbitrary commands. AoH does not attempt to protect clients from malicious servers.

Unlike both Ansible and Ansible Pull however, AoH introduces the risk of clients influencing the execution of commands on the server. AoH implements various protections against this risk by default, including blocking potentially unsafe ansible-playbook arguments and limiting play execution to the AoH client. Additional protections are available via playbook options such as host, password, and output.

The following security measures are recommended as a baseline when deploying AoH:

Limitations

Development

To run AoH locally for development, first install the Python dependencies and set core configuration options via a .env file.

$ pip install -r requirements.txt
$ cp .env.example .env

Then start the AoH server by running the aoh module.

$ python3 -m aoh

Finally, open another terminal and run a playbook via the client CLI.

$ curl -s 127.0.0.1:5000/run | python3

AoH also has unit tests and end-to-end tests, which can be run with the following commands. The end-to-end tests require Docker.

$ python3 -m unittest tests.unit
$ ./tests/e2e/run.sh