Unsafe Permissions

Detects unsafe file permissions, ownership, and access control issues

49 rules in unsafe_permissions.yml

CRITICAL: 5 | HIGH: 25 | MEDIUM: 12 | LOW: 6

Rule IDSeverityTitleDescriptionRefs
ansible_cfg_private_key_file_world_readable_pathCRITICALansible.cfg private_key_file in world-readable locationprivate_key_file = /tmp/…, /home//Desktop/…, ~/Downloads/… - an SSH private key in a world-readable or user-facing directory is effectively leaked. Compare to a proper ~/.ssh/id_rsa with 0600 perms.
become_with_password_plaintextCRITICALPlaintext become_password in Playbookansible_become_password / ansible_become_pass / become_password is set inline to a literal string - the sudo password is stored in cleartext in version control.
inventory_ansible_become_pass_literalCRITICALInventory: ansible_become_pass / ansible_sudo_pass literalansible_become_pass / ansible_sudo_pass is set to a literal value in inventory - committing this gives anyone with repo read access the sudo password for every listed host.
inventory_ansible_ssh_pass_literalCRITICALInventory: ansible_ssh_pass / ansible_password literalInventory file (or inventory-style INI/YAML content) sets ansible_ssh_pass / ansible_password to a literal value - any checkout of the repo has every host’s SSH password in plaintext.
setuid_permissionCRITICALSetUID Permissionmode: ‘<4-7>xxx’ or chmod u+s sets the setuid bit on a file. Any unverified setuid binary becomes a privilege-escalation primitive.
ansible_cfg_allow_world_readable_tmpfilesHIGHansible.cfg allow_world_readable_tmpfiles = TrueSetting allow_world_readable_tmpfiles = True makes Ansible’s per-task temp files (which can briefly contain task parameters including secrets) world-readable. Any local user on the remote host can steal credentials mid-run.
ansible_cfg_host_key_checking_falseHIGHansible.cfg host_key_checking = FalseThe defaults section of ansible.cfg sets host_key_checking = False, disabling host-key verification globally for every playbook run from this config.
ansible_cfg_log_path_world_readableHIGHansible.cfg log_path points to world-readable locationlog_path set to /tmp, /var/tmp, /var/log with default umask - the ansible log captures task names, hostnames, and (without no_log) sometimes module arguments including secrets. A world-readable log is a credential leak waiting to happen.
ansible_cfg_roles_path_writable_locationHIGHansible.cfg roles_path / collections_path in world-writable dirroles_path or collections_path pointing to /tmp, /var/tmp, or another world-writable directory lets any local user on the Ansible controller plant a malicious role/collection that the next playbook run will execute.
ansible_host_key_checking_falseHIGHansible_host_key_checking = falseSetting ansible_host_key_checking to false (as a host_var, group_var, or extra-var) disables host-key verification for every SSH connection Ansible makes from that inventory scope. This turns off the entire trust layer for whole host groups at once.
become_flags_nopasswd_inlineHIGHbecome_flags Injects NOPASSWD or sudoers Overridesbecome_flags contains ‘-n’, ‘-k’, or ‘–preserve-env’ combined with ‘!authenticate’ / NOPASSWD - effectively disables sudo authentication for the remainder of the play.
copy_unsafe_writes_trueHIGHcopy/template with unsafe_writes: trueA copy: or template: task sets unsafe_writes: true, which disables Ansible’s atomic write-to-tempfile -> rename() strategy in favor of a direct open-and-truncate on the destination. Any local reader at that instant gets a half-written file; any local writer (even an unprivileged one, if the parent directory is world-writable) can race with the truncate and land arbitrary content in a root-owned file.
delegate_to_dynamic_hostHIGHdelegate_to With Jinja-Interpolated Hostdelegate_to value contains {{ … }} - if the variable comes from inventory or facts that an attacker can influence, the task runs against an attacker-chosen host with the controller’s credentials.
file_follow_true_with_becomeHIGHfile Module with follow: yes + become (Symlink TOCTOU)An ansible.builtin.file (or legacy file:) task sets follow: yes / follow: true AND runs with become: privilege escalation. If the path: points into a world-writable directory (/tmp, /var/tmp, /dev/shm) or a user-owned directory, a local attacker can swap the target for a symlink to /etc/shadow, /root/.ssh/authorized_keys, or any sensitive file - file then changes mode/owner on the symlink target, as root.
inventory_ansible_connection_paramiko_without_host_keyHIGHInventory: ansible_connection=paramiko with host_key_auto_addansible_connection=paramiko combined with paramiko’s host_key_auto_add (or no known_hosts file) trusts whatever host key the target presents on first connection - MITM is trivial on the initial handshake.
inventory_become_method_sudo_no_passwordHIGHInventory: ansible_become_flags contains -n (NOPASSWD sudo)A host/group var sets ansible_become_flags to contain -n (non-interactive), meaning sudo will NEVER prompt for a password. That requires NOPASSWD in sudoers on the target - silently baked into inventory rather than explicitly reviewed.
inventory_group_vars_all_contains_plaintext_secretHIGHgroup_vars/all: variable name suggests secret, value is plaintextA variable in group_vars/all (or host_vars) named *_password / *_token / *_secret / *_api_key / *_private_key is assigned a non-Jinja, non-vault, non-empty literal. That secret now applies to every host in the inventory.
inventory_winrm_ignore_cert_validationHIGHInventory: ansible_winrm_server_cert_validation=ignoreInventory sets ansible_winrm_server_cert_validation=ignore - the WinRM TLS handshake accepts any certificate, including an attacker-presented one. MITM on the Windows connection becomes trivial.
private_key_copied_outside_dot_sshHIGHPrivate Key Copied Or Linked Outside ~/.sshA shell task uses cp, mv, ln, or install on a path matching id_rsa, id_ed25519, id_ecdsa, *.pem, or *.key whose destination is NOT a .ssh/ directory. Common destinations are /home/root/.ssh/ (which is not root’s real home on standard Linux), /tmp, /var/tmp, /srv, /opt, or shared directories. Off-.ssh/ private keys typically inherit the surrounding directory’s umask (often 0755) and bypass tooling that audits ~/.ssh/ for mode 0600.
recursive_permission_changeHIGHRecursive Permission Changechmod -R 777 or 666 walks an entire subtree. Recursive permits rarely target only what’s intended and frequently widen permissions on credentials buried in the tree.
setfacl_mass_permission_grantHIGHsetfacl Grants rwx to everyone / other on Sensitive PathA task invokes setfacl (or the ansible.posix.acl module) to add an ACL entry of u::rwx,g::rwx,o::rwx, m::rwx, or user:anyone/group:wheel with rwx on paths like /etc, /root, /var/log, /home, /opt, or the recursive form -R on any system path. ACLs silently override POSIX mode bits - a 0600 /etc/shadow with a user:nobody:rwx ACL is world-readable in practice. This is a known AV/EDR-evasion trick used by recent Linux ransomware families.
setgid_permissionHIGHSetGID Permissionmode: ‘<2,3,6,7>xxx’ or chmod g+s sets the setgid bit on a file. Setgid on a binary inherits the file’s group on execution and is a classic group-escalation primitive.
ssh_args_disable_host_keyHIGHansible_ssh_common_args / ssh_args Disables Host-Key Verificationansible_ssh_common_args or ansible_ssh_extra_args contains -o StrictHostKeyChecking=no, -o UserKnownHostsFile=/dev/null, or both, disabling host-key checking for every host under this inventory scope.
ssh_stricthostkey_disabledHIGHSSH StrictHostKeyChecking DisabledPlaybook sets StrictHostKeyChecking=no (via ssh args, ProxyCommand, or -o flag), which accepts any host key and is the primary MitM-enabler for SSH. An attacker on the network path can silently impersonate the target host.
ssh_userknownhosts_devnullHIGHSSH UserKnownHostsFile=/dev/null Discards Host KeysUserKnownHostsFile=/dev/null (often paired with StrictHostKeyChecking=no) causes ssh to forget host keys after each connection - every connection becomes trust-on-first-use with no record, defeating host-key pinning entirely.
ssh_weak_hostkey_algorithms_reenabledHIGHansible_ssh_common_args / ssh_args Re-Enables Deprecated SSH Host-Key Algorithms (ssh-rsa / ssh-dss)A task sets ansible_ssh_common_args, ansible_ssh_extra_args, a playbook environment: ANSIBLE_SSH_ARGS, or ssh_args in ansible.cfg to include -oHostKeyAlgorithms=+ssh-rsa, -oPubkeyAcceptedKeyTypes=+ssh-rsa, -oPubkeyAcceptedAlgorithms=+ssh-rsa, -oHostKeyAlgorithms=+ssh-dss, -oCASignatureAlgorithms=+ssh-rsa, or any equivalent form that re-enables the deprecated SSH SHA-1 host-key family. OpenSSH 8.8 (2021) disabled ssh-rsa (SHA-1) by default and 9.8 (2024) removed DSA entirely because SHA-1 collision attacks (SHAttered, 2017) make host-key forgery practical for a well-resourced on-path attacker. Re-enabling these algorithms to ‘work around’ target OSes stuck on OpenSSH 7.x silently reintroduces MITM risk across every managed node and is a specific CIS/CISA finding in 2024-2025 compliance audits. Distinct from ssh_args_disable_host_key (that rule catches StrictHostKeyChecking=no); this catches the more subtle ‘I kept verification on but trusted a broken algorithm’ failure mode.
template_mode_executable_to_system_pathHIGHtemplate Rendering Executable File to System PathA template: task renders content to /etc/, /usr/bin/, /usr/local/bin/, /usr/sbin/, /opt/, or /root/ AND sets an executable mode (0755, 0775, 0777, 4xxx, or anything with the x-bit). That combination is the classic webshell / persistence-hook pattern: a templated script that runs during cron/systemd/PATH lookup, easy to forget in audits because it looks like ‘just a config file’.
tls_secret_downloaded_to_world_dirHIGHPrivate Key Or TLS Secret Downloaded Into A World-Readable DirectoryA shell task runs aws s3 cp, gcloud storage cp, az storage blob download, curl, or wget and writes a file whose name suggests a private key or TLS secret (*.key, *.pem, *privatekey*, *secret*) into /tmp, /var/tmp, /dev/shm, or /srv/<world-readable>. The destination directory is world-readable on every standard distribution, and the downloader does not enforce a restrictive mode: the way ansible.builtin.copy would; the secret is exposed for the entire interval before it is moved or removed.
winrm_cert_validation_ignoreHIGHWinRM ansible_winrm_server_cert_validation = ignoreSetting ansible_winrm_server_cert_validation to ‘ignore’ disables TLS validation for all WinRM connections - the Windows equivalent of StrictHostKeyChecking=no.
world_writable_filesHIGHWorld Writable Filesmode: ‘0777’ is set on a file resource, or chmod 777 / chmod a+w / chmod o+w is invoked. Any local user can rewrite the path’s contents, defeating integrity assumptions of every later task.
ansible_cfg_callback_whitelist_unpinnedMEDIUMansible.cfg callback_whitelist = callback_whitelist = * (or a comma-separated list including unvetted plugins) loads every callback plugin on the plugin path. Unlike a specific whitelist, this loads attacker-planted callbacks (e.g. a tampered-with slack plugin) that can exfiltrate task output to external services.
ansible_cfg_retry_files_enabled_trueMEDIUMansible.cfg retry_files_enabled = Trueretry_files_enabled writes .retry files containing inventory hostnames after failed runs. On shared systems these files linger with 0644 permissions and can leak inventory structure to other users.
become_user_root_explicitMEDIUMExplicit become_user: rootTask sets become_user: root. When combined with broad play scopes, this grants every downstream module root - violates least-privilege when a dedicated service user would do.
connection_local_with_privilege_sensitive_moduleMEDIUMconnection: local on Privilege-Sensitive TaskTask sets connection: local while executing a module that changes state (shell, command, copy, file, user, service). Controller-side side-effects are rarely the intent and often indicate a misconfigured delegation.
delegate_to_localhost_run_as_remoteMEDIUMdelegate_to: localhost With Privileged Remote ModuleTask uses delegate_to: localhost (or 127.0.0.1) while running a module that otherwise modifies remote state - silently shifts credential context onto the controller, often unnoticed by reviewers.
git_accept_hostkey_yesMEDIUMansible.builtin.git With accept_hostkey: yes (SSH TOFU on Remote Git Clone)An ansible.builtin.git task clones from an SSH remote (git@host:repo.git / ssh://git@host/repo.git) and sets accept_hostkey: yes. This flag tells Ansible to hand OpenSSH a temporary StrictHostKeyChecking=accept-new (or =no on older Ansible) argument for the single clone - effectively a TOFU (trust-on-first-use) bypass for the git remote’s host key. An on-path attacker (rogue GitHub enterprise mirror, DNS-hijacked self-hosted GitLab, BGP-hijacked proxy) gets handed the SSH deploy-key without any fingerprint check, then serves trojanized source code, build artifacts, or Ansible roles that subsequently run with the permissions of the Ansible run. The risk is amplified because most git: tasks run with become: yes to place the clone into /opt/, /srv/, /var/lib/, etc., so the malicious code inherits root privileges on the managed node.
lineinfile_no_backup_on_sensitive_fileMEDIUMlineinfile/blockinfile Editing Sensitive File With backup: noA lineinfile: or blockinfile: task edits /etc/sudoers, /etc/sudoers.d/, /etc/pam.d/, /etc/ssh/sshd_config, /etc/passwd, /etc/shadow, /etc/group, /etc/nsswitch.conf, /etc/hosts.allow, /etc/hosts.deny, /etc/crontab, /etc/cron.d/, or /etc/security/ AND explicitly sets backup: no (or backup: false). A typo in a sudoers line with no backup locks out every admin; a bad sshd_config edit with no backup loses SSH access. The file is version-controlled in the playbook, not on the target - recovery requires console access.
raw_module_with_becomeMEDIUMraw Module Used With becomeThe raw module bypasses Ansible’s module system (no argument sanitisation, no YAML-aware quoting). Combined with become: true, it elevates arbitrary shell commands to root with minimal guardrails.
ssh_keyscan_auto_acceptMEDIUMssh-keyscan Appending Unverified Keys to known_hostsPiping ssh-keyscan directly into known_hosts accepts whatever key the network returns, with no fingerprint comparison - the classic ‘TOFU at deploy time’ pattern that moves the MitM window earlier in the pipeline instead of eliminating it.
ssh_proxycommand_skips_verificationMEDIUMSSH ProxyCommand With Trust-Bypass FlagsA nested ProxyCommand that re-invokes ssh with StrictHostKeyChecking=no turns the jump host into a silent MitM: the outer connection is verified, but the hop through the proxy is not.
sticky_bit_removalMEDIUMSticky Bit RemovalExplicitly removing sticky bit from directories using chmod -t
world_readable_sensitiveMEDIUMWorld Readable Sensitive Filesmode: ‘0744’ is applied to a file containing ‘key’, ‘secret’, ‘password’, or ‘credential’. World-readable secrets are a compliance failure mode under PCI-DSS, HIPAA, and SOC 2.
ansible_cfg_any_errors_fatal_false_in_prodLOWansible.cfg any_errors_fatal = False for privileged playbooksWith any_errors_fatal = False, a task that fails on ONE host won’t stop the run on the other hosts - useful in dev, dangerous in hardening/patching playbooks where partial failure means a fleet in an inconsistent (possibly less-secure) state.
ansible_cfg_display_skipped_hosts_falseLOWansible.cfg display_skipped_hosts = Falsedisplay_skipped_hosts = False hides which hosts skipped a task - useful to reduce noise but also hides evidence that a security-relevant task (firewall, patching, hardening) didn’t run on a host.
ansible_cfg_nocows_disabled_in_prodLOWansible.cfg command_warnings = False (silences shell-over-module warnings)command_warnings = False disables Ansible’s warning when you use the command/shell module for something a native module could do better. That warning is a security signal - silencing it hides shell: apt install ... style anti-patterns.
ansible_cfg_pipelining_without_requirettyLOWansible.cfg pipelining = True (verify sudoers)pipelining = True speeds up runs but REQUIRES Defaults !requiretty in /etc/sudoers on every managed host. If requiretty is enabled (default on RHEL/CentOS), pipelining silently breaks become and can mask security issues.
become_without_explicit_become_method_for_windowsLOWWindows Host Uses become: true Without become_method: runasPlays targeting Windows hosts need become_method: runas. Missing become_method typically defaults to sudo, which will fail silently or do nothing on Windows, hiding escalation bugs during test runs.
inventory_host_pattern_all_hostsLOWInventory: host pattern ‘all’ used in a privileged playA play targets hosts: all AND uses become: true - a single typo, YAML anchor, or rogue PR can apply privileged changes to every host in your inventory at once. A narrower pattern (hosts: webservers, hosts: &prod:!canary) is safer.
legitimate_config_permissionsINFOStandard Configuration File PermissionsStandard permissions for configuration files (informational)