from implicit defaults to controlled audits
File permissions in Bash scripts often arise only implicitly through the inherited umask instead of being set explicitly, and that is exactly what leads to unnecessarily broad access rights. This article shows how umask, chmod, and chown are applied systematically, which risks SUID and SGID pose in automation scripts, and how a dedicated audit script reliably checks file permissions.
Table of Contents
- 1. Why file permissions in Bash scripts are often overlooked
- 2. Understanding umask and setting it explicitly in scripts
- 3. Controlling permissions of newly created files and directories
- 4. chmod, chown, and the principle of least privilege
- 5. SUID, SGID, and the sticky bit: risks in automation scripts
- 6. Checking permissions systematically: building an audit script
- 7. Managing ACLs and extended permissions from Bash
- 8. Avoiding permission mistakes in CI/CD and deployment pipelines
- 9. Comparison: risky vs. safe permission patterns
- 10. Summary
- 11. FAQ
1. Why file permissions in Bash scripts are often overlooked
File permissions are among the most frequently overlooked security aspects in Bash scripts, because they usually arise implicitly through the currently active umask rather than being set deliberately. A script that creates a new file with touch or a redirection automatically inherits the permissions resulting from the default value minus the umask, without a developer ever explicitly reviewing that value. In many production environments this leads to sensitive log files, configuration files containing credentials, or temporary intermediate results ending up with far more open permissions than actually necessary.
The problem gets worse when Bash scripts run in different contexts, for example interactively in a developer shell with a restrictive umask, and automated in a cron job or container with a much more permissive default umask. A script that never explicitly sets its file permissions therefore behaves differently depending on the execution context, which makes security assumptions unreliable and regularly shows up as a finding in audits.
Systematically checking file permissions therefore means two things: first, explicitly set the umask at the start of the script instead of relying on the inherited environment. Second, actively restrict critical files after creation with chmod to the minimally necessary rights, regardless of what the umask would have produced.
2. Understanding umask and setting it explicitly in scripts
The umask is a mask subtracted from the default permissions of new files and directories. For files the default value is 666 (read and write for everyone), for directories 777 (read, write, execute for everyone), and the umask removes the corresponding bits bitwise. A umask of 022 results in 644 for new files (owner reads and writes, group and others only read) and 755 for directories. A more restrictive umask of 077 removes all rights for group and others, resulting in 600 and 700 respectively.
In Bash scripts that process sensitive data, such as backup scripts, deployment scripts with credentials, or maintenance scripts that create temporary files with internal information, the umask should be explicitly set to 077 at the start of the script. That ensures every newly created file is readable and writable only for the owner by default, regardless of which umask the calling environment brings along.
#!/usr/bin/env bash
# Explicit umask at script start, independent of the calling environment
set -euo pipefail
# Restrictive umask: new files 600, new directories 700
umask 077
# Every file created from here on inherits the restrictive default
readonly SECRET_FILE="/var/run/deploy-secret.tmp"
echo "temporary-token-value" > "$SECRET_FILE"
# Verify: should print 600
stat -c '%a' "$SECRET_FILE"
trap 'rm -f "$SECRET_FILE"' EXIT
It is important not to reset the umask when the script is sourced into an interactive shell, because otherwise the restrictive setting unintentionally leaks into the calling shell. In standalone scripts running as their own process this is not an issue, because the umask only applies within the script's process and is discarded automatically on exit.
3. Controlling permissions of newly created files and directories
Beyond umask, Bash scripts should additionally set explicit permissions when creating particularly sensitive files, instead of relying solely on the umask. That increases robustness against the case that the umask was accidentally changed elsewhere in the script, or that a called external tool sets its own umask. install -m 600 is often the more robust choice compared to touch followed by chmod, because it sets the permission atomically at creation time, with no window during which the file is visible with default permissions.
The same principle applies to directories: mkdir -m 700 directory sets the permission directly at creation time, instead of running mkdir and chmod as two separate steps, between which a window with incorrect permissions could occur. This so called Time of Check to Time of Use gap, TOCTOU for short, is a real risk on multi user systems, where other processes could access the file between the two steps.
#!/usr/bin/env bash
set -euo pipefail
# Atomic creation with explicit permissions, no TOCTOU gap
install -m 600 /dev/null /var/run/app-token.tmp
# Directory creation with permissions set atomically
mkdir -m 700 -p /var/backups/app-private
# Avoid this pattern: gap between creation and chmod
# touch /tmp/sensitive.log # world-readable for a brief moment
# chmod 600 /tmp/sensitive.log # too late if another process reads first
4. chmod, chown, and the principle of least privilege
The principle of least privilege means giving every file, every directory, and every process exactly the permissions minimally required for its task, no more. In practice this means for Bash scripts that executable scripts get 755 instead of 777, configuration files with credentials get 600 instead of 644, and directories with sensitive content get 700 instead of 755. chmod in deployment and maintenance scripts should always be documented with symbolic notation, for example chmod u=rw,go= file, because that notation makes the intended permission assignment more explicit than a plain octal number.
chown is the second half of the principle of least privilege: a file with correct permission bits but the wrong owner can still lead to overly broad access. Scripts running on behalf of a deployment user should consistently assign files to that user, instead of accidentally creating them under root or a generic service account, which happens in many environments when a script starts as root without an explicit chown.
#!/usr/bin/env bash
set -euo pipefail
readonly DEPLOY_USER="deploy"
readonly DEPLOY_GROUP="deploy"
readonly CONFIG_FILE="/etc/app/secrets.env"
# Explicit, documented permission and ownership assignment
chmod u=rw,go= "$CONFIG_FILE"
chown "$DEPLOY_USER:$DEPLOY_GROUP" "$CONFIG_FILE"
# Verify before proceeding — abort if permissions drifted
perms=$(stat -c '%a' "$CONFIG_FILE")
if [[ "$perms" != "600" ]]; then
echo "[ERROR] Unexpected permissions on $CONFIG_FILE: $perms" >&2
exit 1
fi
5. SUID, SGID, and the sticky bit: risks in automation scripts
The SUID bit lets an executable run with the permissions of its owner, regardless of which user actually starts it. If SUID is accidentally set on a Bash script, the Linux kernel largely ignores this bit for directly interpreted shell scripts for security reasons, but SUID binaries called by a Bash script can still enable significant privilege escalation if the script does not carefully control how they are invoked. An audit should therefore systematically search for SUID and SGID files touched or created by automation scripts.
The sticky bit on directories, such as on /tmp, prevents users from deleting other users' files in the same directory, even if they have write permission on the directory. Bash scripts that create their own temporary directories with broad write access for multiple users should consistently set the sticky bit to rule out exactly this risk.
#!/usr/bin/env bash
set -euo pipefail
# Audit: find SUID/SGID files that might affect automation
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null \
| while IFS= read -r f; do
echo "[SUID/SGID] $f ($(stat -c '%U:%a' "$f"))"
done
# Shared temp directory with sticky bit: prevents cross-user deletion
mkdir -m 1777 -p /var/tmp/shared-worker-dir
6. Checking permissions systematically: building an audit script
A reusable audit script that regularly checks critical paths against expected permissions makes the principle of least privilege measurable rather than merely claimed. Such a script defines the expected permission and expected owner for every critical path, and reports every deviation as a concrete finding with path, actual, and expected value. This pattern can be integrated as a cron job, as a pre deployment check, or as part of a monitoring pipeline.
It is important not to write the audit script just once, but to run it regularly, because permissions can drift unnoticed over time through manual interventions, faulty deployment scripts, or forgotten temporary changes.
#!/usr/bin/env bash
set -euo pipefail
declare -A EXPECTED_PERMS=(
["/etc/app/secrets.env"]="600"
["/etc/app/config.yml"]="644"
["/var/backups/app-private"]="700"
["/usr/local/bin/deploy.sh"]="755"
)
declare -a findings=()
for path in "${!EXPECTED_PERMS[@]}"; do
[[ -e "$path" ]] || continue
actual=$(stat -c '%a' "$path")
expected="${EXPECTED_PERMS[$path]}"
if [[ "$actual" != "$expected" ]]; then
findings+=("$path: expected $expected, found $actual")
fi
done
if (( ${#findings[@]} > 0 )); then
printf '[PERMISSION DRIFT] %s\n' "${findings[@]}" >&2
exit 1
fi
echo "[OK] All audited paths have expected permissions"
7. Managing ACLs and extended permissions from Bash
Classic Unix permissions with owner, group, and others are often not sufficient in more complex scenarios, for example when several service accounts need different access rights to the same file without a shared group making sense. Access Control Lists, ACLs for short, allow more fine grained rules via setfacl and getfacl, going beyond the classic nine permission bits. Bash scripts that set ACLs should include them in their audit script as well, because stat alone does not show ACL entries, and an audit without ACL checking can convey a false sense of security.
A common mistake is to set ACLs once and then forget that a simple chmod on the same file can partially overwrite or mask the fine grained ACL entries. Scripts that set both classic permissions and ACLs should consistently follow the order: chmod first, then setfacl, so the ACL settings are not unintentionally reset by a later chmod operation.
#!/usr/bin/env bash
set -euo pipefail
readonly SHARED_LOG="/var/log/app/shared-access.log"
# Base permission first, then fine-grained ACL entries
chmod 640 "$SHARED_LOG"
setfacl -m u:monitoring:r-- "$SHARED_LOG"
setfacl -m u:backup:r-- "$SHARED_LOG"
# Include ACL check in the audit
getfacl --omit-header "$SHARED_LOG"
8. Avoiding permission mistakes in CI/CD and deployment pipelines
Deployment pipelines are a particularly common source of permission mistakes, because files are often created by a CI runner with its own umask and then transferred to a target system via rsync or scp, without the target permissions being explicitly checked. rsync transfers source permissions by default, which can cause a permission generously set in the CI runner to land unchanged on the production system.
Deployment scripts should therefore explicitly set the target permissions after every transfer, instead of relying on the transferred source permissions. A final audit step that runs the check shown in the previous section directly after deployment catches permission deviations before they become a security problem in production.
9. Comparison: risky vs. safe permission patterns
The following table compares common risky permission patterns against their safe counterparts, as they typically occur in Bash automation.
| Situation | Risky | Safe | Benefit |
|---|---|---|---|
| New file with secrets | touch file; echo secret > file |
umask 077; install -m 600 … |
No window with open permissions |
| Executable script | chmod 777 deploy.sh |
chmod 755 deploy.sh |
No write access for others |
| Shared temp directory | mkdir -m 777 tmp |
mkdir -m 1777 tmp |
Sticky bit prevents deletion by others |
| Deployment via rsync | Adopting source permissions unchecked | Explicitly setting target permissions after transfer | Consistent, verified rights on target |
| Checking permissions | Only on manual suspicion | Automated audit script, run regularly | Deviations are reliably detected |
Mironsoft
Permission audits and hardening of Bash automation
Unclear file permissions in your deployment pipeline?
We audit umask, file and directory permissions, and SUID/SGID risks in your scripts, and build automated audit checks for your production environment.
Permission audit
Systematically check umask, chmod, chown, and ACLs
SUID/SGID scan
Find privilege escalation risks in automation paths
Audit automation
Integrate recurring check scripts into cron or CI
10. Summary
File permissions in Bash scripts too often arise implicitly through an inherited umask instead of being set deliberately. An explicit umask 077 at the start of the script, atomic file creation with install -m instead of separate touch and chmod steps, and consistent chown to the correct deployment user form the foundation of safe permission management. SUID, SGID, and the sticky bit deserve special attention, because they can enable privilege escalation if set carelessly or touched by automation scripts.
A reusable audit script that regularly checks critical paths against expected permissions makes the principle of least privilege measurable. Especially in deployment pipelines, where files travel across multiple systems with different umask values, an explicit check after every transfer is essential to prevent unnoticed permission drift.
Auditing File Permissions in Bash — The Key Points at a Glance
umask
Set explicitly at the start of the script, for example umask 077 for sensitive data, instead of trusting the inherited environment.
Atomic creation
install -m 600 or mkdir -m 700 instead of separate touch/mkdir and chmod steps, no TOCTOU gap.
SUID/SGID/sticky bit
Regularly search for SUID/SGID files, set the sticky bit on shared temp directories.
Audit
Reusable script with expected permissions, run regularly via cron or in CI.