aoh

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

commit aa63e565d20ad34ab98904d6b1367a55caaca405
parent cff275d906977393de1680ffa5a6288f0778683e
Author: Asher Morgan <asher@ashermorgan.net>
Date:   Sun, 16 Aug 2026 17:25:52 -0700

Handle arbitrary (whitelisted) Ansible args

Diffstat:
Maoh/install.py | 11+++++++++--
Maoh/runner.py | 8++++----
Maoh/server.py | 45++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 57 insertions(+), 7 deletions(-)

diff --git a/aoh/install.py b/aoh/install.py @@ -2,6 +2,7 @@ import base64 import subprocess +import sys import time import requests # TODO: eliminate dependency? @@ -50,9 +51,12 @@ def fetch(args): def main(): res = requests.post(f'{API}/runners/', json={ - 'host': 'myhost', + 'args': ' '.join(sys.argv[1:]), }) - assert res.status_code == 201 + if res.status_code != 201: + print(res.json()['err']) + sys.exit(1) + cookies = res.cookies job_url = f'{API}{res.headers['Location']}' @@ -64,6 +68,9 @@ def main(): print(res['logs'], end='') req = {} + if 'err' in res: + print(res['err']) + sys.exit(1) if res['finished']: break elif 'exec' in res: diff --git a/aoh/runner.py b/aoh/runner.py @@ -14,7 +14,7 @@ class AoHError(Exception): class AoHRunner: - def __init__(self, config, playbook, host): + def __init__(self, config, playbook, args): self.id = str(uuid4()) self._dir = tempfile.mkdtemp() self._LOGS_PATH = f'{self._dir}/artifacts/{self.id}/stdout' @@ -27,7 +27,7 @@ class AoHRunner: self._runner = None try: - self._start_runner(config, playbook, host) + self._start_runner(config, playbook, args) # We assume that ansible-runner will eventually create its log file while not os.path.exists(self._LOGS_PATH): @@ -48,7 +48,7 @@ class AoHRunner: raise - def _start_runner(self, config, playbook, host): + def _start_runner(self, config, playbook, args): raw_config = ansible_runner.get_ansible_config( 'dump', config, @@ -77,9 +77,9 @@ class AoHRunner: ident=self.id, envvars=env, extravars=vars, + cmdline=args, playbook=playbook, - limit=host, # verbosity=3, quiet=True, diff --git a/aoh/server.py b/aoh/server.py @@ -13,6 +13,46 @@ app.secret_key = os.urandom(16) _DATA = {} +def _get_opts(cmdline): + opts = [] + for arg in cmdline.split(): + if arg.startswith('--'): + opts += [arg.split('=')[0]] + elif arg.startswith('-'): + for opt in arg.split('=')[0][1:]: + opts += ['-' + opt] + return opts + + +def _validate_args(cmdline): + # Reject any sign of Jinja expressions + if '{{' in cmdline or '{%' in cmdline: + return False + + # Enforce whitelisted options. Other options either might not be supported + # yet, or may pose security risks. + OPT_WHITELIST = [ + '--check', '-C', + '--diff', '-D', + '--extra-vars', '-e', + '--help', '-h', + '--limit', '-l', + '--list-tags', + '--skip-tags', + '--start-at-task', + '--step', + '--tags', '-t', + '--verbose', '-v', + '--version', + ] + for opt in _get_opts(cmdline): + if opt not in OPT_WHITELIST: + print('bad', opt) + return False + + return True + + @app.get('/install') @app.get('/install.py') def install(): @@ -21,7 +61,10 @@ def install(): @app.post('/runners/') def job_new(): - runner = AoHRunner(CONFIG, PLAYBOOK, request.json['host']) + if not _validate_args(request.json['args']): + return { 'err': 'Bad or banned arguments passed.' }, 400 + + runner = AoHRunner(CONFIG, PLAYBOOK, request.json['args']) _DATA[runner.id] = runner session['runner'] = runner.id