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 ```