UserSpice Ansible
Ship the result. A web UI on UserSpice that runs Ansible playbooks against your fleet — auth, audit logging, dry-run buttons, and parameter validation.
Heads up — this is early software. Pre-1.0, single-digit minor version, and actively developed. The UI is functional, the threat model is documented, and we run it on our own Proxmox host — but features land continuously and edge cases get found through use.
Read the threat model and hardening guide before exposing it on anything beyond a trusted internal network.
Ship the result, without the ten-terminal dance.
When you've been vibe-coding fast with the rest of the toolkit, deployment is usually where the speed advantage evaporates — ten servers, ten SSH sessions, ten near-identical commands typed slightly differently across them. UserSpice Ansible fixes that for the operator who's already managing a small fleet: click a playbook, pick a target, click Check to dry-run, click Run to apply.
It's UserSpice in front, Ansible behind. The UI gives you login, per-target locks, dry-run buttons, parameter validation, and audit logging. Ansible itself does the actual work over SSH. Output streams back to the browser via SSE; every run writes a row with the operator's user ID, command, exit code, target, and full output — so "who pushed what when" is always a query away.
What it is not
- Not a hardened public web app. Don't expose it directly to the internet — the intended deployment is an isolated LXC reached over Tailscale, a VPN, or your LAN.
- Not a multi-tenant SaaS. The sudoers rule grants the web tier passwordless invocation of the
ansiblebinaries — fine for a single trusted operator, problematic if shared with operators on different trust levels. - Not a job queue. The run-start lock prevents double-clicks; it isn't a scheduler. Concurrent operators on the same host may step on each other.
- Not a replacement for knowing Ansible. You still write the playbooks. The UI removes the friction of running them; it doesn't remove the friction of designing them.
How to install
On a Proxmox VE host, as root:
bash -c "$(wget -qO - https://raw.githubusercontent.com/mudmin/userspice-ansible/main/proxmox/install-lxc.sh)"The installer:
- Creates an unprivileged Ubuntu 24.04 LXC with Apache, MariaDB, and Ansible pre-installed.
- Clones the repo, sets up the database, and prompts for an admin email and password.
- Asks "Restrict access to a single IP?" — answer with your operator workstation's IP to firewall the LXC to that one source. Skipping it leaves the LXC reachable from anywhere on its network.
- Prints the LXC's URL and the install-time vault password — back the vault password up immediately; lose it and every encrypted
host_vars/*.ymlfile is unreadable forever.
After install
- Open the URL printed by the installer; log in with the admin credentials you set.
- Run
add-serveron the LXC console to onboard your first remote host. The wizard handles SSH keys, sudo passwords, vault encryption, and inventory grouping. - Click any playbook on the dashboard, pick a target, click Check for a dry run, then Run.
The web UI is empty until at least one host is onboarded — by design, the LXC itself is not pre-registered so a misclick on firewall.yml can't lock you out of your own appliance.
What the threat model enforces
- No shell injection.
ansible-playbookruns viaproc_openwith argv arrays, neverexecwith concatenated strings. Targets and playbook names are whitelisted at the PHP layer. - Web tier can't write playbooks. Apache and PHP run as
www-data; the playbook tree, SSH key, and vault password live under a separateansibleuser. The web reaches Ansible only throughsudo -n -H -u ansiblewith a sudoers rule that whitelists the four ansible binaries. - Playbook tree isn't web-served. Apache denies
playbooks/,db/, andproxmox/. PHP can still read those paths; only direct HTTP fetches are blocked. - Self-management is read-only. The LXC pre-registers itself in inventory with
ansible_connection=local; tasks needingbecome: truefail intentionally. - Every run is audit-logged with the operator's user ID, command, exit code, target, and full output.
Hardening beyond defaults
The installer ships plain HTTP, which is deliberate for an internal LXC reached over LAN, Tailscale, or VPN — a self-signed cert produces a permanent "Not Secure" warning that trains operators to ignore browser warnings. The hardening guide covers:
- Real HTTPS via Tailscale serve (recommended on a tailnet), Let's Encrypt, or a bring-your-own cert. Each option leaves loopback callbacks on plain HTTP — the run-finish callback's curl doesn't follow redirects.
- Multiple operator IPs updated at both the ufw and Apache layers.
- Disabling SSH in favor of
pct enterfrom the Proxmox host. - Vault password backup. Generated at install, printed once, stored on disk at
/home/ansible/.ansible/vault_pass.txt— back it up to a password manager immediately. - Tightening the sudoers rule (e.g. dropping
ansible-vaultfrom the allow list once your fleet is fully onboarded).
Do not enable UserSpice's "Force HTTPS" toggle — it adds a redirect at the PHP layer that fires on every init.php load, including the loopback callback the run wrapper makes after each playbook. Enforce HTTPS at the Apache or reverse-proxy layer instead.
Customization
The web UI auto-discovers playbooks in playbooks/*.yml — drop a YAML file in
and it shows up on the dashboard with no UI registration step. The AGENT_GUIDE.md
shipped in the repo covers adding playbooks, the UI_PARAMS schema for
parameterized playbooks, the permissions model, and what not to touch (the users/
framework folder, the playbook locking semantics, Apache's DocumentRoot).
Known limitations
- Cancel-run is a no-op.
run_cancel.phperrors with "Undefined constant SIGTERM" because Apache's mod_php doesn't loadpcntl. A run started from the UI completes on its own; the button doesn't kill it. - Vault password loss is unrecoverable. Encrypted host_vars files cannot be decrypted without it. Back up the password.
Where the source lives
Source: github.com/mudmin/userspice-ansible.