Hardening

The admin account, sign-in limits, the database, expressions, job data, the browser policy, optional features and role options

  1. The admin account
  2. Sign-in limits and passwords
  3. The database
  4. Expressions and outbound calls
  5. Job data
  6. The Content-Security-Policy
  7. The settings pages and the designer
  8. Optional features
  9. Role options

The admin account

The local admin account is created at first start from ADMIN_USERNAME and ADMIN_PASSWORD, whose default is AnsibleForms!123.

  • A sign-in with the public default password AnsibleForms!123 must change it first: until then the server answers every request but the password change with 403 (code password_change_required), and the browser shows a change-password page that cannot be closed. The new password cannot be the default again.
  • ADMIN_PASSWORD is only read when the account is first created: set your own value before the first start.
  • REINIT_ADMIN=1 resets the password to ADMIN_PASSWORD at every start. Use it only to recover a lost password, then unset it and restart.
  • Set the two master secrets first: see Secrets and encryption.
  • Once LDAP or SSO sign-in works, keep the local admin for emergencies only, with a long password stored in your vault.

Sign-in limits and passwords

Password guessing is slowed down by three limits. The failures are counted in the database, so every app node sees the same attempts, and every limit applies without a restart:

Variable Default What it does
LOGIN_MAX_FAILURES 5 After this many failed logins, a local or LDAP account is locked for LOGIN_LOCKOUT_MINUTES.
LOGIN_MAX_FAILURES_PER_IP 30 An address with this many failed logins within LOGIN_LOCKOUT_MINUTES is held back until that window ends: one address trying many accounts.
LOGIN_LOCKOUT_MINUTES 15 How long an account stays locked, and the window in which failures are counted.
AUTH_RATE_LIMIT 120 Requests a minute from one address, per app node, on the login, sign-out, token refresh and single sign-on callbacks.

A locked account or a held-back address gets 429 with a Retry-After header, before any password is checked and whether the account exists or not; above AUTH_RATE_LIMIT the answer is 429 as well. A successful login clears the account’s count, and so does an admin setting the user’s password. 0 turns a limit off; LOGIN_LOCKOUT_MINUTES=0 turns off both the lockout and the per-address limit.

LOGIN_MAX_FAILURES_PER_IP and AUTH_RATE_LIMIT count per client address: behind a reverse proxy, set TRUST_PROXY so the address is the client’s and not the proxy’s, see Client addresses.

A local password set through the application, for a new user, by an admin or by users changing their own, must have at least PASSWORD_MIN_LENGTH characters (8 by default, 0 checks no length). It may not be the username or the public default password. ADMIN_PASSWORD is not checked.


The database

AnsibleForms holds every credential and job in its MySQL database, so keep that database off the network:

  • The docker-compose project publishes MySQL on the host (MYSQLDB_LOCAL_PORT, 3306) and ships with the root password AnsibleForms!123. Remove that port mapping, or bind it to 127.0.0.1, and change the password.
  • Connect with a dedicated account rather than root, set in DB_USER and DB_PASSWORD. It needs rights on the AnsibleForms schema only:

    CREATE USER 'ansibleforms'@'%' IDENTIFIED BY '<password>';
    GRANT ALL PRIVILEGES ON `AnsibleForms`.* TO 'ansibleforms'@'%';
    

    That covers creating the schema (the CREATE DATABASE included), patching it at upgrades, and the backups and restores, which run as the same user: the dump is taken with --no-tablespaces, so no server-wide right such as PROCESS is needed. Replace % with the address of the application where you can. A user with rights on every database, as root has, turns a hole in the application into a hole in the whole database server: the Status page warns about it in its Database user check, and the log at every start.

  • Set ALLOW_SCHEMA_CREATION to 0 once the schema exists. Creating the schema drops every table first; with 0 neither the startup bootstrap nor the /api/v2/schema endpoint can run it.

Expressions and outbound calls

Server expressions run on the server for any signed-in user, so the sanitizer and the outbound filters matter:

  • EXPRESSION_SANITIZER : strict or paranoid, never legacy, which lets users run server code.
  • REST_DENIED_HOSTS : hosts REST expressions may not reach, e.g. 169.254.169.254,127.0.0.0/8.
  • REST_ALLOWED_HOSTS : when set, REST expressions can reach only these hosts and ranges. The denied list wins over it.

