Shell umask in Production: Creation Modes, Inheritance, and Permission Audits
Understand umask as an inherited process mask, calculate requested versus resulting modes, scope changes safely, and verify ACL and shell behavior.
umask is often described as a default permission setting, but it is more precise to think of it as a process-level mask applied when a program creates a filesystem object. It removes permission bits requested by the creating operation. It does not repair an existing file, add permissions that the application did not request, set ownership, or guarantee the final policy when access-control lists and platform-specific rules are involved.
For shell automation, that distinction affects every artifact a script creates: logs, caches, lock files, generated configuration, Unix sockets, and temporary data. A permissive mask can expose sensitive output to other users; an overly restrictive mask can break a group workflow. The production-safe approach is to set the mask deliberately in the process that owns creation, understand how child commands inherit it, and inspect the resulting object rather than assuming a shell setting proves the final permissions.
A mask removes bits from the requested mode
The creating program supplies a requested mode. In the ordinary permission-bit model, the process umask clears selected bits from that request. If a program requests 0666 for a new regular file and the mask is 0027, the result is 0640: 0666 & ~0027 removes group write and all permissions for others. A directory commonly starts from a request of 0777; with the same mask its initial mode is 0750.
This is bitwise filtering, not arithmetic subtraction. For each owner/group/other permission class, a one in the mask prevents the corresponding requested bit from being granted at creation. Bits absent from the requested mode remain absent even if the mask would permit them. In particular, a typical file creation mode of 0666 does not request execute permission, so umask 000 does not turn every newly created data file into an executable file.
The exact requested mode belongs to the program making the system call. open() with O_CREAT, mkdir(), a shell redirection that creates a missing file, mkfifo(), or an application-specific library can choose different modes. Therefore, saying “umask 022 makes every file mode 644” is too broad. It yields 0644 only when the creator requests the usual 0666 and no ACL or other platform rule changes the result.
An existing file is different. Opening an existing path for truncation does not recreate its directory entry, so changing the mask does not retroactively change its permissions. A program can later call chmod() or fchmod() and choose another mode. Treat the umask as one input to object creation, not as a persistent access-control rule enforced against every later operation.
The shell changes its own process state
In Bash and other shells, umask is a builtin because the mask belongs to the process running the shell. A child process inherits the parent’s mask when it is created; replacing that child with another program leaves the mask in effect. A child that changes its own umask does not normally change the already-running parent shell.
This matters in scripts that set a mask for one operation. A top-level umask 077 changes subsequent creations by the script and by commands it launches. A call inside a command substitution, parenthesized group, or pipeline component may run in a subshell environment, so the changed mask may end when that child exits. Pipeline process placement varies among shells and options; don’t rely on a pipeline-side umask change to configure later commands in the parent.
For a short, deliberately private scope, a subshell makes the lifetime explicit:
(
umask 077 || exit 1
generate_private_report >"$private_report" || exit 1
validate_private_report "$private_report" || exit 1
)
The child shell and commands it starts inherit the restrictive mask, while the parent shell retains its previous process state. This example assumes the path is already securely selected and that the application requests a mode no broader than the intended policy. It does not make an unsafe predictable path safe; use an atomic temporary-object creation method for that, as described in the dedicated temporary-file guide.
If a script sets a mask at startup and leaves it in place until exit, document that policy near the entry point. Avoid burying a process-wide change inside a helper that callers may source or reuse. A library function should not silently alter the caller’s creation policy for unrelated files. If a helper must temporarily change the mask in the current shell, it needs a deliberate save/restore interface and tests for every exit path; a subshell is often simpler.
Choose the mask from the sharing model
There is no universally correct value. A service that handles credentials or private build artifacts may choose 077, which prevents group and other permission bits from being granted at creation. A team workflow may choose 002 or 007 so group members can collaborate, but that only works when group ownership, parent-directory permissions, and application-requested modes agree with the intended policy.
| Policy example | File requested as 0666 |
Directory requested as 0777 |
Typical use and caveat |
|---|---|---|---|
umask 077 |
0600 |
0700 |
Private per-user output; group collaboration is intentionally disabled. |
umask 027 |
0640 |
0750 |
Group-readable output without group write; useful only if group ownership is correct. |
umask 002 |
0664 |
0775 |
Group collaboration where the parent directory and group policy are designed for it. |
These examples describe the ordinary permission-bit calculation, not a universal filesystem result. Group ownership may come from the process or be inherited from a set-group-ID directory, according to filesystem and platform rules. A restrictive mask does not make a shared directory private, and a collaborative mask does not assign a team group. Decide ownership, directory traversal, default ACLs, and cleanup together with the mask.
Avoid umask 000 as a quick fix for collaboration. It removes no bits that the application requested, which may expose every requested group or world permission. If multiple users need write access, build that around an approved group or ACL policy and verify the actual owner, group, and mode after creation.
ACLs can change the calculation
On Linux, a directory with a default POSIX ACL changes how new children inherit access. The Linux umask(2) documentation states that when the parent has a default ACL, the usual mask calculation is not used; the inherited ACL is applied, while permission bits absent from the mode argument remain unavailable. That behavior is documented for Linux and should not be generalized blindly to every filesystem or operating system.
The operational consequence is that umask output alone may not explain a surprising mode. Inspect the directory’s default ACL and the created object’s ACL using the platform’s ACL tools, such as getfacl and getfattr where available. Network filesystems, container mounts, and storage appliances can also enforce server-side policies. A local process setting is not a complete description of the remote authorization decision.
Default ACLs are useful for shared trees because they can define inherited group access more precisely than a single mask. They also require a policy for the ACL mask entry, ownership, and subsequent chmod operations. Test creation and later edits under the real service account, parent directory, mount type, and identity-mapping configuration instead of extrapolating from a developer workstation.
Read the current mask without changing it accidentally
The no-argument umask builtin displays the current mask, but output formatting is not identical across all shell implementations. Bash supports options such as -p and -S; those are Bash conveniences, not a reason to assume every /bin/sh supports the same syntax or output. Keep diagnostic parsing specific to a named shell and version.
In native multithreaded programs, querying the mask through the umask() system call is not a pure read: the API changes the calling process’s mask and returns the previous value. Reading it by setting and then restoring the same value can race with another thread that creates an object in between. Linux exposes the current value in /proc/<pid>/status, but that is a Linux-specific observation path. Prefer configuring a process’s creation policy before starting concurrent workers instead of repeatedly changing a shared process attribute.
In shell, set the mask once at a well-defined boundary, or confine the change in a subshell. Do not run an external helper and expect its mask change to persist in its parent. To diagnose a child process, print the mask from inside the exact command context that creates the object, but ensure the output does not reveal unrelated environment secrets.
Verify what was actually created
Permission review should test the application and its execution context, not only the shell command in isolation. Create a file, directory, FIFO, and socket if the program uses each type. Compare the requested mode, resulting mode, owner, group, ACL, and parent-directory policy. Repeat with existing files to confirm that truncation does not silently change their mode and with each service account that runs the automation.
For a controlled local test, use a fresh directory and a known mask:
umask 027
printf '%s\n' 'sample' > created-file
mkdir created-directory
stat -c '%a %U %G %n' created-file created-directory 2>/dev/null || \
stat -f '%Lp %Su %Sg %N' created-file created-directory
The stat variants are platform-specific alternatives: GNU systems use the first format, while BSD-derived systems such as macOS use the second. This is an inspection example, not portable production logic. Run it in a disposable directory, and use the filesystem’s ACL inspection commands when the parent has inherited ACL policy.
Add tests for the mask inherited from the service manager, the shell and script interpreter actually selected, a command launched through sudo or a container runtime if applicable, an existing output path, a setgid parent directory, a default ACL, and a network-mounted directory. Check failures during creation and cleanup; do not report a secure result if the file was created with a broader mode before a later chmod step.
A short production checklist
- Set the mask before the first sensitive creation, in the process that will launch the writers.
- Calculate the expected mode from the creator’s requested mode and the mask; do not treat the mask as subtraction or as a chmod default.
- Keep private and collaboration policies separate, and document which users or group should read and modify outputs.
- Use secure atomic creation for temporary objects; a restrictive mask does not prevent path races or replace trusted directory selection.
- Account for inherited ACLs and server-side filesystem policy where applicable.
- Test as the production identity and inspect mode, ownership, ACL, and existing-file behavior.
The reliable mental model is narrow but powerful: umask removes permissions requested during object creation, and child processes inherit that creation policy. The application still chooses a requested mode, the directory can contribute ACL or group-inheritance rules, and later operations can change metadata. Put the mask at a clear process boundary, verify the object on the real filesystem, and treat the result as evidence rather than assumption.
Related:
- How to Create Temporary Files Safely from a Shell Script
- How to Write Robust, Portable POSIX Shell Scripts
Sources: