Automation

AWX: design job templates that do not become a dangerous remote console

Turn AWX into a controlled operations tool with bounded job templates, limited variables, separated credentials, explicit inventories and post-action validation.

19 May 2026 awxansiblejob-templatecredentialsautomationlinux

AWX quickly provides a convenient interface for running Ansible. That is exactly what can make it dangerous. A generic playbook, an overly broad inventory, an overprivileged SSH account and a few free variables can turn an automation platform into a remote console with centralized history but weak controls.

The scenario here is a team that wants to expose a few recurring Linux operations: check server state, restart an approved service, install a validated package, apply a configuration role to a limited group. The goal is not to let AWX do everything. The goal is to publish bounded, understandable and verifiable actions.

Start from allowed actions

The first mistake is to create an AWX template from an existing playbook without redefining the scope. An administrator playbook can be flexible. A job template exposed to a team must be constrained. Start with the operational action: what should this button do, on which machines, with which account and with which proof of result?

text allowed-actions.txt
Actions exposed at the start
Check disk usage on production_web
Restart nginx or telegraf on production_web
Install a validated package on production_tools
Apply linux_baseline role on preproduction

Actions not exposed
Restart a free-form group
Execute arbitrary shell commands
Modify networking
Change secrets
Reboot a full inventory

This list should live close to the Ansible repository or operations documentation. Without it, AWX becomes a collection of buttons whose real scope depends on team memory.

Reduce free variables

A free variable in AWX can be useful, but each open field increases risk. For a service restart, checking an allowlist in the playbook is safer than allowing any service name. For package installation, scope should be limited to expected repositories and environments.

yaml restart-approved-service.yml
---
- name: Restart an approved service
hosts: "{{ target_group }}"
become: true
vars:
  allowed_services:
    - nginx
    - telegraf
    - chronyd
tasks:
  - name: Reject unauthorized service
    ansible.builtin.fail:
      msg: "Service not allowed by this job"
    when: service_name not in allowed_services

  - name: Restart approved service
    ansible.builtin.service:
      name: "{{ service_name }}"
      state: restarted

  - name: Collect service facts
    ansible.builtin.service_facts:

  - name: Show service state
    ansible.builtin.debug:
      msg: "{{ service_name }} state: {{ ansible_facts.services[service_name + '.service'].state | default('unknown') }}"

The control must exist in code, not only in the interface. An AWX option can be modified. A reviewed and versioned playbook keeps the rule where it belongs.

Separate credentials by risk level

A single account with NOPASSWD: ALL is comfortable at the beginning and bad for operations. AWX should use credentials adapted to the job type. A read job does not need sudo. A maintenance job can have limited sudo. A more sensitive job should be rare, visible and associated with restricted inventories.

text credential-model.txt
Read-only credential
Check jobs
No sudo

Maintenance credential
Approved service restarts
Validated package installation
Sudo limited to required commands

Restricted-admin credential
More sensitive changes
Limited inventories
Monitored and documented usage

The real barrier is on the target machines. AWX stores and presents the credential, but sudoers decides what the account can actually do.

Avoid inventories that are too broad

A template that accepts any inventory becomes unpredictable. A web service restart template should target the expected web group, not the whole estate. Inventories should reflect risk: preproduction, production, bastions, databases, tools and restricted zones.

yaml inventory-production.yml
all:
children:
  production_web:
    hosts:
      web01.example.local:
      web02.example.local:
  production_tools:
    hosts:
      tools01.example.local:
  restricted:
    hosts:
      bastion01.example.local:
      db01.example.local:

Restricted groups should not be an oral convention. They must appear in the inventory, documentation and template configuration.

Add proof after the action

A useful AWX job does more than execute a command. It shows the result. After a restart, collect service state. After installation, display the version. After configuration, validate the file or test the daemon. The AWX report becomes operational evidence.

yaml verify-package.yml
- name: Collect package facts
ansible.builtin.package_facts:
  manager: auto

- name: Display installed version
ansible.builtin.debug:
  msg: "{{ package_name }} version: {{ ansible_facts.packages[package_name][0].version }}"
when: package_name in ansible_facts.packages

Conclusion

AWX is safer when it exposes fewer actions, but better ones. A job template should be designed as an operations interface: clear scope, limited variables, suitable credential, precise inventory and visible validation. The goal is not to hide Ansible behind a UI. The goal is to make repeated operations reliable without granting implicit access to the whole estate.

A good starting point is modest: five useful templates, three credential levels, inventories that prevent obvious mistakes and playbooks that reject unauthorized parameters. With that discipline, AWX becomes a controlled operations tool rather than a remote console with a launch button.