Both lists only cover the REST helper functions of AnsibleForms itself; playbooks run outside them and are not filtered.

From 7.3.0 the REST helper functions check the certificate of the endpoint, as every other connection does. REST_IGNORE_CERTS=1 turns the check off: use it only for endpoints with a self-signed certificate.


Job data

Job extravars and output are stored in the database and shown to everyone who can see the job:

  • MASK_EXTRAVARS_REGEX : extravars whose key matches are shown as ********. The default is password|secret|token; password fields are always masked. Extend it with the names your forms use, for example password|secret|token|apikey|passphrase.
  • EXTRAVARS_USER_FIELDS : trims the ansibleforms_user object sent with every job, which otherwise holds the user’s whole group membership.
  • REGEX_FILTER_JOB_OUTPUT only hides noisy tasks; hide secrets with no_log.

The Content-Security-Policy

The interface is served with a Content-Security-Policy header, which limits what a page may load and run in the browser:

  • Scripts come from the application itself only, so a script injected into a page does not run.
  • No other site may show the interface in a frame.
  • It allows what the application needs: local form expressions evaluated in the browser ('unsafe-eval'), inline styles, images from any URL (form and category images), and the workers of the code editor.

CONTENT_SECURITY_POLICY is 1 by default and should stay so. With 0 the policy is sent as Content-Security-Policy-Report-Only: the browser console lists what it would block, and nothing is blocked. Use 0 only for a setup that cannot work with the policy yet, such as a reverse proxy that injects a script of its own. A change on the settings page applies without a restart.


The settings pages and the designer

Administrators can change much of the configuration from the browser. Narrow that where the configuration lives elsewhere:


Optional features

These features are off by default. Switch them on only when you use them; a change on the settings page applies without a restart:

  • ENABLE_MCP : serves the MCP server on /api/v2/mcp, so MCP clients can launch forms with a user’s token.
  • ENABLE_CHAT : the chat assistant. Form definitions and the values being filled in are sent to the configured model provider.

LAUNCH_VALIDATION works the other way round: it is enforce by default, so the server refuses a launch whose values break the form’s rules, including launches through the REST API. Keep it there: log only logs what would be refused and off checks nothing, see Launch validation.


Role options

Some role options in config.yaml give more than their name suggests. Grant them with care:

Option Why it matters
allowScheduledJobs A user with it sees and can change every schedule, and a schedule runs with admin rights, for any form. Treat it as admin-level.
allowBackupOps Includes restore, which replaces the whole database.
showSettings Opens the administration pages. Without the admin role, it cannot change the accounts and groups the admin role names, see Users and groups.
showLogs Shows the server log, which can contain host names, user names and error details.
showConnectionsSettings Opens the connections pages: credentials, runners, notification channels and more. Adding, changing and deleting runners and secret stores stay an admin’s, and so do the credentials of connections and sign-ins. Of a repository, adding or deleting it and changing its address, branch or use_for_* flags stay an admin’s.
showAllJobLogs Shows the jobs of every user, not only the user’s own.
manageAllJobs Aborts, deletes and relaunches the jobs of every user.
allowApiTokens Lets the user create API tokens that live up to API_TOKEN_MAX_DAYS (90 by default). Signing out with a token or revoking it ends it, and a password change ends all of the user’s tokens, see Tokens.
extendedTokenExpiration With allowApiTokens, also lets the user create API tokens that never expire.
allowFileUploads Lets the user upload files through a form’s file fields. On for every user unless a role turns it off.
allowChat, allowMcp Let the user use the chat assistant and the MCP server, when they are switched on. allowChat is on for every user unless a role turns it off, and allowMcp follows allowChat unless a role sets it.

A user without the admin role gets none of the admin-level options above unless a role gives them. When the admin role sets no options, its users get showSettings, showDesigner, showLogs, showAuditLog, allowBackupOps, allowJobRelaunch, showAllJobLogs, manageAllJobs and allowScheduledJobs. allowApiTokens and extendedTokenExpiration stay off even for the admin role: set them on the roles that need them.

When several roles of a user set the same option, false wins. The options of the default role only fill in the options that none of the user’s own roles set; they never overrule them.