aoh

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

README.md (11849B)


      1 # AoH: Ansible-over-HTTP
      2 
      3 Run an Ansible playbook with a single `curl <aoh server url> | python` command.
      4 
      5 More specifically, this triggers an `ansible-playbook` run on the AoH server,
      6 with the local client fetching commands for its own tasks over HTTP. This is
      7 especially useful for machines that are behind a NAT or firewall (thus
      8 preventing the use of `ansible-playbook` directly), but also either run Windows
      9 or aren't trusted with full repository access (thus preventing the use of
     10 `ansible-pull`). For example, AoH can be used to easily run a dotfile
     11 installation playbook on new machines, anytime, anywhere.
     12 
     13 
     14 ## Comparison with Traditional Ansible & Ansible Pull
     15 
     16 |                                   | Ansible            | AoH         | Ansible Pull     |
     17 | --------------------------------- | ------------------ | ----------- | ---------------- |
     18 | Architecture                      | Push               | Pull        | Pull             |
     19 | Server requirements               | `ansible-playbook` | `aoh`       | Any git server   |
     20 | Client requirements               | `python`           | `python`    | `ansible-pull`   |
     21 | Ansible controller                | Server             | Server      | Client           |
     22 | Client has full repository access | No                 | No          | Yes              |
     23 | Ansible connection method         | Usually SSH        | HTTP        | Local connection |
     24 
     25 Note that Ansible controllers don't support Windows, so the `ansible-playbook`,
     26 `aoh`, and `ansible-pull` programs all must be run on a Unix-based system.
     27 
     28 
     29 ## Getting Started
     30 
     31 First, run the AoH server docker image.
     32 
     33 ```
     34 $ docker run --rm --detach --name aoh -p 8000:8000 ghcr.io/ashermorgan/aoh
     35 ```
     36 
     37 Then run the [demo playbook](demo/my-playbook.yml) via the client Python script.
     38 The demo AoH password is `hunter2`.
     39 
     40 ```
     41 $ curl -s 127.0.0.1:8000/run | python3
     42 AoH password:
     43 
     44 PLAY [Demo play] ***************************************************************
     45 
     46 TASK [Gathering Facts] *********************************************************
     47 ok: [my-host]
     48 
     49 TASK [Write message to ~/aoh-demo.txt] *****************************************
     50 --- before
     51 +++ after: /home/me/.ansible/tmp/ansible-local-23520hhz8owqd/tmpddlm2qex/message.j2
     52 @@ -0,0 +1,8 @@
     53 +Welcome to AoH!
     54 +
     55 +The server is running this playbook with ansible-playbook, and your client is
     56 +fetching commands for its own tasks over HTTP.
     57 +
     58 +Username: me
     59 +Hostname: my-host
     60 +Timestamp: 2026-09-12 22:58:10 UTC
     61 
     62 changed: [my-host]
     63 
     64 PLAY RECAP *********************************************************************
     65 my-host                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
     66 ```
     67 
     68 Clients can pass options to control which playbook is executed and what
     69 `ansible-playbook` arguments are used. Use the `--help` flag to view the CLI
     70 syntax.
     71 
     72 ```
     73 $ curl -s 127.0.0.1:8000/run | python3 - --help
     74 Usage: curl -s http://127.0.0.1:8000/run | python3 - [-h] [<playbook>] [<opts>...]
     75 
     76 Runs Ansible playbooks on a remote server and fetches commands for the local
     77 host's tasks over HTTP.
     78 
     79 Arguments:
     80   playbook          The name of the playbook to run (defaults to "main.yml")
     81 
     82 Options:
     83   -h, --help        Show this help message and exit
     84   <opts>            Any (server-approved) ansible-playbook(1) options
     85 ```
     86 
     87 Windows clients are also supported. Just use the `Invoke-RestMethod` PowerShell
     88 cmdlet instead of `curl`.
     89 
     90 ```
     91 PS> irm http://<server IP>:8000 | python3
     92 ```
     93 
     94 Finally, stop the AoH server when you're done using it.
     95 
     96 ```
     97 $ docker stop aoh
     98 ```
     99 
    100 Next steps for deploying AoH include:
    101 
    102 - Copy your own Ansible playbooks to the server
    103 - Create your own `playbooks.yml` and `passwords.yml` files and mount them under
    104   `/aoh/` (see the [playbooks](#playbooks) and [playbook
    105   passwords](#playbook-passwords) sections for reference)
    106 - Apply the recommended [security measures](#security)
    107 
    108 
    109 ## Configuration
    110 
    111 ### Environment Variables
    112 
    113 AoH supports the following configuration options, which are set via environment
    114 variables.
    115 
    116 - `AOH_DEBUG`: If set to `1`, the AoH server will copy `ansible-playbook` output
    117   to the server's stdout. Defaults to `0`.
    118 
    119 - `AOH_GC_INTERVAL`: The interval on which to clean up exited runners, in
    120   seconds. Defaults to `60`.
    121 
    122 - `AOH_GC_THRESHOLD`: The delay after a runner exits before is may be cleaned
    123   up, in seconds. This delay exists so that clients have time to fetch the final
    124   lines of Ansible output. Defaults to `60`.
    125 
    126 - `AOH_LOG_LEVEL`: The threshold for AoH logs, which are written to the server's
    127   stdout. Must be one of `CRITICAL`, `ERROR`, `WARNING`, `INFO`, or `DEBUG`.
    128   Defaults to `ERROR`.
    129 
    130 - `AOH_MAX_RUNNERS`: The maximum number of concurrent `ansible-playbook`
    131   sessions, or `0` for no limit. Defaults to `0`.
    132 
    133 - `AOH_ORIGIN`: The origin of the AoH server (e.g.
    134   `https://aoh.example.com:5000`). This must be set if AoH is running behind a
    135   reverse proxy. Defaults to the origin on which requests are received.
    136 
    137 - `AOH_PASSWORDS_FILE`: The file containing passwords for password-protected
    138   playbooks (see the [playbook passwords](#playbook-passwords) section below).
    139   Defaults to `/aoh/passwords.yml` when running the Docker image and
    140   `./passwords.yml` otherwise.
    141 
    142 - `AOH_PLAYBOOKS_FILE`: The file containing playbook configuration (see the
    143   [playbooks](#playbooks) section below). The parent directory of this file is
    144   also used as the working directory for all Ansible operations. Defaults to
    145   `/aoh/playbooks.yml` when running the Docker image and `./playbooks.yml`
    146   otherwise.
    147 
    148 
    149 ### Playbooks
    150 
    151 AoH only executes playbooks that are listed in the file specified by the
    152 `$AOH_PLAYBOOKS_FILE` option. Below is an example playbooks file that documents
    153 the required structure and available options.
    154 
    155 <!-- EXAMPLE COPIED FROM demo/playbooks.yml: -->
    156 
    157 ```yml
    158 main.yml: # The playbook name
    159 
    160   # The path to the playbook. Relative paths are interpreted as relative to
    161   # $AOH_PLAYBOOKS_FILE. Defaults to the playbook name.
    162   path: my-playbook.yml
    163 
    164   # The hostname to assign to clients. If set to null, clients are assigned the
    165   # hostname that they report for themselves, which could be spoofed. Defaults
    166   # to null.
    167   host: my-host
    168 
    169   # The groups to assign to clients, in addition to the "aoh" group and any
    170   # groups defined in inventory files.
    171   groups:
    172     - group1
    173     - group2
    174 
    175   # A dictionary of environment variables to pass to ansible-playbook. Note that
    176   # the ANSIBLE_FORCE_COLOR and ANSIBLE_NOCOLOR variables are not supported here
    177   # as they are parsed client-side.
    178   env:
    179     ANSIBLE_PIPELINING: 'true'
    180 
    181   # Whether to restrict play execution to the AoH client only using
    182   # ansible-playbook's --limit option. If enabled, the --limit option must not
    183   # be present in allow_opts or extra_args. Defaults to true.
    184   limit: true
    185 
    186   # Whether to require a password to execute the playbook. Passwords must be
    187   # specified separately in the $AOH_PASSWORDS_FILE file (see the playbook
    188   # passwords section below). Defaults to false.
    189   password: true
    190 
    191   # Whether ansible-playbook output is sent to the client. Defaults to true.
    192   output: true
    193 
    194   # A list of ansible-playbooks options that users are allowed to invoke. Short
    195   # and long option forms must be specified separately. The following options
    196   # are allowed by default:
    197   #   --ask-become-pass / -K
    198   #   --ask-pass / -k
    199   #   --ask-vault-password / --ask-vault-pass / -J
    200   #   --check / -C
    201   #   --diff / -D
    202   #   --list-tags
    203   #   --skip-tags
    204   #   --start-at-task
    205   #   --tags / -t
    206   allow_opts:
    207     - '--become-method'
    208 
    209   # A list of ansible-playbook options that users are not allowed to invoke,
    210   # overriding the default allowed options.
    211   block_opts:
    212     - '--list-tags'
    213 
    214   # Whether to allow clients to use Jinja expressions in ansible-playbook
    215   # arguments. Defaults to false.
    216   jinja_args: false
    217 
    218   # A list of additional arguments to pass to ansible-playbook. These are not
    219   # subject to the allow_opts/block_opts/jinja_args restrictions. Note that the
    220   # --inventory option is not supported and the env.ANSIBLE_INVENTORY playbook
    221   # option should be used instead for specifying inventory files.
    222   extra_args:
    223     - '--diff'
    224 
    225   # The description displayed on the playbook web page, which is located at
    226   # /<playbook name>. Defaults to null, which disables the playbook web page.
    227   web_description: 'Run the main.yml playbook with a single command:'
    228 
    229 another-playbook.yml:
    230   # and so on...
    231 ```
    232 
    233 
    234 ### Playbook Passwords
    235 
    236 Passwords for password-protected playbooks must be included as bcrypt hashes in
    237 the file specified by the `$AOH_PASSWORDS_FILE` option. For example:
    238 
    239 <!-- EXAMPLE COPIED FROM demo/passwords.yml: -->
    240 
    241 ```yml
    242 # Require the password "hunter2" to execute the main.yml playbook
    243 main.yml: $2a$14$LfC6Bcczc0WM.OObqgQPYeVvYa10g1Z4Z8C.eERyd.GarPmNa5/ve
    244 ```
    245 
    246 There are many tools for generating bcrypt hashes. Here is a one-liner that uses
    247 the same `bcrypt` Python library that AoH depends on internally:
    248 
    249 ```
    250 $ python3 -c 'import bcrypt, getpass; print(bcrypt.hashpw(getpass.getpass().encode(), bcrypt.gensalt()).decode())'
    251 ```
    252 
    253 
    254 ## Security
    255 
    256 Like both Ansible and Ansible Pull, AoH clients must completely trust the server
    257 and its playbooks, since they are allowed to execute arbitrary commands. AoH
    258 does not attempt to protect clients from malicious servers.
    259 
    260 Unlike both Ansible and Ansible Pull however, AoH introduces the risk of clients
    261 influencing the execution of commands on the server. AoH implements various
    262 protections against this risk by default, including blocking potentially unsafe
    263 `ansible-playbook` arguments and limiting play execution to the AoH client.
    264 Additional protections are available via playbook options such as `host`,
    265 `password`, and `output`.
    266 
    267 The following security measures are recommended as a baseline when deploying
    268 AoH:
    269 
    270 -  Ensure all Ansible playbooks are trusted and do not contain any potential
    271    security holes
    272 
    273 -  Run the AoH server behind a reverse proxy that enforces HTTPS (and set the
    274    `$AOH_ORIGIN` option accordingly)
    275 
    276 -  Restrict access for each playbook to authorized clients using the `password`
    277    playbook option
    278 
    279 -  Encrypt sensitive variables using Ansible Vault and require clients to supply
    280    the decryption password via Ansible's `--ask-vault-pass` option
    281 
    282 -  If clients are untrusted and an Ansible playbook contains tasks for multiple
    283    hosts, create separate entries in the AoH playbooks file for each host with
    284    the `host` option set explicitly to prevent hostname spoofing
    285 
    286 
    287 ## Limitations
    288 
    289 - AoH directs the client's tasks to be executed over HTTP by setting the
    290   `ansible_connection` option via a custom inventory host variable. This
    291   variable must not be overridden by, for example, variables in `host_vars/*`
    292   files. Refer to Ansible's [variable precedence
    293   documentation][ansible-precedence] for more details.
    294 
    295 - With the exception of become/connection/vault password prompts, interactive
    296   Ansible features are not supported. This includes the `--step` option, the
    297   `var_prompts` playbook section, and the `pause` module's `prompt` option.
    298 
    299 [ansible-precedence]: https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_variables.html#understanding-variable-precedence
    300 
    301 
    302 ## Development
    303 
    304 To run AoH locally for development, first install the Python dependencies and
    305 set core configuration options via a `.env` file.
    306 
    307 ```
    308 $ pip install -r requirements.txt
    309 $ cp .env.example .env
    310 ```
    311 
    312 Then start the AoH server by running the `aoh` module.
    313 
    314 ```
    315 $ python3 -m aoh
    316 ```
    317 
    318 Finally, open another terminal and run a playbook via the client CLI.
    319 
    320 ```
    321 $ curl -s 127.0.0.1:5000/run | python3
    322 ```
    323 
    324 AoH also has unit tests and end-to-end tests, which can be run with the
    325 following commands. The end-to-end tests require Docker.
    326 
    327 ```
    328 $ python3 -m unittest tests.unit
    329 $ ./tests/e2e/run.sh
    330 ```